From e198ef55f40b62f1518233a8248e40f7346268c8 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Fri, 20 Feb 2026 11:00:03 +0000 Subject: [PATCH 1/2] Python 3.9 drop --- .github/workflows/python-test.yml | 2 +- .pre-commit-config.yaml | 2 +- poetry.lock | 137 ++---------------------------- pyproject.toml | 3 +- tox.ini | 2 +- 5 files changed, 9 insertions(+), 137 deletions(-) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 24a03c03..7f9bb3da 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13'] os: [windows-latest, ubuntu-latest] fail-fast: false steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1d43d39f..7fdde7e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: rev: v3.21.2 hooks: - id: pyupgrade - args: ["--py39-plus"] + args: ["--py310-plus"] - repo: https://github.com/psf/black-pre-commit-mirror rev: 25.12.0 diff --git a/poetry.lock b/poetry.lock index 05534ad4..f054289b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -71,59 +71,6 @@ files = [ [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] -[[package]] -name = "black" -version = "25.11.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"}, - {file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"}, - {file = "black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37"}, - {file = "black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03"}, - {file = "black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a"}, - {file = "black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170"}, - {file = "black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc"}, - {file = "black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e"}, - {file = "black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac"}, - {file = "black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96"}, - {file = "black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd"}, - {file = "black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409"}, - {file = "black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b"}, - {file = "black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd"}, - {file = "black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993"}, - {file = "black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c"}, - {file = "black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170"}, - {file = "black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545"}, - {file = "black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda"}, - {file = "black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664"}, - {file = "black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06"}, - {file = "black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2"}, - {file = "black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc"}, - {file = "black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc"}, - {file = "black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b"}, - {file = "black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -pytokens = ">=0.3.0" -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.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "black" version = "25.12.0" @@ -131,7 +78,6 @@ description = "The uncompromising code formatter." optional = false python-versions = ">=3.10" groups = ["dev"] -markers = "python_version >= \"3.10\"" files = [ {file = "black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8"}, {file = "black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a"}, @@ -531,7 +477,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version < \"3.11\"" +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -543,24 +489,6 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "filelock" -version = "3.18.0" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version == \"3.9\"" -files = [ - {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, - {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] -typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] - [[package]] name = "filelock" version = "3.20.3" @@ -568,7 +496,6 @@ description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev"] -markers = "python_version >= \"3.10\"" files = [ {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, @@ -651,31 +578,6 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev", "docs"] -markers = "python_version == \"3.9\"" -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.1.0" @@ -700,9 +602,6 @@ files = [ {file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"}, ] -[package.dependencies] -importlib-metadata = {version = ">=4.6.0", markers = "python_version < \"3.10\""} - [package.extras] colors = ["colorama"] plugins = ["setuptools"] @@ -763,7 +662,6 @@ files = [ pathable = ">=0.5.0b6,<0.6.0" PyYAML = ">=5.1" referencing = "<0.37.0" -typing-extensions = {version = ">=4.10.0,<5.0.0", markers = "python_version < \"3.10\""} [package.extras] requests = ["requests (>=2.31.0,<3.0.0)"] @@ -1880,7 +1778,6 @@ babel = ">=2.13" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} docutils = ">=0.20,<0.22" imagesize = ">=1.3" -importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} Jinja2 = ">=3.1" packaging = ">=23.0" Pygments = ">=2.17" @@ -2102,7 +1999,7 @@ files = [ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] -markers = {dev = "python_full_version <= \"3.11.0a6\"", docs = "python_version < \"3.11\""} +markers = {dev = "python_full_version <= \"3.11.0a6\"", docs = "python_version == \"3.10\""} [[package]] name = "tomlkit" @@ -2235,10 +2132,7 @@ files = [ [package.dependencies] distlib = ">=0.3.7,<1" -filelock = [ - {version = ">=3.16.1,<4", markers = "python_version < \"3.10\""}, - {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""}, -] +filelock = {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} @@ -2246,28 +2140,7 @@ typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\"" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["dev", "docs"] -markers = "python_version == \"3.9\"" -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [metadata] lock-version = "2.1" -python-versions = ">=3.9,<4.0" -content-hash = "e286dd0d4f0dc6d0eb7c19ae46c8ca644848d0287667a616ca1ced3469e9e6f4" +python-versions = ">=3.10,<4.0" +content-hash = "90aad202f109e36fa4dd23a71dd1ac23341c11a4c6aafb9632df48241841e9e4" diff --git a/pyproject.toml b/pyproject.toml index b99bbcb5..bf80eefa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ authors = [ ] license = "Apache-2.0" readme = "README.rst" -requires-python = ">=3.9,<4.0" +requires-python = ">=3.10,<4.0" keywords = ["openapi", "swagger", "schema"] classifiers = [ "Development Status :: 4 - Beta", @@ -15,7 +15,6 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/tox.ini b/tox.ini index af594b15..ea2f0c70 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{3.9,3.10,3.11,3.12,3.13} + py{3.10,3.11,3.12,3.13} [testenv] skip_install = true From 0c323a2561465e37db24f2d39e5d4e8ffeaa88b9 Mon Sep 17 00:00:00 2001 From: p1c2u Date: Fri, 20 Feb 2026 11:07:11 +0000 Subject: [PATCH 2/2] Python 3.1-+ modernization --- openapi_spec_validator/__main__.py | 5 +- openapi_spec_validator/schemas/types.py | 4 +- openapi_spec_validator/shortcuts.py | 15 +++--- .../validation/decorators.py | 24 ++++----- openapi_spec_validator/validation/keywords.py | 10 ++-- .../validation/protocols.py | 5 +- openapi_spec_validator/validation/proxies.py | 14 +++-- .../validation/registries.py | 6 +-- .../validation/validators.py | 3 +- tests/bench/runner.py | 54 +++++++++++-------- 10 files changed, 71 insertions(+), 69 deletions(-) diff --git a/openapi_spec_validator/__main__.py b/openapi_spec_validator/__main__.py index aed7114a..96fefe63 100644 --- a/openapi_spec_validator/__main__.py +++ b/openapi_spec_validator/__main__.py @@ -2,7 +2,6 @@ import sys from argparse import ArgumentParser from collections.abc import Sequence -from typing import Optional from jsonschema.exceptions import ValidationError from jsonschema.exceptions import best_match @@ -51,7 +50,7 @@ def print_validationerror( ) -def main(args: Optional[Sequence[str]] = None) -> None: +def main(args: Sequence[str] | None = None) -> None: parser = ArgumentParser() parser.add_argument( "file", @@ -78,7 +77,7 @@ def main(args: Optional[Sequence[str]] = None) -> None: for filename in args_parsed.file: # choose source reader = read_from_filename - if filename in ["-", "/-"]: + if filename in {"-", "/-"}: filename = "stdin" reader = read_from_stdin diff --git a/openapi_spec_validator/schemas/types.py b/openapi_spec_validator/schemas/types.py index d48c5163..8217cb5f 100644 --- a/openapi_spec_validator/schemas/types.py +++ b/openapi_spec_validator/schemas/types.py @@ -1,6 +1,4 @@ -from typing import Union - from jsonschema_path.paths import SchemaPath from jsonschema_path.typing import Schema -AnySchema = Union[Schema, SchemaPath] +AnySchema = Schema | SchemaPath diff --git a/openapi_spec_validator/shortcuts.py b/openapi_spec_validator/shortcuts.py index a1e2e574..884d0792 100644 --- a/openapi_spec_validator/shortcuts.py +++ b/openapi_spec_validator/shortcuts.py @@ -2,7 +2,6 @@ import warnings from collections.abc import Mapping -from typing import Optional from jsonschema_path import SchemaPath from jsonschema_path.handlers import all_urls_handler @@ -39,7 +38,7 @@ def get_validator_cls(spec: Schema) -> SpecValidatorType: def validate( spec: Schema, base_uri: str = "", - cls: Optional[SpecValidatorType] = None, + cls: SpecValidatorType | None = None, ) -> None: if cls is None: cls = get_validator_cls(spec) @@ -50,7 +49,7 @@ def validate( def validate_url( spec_url: str, - cls: Optional[type[SpecValidator]] = None, + cls: type[SpecValidator] | None = None, ) -> None: spec = all_urls_handler(spec_url) return validate(spec, base_uri=spec_url, cls=cls) @@ -59,9 +58,9 @@ def validate_url( def validate_spec( spec: Schema, base_uri: str = "", - validator: Optional[SupportsValidation] = None, - cls: Optional[SpecValidatorType] = None, - spec_url: Optional[str] = None, + validator: SupportsValidation | None = None, + cls: SpecValidatorType | None = None, + spec_url: str | None = None, ) -> None: warnings.warn( "validate_spec shortcut is deprecated. Use validate instead.", @@ -81,8 +80,8 @@ def validate_spec( def validate_spec_url( spec_url: str, - validator: Optional[SupportsValidation] = None, - cls: Optional[type[SpecValidator]] = None, + validator: SupportsValidation | None = None, + cls: type[SpecValidator] | None = None, ) -> None: warnings.warn( "validate_spec_url shortcut is deprecated. Use validate_url instead.", diff --git a/openapi_spec_validator/validation/decorators.py b/openapi_spec_validator/validation/decorators.py index 3fa49860..394cb478 100644 --- a/openapi_spec_validator/validation/decorators.py +++ b/openapi_spec_validator/validation/decorators.py @@ -1,11 +1,11 @@ """OpenAPI spec validator validation decorators module.""" import logging +from collections.abc import Callable from collections.abc import Iterable from collections.abc import Iterator from functools import wraps -from typing import Any -from typing import Callable +from typing import ParamSpec from typing import TypeVar from jsonschema.exceptions import ValidationError @@ -13,17 +13,17 @@ from openapi_spec_validator.validation.caches import CachedIterable from openapi_spec_validator.validation.exceptions import OpenAPIValidationError -Args = TypeVar("Args") +P = ParamSpec("P") T = TypeVar("T") log = logging.getLogger(__name__) def wraps_errors( - func: Callable[..., Any], -) -> Callable[..., Iterator[ValidationError]]: + func: Callable[P, Iterator[ValidationError]], +) -> Callable[P, Iterator[ValidationError]]: @wraps(func) - def wrapper(*args: Any, **kwds: Any) -> Iterator[ValidationError]: + def wrapper(*args: P.args, **kwds: P.kwargs) -> Iterator[ValidationError]: errors = func(*args, **kwds) for err in errors: if not isinstance(err, OpenAPIValidationError): @@ -36,10 +36,10 @@ def wrapper(*args: Any, **kwds: Any) -> Iterator[ValidationError]: def wraps_cached_iter( - func: Callable[[Args], Iterator[T]], -) -> Callable[[Args], CachedIterable[T]]: + func: Callable[P, Iterator[T]], +) -> Callable[P, CachedIterable[T]]: @wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> CachedIterable[T]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> CachedIterable[T]: result = func(*args, **kwargs) return CachedIterable(result) @@ -47,10 +47,10 @@ def wrapper(*args: Any, **kwargs: Any) -> CachedIterable[T]: def unwraps_iter( - func: Callable[[Args], Iterable[T]], -) -> Callable[[Args], Iterator[T]]: + func: Callable[P, Iterable[T]], +) -> Callable[P, Iterator[T]]: @wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> Iterator[T]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> Iterator[T]: result = func(*args, **kwargs) return iter(result) diff --git a/openapi_spec_validator/validation/keywords.py b/openapi_spec_validator/validation/keywords.py index 09dbda8a..2d1561e7 100644 --- a/openapi_spec_validator/validation/keywords.py +++ b/openapi_spec_validator/validation/keywords.py @@ -1,9 +1,9 @@ import string from collections.abc import Iterator +from collections.abc import Callable from collections.abc import Sequence from typing import TYPE_CHECKING from typing import Any -from typing import Optional from typing import cast from jsonschema._format import FormatChecker @@ -38,7 +38,7 @@ def __init__(self, registry: "KeywordValidatorRegistry"): class ValueValidator(KeywordValidator): - value_validator_cls: Validator = NotImplemented + value_validator_cls: Callable[..., Validator] = NotImplemented value_validator_format_checker: FormatChecker = NotImplemented def __call__( @@ -67,7 +67,7 @@ class SchemaValidator(KeywordValidator): def __init__(self, registry: "KeywordValidatorRegistry"): super().__init__(registry) - self.schema_ids_registry: Optional[list[int]] = [] + self.schema_ids_registry: list[int] | None = [] @property def default_validator(self) -> ValueValidator: @@ -309,7 +309,7 @@ class OperationValidator(KeywordValidator): def __init__(self, registry: "KeywordValidatorRegistry"): super().__init__(registry) - self.operation_ids_registry: Optional[list[str]] = [] + self.operation_ids_registry: list[str] | None = [] @property def responses_validator(self) -> ResponsesValidator: @@ -324,7 +324,7 @@ def __call__( url: str, name: str, operation: SchemaPath, - path_parameters: Optional[SchemaPath], + path_parameters: SchemaPath | None, ) -> Iterator[ValidationError]: assert self.operation_ids_registry is not None diff --git a/openapi_spec_validator/validation/protocols.py b/openapi_spec_validator/validation/protocols.py index 1dd8b006..bd95f2ad 100644 --- a/openapi_spec_validator/validation/protocols.py +++ b/openapi_spec_validator/validation/protocols.py @@ -1,5 +1,4 @@ from collections.abc import Iterator -from typing import Optional from typing import Protocol from typing import runtime_checkable @@ -16,12 +15,12 @@ def iter_errors( self, instance: Schema, base_uri: str = "", - spec_url: Optional[str] = None, + spec_url: str | None = None, ) -> Iterator[OpenAPIValidationError]: ... def validate( self, instance: Schema, base_uri: str = "", - spec_url: Optional[str] = None, + spec_url: str | None = None, ) -> None: ... diff --git a/openapi_spec_validator/validation/proxies.py b/openapi_spec_validator/validation/proxies.py index 81250be8..cd53f789 100644 --- a/openapi_spec_validator/validation/proxies.py +++ b/openapi_spec_validator/validation/proxies.py @@ -3,12 +3,10 @@ import warnings from collections.abc import Iterator from collections.abc import Mapping -from typing import Optional from jsonschema.exceptions import ValidationError from jsonschema_path.typing import Schema -from openapi_spec_validator.validation.exceptions import OpenAPIValidationError from openapi_spec_validator.validation.exceptions import ValidatorDetectError from openapi_spec_validator.validation.types import SpecValidatorType @@ -18,7 +16,7 @@ def __init__( self, cls: SpecValidatorType, deprecated: str = "SpecValidator", - use: Optional[str] = None, + use: str | None = None, ): self.cls = cls @@ -29,7 +27,7 @@ def validate( self, schema: Schema, base_uri: str = "", - spec_url: Optional[str] = None, + spec_url: str | None = None, ) -> None: for err in self.iter_errors( schema, @@ -46,7 +44,7 @@ def iter_errors( self, schema: Schema, base_uri: str = "", - spec_url: Optional[str] = None, + spec_url: str | None = None, ) -> Iterator[ValidationError]: warnings.warn( f"{self.deprecated} is deprecated. Use {self.use} instead.", @@ -70,7 +68,7 @@ def validate( self, instance: Schema, base_uri: str = "", - spec_url: Optional[str] = None, + spec_url: str | None = None, ) -> None: validator = self.detect(instance) for err in validator.iter_errors( @@ -87,8 +85,8 @@ def iter_errors( self, instance: Schema, base_uri: str = "", - spec_url: Optional[str] = None, - ) -> Iterator[OpenAPIValidationError]: + spec_url: str | None = None, + ) -> Iterator[ValidationError]: warnings.warn( "openapi_spec_validator_proxy is deprecated.", DeprecationWarning, diff --git a/openapi_spec_validator/validation/registries.py b/openapi_spec_validator/validation/registries.py index 85bcf4e7..988df376 100644 --- a/openapi_spec_validator/validation/registries.py +++ b/openapi_spec_validator/validation/registries.py @@ -1,12 +1,10 @@ -from __future__ import annotations - +from collections import defaultdict from collections.abc import Mapping -from typing import DefaultDict from openapi_spec_validator.validation.keywords import KeywordValidator -class KeywordValidatorRegistry(DefaultDict[str, KeywordValidator]): +class KeywordValidatorRegistry(defaultdict[str, KeywordValidator]): def __init__( self, keyword_validators: Mapping[str, type[KeywordValidator]] ): diff --git a/openapi_spec_validator/validation/validators.py b/openapi_spec_validator/validation/validators.py index 5e5703bb..3487d2a1 100644 --- a/openapi_spec_validator/validation/validators.py +++ b/openapi_spec_validator/validation/validators.py @@ -5,7 +5,6 @@ from collections.abc import Iterator from collections.abc import Mapping from functools import lru_cache -from typing import Optional from typing import cast from jsonschema.exceptions import ValidationError @@ -40,7 +39,7 @@ def __init__( self, schema: AnySchema, base_uri: str = "", - spec_url: Optional[str] = None, + spec_url: str | None = None, ) -> None: if spec_url is not None: warnings.warn( diff --git a/tests/bench/runner.py b/tests/bench/runner.py index 26a9dfe7..d93f3756 100644 --- a/tests/bench/runner.py +++ b/tests/bench/runner.py @@ -18,13 +18,15 @@ from dataclasses import dataclass from functools import cached_property from pathlib import Path -from typing import Any, Dict, Iterator, List, Optional +from typing import Any +from collections.abc import Iterator from jsonschema_path import SchemaPath +from jsonschema_path.typing import Schema from openapi_spec_validator import validate from openapi_spec_validator.readers import read_from_filename -from openapi_spec_validator.schemas import _FORCE_PYTHON, _FORCE_RUST +from openapi_spec_validator import schemas from openapi_spec_validator.shortcuts import get_validator_cls @@ -37,35 +39,35 @@ class BenchResult: schemas_count: int repeats: int warmup: int - seconds: List[float] + seconds: list[float] success: bool - error: Optional[str] = None + error: str | None = None @cached_property - def median_s(self) -> Optional[float]: + def median_s(self) -> float | None: if self.seconds: return statistics.median(self.seconds) return None @cached_property - def mean_s(self) -> Optional[float]: + def mean_s(self) -> float | None: if self.seconds: return statistics.mean(self.seconds) return None @cached_property - def stdev_s(self) -> Optional[float]: + def stdev_s(self) -> float | None: if len(self.seconds) > 1: return statistics.pstdev(self.seconds) return None @cached_property - def validations_per_sec(self) -> Optional[float]: + def validations_per_sec(self) -> float | None: if self.median_s: return 1 / self.median_s return None - def as_dict(self) -> Dict[str, Any]: + def as_dict(self) -> dict[str, Any]: return { "spec_name": self.spec_name, "spec_version": self.spec_version, @@ -84,19 +86,19 @@ def as_dict(self) -> Dict[str, Any]: } -def count_paths(spec: dict) -> int: +def count_paths(spec: Schema) -> int: """Count paths in OpenAPI spec.""" return len(spec.get("paths", {})) -def count_schemas(spec: dict) -> int: +def count_schemas(spec: Schema) -> int: """Count schemas in OpenAPI spec.""" components = spec.get("components", {}) definitions = spec.get("definitions", {}) # OpenAPI 2.0 return len(components.get("schemas", {})) + len(definitions) -def get_spec_version(spec: dict) -> str: +def get_spec_version(spec: Schema) -> str: """Detect OpenAPI version.""" if "openapi" in spec: return spec["openapi"] @@ -105,7 +107,7 @@ def get_spec_version(spec: dict) -> str: return "unknown" -def run_once(spec: dict) -> float: +def run_once(spec: Schema) -> float: """Run validation once and return elapsed time.""" t0 = time.perf_counter() cls = get_validator_cls(spec) @@ -133,7 +135,7 @@ def benchmark_spec_file( def benchmark_spec( - spec: dict, + spec: Schema, repeats: int = 7, warmup: int = 2, no_gc: bool = False, @@ -155,17 +157,19 @@ def benchmark_spec( for _ in range(warmup): run_once(spec) + pr: cProfile.Profile | None = None if profile: print("\n🔬 Profiling mode enabled...") pr = cProfile.Profile() pr.enable() # Actual benchmark - seconds: List[float] = [] + seconds: list[float] = [] for _ in range(repeats): seconds.append(run_once(spec)) if profile: + assert pr is not None pr.disable() # Print profile stats @@ -209,7 +213,11 @@ def benchmark_spec( ) -def generate_synthetic_spec(paths: int, schemas: int, version: str = "3.0.0") -> dict: +def generate_synthetic_spec( + paths: int, + schemas: int, + version: str = "3.0.0", +) -> dict[str, Any]: """Generate synthetic OpenAPI spec for stress testing.""" paths_obj = {} for i in range(paths): @@ -247,14 +255,18 @@ def generate_synthetic_spec(paths: int, schemas: int, version: str = "3.0.0") -> } -def get_synthetic_specs_iterator(configs: List[tuple[int, int, str]]) -> Iterator[dict]: +def get_synthetic_specs_iterator( + configs: list[tuple[int, int, str]], +) -> Iterator[tuple[dict[str, Any], str, float]]: """Iterator over synthetic specs based on provided configurations.""" for paths, schemas, size in configs: spec = generate_synthetic_spec(paths, schemas) yield spec, f"synthetic_{size}", 0 -def get_specs_iterator(spec_files: List[Path]) -> Iterator[dict]: +def get_specs_iterator( + spec_files: list[Path], +) -> Iterator[tuple[Schema, str, float]]: """Iterator over provided spec files.""" for spec_file in spec_files: spec, _ = read_from_filename(str(spec_file)) @@ -271,11 +283,11 @@ def main(): parser.add_argument("--profile", type=str, help="Profile file path (cProfile)") args = parser.parse_args() - results: List[Dict[str, Any]] = [] + results: list[dict[str, Any]] = [] print("Spec schema validator backend selection:") - print(f" Force Python: {_FORCE_PYTHON}") - print(f" Force Rust: {_FORCE_RUST}") + print(f" Force Python: {getattr(schemas, '_FORCE_PYTHON', False)}") + print(f" Force Rust: {getattr(schemas, '_FORCE_RUST', False)}") # Benchmark custom specs if args.specs: