diff --git a/.github/actions/setup-python-poetry/action.yaml b/.github/actions/setup-python-poetry/action.yaml new file mode 100644 index 0000000..c8a614a --- /dev/null +++ b/.github/actions/setup-python-poetry/action.yaml @@ -0,0 +1,26 @@ +name: 'Setup Python and Poetry' +description: 'Checkout code and setup Python with Poetry' +inputs: + python-version: + description: 'The Python version to set up' + required: true + default: '3.10' + poetry-version: + description: 'The Poetry version to set up' + required: true + default: '1.7.1' + +runs: + using: 'composite' + steps: + - uses: abatilo/actions-poetry@v2 + with: + poetry-version: ${{ inputs.poetry-version }} + - uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python-version }} + cache: 'poetry' + - run: | + poetry install --all-extras + echo "$(poetry env info --path)/bin" >> $GITHUB_PATH + shell: bash diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2a057ec..25c7b6a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,21 +20,28 @@ jobs: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11"] - poetry-version: ["1.2.2"] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v4 + - uses: ./.github/actions/setup-python-poetry with: python-version: ${{ matrix.python-version }} - - name: Install poetry - uses: abatilo/actions-poetry@v2 - with: - poetry-version: ${{ matrix.poetry-version }} - - name: Install dependencies - run: poetry install - name: Test - run: make test - - name: Upload coverage - uses: codecov/codecov-action@v3 + run: poetry run coverage run --parallel-mode -m pytest . + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: coverage-directory + path: ./coverage + + Upload-Coverage: + runs-on: ubuntu-latest + needs: [ Tests ] + steps: + - uses: actions/checkout@v2 + - uses: ./.github/actions/setup-python-poetry + - uses: actions/download-artifact@v2 + - run: poetry run coverage combine + - name: Upload to Codecov + uses: codecov/codecov-action@v2 diff --git a/.gitignore b/.gitignore index b6e4761..27542d0 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,5 @@ dmypy.json # Pyre type checker .pyre/ + +coverage/ \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index c5abd15..feeb345 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,65 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. - -[[package]] -name = "autoflake" -version = "2.3.0" -description = "Removes unused imports and unused variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "autoflake-2.3.0-py3-none-any.whl", hash = "sha256:79a51eb8c0744759d2efe052455ab20aa6a314763510c3fd897499a402126327"}, - {file = "autoflake-2.3.0.tar.gz", hash = "sha256:8c2011fa34701b9d7dcf05b9873bc4859d4fce4e62dfea90dffefd1576f5f01d"}, -] - -[package.dependencies] -pyflakes = ">=3.0.0" -tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} - -[[package]] -name = "black" -version = "24.2.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "cached-classproperty" @@ -72,20 +11,6 @@ files = [ {file = "cached_classproperty-1.0.1.tar.gz", hash = "sha256:24ac50911c5a87bd57fafc348ce14ba58584975e73d1dfea329d1dcdc9f774c0"}, ] -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" @@ -189,31 +114,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - [[package]] name = "packaging" version = "23.2" @@ -225,32 +125,6 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] - [[package]] name = "pluggy" version = "1.4.0" @@ -318,17 +192,6 @@ typing-extensions = ">=4.2.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] -[[package]] -name = "pyflakes" -version = "3.2.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, - {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, -] - [[package]] name = "pytest" version = "8.0.2" @@ -370,28 +233,29 @@ pytest = ">=4.6" testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] -name = "pyupgrade" -version = "3.8.0" -description = "A tool to automatically upgrade syntax for newer versions." +name = "ruff" +version = "0.3.3" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false -python-versions = ">=3.8" -files = [ - {file = "pyupgrade-3.8.0-py2.py3-none-any.whl", hash = "sha256:08d0e6129f5e9da7e7a581bdbea689e0d49c3c93eeaf156a07ae2fd794f52660"}, - {file = "pyupgrade-3.8.0.tar.gz", hash = "sha256:1facb0b8407cca468dfcc1d13717e3a85aa37b9e6e7338664ad5bfe5ef50c867"}, -] - -[package.dependencies] -tokenize-rt = ">=3.2.0" - -[[package]] -name = "tokenize-rt" -version = "5.2.0" -description = "A wrapper around the stdlib `tokenize` which roundtrips." -optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "tokenize_rt-5.2.0-py2.py3-none-any.whl", hash = "sha256:b79d41a65cfec71285433511b50271b05da3584a1da144a0752e9c621a285289"}, - {file = "tokenize_rt-5.2.0.tar.gz", hash = "sha256:9fe80f8a5c1edad2d3ede0f37481cc0cc1538a2f442c9c2f9e4feacd2792d054"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, + {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, + {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, + {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, + {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, ] [[package]] @@ -419,4 +283,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "71420e44870f83a8191f87bcaa2dd09c403be23f9f386c02b140f65a92ac3c65" +content-hash = "2d0e2ddc0b7fa26e26b7cc30921e4debd6e171debab67068ffc9249f478b491f" diff --git a/pydantic_duality/__init__.py b/pydantic_duality/__init__.py index 4cf7499..9c108be 100644 --- a/pydantic_duality/__init__.py +++ b/pydantic_duality/__init__.py @@ -1,17 +1,19 @@ import importlib.metadata import inspect import sys +from collections.abc import Iterable from typing import ( TYPE_CHECKING, Any, Callable, ClassVar, Dict, - List, Mapping, Optional, Tuple, + Type, Union, + _BaseGenericAlias, # pyright: ignore[reportAttributeAccessIssue] get_args, get_origin, ) @@ -20,12 +22,14 @@ from pydantic import BaseConfig, BaseModel, Extra, Field from pydantic.fields import FieldInfo from pydantic.main import ModelMetaclass -from typing_extensions import _AnnotatedAlias, Self, dataclass_transform, Annotated, Iterable -from typing import Type +from typing_extensions import Annotated, Self, _AnnotatedAlias, dataclass_transform if sys.version_info >= (3, 9): from types import GenericAlias + GenericAliasUnion = GenericAlias | _BaseGenericAlias +else: + GenericAliasUnion = _BaseGenericAlias __version__ = importlib.metadata.version("pydantic_duality") @@ -34,7 +38,7 @@ PATCH_REQUEST_ATTR = "__patch_request__" -def _resolve_annotation(annotation, attr: str) -> Any: +def _resolve_annotation(annotation: Any, attr: str) -> Any: if inspect.isclass(annotation) and isinstance(annotation, DualBaseModelMeta): return getattr(annotation, attr) if get_origin(annotation) is Annotated: @@ -50,16 +54,11 @@ def _resolve_annotation(annotation, attr: str) -> Any: ) if inspect.isclass(annotation) and issubclass(annotation, BaseModel): return annotation - if sys.version_info >= (3, 9): - if isinstance(annotation, GenericAlias): - return GenericAlias( - get_origin(annotation), - tuple(_resolve_annotation(a, attr) for a in get_args(annotation)), - ) - if get_origin(annotation) is Union: - return Union.__getitem__(tuple(_resolve_annotation(a, attr) for a in get_args(annotation))) - if get_origin(annotation) is list: - return List.__getitem__(tuple(_resolve_annotation(a, attr) for a in get_args(annotation))) + if isinstance(annotation, GenericAliasUnion): + return GenericAlias( + get_origin(annotation), + tuple(_resolve_annotation(a, attr) for a in get_args(annotation)), + ) return annotation @@ -74,7 +73,7 @@ def _alter_attrs(attrs: Dict[str, object], name: str, attr: str): if attr == PATCH_REQUEST_ATTR: if get_origin(annotations[key]) is Annotated: args = get_args(annotations[key]) - annotations[key] = Annotated.__class_getitem__(tuple([Optional[args[0]], *args[1:]])) + annotations[key] = Annotated.__class_getitem__((Optional[args[0]], *args[1:])) elif isinstance(annotations[key], str): annotations[key] = f"Optional[{annotations[key]}]" else: @@ -84,11 +83,11 @@ def _alter_attrs(attrs: Dict[str, object], name: str, attr: str): def _lazily_initalize_models(request_cls: type, own_attr_name: str, constructor: Callable[[], Any]): - def constructor_wrapper(*a, **kw) -> object: + def constructor_wrapper(*_: Any, **__: Any) -> object: obj = constructor() obj.__request__ = request_cls - obj.__response__ = cached_classproperty(lambda cls: request_cls.__response__, RESPONSE_ATTR) - obj.__patch_request__ = cached_classproperty(lambda cls: request_cls.__patch_request__, PATCH_REQUEST_ATTR) + obj.__response__ = cached_classproperty(lambda _: request_cls.__response__, RESPONSE_ATTR) + obj.__patch_request__ = cached_classproperty(lambda _: request_cls.__patch_request__, PATCH_REQUEST_ATTR) return obj return cached_classproperty(constructor_wrapper, own_attr_name) @@ -109,12 +108,13 @@ def __new__( request_suffix: Optional[str] = None, response_suffix: Optional[str] = None, patch_request_suffix: Optional[str] = None, - **kwargs, + **kwargs: Any, ) -> Self: new_class = type.__new__(cls, name, bases, attrs) if not bases or not any(isinstance(b, (ModelMetaclass, DualBaseModelMeta)) for b in bases): raise TypeError( - f"ModelDuplicatorMeta's instances must be created with a DualBaseModel base class or a BaseModel base class." + "ModelDuplicatorMeta's instances must be created with a DualBaseModel base class " + "or a BaseModel base class." ) # DualBaseModel case elif bases == (BaseModel,): @@ -126,7 +126,8 @@ def __new__( raise TypeError("The __config__ argument must be a class.") elif request_suffix is None or response_suffix is None or patch_request_suffix is None: raise TypeError( - "The first instance of DualBaseModel must pass suffixes for the request, response, and patch request models." + "The first instance of DualBaseModel must pass suffixes for the " + "request, response, and patch request models." ) new_class._generate_base_alternative_classes(request_suffix, response_suffix, kwargs) else: @@ -145,7 +146,7 @@ def __new__( return new_class - def _generate_base_alternative_classes(self, request_suffix, response_suffix, kwargs): + def _generate_base_alternative_classes(self, request_suffix: str, response_suffix: str, kwargs: Dict[str, Any]): class Config(kwargs["__config__"]): # type: ignore extra = Extra.forbid @@ -162,7 +163,14 @@ class Config(kwargs["__config__"]): BaseRequest.__patch_request__ = BaseRequest # type: ignore def _generate_alternative_classes( - self, name, bases, attrs, request_suffix, response_suffix, patch_request_suffix, kwargs + self, + name: str, + bases: Tuple[type, ...], + attrs: Dict[str, object], + request_suffix: str, + response_suffix: str, + patch_request_suffix: str, + kwargs: Dict[str, object], ): request_bases = tuple(_resolve_annotation(b, REQUEST_ATTR) for b in bases) request_class = ModelMetaclass( diff --git a/pyproject.toml b/pyproject.toml index 293de49..5dcc1c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,11 +7,13 @@ readme = "README.md" authors = ["Stanislav Zmiev "] license = "MIT" +[tool.coverage.run] +data_file = "coverage/coverage" + [tool.coverage.report] fail_under = 100 skip_covered = true skip_empty = true -omit = ["tests/*"] # Taken from https://coverage.readthedocs.io/en/7.1.0/excluding.html#advanced-exclusion exclude_lines = [ "pragma: no cover", @@ -38,29 +40,9 @@ pydantic = "^1.9.2" cached-classproperty = ">=0.1.0" [tool.poetry.dev-dependencies] -pyupgrade = "*" -black = "*" -autoflake = "*" +ruff = "*" pytest = ">=7.2.1" pytest-cov = ">=4.0.0" -isort = "*" - - -[tool.isort] -profile = "black" -multi_line_output = 3 -skip_glob = ['.venv/*', "_compat/*"] - -[tool.black] -line-length = 120 -target-version = ["py310"] - -[tool.bandit] -exclude_dirs = ["/venv", "/tests", "/_compat"] -skips = ["B104"] - -[tool.deptry] -extend_exclude = ["settings/config.py"] [build-system] diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..5b2c057 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,106 @@ +target-version = "py38" +line-length = 120 +extend-exclude = ["scripts/*.py"] + +[format] +quote-style = "double" +indent-style = "space" + +[lint] +select = [ + "F", # pyflakes + "E", # pycodestyle errors + "W", # pycodestyle warnings + "C90", # mccabe + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "YTT", # flake8-2020 + "S", # flake8-bandit + "BLE", # flake8-blind-except + "FBT003", # flake8-boolean-trap + "B", # flake8-bugbear + "COM", # flake8-commas + "C4", # flake8-comprehensions + "T10", # flake8-debugger + "ISC", # flake8-implicit-str-concat + "G010", # Logging statement uses warn instead of warning + "G201", # Logging .exception(...) should be used instead of .error(..., exc_info=True) + "G202", # Logging statement has redundant exc_info + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "PYI", # flake8-pyi + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SIM", # flake8-simplify + "TCH", # flake8-type-checking + "ARG", # flake8-unused-arguments + "PTH", # flake8-use-pathlib + "ERA", # flake8-eradicate + "LOG", # flake8-logging + "G", # flake8-logging-format + "PGH", # pygrep-hooks + "PLC0414", # Import alias does not rename original package + "PLE", # Error + "PLW", # Warning + "TRY", # tryceratops + "FLY", # flynt + "RUF", # ruff-specific rules + "ANN001", # missing type annotation for arguments + "ANN002", # missing type annotation for *args + "ANN003", # missing type annotation for **kwargs +] +unfixable = [ + "ERA001", # eradicate: found commented out code (can be dangerous if fixed automatically) +] +ignore = [ + "D203", # 1 blank line required before class docstring + "ARG001", # Unused first argument + "ARG002", # Unused method argument + "TRY003", # Avoid specifying long messages outside the exception class + "TRY300", # Consider moving statement into the else clause + "PT019", # Fixture without value is injected as parameter, use @pytest.mark.usefixtures instead + # (usefixtures doesn't play well with IDE features such as auto-renaming) + "SIM108", # Use ternary operator instead of if-else block (ternaries lie to coverage) + "RET505", # Unnecessary `else` after `return` statement + "N805", # First argument of a method should be named `self` (pydantic validators don't play well with this) + "UP007", # Use `X | Y` for type annotations (we need this for testing and our runtime logic) + + # The following rules are recommended to be ignored by ruff when using ruff format + "ISC001", # Checks for implicitly concatenated strings on a single line + "ISC002", # Checks for implicitly concatenated strings that span multiple lines + "W191", # Checks for indentation that uses tabs + "E111", # Checks for indentation with a non-multiple of 4 spaces + "E114", # Checks for indentation of comments with a non-multiple of 4 spaces + "E117", # Checks for over-indented code + "D206", # Checks for docstrings that are indented with tabs + "D300", # Checks for docstrings that use '''single quotes''' instead of """double quotes""" + "Q000", # Checks for inline strings that use single quotes or double quotes + "Q001", # Checks for multiline strings that use single quotes or double quotes + "Q002", # Checks for docstrings that use single quotes or double quotes + "Q003", # Checks for strings that include escaped quotes + "COM812", # Checks for the absence of trailing commas + "COM819", # Checks for the presence of prohibited trailing commas + "RET506", # Unnecessary `elif` after `raise` statement +] + + +[lint.per-file-ignores] +"tests/*" = [ + "S", # ignore bandit security issues in tests + "B018", # ignore useless expressions in tests + "PT012", # ignore complex with pytest.raises clauses + "RUF012", # ignore mutable class attributes ClassVar typehint requirement + "ANN001", # ignore missing type annotation for function argument + "ANN002", # ignore missing type annotation for *args + "ANN003", # ignore missing type annotation for **kwargs + "PGH003", # ignore using non-specific rule codes when ignoring type issues + "B008", # ignore performing function calls in argument defaults +] + + +[lint.mccabe] +max-complexity = 13 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index 3afbf04..0a410de 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import sys from typing import List, Optional + import pytest from pydantic_duality import DualBaseModel, generate_dual_base_model @@ -28,6 +29,7 @@ class H(Base): h: str if sys.version_info >= (3, 10): + class B(Base): my: str old: list[C] @@ -36,7 +38,9 @@ class B(Base): class A(Base): hello: str darkness: B + else: + class B(Base): my: str old: List[C] diff --git a/tests/test_duality.py b/tests/test_duality.py index 5d3a2c2..a98bfb8 100644 --- a/tests/test_duality.py +++ b/tests/test_duality.py @@ -1,36 +1,16 @@ import abc import sys -from typing import Literal, Union +from typing import Any, Literal, Union import pytest from pydantic import BaseModel, Extra, Field, ValidationError - -from pydantic_duality import DualBaseModel, DualBaseModelMeta, _resolve_annotation from typing_extensions import Annotated -def test_new(schemas): - assert schemas["H"](h="h").h == "h" - +from pydantic_duality import DualBaseModel, DualBaseModelMeta -def test_union(schemas): - class UnionSchema(schemas["Base"]): - model: Union[schemas["G"], schemas["H"]] - g = UnionSchema(model=dict(g="g")).model - h = UnionSchema.__request__(model=dict(h="h")).model - assert g.g == "g" - assert h.h == "h" - - -if sys.version_info >= (3, 10): - def test_union_operator(schemas): - class UnionSchema(schemas["Base"]): - model: schemas["G"] | schemas["H"] - - g = UnionSchema(model=dict(g="g")).model - h = UnionSchema.__request__(model=dict(h="h")).model - assert g.g == "g" - assert h.h == "h" +def test_new(schemas): + assert schemas["H"](h="h").h == "h" def test_base_model(): @@ -98,8 +78,7 @@ class Schema( request_suffix="Request", response_suffix="Response", patch_request_suffix="PatchRequest", - ): - ... + ): ... @pytest.mark.parametrize("config", ["123", {"extra": "forbid"}, None]) @@ -116,8 +95,7 @@ class Schema( request_suffix="Request", response_suffix="Response", patch_request_suffix="PatchRequest", - ): - ... + ): ... def test_issubclass_basemodel(schemas): @@ -184,21 +162,6 @@ class Schema(DualBaseModel): assert Schema.__request__.__name__ == "Hewwo" -def test_resolving(schemas): - _resolve_annotation( - Annotated[Union[schemas["A"], schemas["B"]], Field(discriminator="object_type")], - "__request__", - ) - - -if sys.version_info >= (3, 10): - def test_resolving_union_operator(schemas): - _resolve_annotation( - Annotated[schemas["A"] | schemas["B"], Field(discriminator="object_type")], - "__request__", - ) - - def test_model_creation(schemas): schemas["A"].__response__.parse_obj( { diff --git a/tests/test_naming.py b/tests/test_naming.py index b6c4c83..f3df8c3 100644 --- a/tests/test_naming.py +++ b/tests/test_naming.py @@ -1,4 +1,5 @@ from typing import Dict + import pytest from pydantic import BaseModel @@ -101,7 +102,10 @@ def test_lack_of_suffix_for_base_class(overrides: Dict[str, str]): return with pytest.raises( TypeError, - match="The first instance of DualBaseModel must pass suffixes for the request, response, and patch request models.", + match=( + "The first instance of DualBaseModel must pass suffixes for the " + "request, response, and patch request models." + ), ): class Schema(BaseModel, metaclass=DualBaseModelMeta, __config__=DualBaseModel.__config__, **overrides):