diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 658fd0b..97e9037 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,6 +17,7 @@ jobs: matrix: os: [ "ubuntu-latest", "macos-13", "windows-latest" ] python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] + pydantic-version: [ "1.0", "2.0" ] fail-fast: false defaults: run: @@ -34,9 +35,11 @@ jobs: uses: actions/cache@v4 with: path: .venv - key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml') }} - name: Install Hatch run: pip install git+https://github.com/pypa/hatch.git@master + - name: Install Pydantic + run: hatch run uv pip install pydantic~=${{ matrix.pydantic-version }} - name: Run Build Hooks run: hatch build --hooks-only - name: Run Tests @@ -45,7 +48,7 @@ jobs: uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: ${{ matrix.os }}-${{ matrix.python-version }} + flag-name: ${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.pydantic-version }} parallel: true coverage: diff --git a/README.md b/README.md index c726481..e5e4f7b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ ## Help See [documentation](https://pydantic-argparse.supimdos.com) for help. +## Requirements +Requires Python 3.8+, and is compatible with the Pydantic v1 API. + ## Installation Installation with `pip` is simple: ```console @@ -46,7 +49,7 @@ $ pip install pydantic-argparse ## Example ```py -import pydantic +import pydantic.v1 as pydantic import pydantic_argparse diff --git a/docs/index.md b/docs/index.md index 9742264..7c39e66 100644 --- a/docs/index.md +++ b/docs/index.md @@ -41,7 +41,8 @@ provides declarative *typed* argument parsing using `pydantic` models. ## Requirements -`pydantic-argparse` requires Python 3.8+ +`pydantic-argparse` requires Python 3.8+, and is compatible with the Pydantic +v1 API. ## Installation Installation with `pip` is simple: diff --git a/docs/showcase.md b/docs/showcase.md index 86b4cc3..416b5fa 100644 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -7,7 +7,7 @@ The `pydantic-argparse` command-line interface construction is simple. === "Pydantic Argparse" ```python - import pydantic + import pydantic.v1 as pydantic import pydantic_argparse # Declare Arguments diff --git a/examples/commands.py b/examples/commands.py index 075a383..a850496 100644 --- a/examples/commands.py +++ b/examples/commands.py @@ -2,7 +2,7 @@ # Third-Party -import pydantic +import pydantic.v1 as pydantic import pydantic_argparse # Typing diff --git a/examples/simple.py b/examples/simple.py index cadf015..c50a237 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -2,7 +2,7 @@ # Third-Party -import pydantic +import pydantic.v1 as pydantic import pydantic_argparse diff --git a/pyproject.toml b/pyproject.toml index 0a63879..e1bf8f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [{ name = "Hayden Richards", email = "supimdos@gmail.com" }] readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.8" -dependencies = ["pydantic<2"] +dependencies = ["pydantic"] classifiers = [ "Development Status :: 4 - Beta", 'Programming Language :: Python', @@ -102,7 +102,7 @@ warn_return_any = true warn_unused_ignores = true no_implicit_optional = true show_error_codes = true -plugins = ["pydantic.mypy"] +plugins = ["pydantic.v1.mypy"] [tool.pydantic-mypy] init_forbid_extra = true diff --git a/src/pydantic_argparse/argparse/actions.py b/src/pydantic_argparse/argparse/actions.py index 0ed9b69..6258c0e 100644 --- a/src/pydantic_argparse/argparse/actions.py +++ b/src/pydantic_argparse/argparse/actions.py @@ -7,8 +7,8 @@ """ -# Standard -import argparse +# Local +from pydantic_argparse.compatibility import argparse # Typing from typing import Any, Callable, Iterable, List, Optional, Sequence, Tuple, TypeVar, Union, cast diff --git a/src/pydantic_argparse/argparse/parser.py b/src/pydantic_argparse/argparse/parser.py index 60d0237..ac5956c 100644 --- a/src/pydantic_argparse/argparse/parser.py +++ b/src/pydantic_argparse/argparse/parser.py @@ -16,17 +16,14 @@ # Standard -import argparse import sys -# Third-Party -import pydantic - # Local from pydantic_argparse import parsers from pydantic_argparse import utils from pydantic_argparse.argparse import actions -from pydantic_argparse.argparse import patches # noqa: F401 +from pydantic_argparse.compatibility import argparse +from pydantic_argparse.compatibility import pydantic # Typing from typing import Any, Dict, Generic, List, NoReturn, Optional, Type, TypeVar diff --git a/src/pydantic_argparse/compatibility/__init__.py b/src/pydantic_argparse/compatibility/__init__.py new file mode 100644 index 0000000..7f19e26 --- /dev/null +++ b/src/pydantic_argparse/compatibility/__init__.py @@ -0,0 +1,11 @@ +"""Compatibiltity Shims for Declarative Typed Argument Parsing. + +This package contains compatibility shims for the `pydantic` and `argparse` +modules, so that we can properly maintain version compatibility in one place. + +The public interface exposed by this package is the module shims themselves. +""" + +# Local +from pydantic_argparse.compatibility.argparse import argparse as argparse +from pydantic_argparse.compatibility.pydantic import pydantic as pydantic diff --git a/src/pydantic_argparse/argparse/patches.py b/src/pydantic_argparse/compatibility/argparse.py similarity index 97% rename from src/pydantic_argparse/argparse/patches.py rename to src/pydantic_argparse/compatibility/argparse.py index 8797660..ea709da 100644 --- a/src/pydantic_argparse/argparse/patches.py +++ b/src/pydantic_argparse/compatibility/argparse.py @@ -1,4 +1,4 @@ -"""Monkey patches for ArgumentParser. +"""Compatibility Shim for ArgumentParser. In order to support Python 3.8 while retaining the unit tests, we need to backport the bugfix for [`BPO-29298`](https://bugs.python.org/issue29298). diff --git a/src/pydantic_argparse/compatibility/pydantic.py b/src/pydantic_argparse/compatibility/pydantic.py new file mode 100644 index 0000000..b83a34c --- /dev/null +++ b/src/pydantic_argparse/compatibility/pydantic.py @@ -0,0 +1,19 @@ +"""Compatibility Shim for Pydantic. + +In order to support both Pydantic v1 and Pydantic v2, we need to make sure we +import the module correctly: + +* For `pydantic~=2.0` there is a working `v1` module. +* For `pydantic==1.10.15` there is a broken `v1` module (with no `fields`). +* For `pydantic<1.10.14` there is no `v1` module. +""" + + +# Pydantic Shim +# There is a bit of fiddling around here to accomodate for the cases outlined +# above, as well as to keep the type-checker and language-server happy. +try: # pragma: no cover + from pydantic import v1 as pydantic + pydantic.fields # noqa: B018 +except (ImportError, AttributeError): # pragma: no cover + import pydantic # type: ignore[no-redef] diff --git a/src/pydantic_argparse/parsers/boolean.py b/src/pydantic_argparse/parsers/boolean.py index c687600..1de1fa4 100644 --- a/src/pydantic_argparse/parsers/boolean.py +++ b/src/pydantic_argparse/parsers/boolean.py @@ -7,18 +7,14 @@ """ -# Standard -import argparse - -# Third-Party -import pydantic - # Typing from typing import Optional # Local from pydantic_argparse import utils from pydantic_argparse.argparse import actions +from pydantic_argparse.compatibility import argparse +from pydantic_argparse.compatibility import pydantic def should_parse(field: pydantic.fields.ModelField) -> bool: diff --git a/src/pydantic_argparse/parsers/command.py b/src/pydantic_argparse/parsers/command.py index 633ec84..9ff8cc5 100644 --- a/src/pydantic_argparse/parsers/command.py +++ b/src/pydantic_argparse/parsers/command.py @@ -7,17 +7,13 @@ """ -# Standard -import argparse - -# Third-Party -import pydantic - # Typing from typing import Optional # Local from pydantic_argparse import utils +from pydantic_argparse.compatibility import argparse +from pydantic_argparse.compatibility import pydantic def should_parse(field: pydantic.fields.ModelField) -> bool: diff --git a/src/pydantic_argparse/parsers/container.py b/src/pydantic_argparse/parsers/container.py index 0155fed..2bc2010 100644 --- a/src/pydantic_argparse/parsers/container.py +++ b/src/pydantic_argparse/parsers/container.py @@ -8,18 +8,16 @@ # Standard -import argparse import collections.abc import enum -# Third-Party -import pydantic - # Typing from typing import Optional # Local from pydantic_argparse import utils +from pydantic_argparse.compatibility import argparse +from pydantic_argparse.compatibility import pydantic def should_parse(field: pydantic.fields.ModelField) -> bool: diff --git a/src/pydantic_argparse/parsers/enum.py b/src/pydantic_argparse/parsers/enum.py index f8b6b7f..582fbe8 100644 --- a/src/pydantic_argparse/parsers/enum.py +++ b/src/pydantic_argparse/parsers/enum.py @@ -8,14 +8,12 @@ # Standard -import argparse import enum -# Third-Party -import pydantic - # Local from pydantic_argparse import utils +from pydantic_argparse.compatibility import argparse +from pydantic_argparse.compatibility import pydantic # Typing from typing import Optional, Type diff --git a/src/pydantic_argparse/parsers/literal.py b/src/pydantic_argparse/parsers/literal.py index f3a7c69..0ba219d 100644 --- a/src/pydantic_argparse/parsers/literal.py +++ b/src/pydantic_argparse/parsers/literal.py @@ -7,14 +7,10 @@ """ -# Standard -import argparse - -# Third-Party -import pydantic - # Local from pydantic_argparse import utils +from pydantic_argparse.compatibility import argparse +from pydantic_argparse.compatibility import pydantic # Typing from typing import Optional, Literal, get_args diff --git a/src/pydantic_argparse/parsers/mapping.py b/src/pydantic_argparse/parsers/mapping.py index a6f0590..bbb4634 100644 --- a/src/pydantic_argparse/parsers/mapping.py +++ b/src/pydantic_argparse/parsers/mapping.py @@ -8,18 +8,16 @@ # Standard -import argparse import ast import collections.abc -# Third-Party -import pydantic - # Typing from typing import Optional # Local from pydantic_argparse import utils +from pydantic_argparse.compatibility import argparse +from pydantic_argparse.compatibility import pydantic def should_parse(field: pydantic.fields.ModelField) -> bool: diff --git a/src/pydantic_argparse/parsers/standard.py b/src/pydantic_argparse/parsers/standard.py index 89b8932..3932742 100644 --- a/src/pydantic_argparse/parsers/standard.py +++ b/src/pydantic_argparse/parsers/standard.py @@ -9,17 +9,13 @@ """ -# Standard -import argparse - -# Third-Party -import pydantic - # Typing from typing import Optional # Local from pydantic_argparse import utils +from pydantic_argparse.compatibility import argparse +from pydantic_argparse.compatibility import pydantic def parse_field( diff --git a/src/pydantic_argparse/utils/arguments.py b/src/pydantic_argparse/utils/arguments.py index 899f7fc..f11bca9 100644 --- a/src/pydantic_argparse/utils/arguments.py +++ b/src/pydantic_argparse/utils/arguments.py @@ -5,8 +5,8 @@ """ -# Third-Party -import pydantic +# Local +from pydantic_argparse.compatibility import pydantic def name(field: pydantic.fields.ModelField, invert: bool = False) -> str: diff --git a/src/pydantic_argparse/utils/errors.py b/src/pydantic_argparse/utils/errors.py index 8fa5112..200f61d 100644 --- a/src/pydantic_argparse/utils/errors.py +++ b/src/pydantic_argparse/utils/errors.py @@ -5,8 +5,8 @@ """ -# Third-Party -import pydantic +# Local +from pydantic_argparse.compatibility import pydantic # Typing from typing import Union diff --git a/src/pydantic_argparse/utils/namespaces.py b/src/pydantic_argparse/utils/namespaces.py index 49ac593..7c031be 100644 --- a/src/pydantic_argparse/utils/namespaces.py +++ b/src/pydantic_argparse/utils/namespaces.py @@ -5,8 +5,8 @@ """ -# Standard -import argparse +# Local +from pydantic_argparse.compatibility import argparse # Typing from typing import Any, Dict diff --git a/src/pydantic_argparse/utils/pydantic.py b/src/pydantic_argparse/utils/pydantic.py index ceba57f..23af412 100644 --- a/src/pydantic_argparse/utils/pydantic.py +++ b/src/pydantic_argparse/utils/pydantic.py @@ -10,8 +10,8 @@ # Standard import contextlib -# Third-Party -import pydantic +# Local +from pydantic_argparse.compatibility import pydantic # Typing from typing import Any, Callable, Dict, Optional, Type, TypeVar, Union diff --git a/src/pydantic_argparse/utils/types.py b/src/pydantic_argparse/utils/types.py index 4378877..f3fb92e 100644 --- a/src/pydantic_argparse/utils/types.py +++ b/src/pydantic_argparse/utils/types.py @@ -5,8 +5,8 @@ """ -# Third-Party -import pydantic +# Local +from pydantic_argparse.compatibility import pydantic # Typing from typing import Any, Tuple, Union, get_origin diff --git a/tests/argparse/test_actions.py b/tests/argparse/test_actions.py index 68ed172..e29efc8 100644 --- a/tests/argparse/test_actions.py +++ b/tests/argparse/test_actions.py @@ -6,14 +6,12 @@ class by testing the expected nested namespace functionality. """ -# Standard -import argparse - # Third-Party import pytest # Local from pydantic_argparse.argparse import actions +from pydantic_argparse.compatibility import argparse from tests import conftest as conf diff --git a/tests/argparse/test_parser.py b/tests/argparse/test_parser.py index a86fbe2..c5ce030 100644 --- a/tests/argparse/test_parser.py +++ b/tests/argparse/test_parser.py @@ -7,19 +7,19 @@ class by testing a large number of expected use-cases. # Standard -import argparse import collections as coll import datetime as dt import re import textwrap # Third-Party -import pydantic import pytest # Local import pydantic_argparse -import tests.conftest as conf +from pydantic_argparse.compatibility import argparse +from pydantic_argparse.compatibility import pydantic +from tests import conftest as conf # Typing from typing import Deque, Dict, FrozenSet, List, Literal, Optional, Set, Tuple, Type, TypeVar diff --git a/tests/conftest.py b/tests/conftest.py index f561665..dc8bd42 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,16 +7,14 @@ # Standard -import argparse import collections import datetime import enum -# Third-Party -import pydantic - # Local from pydantic_argparse.argparse import actions +from pydantic_argparse.compatibility import argparse +from pydantic_argparse.compatibility import pydantic # Typing from typing import Any, Deque, Dict, FrozenSet, List, Literal, Optional, Set, Tuple, Type diff --git a/tests/functional/test_environment_variables.py b/tests/functional/test_environment_variables.py index e9bc70e..71c68a7 100644 --- a/tests/functional/test_environment_variables.py +++ b/tests/functional/test_environment_variables.py @@ -6,7 +6,6 @@ # Standard -import argparse import collections as coll import datetime as dt import os @@ -17,7 +16,8 @@ # Local import pydantic_argparse -import tests.conftest as conf +from pydantic_argparse.compatibility import argparse +from tests import conftest as conf # Typing from typing import Deque, Dict, FrozenSet, List, Literal, Optional, Set, Tuple, Type, TypeVar diff --git a/tests/utils/test_errors.py b/tests/utils/test_errors.py index f96f0a8..7799cf6 100644 --- a/tests/utils/test_errors.py +++ b/tests/utils/test_errors.py @@ -9,11 +9,11 @@ import textwrap # Third-Party -import pydantic import pytest # Local from pydantic_argparse import utils +from pydantic_argparse.compatibility import pydantic from tests import conftest as conf # Typing diff --git a/tests/utils/test_namespaces.py b/tests/utils/test_namespaces.py index b810632..6bf1b11 100644 --- a/tests/utils/test_namespaces.py +++ b/tests/utils/test_namespaces.py @@ -5,11 +5,9 @@ """ -# Standard -import argparse - # Local from pydantic_argparse import utils +from pydantic_argparse.compatibility import argparse def test_namespace_to_dict() -> None: diff --git a/tests/utils/test_types.py b/tests/utils/test_types.py index 9ad425e..2f10fa6 100644 --- a/tests/utils/test_types.py +++ b/tests/utils/test_types.py @@ -11,11 +11,11 @@ import enum # Third-Party -import pydantic import pytest # Local from pydantic_argparse import utils +from pydantic_argparse.compatibility import pydantic from tests import conftest as conf # Typing