Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 16 additions & 24 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
7 changes: 3 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
24 changes: 4 additions & 20 deletions benchmarks/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
},
},
},
Expand Down
29 changes: 8 additions & 21 deletions benchmarks/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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}")
Expand All @@ -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

Expand All @@ -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}")
Expand Down
1 change: 0 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 6 additions & 6 deletions openapi_schema_validator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
11 changes: 3 additions & 8 deletions openapi_schema_validator/_caches.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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):
Expand Down
4 changes: 1 addition & 3 deletions openapi_schema_validator/_dialects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 1 addition & 4 deletions openapi_schema_validator/_format.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import binascii
from base64 import b64decode
from base64 import b64encode
from numbers import Number

from jsonschema._format import FormatChecker
Expand Down Expand Up @@ -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)

Expand Down
41 changes: 12 additions & 29 deletions openapi_schema_validator/_keywords.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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(
Expand Down
7 changes: 5 additions & 2 deletions openapi_schema_validator/_regex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion openapi_schema_validator/_specifications.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading
Loading