diff --git a/.github/workflows/build-docs.yml b/.github/workflows/docs-build.yml similarity index 94% rename from .github/workflows/build-docs.yml rename to .github/workflows/docs-build.yml index 8f381ee..9573844 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/docs-build.yml @@ -1,4 +1,4 @@ -name: Build documentation +name: CI / Docs on: push: @@ -6,7 +6,8 @@ on: types: [opened, synchronize] jobs: - build: + docs_build: + name: "Build" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -14,7 +15,7 @@ jobs: - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: "3.12" - name: Get full Python version id: full-python-version diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-tests.yml similarity index 94% rename from .github/workflows/python-test.yml rename to .github/workflows/python-tests.yml index 521fc86..97469d1 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-tests.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: Test python code +name: CI / Tests on: push: @@ -9,12 +9,12 @@ on: types: [opened, synchronize] jobs: - test: - name: "Tests" + tests: + name: "py${{ matrix.python-version }}" runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13'] fail-fast: false steps: - uses: actions/checkout@v4 @@ -62,7 +62,7 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v5 - static-checks: + static_checks: name: "Static checks" runs-on: ubuntu-latest steps: @@ -72,7 +72,7 @@ jobs: - name: "Setup Python" uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.10" - name: Get full Python version id: full-python-version diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86aa80a..409fe45 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ --- -default_stages: [commit, push] +default_stages: [pre-commit, pre-push] default_language_version: # force all unspecified python hooks to run python3 python: python3 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 59848f9..d17ec2a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,7 +8,7 @@ formats: all build: os: ubuntu-20.04 tools: - python: "3.9" + python: "3.10" jobs: post_create_environment: - pip install poetry diff --git a/.travis.yml b/.travis.yml index fe09d75..3415e1a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: python sudo: false matrix: include: - - python: 3.9 - python: 3.10 - python: 3.11 - python: 3.12 diff --git a/openapi_schema_validator/_format.py b/openapi_schema_validator/_format.py index 373359b..b5f6297 100644 --- a/openapi_schema_validator/_format.py +++ b/openapi_schema_validator/_format.py @@ -2,13 +2,11 @@ from base64 import b64decode from base64 import b64encode from numbers import Number -from typing import Any -from typing import Union from jsonschema._format import FormatChecker -def is_int32(instance: Any) -> bool: +def is_int32(instance: object) -> bool: # bool inherits from int, so ensure bools aren't reported as ints if isinstance(instance, bool): return True @@ -17,7 +15,7 @@ def is_int32(instance: Any) -> bool: return ~(1 << 31) < instance < 1 << 31 -def is_int64(instance: Any) -> bool: +def is_int64(instance: object) -> bool: # bool inherits from int, so ensure bools aren't reported as ints if isinstance(instance, bool): return True @@ -26,7 +24,7 @@ def is_int64(instance: Any) -> bool: return ~(1 << 63) < instance < 1 << 63 -def is_float(instance: Any) -> bool: +def is_float(instance: object) -> bool: # bool inherits from int if isinstance(instance, int): return True @@ -35,7 +33,7 @@ def is_float(instance: Any) -> bool: return isinstance(instance, float) -def is_double(instance: Any) -> bool: +def is_double(instance: object) -> bool: # bool inherits from int if isinstance(instance, int): return True @@ -46,7 +44,7 @@ def is_double(instance: Any) -> bool: return isinstance(instance, float) -def is_binary(instance: Any) -> bool: +def is_binary(instance: object) -> bool: if not isinstance(instance, (str, bytes)): return True if isinstance(instance, str): @@ -54,7 +52,7 @@ def is_binary(instance: Any) -> bool: return True -def is_byte(instance: Union[str, bytes]) -> bool: +def is_byte(instance: object) -> bool: if not isinstance(instance, (str, bytes)): return True if isinstance(instance, str): @@ -64,7 +62,7 @@ def is_byte(instance: Union[str, bytes]) -> bool: return encoded == instance -def is_password(instance: Any) -> bool: +def is_password(instance: object) -> bool: # A hint to UIs to obscure input return True diff --git a/openapi_schema_validator/_keywords.py b/openapi_schema_validator/_keywords.py index 6692580..8396418 100644 --- a/openapi_schema_validator/_keywords.py +++ b/openapi_schema_validator/_keywords.py @@ -1,12 +1,7 @@ -from copy import deepcopy from typing import Any -from typing import Dict -from typing import Hashable -from typing import ItemsView from typing import Iterator -from typing import List from typing import Mapping -from typing import Union +from typing import cast from jsonschema._keywords import allOf as _allOf from jsonschema._keywords import anyOf as _anyOf @@ -15,11 +10,10 @@ from jsonschema._utils import find_additional_properties from jsonschema.exceptions import FormatError from jsonschema.exceptions import ValidationError -from jsonschema.protocols import Validator def handle_discriminator( - validator: Validator, _: Any, instance: Any, schema: Mapping[Hashable, Any] + validator: Any, _: Any, instance: Any, schema: Mapping[str, Any] ) -> Iterator[ValidationError]: """ Handle presence of discriminator in anyOf, oneOf and allOf. @@ -45,16 +39,14 @@ def handle_discriminator( if not isinstance(ref, str): # this is a schema error yield ValidationError( - "{!r} mapped value for {!r} should be a string, was {!r}".format( - instance, prop_value, ref - ), + f"{instance!r} mapped value for {prop_value!r} should be a string, was {ref!r}", context=[], ) return try: validator._validate_reference(ref=ref, instance=instance) - except: + except Exception: yield ValidationError( f"{instance!r} reference {ref!r} could not be resolved", context=[], @@ -65,46 +57,55 @@ def handle_discriminator( def anyOf( - validator: Validator, - anyOf: List[Mapping[Hashable, Any]], + validator: Any, + anyOf: list[Mapping[str, Any]], instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: if "discriminator" not in schema: - yield from _anyOf(validator, anyOf, instance, schema) + yield from cast( + Iterator[ValidationError], + _anyOf(validator, anyOf, instance, schema), + ) else: yield from handle_discriminator(validator, anyOf, instance, schema) def oneOf( - validator: Validator, - oneOf: List[Mapping[Hashable, Any]], + validator: Any, + oneOf: list[Mapping[str, Any]], instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: if "discriminator" not in schema: - yield from _oneOf(validator, oneOf, instance, schema) + yield from cast( + Iterator[ValidationError], + _oneOf(validator, oneOf, instance, schema), + ) else: yield from handle_discriminator(validator, oneOf, instance, schema) def allOf( - validator: Validator, - allOf: List[Mapping[Hashable, Any]], + validator: Any, + allOf: list[Mapping[str, Any]], instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: if "discriminator" not in schema: - yield from _allOf(validator, allOf, instance, schema) + yield from cast( + Iterator[ValidationError], + _allOf(validator, allOf, instance, schema), + ) else: yield from handle_discriminator(validator, allOf, instance, schema) def type( - validator: Validator, + validator: Any, data_type: str, instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: if instance is None: # nullable implementation based on OAS 3.0.3 @@ -112,7 +113,7 @@ def type( # * nullable: true is only meaningful in combination with a type # assertion specified in the same Schema Object. # * nullable: true operates within a single Schema Object - if "nullable" in schema and schema["nullable"] == True: + if schema.get("nullable") is True: return yield ValidationError("None for not nullable") @@ -122,10 +123,10 @@ def type( def format( - validator: Validator, + validator: Any, format: str, instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: if instance is None: return @@ -138,10 +139,10 @@ def format( def items( - validator: Validator, - items: Mapping[Hashable, Any], + validator: Any, + items: Mapping[str, Any], instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: if not validator.is_type(instance, "array"): return @@ -151,10 +152,10 @@ def items( def required( - validator: Validator, - required: List[str], + validator: Any, + required: list[str], instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: if not validator.is_type(instance, "object"): return @@ -175,10 +176,10 @@ def required( def read_required( - validator: Validator, - required: List[str], + validator: Any, + required: list[str], instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: if not validator.is_type(instance, "object"): return @@ -193,10 +194,10 @@ def read_required( def write_required( - validator: Validator, - required: List[str], + validator: Any, + required: list[str], instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: if not validator.is_type(instance, "object"): return @@ -211,10 +212,10 @@ def write_required( def additionalProperties( - validator: Validator, - aP: Union[Mapping[Hashable, Any], bool], + validator: Any, + aP: Any, instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: if not validator.is_type(instance, "object"): return @@ -235,28 +236,27 @@ def additionalProperties( def write_readOnly( - validator: Validator, + validator: Any, ro: bool, instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: yield ValidationError(f"Tried to write read-only property with {instance}") def read_writeOnly( - validator: Validator, + validator: Any, wo: bool, instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: yield ValidationError(f"Tried to read write-only property with {instance}") def not_implemented( - validator: Validator, + validator: Any, value: Any, instance: Any, - schema: Mapping[Hashable, Any], + schema: Mapping[str, Any], ) -> Iterator[ValidationError]: - return - yield + yield from () diff --git a/openapi_schema_validator/_types.py b/openapi_schema_validator/_types.py index 8bb044d..83613e9 100644 --- a/openapi_schema_validator/_types.py +++ b/openapi_schema_validator/_types.py @@ -1,4 +1,5 @@ from typing import Any +from typing import cast from jsonschema._types import TypeChecker from jsonschema._types import draft202012_type_checker @@ -9,18 +10,21 @@ from jsonschema._types import is_object -def is_string(checker: TypeChecker, instance: Any) -> bool: +def is_string(checker: Any, instance: Any) -> bool: return isinstance(instance, (str, bytes)) oas30_type_checker = TypeChecker( - { - "string": is_string, - "number": is_number, - "integer": is_integer, - "boolean": is_bool, - "array": is_array, - "object": is_object, - }, + cast( + Any, + { + "string": is_string, + "number": is_number, + "integer": is_integer, + "boolean": is_bool, + "array": is_array, + "object": is_object, + }, + ), ) oas31_type_checker = draft202012_type_checker diff --git a/openapi_schema_validator/shortcuts.py b/openapi_schema_validator/shortcuts.py index d922bb8..f6e940d 100644 --- a/openapi_schema_validator/shortcuts.py +++ b/openapi_schema_validator/shortcuts.py @@ -1,7 +1,6 @@ from typing import Any -from typing import Hashable from typing import Mapping -from typing import Type +from typing import cast from jsonschema.exceptions import best_match from jsonschema.protocols import Validator @@ -11,13 +10,16 @@ def validate( instance: Any, - schema: Mapping[Hashable, Any], - cls: Type[Validator] = OAS31Validator, + schema: Mapping[str, Any], + cls: type[Validator] = OAS31Validator, *args: Any, **kwargs: Any ) -> None: - cls.check_schema(schema) - validator = cls(schema, *args, **kwargs) - error = best_match(validator.evolve(schema=schema).iter_errors(instance)) + schema_dict = cast(dict[str, Any], schema) + cls.check_schema(schema_dict) + validator = cls(schema_dict, *args, **kwargs) + error = best_match( + validator.evolve(schema=schema_dict).iter_errors(instance) + ) if error is not None: raise error diff --git a/openapi_schema_validator/validators.py b/openapi_schema_validator/validators.py index 44b5a42..00e6458 100644 --- a/openapi_schema_validator/validators.py +++ b/openapi_schema_validator/validators.py @@ -1,6 +1,5 @@ -import warnings from typing import Any -from typing import Type +from typing import cast from jsonschema import _keywords from jsonschema import _legacy_keywords @@ -14,11 +13,9 @@ from openapi_schema_validator import _types as oas_types from openapi_schema_validator._types import oas31_type_checker -OAS30Validator = create( - meta_schema=SPECIFICATIONS.contents( - "http://json-schema.org/draft-04/schema#", - ), - validators={ +OAS30_VALIDATORS = cast( + Any, + { "multipleOf": _keywords.multipleOf, # exclusiveMaximum supported inside maximum_draft3_draft4 "maximum": _legacy_keywords.maximum_draft3_draft4, @@ -56,12 +53,21 @@ "example": oas_keywords.not_implemented, "deprecated": oas_keywords.not_implemented, }, +) + +OAS30Validator = create( + meta_schema=SPECIFICATIONS.contents( + "http://json-schema.org/draft-04/schema#", + ), + validators=OAS30_VALIDATORS, type_checker=oas_types.oas30_type_checker, format_checker=oas_format.oas30_format_checker, # NOTE: version causes conflict with global jsonschema validator # See https://github.com/python-openapi/openapi-schema-validator/pull/12 # version="oas30", - id_of=lambda schema: schema.get("id", ""), + id_of=lambda schema: ( + schema.get("id", "") if isinstance(schema, dict) else "" + ), ) OAS30ReadValidator = extend( diff --git a/poetry.lock b/poetry.lock index c2c4bdb..bdb0da5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -548,31 +548,6 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -[[package]] -name = "importlib-metadata" -version = "8.5.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -markers = "python_version < \"3.10\"" -files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, -] - -[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.0.0" @@ -606,7 +581,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" -groups = ["dev", "docs"] +groups = ["docs"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -661,7 +636,7 @@ version = "3.0.1" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] +groups = ["docs"] files = [ {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, @@ -1419,7 +1394,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" @@ -1631,31 +1605,10 @@ platformdirs = ">=3.9.1,<5" 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 == \"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.20.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -groups = ["docs"] -markers = "python_version < \"3.10\"" -files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, -] - -[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", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [extras] docs = [] [metadata] lock-version = "2.1" -python-versions = "^3.9.0" -content-hash = "04e8a4e28d381d83214ac6d31174af00cfd2b099c8b34e9bbfff01e9b4508ccc" +python-versions = "^3.10.0" +content-hash = "eaa0642798b4afcfcff7d6908d5946c1e5f0df91d04f4547de4c3080c74dc269" diff --git a/pyproject.toml b/pyproject.toml index ce24fa3..a8a0fe8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,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", @@ -56,7 +55,7 @@ include = [ ] [tool.poetry.dependencies] -python = "^3.9.0" +python = "^3.10.0" jsonschema = "^4.19.1" rfc3339-validator = "*" # requred by jsonschema for date-time checker jsonschema-specifications = ">=2024.10.1" diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index e4a282a..89e9893 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -142,7 +142,7 @@ def test_referencing(self, validator_class): } validator = validator_class(schema, registry=registry) - result = validator.validate({"name": "John", "age": 23}, schema) + result = validator.validate({"name": "John", "age": 23}) assert result is None