diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 409fe45..d8be559 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,37 +3,29 @@ default_stages: [pre-commit, pre-push] default_language_version: # force all unspecified python hooks to run python3 python: python3 -minimum_pre_commit_version: "1.20.0" +minimum_pre_commit_version: "4.5.1" repos: - repo: meta hooks: - id: check-hooks-apply - - repo: https://github.com/asottile/pyupgrade - rev: v2.38.4 - hooks: - - id: pyupgrade - args: ["--py36-plus"] - - repo: local hooks: - - id: flynt - name: Convert to f-strings with flynt - entry: flynt - language: python - additional_dependencies: ['flynt==0.76'] - - - id: black - name: black - entry: black + - id: ruff-format + name: ruff format + entry: ruff + args: + - format + types: [ python ] language: system - require_serial: true - types: [python] - - id: isort - name: isort - entry: isort - args: ['--filter-files'] + - id: ruff-check + name: ruff check + entry: ruff + args: + - check + - --fix + - --exit-non-zero-on-fix + - --show-fixes + types: [ python ] language: system - require_serial: true - types: [python] diff --git a/AGENTS.md b/AGENTS.md index 1558910..85dddb2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Use this as the default operating playbook when making changes. - Runtime package: `openapi_schema_validator/`. - Tests: `pytest` in `tests/unit` and `tests/integration`. - Type checking: `mypy` with `strict = true`. -- Formatting and imports: `black` and `isort`. +- Formatting and imports: `ruff`. - Extra static analysis: `deptry`. - Supported Python versions: 3.10, 3.11, 3.12, 3.13, 3.14. @@ -68,9 +68,8 @@ Run commands from repository root. - Full pre-commit run: `poetry run pre-commit run -a` - Staged files pre-commit run: `poetry run pre-commit run` -- Format explicitly: `poetry run black .` -- Sort imports explicitly: `poetry run isort .` -- Convert to f-strings where safe: `poetry run flynt .` +- Format explicitly: `poetry run ruff format .` +- Lint code: `poetry run ruff check .` - Dependency hygiene: `poetry run deptry .` ### Build package / docs diff --git a/benchmarks/cases.py b/benchmarks/cases.py index 6a0d817..f3561d1 100644 --- a/benchmarks/cases.py +++ b/benchmarks/cases.py @@ -117,30 +117,14 @@ def build_cases() -> list[BenchmarkCase]: }, "Route": { "oneOf": [ - { - "$ref": ( - "#/components/schemas/" - "MountainHiking" - ) - }, - { - "$ref": ( - "#/components/schemas/" - "AlpineClimbing" - ) - }, + {"$ref": ("#/components/schemas/MountainHiking")}, + {"$ref": ("#/components/schemas/AlpineClimbing")}, ], "discriminator": { "propertyName": "discipline", "mapping": { - "mountain_hiking": ( - "#/components/schemas/" - "MountainHiking" - ), - "alpine_climbing": ( - "#/components/schemas/" - "AlpineClimbing" - ), + "mountain_hiking": ("#/components/schemas/MountainHiking"), + "alpine_climbing": ("#/components/schemas/AlpineClimbing"), }, }, }, diff --git a/benchmarks/compare.py b/benchmarks/compare.py index b4dfde9..42330f1 100644 --- a/benchmarks/compare.py +++ b/benchmarks/compare.py @@ -45,10 +45,7 @@ def _parse_args() -> argparse.Namespace: "--regression-threshold", type=float, default=0.0, - help=( - "Percent threshold for regressions. " - "Example: 5 means fail only when regression exceeds 5%%." - ), + help="Percent threshold for regressions. Example: 5 means fail only when regression exceeds 5%%.", ) parser.add_argument( "--fail-on-regression", @@ -102,9 +99,7 @@ def _compare_reports( for case_name in sorted(baseline_cases): if case_name not in candidate_cases: - regressions.append( - f"Missing case in candidate report: {case_name}" - ) + regressions.append(f"Missing case in candidate report: {case_name}") continue report_lines.append(f"Case: {case_name}") @@ -119,19 +114,14 @@ def _compare_reports( status = _format_status(regression, change) report_lines.append( - " " - f"{metric}: baseline={baseline_value:.6f} " - f"candidate={candidate_value:.6f} -> {status}" + f" {metric}: baseline={baseline_value:.6f} candidate={candidate_value:.6f} -> {status}" ) if regression and abs(change) > regression_threshold: - regressions.append( - f"{case_name} {metric} regressed by {abs(change):.2f}%" - ) + regressions.append(f"{case_name} {metric} regressed by {abs(change):.2f}%") extra_candidate_cases = set(candidate_cases).difference(baseline_cases) - for case_name in sorted(extra_candidate_cases): - report_lines.append(f"Case present only in candidate: {case_name}") + report_lines.extend(f"Case present only in candidate: {case_name}" for case_name in sorted(extra_candidate_cases)) return report_lines, regressions @@ -146,15 +136,12 @@ def main() -> int: args.regression_threshold, ) - print( - f"Comparing candidate {args.candidate} " - f"against baseline {args.baseline}" - ) - print("") + print(f"Comparing candidate {args.candidate} against baseline {args.baseline}") + print() print("\n".join(report_lines)) if regressions: - print("") + print() print("Regressions above threshold:") for regression in regressions: print(f"- {regression}") diff --git a/docs/conf.py b/docs/conf.py index f94242d..87cf9f3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,7 +61,6 @@ def _read_project_version() -> str: "scheme": "slate", "primary": "lime", "accent": "amber", - "scheme": "slate", "toggle": { "icon": "material/toggle-switch", "name": "Switch to light mode", diff --git a/openapi_schema_validator/__init__.py b/openapi_schema_validator/__init__.py index 9b2eb75..8b1588d 100644 --- a/openapi_schema_validator/__init__.py +++ b/openapi_schema_validator/__init__.py @@ -19,17 +19,17 @@ __license__ = "3-clause BSD License" __all__ = [ - "validate", + "OAS31_BASE_DIALECT_ID", + "OAS32_BASE_DIALECT_ID", "OAS30ReadValidator", "OAS30StrictValidator", - "OAS30WriteValidator", "OAS30Validator", + "OAS30WriteValidator", + "OAS31Validator", + "OAS32Validator", "oas30_format_checker", "oas30_strict_format_checker", - "OAS31Validator", "oas31_format_checker", - "OAS32Validator", "oas32_format_checker", - "OAS31_BASE_DIALECT_ID", - "OAS32_BASE_DIALECT_ID", + "validate", ] diff --git a/openapi_schema_validator/_caches.py b/openapi_schema_validator/_caches.py index b684226..df79ddd 100644 --- a/openapi_schema_validator/_caches.py +++ b/openapi_schema_validator/_caches.py @@ -1,9 +1,9 @@ from collections import OrderedDict +from collections.abc import Hashable +from collections.abc import Mapping from dataclasses import dataclass from threading import RLock from typing import Any -from typing import Hashable -from typing import Mapping from jsonschema.protocols import Validator @@ -23,12 +23,7 @@ def __init__(self) -> None: def _freeze_value(self, value: Any) -> Hashable: if isinstance(value, dict): - return tuple( - sorted( - (str(key), self._freeze_value(item)) - for key, item in value.items() - ) - ) + return tuple(sorted((str(key), self._freeze_value(item)) for key, item in value.items())) if isinstance(value, list): return tuple(self._freeze_value(item) for item in value) if isinstance(value, tuple): diff --git a/openapi_schema_validator/_dialects.py b/openapi_schema_validator/_dialects.py index f301ed0..a5c46d7 100644 --- a/openapi_schema_validator/_dialects.py +++ b/openapi_schema_validator/_dialects.py @@ -2,9 +2,7 @@ from jsonschema.validators import validates -from openapi_schema_validator._specifications import ( - REGISTRY as OPENAPI_SPECIFICATIONS, -) +from openapi_schema_validator._specifications import REGISTRY as OPENAPI_SPECIFICATIONS __all__ = [ "OAS31_BASE_DIALECT_ID", diff --git a/openapi_schema_validator/_format.py b/openapi_schema_validator/_format.py index a4544d1..331ace5 100644 --- a/openapi_schema_validator/_format.py +++ b/openapi_schema_validator/_format.py @@ -1,6 +1,5 @@ import binascii from base64 import b64decode -from base64 import b64encode from numbers import Number from jsonschema._format import FormatChecker @@ -106,9 +105,7 @@ def is_regex(instance: object) -> bool: oas30_strict_format_checker.checks("float")(is_float) oas30_strict_format_checker.checks("double")(is_double) oas30_strict_format_checker.checks("binary")(is_binary_strict) -oas30_strict_format_checker.checks("byte", (binascii.Error, TypeError))( - is_byte -) +oas30_strict_format_checker.checks("byte", (binascii.Error, TypeError))(is_byte) oas30_strict_format_checker.checks("password")(is_password) oas30_strict_format_checker.checks("regex")(is_regex) diff --git a/openapi_schema_validator/_keywords.py b/openapi_schema_validator/_keywords.py index 7ebe467..1bcc694 100644 --- a/openapi_schema_validator/_keywords.py +++ b/openapi_schema_validator/_keywords.py @@ -1,6 +1,6 @@ +from collections.abc import Iterator +from collections.abc import Mapping from typing import Any -from typing import Iterator -from typing import Mapping from typing import cast from jsonschema._keywords import allOf as _allOf @@ -18,9 +18,7 @@ from openapi_schema_validator._regex import search as regex_search -def handle_discriminator( - validator: Any, _: Any, instance: Any, schema: Mapping[str, Any] -) -> Iterator[ValidationError]: +def handle_discriminator(validator: Any, _: Any, instance: Any, schema: Mapping[str, Any]) -> Iterator[ValidationError]: """ Handle presence of discriminator in anyOf, oneOf and allOf. The behaviour is the same in all 3 cases because at most 1 schema will match. @@ -29,9 +27,7 @@ def handle_discriminator( prop_name = discriminator["propertyName"] if not validator.is_type(instance, "object"): - yield ValidationError( - f"{instance!r} is not of type 'object'", context=[] - ) + yield ValidationError(f"{instance!r} is not of type 'object'", context=[]) return prop_value = instance.get(prop_name) @@ -44,10 +40,7 @@ def handle_discriminator( return # Use explicit mapping if available, otherwise try implicit value - ref = ( - discriminator.get("mapping", {}).get(prop_value) - or f"#/components/schemas/{prop_value}" - ) + ref = discriminator.get("mapping", {}).get(prop_value) or f"#/components/schemas/{prop_value}" if not isinstance(ref, str): # this is a schema error @@ -132,11 +125,7 @@ def type( yield ValidationError("None for not nullable") # Pragmatic: allow bytes for binary format (common in Python use cases) - if ( - data_type == "string" - and schema.get("format") == "binary" - and isinstance(instance, bytes) - ): + if data_type == "string" and schema.get("format") == "binary" and isinstance(instance, bytes): return if not validator.is_type(instance, data_type): @@ -183,9 +172,7 @@ def pattern( try: matches = regex_search(patrn, instance) except ECMARegexSyntaxError as exc: - yield ValidationError( - f"{patrn!r} is not a valid regular expression ({exc})" - ) + yield ValidationError(f"{patrn!r} is not a valid regular expression ({exc})") return if not matches: @@ -235,11 +222,8 @@ def required( if prop_schema: read_only = prop_schema.get("readOnly", False) write_only = prop_schema.get("writeOnly", False) - if ( - getattr(validator, "write", True) - and read_only - or getattr(validator, "read", True) - and write_only + if (getattr(validator, "write", True) and read_only) or ( + getattr(validator, "read", True) and write_only ): continue yield ValidationError(f"{property!r} is a required property") @@ -299,10 +283,9 @@ def additionalProperties( for extra in extras: for error in validator.descend(instance[extra], aP, path=extra): yield error - elif validator.is_type(aP, "boolean"): - if not aP: - error = "Additional properties are not allowed (%s %s unexpected)" - yield ValidationError(error % extras_msg(extras)) + elif validator.is_type(aP, "boolean") and not aP: + error = "Additional properties are not allowed (%s %s unexpected)" + yield ValidationError(error % extras_msg(extras)) def write_readOnly( diff --git a/openapi_schema_validator/_regex.py b/openapi_schema_validator/_regex.py index df50e02..60a414c 100644 --- a/openapi_schema_validator/_regex.py +++ b/openapi_schema_validator/_regex.py @@ -5,10 +5,13 @@ _REGRESS_ERROR: type[Exception] = Exception try: - from regress import Regex as _REGEX_CLASS - from regress import RegressError as _REGRESS_ERROR + from regress import Regex as _RegressRegex + from regress import RegressError as _RegressError except ImportError: # pragma: no cover - optional dependency pass +else: + _REGEX_CLASS = _RegressRegex + _REGRESS_ERROR = _RegressError class ECMARegexSyntaxError(ValueError): diff --git a/openapi_schema_validator/_specifications.py b/openapi_schema_validator/_specifications.py index 319192c..60952a2 100644 --- a/openapi_schema_validator/_specifications.py +++ b/openapi_schema_validator/_specifications.py @@ -1,7 +1,7 @@ import json +from collections.abc import Iterator from importlib.resources import files from typing import Any -from typing import Iterator from jsonschema_specifications import REGISTRY as JSONSCHEMA_REGISTRY from referencing import Resource diff --git a/openapi_schema_validator/shortcuts.py b/openapi_schema_validator/shortcuts.py index 496f65b..299f361 100644 --- a/openapi_schema_validator/shortcuts.py +++ b/openapi_schema_validator/shortcuts.py @@ -1,7 +1,7 @@ from __future__ import annotations +from collections.abc import Mapping from typing import Any -from typing import Mapping from typing import cast from jsonschema.exceptions import best_match @@ -106,9 +106,7 @@ def validate( else: _VALIDATOR_CACHE.touch(key) - error = best_match( - cached.validator.evolve(schema=schema_dict).iter_errors(instance) - ) + error = best_match(cached.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 2dad167..6c00b69 100644 --- a/openapi_schema_validator/validators.py +++ b/openapi_schema_validator/validators.py @@ -4,7 +4,6 @@ from jsonschema import _keywords from jsonschema import _legacy_keywords from jsonschema.exceptions import SchemaError -from jsonschema.exceptions import ValidationError from jsonschema.validators import Draft202012Validator from jsonschema.validators import create from jsonschema.validators import extend @@ -18,9 +17,7 @@ from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_ID from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_METASCHEMA from openapi_schema_validator._dialects import register_openapi_dialect -from openapi_schema_validator._specifications import ( - REGISTRY as OPENAPI_SPECIFICATIONS, -) +from openapi_schema_validator._specifications import REGISTRY as OPENAPI_SPECIFICATIONS from openapi_schema_validator._types import oas31_type_checker _CHECK_SCHEMA_UNSET = object() diff --git a/poetry.lock b/poetry.lock index db04cc0..cafa61f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "alabaster" @@ -63,59 +63,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 = "26.1.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168"}, - {file = "black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d"}, - {file = "black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0"}, - {file = "black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24"}, - {file = "black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89"}, - {file = "black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5"}, - {file = "black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68"}, - {file = "black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14"}, - {file = "black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c"}, - {file = "black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4"}, - {file = "black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f"}, - {file = "black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6"}, - {file = "black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a"}, - {file = "black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791"}, - {file = "black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954"}, - {file = "black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304"}, - {file = "black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9"}, - {file = "black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b"}, - {file = "black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b"}, - {file = "black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca"}, - {file = "black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115"}, - {file = "black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79"}, - {file = "black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af"}, - {file = "black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f"}, - {file = "black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0"}, - {file = "black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede"}, - {file = "black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=1.0.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 = "certifi" version = "2026.1.4" @@ -546,24 +493,6 @@ mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.14.0,<2.15.0" pyflakes = ">=3.4.0,<3.5.0" -[[package]] -name = "flynt" -version = "1.0.6" -description = "CLI tool to convert a python project's %-formatted strings to f-strings." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "flynt-1.0.6-py3-none-any.whl", hash = "sha256:4e837c9597036b634a347855a89acf1483c4f8b73daa82c49372b10b6e1d1778"}, - {file = "flynt-1.0.6.tar.gz", hash = "sha256:471b7ff00756678e2912d4261dcbcd8fc1395129b66bf6977f88a3b3ad220c90"}, -] - -[package.dependencies] -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["build", "pre-commit", "pytest", "pytest-cov", "ruff", "twine"] - [[package]] name = "identify" version = "2.6.16" @@ -618,21 +547,6 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] -[[package]] -name = "isort" -version = "8.0.1" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.10.0" -groups = ["dev"] -files = [ - {file = "isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"}, - {file = "isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"}, -] - -[package.extras] -colors = ["colorama"] - [[package]] name = "jinja2" version = "3.1.6" @@ -665,7 +579,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.25.0" @@ -1390,61 +1304,6 @@ files = [ [package.extras] cli = ["click (>=5.0)"] -[[package]] -name = "pytokens" -version = "0.4.1" -description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, - {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, - {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"}, - {file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"}, - {file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"}, - {file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"}, - {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"}, - {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"}, - {file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"}, - {file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"}, - {file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"}, - {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"}, - {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"}, - {file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"}, - {file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"}, - {file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"}, - {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"}, - {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"}, - {file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"}, - {file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"}, - {file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"}, - {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"}, - {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"}, - {file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"}, - {file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"}, - {file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"}, - {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"}, - {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"}, - {file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"}, - {file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"}, - {file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"}, - {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"}, - {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"}, - {file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"}, - {file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"}, - {file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"}, - {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"}, - {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"}, - {file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"}, - {file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"}, - {file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"}, - {file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"}, -] - -[package.extras] -dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] - [[package]] name = "pyyaml" version = "6.0.3" @@ -1852,6 +1711,34 @@ files = [ {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, ] +[[package]] +name = "ruff" +version = "0.15.7" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e"}, + {file = "ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477"}, + {file = "ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12"}, + {file = "ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c"}, + {file = "ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4"}, + {file = "ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d"}, + {file = "ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580"}, + {file = "ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de"}, + {file = "ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1"}, + {file = "ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2"}, + {file = "ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac"}, +] + [[package]] name = "schema" version = "0.7.8" @@ -2241,4 +2128,4 @@ ecma-regex = ["regress"] [metadata] lock-version = "2.1" python-versions = "^3.10.0" -content-hash = "8fe026e86eb5cc5fca2b05d1f0b4c3313eca02c7eebb30bff216d1d381a95ee7" +content-hash = "54cddd6d15e20751883340b4dfabfdcea8693dc6ce9b1d2346861a13ac699a7a" diff --git a/pyproject.toml b/pyproject.toml index 22db0f3..d92b2e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,14 +97,12 @@ docs = ["sphinx", "sphinx-immaterial"] ecma-regex = ["regress"] [tool.poetry.group.dev.dependencies] -black = ">=24.4,<27.0" -isort = ">=5.13.2,<9.0.0" -pre-commit = "*" +ruff = "^0.15.7,<1.0" +pre-commit = ">=4.5.1" pytest = "^9" pytest-flake8 = "*" pytest-cov = "*" mypy = "^1.14" -flynt = "^1.0" deptry = ">=0.16.2,<0.25.0" tbump = "^6.11.0" @@ -123,11 +121,3 @@ addopts = """ --cov-report=term-missing --cov-report=xml """ - -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -line_length = 79 -force_single_line = true diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..13898a6 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,90 @@ +exclude = [ + "allure_dir", + ".idea", + "__pycache__", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "site-packages", + "venv", +] + +line-length = 120 +indent-width = 4 +target-version = "py310" + +[lint.pylint] +max-positional-args = 8 +max-args = 8 + +[lint] +preview = true + +select = [ + "E", # pycodestyle (errors) + "W", # pycodestyle (warnings) + "F", # pyflakes + "I", # isort (imports ordering) + "UP", # pyupgrade + "B", # bugbear + "C4", # comprehensions + "SIM", # simplify + "TID", # tidy imports + "G", # f-string is forbidden + "FURB", # refurb + "ASYNC", # async + "PERF", # perflint + "PLE", # pylint (error) + "T10", # flake8-debugger + "EXE", # flake8-executable + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "PT", # pytest + "LOG", # logging + "FAST", # fastapi + "S", # bandit + "RSE", # flake8-raise + "PLR5501", # pylint refactor - collapsible-else-if + "RUF", # Ruff-specific rules + "PYI" # flake8-pyi +] + +ignore = [ + "COM812", # puts unnecessary commas at the end of lists/tuples + "PLR6201", # puts the set where it is not needed + "ANN002", # args и kwargs + "ANN003", # args и kwargs +] + +[lint.per-file-ignores] +"**/{tests}/*" = ["ANN", "PLR", "D", "S"] +"openapi_schema_validator/__init__.py" = ["RUF067"] + + +[lint.isort] +force-single-line = true + +[format] +quote-style = "double" +indent-style = "space" +docstring-code-format = true + + diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 60a9c7f..ceee727 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -1,3 +1,4 @@ +import math import re import warnings from base64 import b64encode @@ -8,9 +9,7 @@ import pytest from jsonschema import SchemaError from jsonschema import ValidationError -from jsonschema.exceptions import ( - _WrappedReferencingError as WrappedReferencingError, -) +from jsonschema.exceptions import _WrappedReferencingError as WrappedReferencingError from jsonschema.validators import Draft202012Validator from jsonschema.validators import extend from jsonschema.validators import validator_for @@ -57,14 +56,12 @@ def test_required_checkers(self, format_checker): "date-time", "password", } - assert required_formats_set.issubset( - set(format_checker.checkers.keys()) - ) + assert required_formats_set.issubset(set(format_checker.checkers.keys())) class BaseTestOASValidatorValidate: @pytest.mark.parametrize( - "format,value", + ("format", "value"), [ ("int32", "test"), ("int32", True), @@ -88,9 +85,7 @@ class BaseTestOASValidatorValidate: ("password", ["test"]), ], ) - def test_formats_ignored( - self, format, value, validator_class, format_checker - ): + def test_formats_ignored(self, format, value, validator_class, format_checker): schema = {"format": format} validator = validator_class(schema, format_checker=format_checker) @@ -99,10 +94,8 @@ def test_formats_ignored( assert result is None @pytest.mark.parametrize("format", ["float", "double"]) - @pytest.mark.parametrize("value", [3, 3.14, 1.0]) - def test_number_float_and_double_valid( - self, format, value, validator_class, format_checker - ): + @pytest.mark.parametrize("value", [3, math.pi, 1.0]) + def test_number_float_and_double_valid(self, format, value, validator_class, format_checker): schema = {"type": "number", "format": format} validator = validator_class(schema, format_checker=format_checker) @@ -141,17 +134,13 @@ def test_invalid_pattern_raises_expected_error(self, validator_class): with pytest.raises(re.error): validator.validate("foo") - def test_invalid_pattern_rejected_by_validate_helper( - self, validator_class - ): + def test_invalid_pattern_rejected_by_validate_helper(self, validator_class): schema = {"type": "string", "pattern": "["} with pytest.raises(SchemaError, match="is not a 'regex'"): validate("foo", schema, cls=validator_class) - @pytest.mark.skipif( - not has_ecma_regex(), reason="requires optional ecma-regex extra" - ) + @pytest.mark.skipif(not has_ecma_regex(), reason="requires optional ecma-regex extra") def test_z_escape_behaves_as_ecma_literal_escape(self, validator_class): schema = {"type": "string", "pattern": r"^foo\z"} validator = validator_class(schema) @@ -167,12 +156,8 @@ def test_z_escape_behaves_as_ecma_literal_escape(self, validator_class): assert result is None - @pytest.mark.skipif( - not has_ecma_regex(), reason="requires optional ecma-regex extra" - ) - def test_escaped_z_pattern_is_allowed_with_ecma_regex( - self, validator_class - ): + @pytest.mark.skipif(not has_ecma_regex(), reason="requires optional ecma-regex extra") + def test_escaped_z_pattern_is_allowed_with_ecma_regex(self, validator_class): schema = {"type": "string", "pattern": r"^foo\\z$"} validator = validator_class(schema) @@ -236,7 +221,7 @@ def format_checker(self): return oas30_format_checker @pytest.mark.parametrize( - "format,value", + ("format", "value"), [ ("binary", True), ("binary", 3), @@ -248,9 +233,7 @@ def format_checker(self): ("byte", ["test"]), ], ) - def test_oas30_formats_ignored( - self, format, value, validator_class, format_checker - ): + def test_oas30_formats_ignored(self, format, value, validator_class, format_checker): schema = {"format": format} validator = validator_class(schema, format_checker=format_checker) @@ -276,9 +259,7 @@ def test_string_binary_valid(self, validator_class, format_checker, value): assert result is None @pytest.mark.parametrize("value", [True, 3, 3.12, None]) - def test_string_binary_invalid( - self, validator_class, format_checker, value - ): + def test_string_binary_invalid(self, validator_class, format_checker, value): schema = {"type": "string", "format": "binary"} validator = validator_class(schema, format_checker=format_checker) @@ -361,9 +342,7 @@ def test_nullable_enum_with_none(self, validator_class): ) def test_string_format_byte_valid(self, validator_class, value): schema = {"type": "string", "format": "byte"} - validator = validator_class( - schema, format_checker=oas30_format_checker - ) + validator = validator_class(schema, format_checker=oas30_format_checker) result = validator.validate(value) @@ -384,9 +363,7 @@ def test_string_format_byte_valid(self, validator_class, value): ) def test_string_format_byte_invalid(self, validator_class, value): schema = {"type": "string", "format": "byte"} - validator = validator_class( - schema, format_checker=oas30_format_checker - ) + validator = validator_class(schema, format_checker=oas30_format_checker) with pytest.raises(ValidationError, match="is not a 'byte'"): validator.validate(value) @@ -401,12 +378,8 @@ def test_allof_required(self, validator_class): {"type": "object", "required": ["some_prop"]}, ] } - validator = validator_class( - schema, format_checker=oas30_format_checker - ) - with pytest.raises( - ValidationError, match="'some_prop' is a required property" - ): + validator = validator_class(schema, format_checker=oas30_format_checker) + with pytest.raises(ValidationError, match="'some_prop' is a required property"): validator.validate({"another_prop": "bla"}) def test_required(self, validator_class): @@ -416,12 +389,8 @@ def test_required(self, validator_class): "required": ["some_prop"], } - validator = validator_class( - schema, format_checker=oas30_format_checker - ) - with pytest.raises( - ValidationError, match="'some_prop' is a required property" - ): + validator = validator_class(schema, format_checker=oas30_format_checker) + with pytest.raises(ValidationError, match="'some_prop' is a required property"): validator.validate({"another_prop": "bla"}) assert validator.validate({"some_prop": "hello"}) is None @@ -440,9 +409,7 @@ def test_oneof_required(self, validator_class): {"required": ["wagfId"]}, ], } - validator = validator_class( - schema, format_checker=oas30_format_checker - ) + validator = validator_class(schema, format_checker=oas30_format_checker) result = validator.validate(instance) assert result is None @@ -454,9 +421,7 @@ def test_oneof_required(self, validator_class): "#bad/frag", ], ) - def test_discriminator_handles_unresolvable_reference_kinds( - self, mapping_ref - ): + def test_discriminator_handles_unresolvable_reference_kinds(self, mapping_ref): schema = { "oneOf": [{"$ref": "#/components/schemas/MountainHiking"}], "discriminator": { @@ -493,16 +458,14 @@ def test_discriminator_handles_unresolvable_reference_kinds( ) @pytest.mark.parametrize( - "mapping_ref, expected_cause", + ("mapping_ref", "expected_cause"), [ ("#/components/schemas/Missing", PointerToNowhere), ("#missing-anchor", NoSuchAnchor), ("#bad/frag", InvalidAnchor), ], ) - def test_discriminator_unresolvable_reference_causes( - self, mapping_ref, expected_cause - ): + def test_discriminator_unresolvable_reference_causes(self, mapping_ref, expected_cause): schema = { "oneOf": [{"$ref": "#/components/schemas/MountainHiking"}], "discriminator": { @@ -592,31 +555,21 @@ def test_oneof_discriminator(self, validator_class, schema_type): if schema_type != "allOf": # use jsonschema validator when no discriminator is defined - validator = validator_class( - schema, format_checker=oas30_format_checker - ) + validator = validator_class(schema, format_checker=oas30_format_checker) with pytest.raises( ValidationError, match="is not valid under any of the given schemas", ): - validator.validate( - {"something": "matching_none_of_the_schemas"} - ) - assert False + validator.validate({"something": "matching_none_of_the_schemas"}) if schema_type == "anyOf": # use jsonschema validator when no discriminator is defined - validator = validator_class( - schema, format_checker=oas30_format_checker - ) + validator = validator_class(schema, format_checker=oas30_format_checker) with pytest.raises( ValidationError, match="is not valid under any of the given schemas", ): - validator.validate( - {"something": "matching_none_of_the_schemas"} - ) - assert False + validator.validate({"something": "matching_none_of_the_schemas"}) discriminator = { "propertyName": "discipline", @@ -625,14 +578,10 @@ def test_oneof_discriminator(self, validator_class, schema_type): "alpine_climbing": "#/components/schemas/AlpineClimbing", }, } - schema["components"]["schemas"]["Route"][ - "discriminator" - ] = discriminator + schema["components"]["schemas"]["Route"]["discriminator"] = discriminator # Optional: check we return useful result when the schema is wrong - validator = validator_class( - schema, format_checker=oas30_format_checker - ) + validator = validator_class(schema, format_checker=oas30_format_checker) with pytest.raises( ValidationError, @@ -640,20 +589,12 @@ def test_oneof_discriminator(self, validator_class, schema_type): ): validator.validate("not-an-object") - with pytest.raises( - ValidationError, match="does not contain discriminating property" - ): + with pytest.raises(ValidationError, match="does not contain discriminating property"): validator.validate({"something": "missing"}) - assert False # Check we get a non-generic, somehow usable, error message when a discriminated schema is failing - with pytest.raises( - ValidationError, match="'bad_string' is not of type 'integer'" - ): - validator.validate( - {"discipline": "mountain_hiking", "length": "bad_string"} - ) - assert False + with pytest.raises(ValidationError, match="'bad_string' is not of type 'integer'"): + validator.validate({"discipline": "mountain_hiking", "length": "bad_string"}) # Check explicit MountainHiking resolution validator.validate({"discipline": "mountain_hiking", "length": 10}) @@ -666,8 +607,7 @@ def test_oneof_discriminator(self, validator_class, schema_type): ValidationError, match="reference '#/components/schemas/other' could not be resolved", ): - result = validator.validate({"discipline": "other"}) - assert False + validator.validate({"discipline": "other"}) @pytest.mark.parametrize("is_nullable", [True, False]) def test_nullable_ref(self, validator_class, is_nullable): @@ -704,10 +644,9 @@ def test_nullable_ref(self, validator_class, is_nullable): match="None for not nullable", ): validator.validate({"testfield": None}) - assert False @pytest.mark.parametrize( - "schema_type, not_nullable_regex", + ("schema_type", "not_nullable_regex"), [ ("oneOf", "None is not valid under any of the given schemas"), ("anyOf", "None is not valid under any of the given schemas"), @@ -715,9 +654,7 @@ def test_nullable_ref(self, validator_class, is_nullable): ], ) @pytest.mark.parametrize("is_nullable", [True, False]) - def test_nullable_schema_combos( - self, validator_class, is_nullable, schema_type, not_nullable_regex - ): + def test_nullable_schema_combos(self, validator_class, is_nullable, schema_type, not_nullable_regex): """ This test ensures that nullablilty semantics are correct for oneOf, anyOf and allOf Specifically, nullable should checked on the children schemas @@ -730,9 +667,7 @@ def test_nullable_schema_combos( "$defs": { "NullableText": { "type": "string", - "nullable": ( - False if schema_type == "oneOf" else is_nullable - ), + "nullable": (False if schema_type == "oneOf" else is_nullable), }, "NullableEnum": { "type": "string", @@ -762,7 +697,6 @@ def test_nullable_schema_combos( else: with pytest.raises(ValidationError, match=not_nullable_regex): validator.validate({"testfield": None}) - assert False class TestOAS30ReadWriteValidatorValidate: @@ -829,9 +763,7 @@ def test_required_read_only(self): schema, format_checker=oas30_format_checker, ) - with pytest.raises( - ValidationError, match="'some_prop' is a required property" - ): + with pytest.raises(ValidationError, match="'some_prop' is a required property"): validator.validate({"another_prop": "hello"}) validator = OAS30WriteValidator( schema, @@ -850,9 +782,7 @@ def test_required_write_only(self): schema, format_checker=oas30_format_checker, ) - with pytest.raises( - ValidationError, match="'some_prop' is a required property" - ): + with pytest.raises(ValidationError, match="'some_prop' is a required property"): validator.validate({"another_prop": "hello"}) validator = OAS30ReadValidator( schema, @@ -875,9 +805,7 @@ def test_read_only_false(self): def test_write_only_false(self): schema = { "type": "object", - "properties": { - "some_prop": {"type": "string", "writeOnly": False} - }, + "properties": {"some_prop": {"type": "string", "writeOnly": False}}, } validator = OAS30ReadValidator( @@ -901,9 +829,7 @@ def test_required_checkers(self, format_checker): "double", "password", } - assert required_formats_set.issubset( - set(format_checker.checkers.keys()) - ) + assert required_formats_set.issubset(set(format_checker.checkers.keys())) class TestOAS31ValidatorValidate(BaseTestOASValidatorValidate): @@ -924,9 +850,7 @@ def test_string_disallow_binary(self, validator_class, value): validator.validate(value) @pytest.mark.parametrize("value", [b"test"]) - def test_string_binary_rejects_bytes( - self, validator_class, format_checker, value - ): + def test_string_binary_rejects_bytes(self, validator_class, format_checker, value): schema = {"type": "string", "format": "binary"} validator = validator_class(schema, format_checker=format_checker) @@ -934,9 +858,7 @@ def test_string_binary_rejects_bytes( validator.validate(value) @pytest.mark.parametrize("value", [True, 3, 3.12, None]) - def test_string_binary_invalid( - self, validator_class, format_checker, value - ): + def test_string_binary_invalid(self, validator_class, format_checker, value): schema = {"type": "string", "format": "binary"} validator = validator_class(schema, format_checker=format_checker) @@ -1150,9 +1072,7 @@ def test_discriminator_is_annotation_only(self, validator_class): "#bad/frag", ], ) - def test_discriminator_unresolvable_reference_ignored( - self, validator_class, mapping_ref - ): + def test_discriminator_unresolvable_reference_ignored(self, validator_class, mapping_ref): schema = { "oneOf": [{"$ref": "#/components/schemas/MountainHiking"}], "discriminator": { @@ -1206,9 +1126,7 @@ def test_validator_has_oas32_dialect_metaschema(self): def test_format_validation_int32(self, validator_class): schema = {"type": "integer", "format": "int32"} - validator = validator_class( - schema, format_checker=oas32_format_checker - ) + validator = validator_class(schema, format_checker=oas32_format_checker) result = validator.validate(42) assert result is None @@ -1218,9 +1136,7 @@ def test_format_validation_int32(self, validator_class): def test_format_validation_date(self, validator_class): schema = {"type": "string", "format": "date"} - validator = validator_class( - schema, format_checker=oas32_format_checker - ) + validator = validator_class(schema, format_checker=oas32_format_checker) result = validator.validate("2024-01-15") assert result is None @@ -1307,9 +1223,7 @@ def test_strict_string_accepts_str(self): def test_strict_binary_format_rejects_bytes(self): """Strict validator rejects bytes even with binary format.""" schema = {"type": "string", "format": "binary"} - validator = OAS30StrictValidator( - schema, format_checker=oas30_format_checker - ) + validator = OAS30StrictValidator(schema, format_checker=oas30_format_checker) with pytest.raises(ValidationError): validator.validate(b"test") @@ -1320,9 +1234,7 @@ def test_strict_binary_format_rejects_str(self): Binary format is for bytes in OAS, not plain strings. """ schema = {"type": "string", "format": "binary"} - validator = OAS30StrictValidator( - schema, format_checker=oas30_strict_format_checker - ) + validator = OAS30StrictValidator(schema, format_checker=oas30_strict_format_checker) # Binary format expects actual binary data (bytes in Python) # Plain strings fail format validation because they are not valid base64 @@ -1346,10 +1258,7 @@ def test_oas31_base_dialect_discovery_has_no_deprecation_warning(self): warnings.simplefilter("always") validator_for(schema) - assert not any( - issubclass(warning.category, DeprecationWarning) - for warning in caught - ) + assert not any(issubclass(warning.category, DeprecationWarning) for warning in caught) def test_oas32_base_dialect_resolves_to_oas32_validator(self): schema = {"$schema": OAS32_BASE_DIALECT_ID} @@ -1365,10 +1274,7 @@ def test_oas32_base_dialect_discovery_has_no_deprecation_warning(self): warnings.simplefilter("always") validator_for(schema) - assert not any( - issubclass(warning.category, DeprecationWarning) - for warning in caught - ) + assert not any(issubclass(warning.category, DeprecationWarning) for warning in caught) def test_oas31_base_dialect_keeps_oas_keyword_behavior(self): schema = { @@ -1382,9 +1288,7 @@ def test_oas31_base_dialect_keeps_oas_keyword_behavior(self): } validator_class = validator_for(schema) - validator = validator_class( - schema, format_checker=oas31_format_checker - ) + validator = validator_class(schema, format_checker=oas31_format_checker) result = validator.validate({"kind": "cat"}) @@ -1426,9 +1330,7 @@ def test_openapi_dialect_registration_does_not_replace_validator(self): ) assert registered_validator is OAS31Validator - assert ( - validator_for({"$schema": OAS31_BASE_DIALECT_ID}) is OAS31Validator - ) + assert validator_for({"$schema": OAS31_BASE_DIALECT_ID}) is OAS31Validator def test_openapi_oas32_dialect_registration_is_idempotent(self): register_openapi_dialect( diff --git a/tests/unit/test_shortcut.py b/tests/unit/test_shortcut.py index c37acc2..a86f264 100644 --- a/tests/unit/test_shortcut.py +++ b/tests/unit/test_shortcut.py @@ -7,6 +7,7 @@ from jsonschema.exceptions import ValidationError from referencing import Registry from referencing import Resource +from referencing.exceptions import Unresolvable from openapi_schema_validator import OAS32Validator from openapi_schema_validator import validate @@ -15,7 +16,7 @@ from openapi_schema_validator.shortcuts import clear_validate_cache -@pytest.fixture(scope="function") +@pytest.fixture def schema(): return { "type": "object", @@ -43,7 +44,7 @@ def test_validate_does_not_add_nullable_to_schema(schema): Verify that calling validate does not add the 'nullable' key to the schema """ validate({"email": "foo@bar.com"}, schema) - assert "nullable" not in schema["properties"]["email"].keys() + assert "nullable" not in schema["properties"]["email"] def test_validate_does_not_mutate_schema(schema): @@ -78,9 +79,8 @@ def test_oas32_validate_does_not_fetch_remote_metaschemas(schema): def test_validate_blocks_implicit_remote_http_references_by_default(): schema = {"$ref": "http://example.com/remote-schema.json"} - with patch("urllib.request.urlopen") as urlopen: - with pytest.raises(Exception, match="Unresolvable"): - validate({}, schema) + with patch("urllib.request.urlopen") as urlopen, pytest.raises(Exception, match="Unresolvable"): + validate({}, schema) urlopen.assert_not_called() @@ -88,9 +88,8 @@ def test_validate_blocks_implicit_remote_http_references_by_default(): def test_validate_blocks_implicit_file_references_by_default(): schema = {"$ref": "file:///etc/hosts"} - with patch("urllib.request.urlopen") as urlopen: - with pytest.raises(Exception, match="Unresolvable"): - validate({}, schema) + with patch("urllib.request.urlopen") as urlopen, pytest.raises(Exception, match="Unresolvable"): + validate({}, schema) urlopen.assert_not_called() @@ -128,9 +127,14 @@ def test_validate_honors_explicit_registry(): def test_validate_can_allow_implicit_remote_references(): schema = {"$ref": "http://example.com/remote-schema.json"} - with patch("urllib.request.urlopen") as urlopen: - with pytest.raises(Exception): - validate({}, schema, allow_remote_references=True) + with ( + patch("urllib.request.urlopen") as urlopen, + pytest.raises( + Unresolvable, + match=r"http://example\.com/remote-schema\.json", + ), + ): + validate({}, schema, allow_remote_references=True) assert urlopen.called @@ -142,9 +146,7 @@ def test_validate_skip_schema_check(): validate("foo", schema) if has_ecma_regex(): - with pytest.raises( - ValidationError, match="is not a valid regular expression" - ): + with pytest.raises(ValidationError, match="is not a valid regular expression"): validate("foo", schema, check_schema=False) else: with pytest.raises(re.error): @@ -152,9 +154,7 @@ def test_validate_skip_schema_check(): def test_validate_cache_avoids_rechecking_schema(schema): - with patch( - "openapi_schema_validator.shortcuts.check_openapi_schema" - ) as check_schema_mock: + with patch("openapi_schema_validator.shortcuts.check_openapi_schema") as check_schema_mock: validate({"email": "foo@bar.com"}, schema, cls=OAS32Validator) validate({"email": "foo@bar.com"}, schema, cls=OAS32Validator) @@ -162,9 +162,7 @@ def test_validate_cache_avoids_rechecking_schema(schema): def test_validate_cache_promotes_unchecked_validator(schema): - with patch( - "openapi_schema_validator.shortcuts.check_openapi_schema" - ) as check_schema_mock: + with patch("openapi_schema_validator.shortcuts.check_openapi_schema") as check_schema_mock: validate( {"email": "foo@bar.com"}, schema, @@ -187,9 +185,7 @@ def test_validate_cache_max_size_from_env(monkeypatch): ) reset_settings_cache() - with patch( - "openapi_schema_validator.shortcuts.check_openapi_schema" - ) as check_schema_mock: + with patch("openapi_schema_validator.shortcuts.check_openapi_schema") as check_schema_mock: validate("foo", schema_a, cls=OAS32Validator) validate(1, schema_b, cls=OAS32Validator) validate("foo", schema_a, cls=OAS32Validator)