Skip to content

Commit 78146c8

Browse files
committed
Expose resolved cache
1 parent 023da95 commit 78146c8

File tree

8 files changed

+211
-17
lines changed

8 files changed

+211
-17
lines changed

openapi_spec_validator/config.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from pydantic import field_validator
2+
from pydantic_settings import BaseSettings
3+
from pydantic_settings import SettingsConfigDict
4+
5+
ENV_PREFIX = "OPENAPI_SPEC_VALIDATOR_"
6+
RESOLVED_CACHE_MAXSIZE_DEFAULT = 128
7+
8+
9+
class OpenAPISpecValidatorSettings(BaseSettings):
10+
model_config = SettingsConfigDict(
11+
env_prefix=ENV_PREFIX,
12+
extra="ignore",
13+
)
14+
15+
resolved_cache_maxsize: int = RESOLVED_CACHE_MAXSIZE_DEFAULT
16+
17+
@field_validator("resolved_cache_maxsize", mode="before")
18+
@classmethod
19+
def normalize_resolved_cache_maxsize(
20+
cls, value: int | str | None
21+
) -> int:
22+
if value is None:
23+
return RESOLVED_CACHE_MAXSIZE_DEFAULT
24+
25+
if isinstance(value, int):
26+
parsed_value = value
27+
elif isinstance(value, str):
28+
try:
29+
parsed_value = int(value)
30+
except ValueError:
31+
return RESOLVED_CACHE_MAXSIZE_DEFAULT
32+
else:
33+
return RESOLVED_CACHE_MAXSIZE_DEFAULT
34+
35+
if parsed_value < 0:
36+
return RESOLVED_CACHE_MAXSIZE_DEFAULT
37+
38+
return parsed_value
39+
40+
41+
def get_resolved_cache_maxsize() -> int:
42+
settings = OpenAPISpecValidatorSettings()
43+
return settings.resolved_cache_maxsize

openapi_spec_validator/shortcuts.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from jsonschema_path.handlers import all_urls_handler
88
from jsonschema_path.typing import Schema
99

10+
from openapi_spec_validator.config import get_resolved_cache_maxsize
1011
from openapi_spec_validator.validation import OpenAPIV2SpecValidator
1112
from openapi_spec_validator.validation import OpenAPIV30SpecValidator
1213
from openapi_spec_validator.validation import OpenAPIV31SpecValidator
@@ -44,7 +45,11 @@ def validate(
4445
) -> None:
4546
if cls is None:
4647
cls = get_validator_cls(spec)
47-
sp = SchemaPath.from_dict(spec, base_uri=base_uri)
48+
sp = SchemaPath.from_dict(
49+
spec,
50+
base_uri=base_uri,
51+
resolved_cache_maxsize=get_resolved_cache_maxsize(),
52+
)
4853
v = cls(sp)
4954
return v.validate()
5055

openapi_spec_validator/validation/validators.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from jsonschema_path.handlers import default_handlers
1313
from jsonschema_path.paths import SchemaPath
1414

15+
from openapi_spec_validator.config import get_resolved_cache_maxsize
1516
from openapi_spec_validator.schemas import openapi_v2_schema_validator
1617
from openapi_spec_validator.schemas import openapi_v30_schema_validator
1718
from openapi_spec_validator.schemas import openapi_v31_schema_validator
@@ -59,6 +60,7 @@ def __init__(
5960
self.schema,
6061
base_uri=self.base_uri,
6162
handlers=self.resolver_handlers,
63+
resolved_cache_maxsize=get_resolved_cache_maxsize(),
6264
)
6365

6466
self.keyword_validators_registry = KeywordValidatorRegistry(

poetry.lock

Lines changed: 44 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies = [
2828
"openapi-schema-validator >=0.7.3,<0.9.0",
2929
"jsonschema-path >=0.4.3,<0.5.0",
3030
"lazy-object-proxy >=1.7.1,<2.0",
31+
"pydantic-settings (>=2.10.1,<3.0.0)",
3132
]
3233

3334
[project.urls]

tests/bench/runner.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,11 @@
2121
from pathlib import Path
2222
from typing import Any
2323

24-
from jsonschema_path import SchemaPath
2524
from jsonschema_path.typing import Schema
2625

2726
from openapi_spec_validator import schemas
2827
from openapi_spec_validator import validate
2928
from openapi_spec_validator.readers import read_from_filename
30-
from openapi_spec_validator.shortcuts import get_validator_cls
3129

3230

3331
@dataclass
@@ -110,11 +108,7 @@ def get_spec_version(spec: Schema) -> str:
110108
def run_once(spec: Schema) -> float:
111109
"""Run validation once and return elapsed time."""
112110
t0 = time.perf_counter()
113-
cls = get_validator_cls(spec)
114-
sp = SchemaPath.from_dict(spec)
115-
v = cls(sp)
116-
v.validate()
117-
# validate(spec)
111+
validate(spec)
118112
return time.perf_counter() - t0
119113

120114

@@ -271,8 +265,8 @@ def get_synthetic_specs_iterator(
271265
configs: list[tuple[int, int, str]],
272266
) -> Iterator[tuple[dict[str, Any], str, float]]:
273267
"""Iterator over synthetic specs based on provided configurations."""
274-
for paths, schemas, size in configs:
275-
spec = generate_synthetic_spec(paths, schemas)
268+
for paths, schema_count, size in configs:
269+
spec = generate_synthetic_spec(paths, schema_count)
276270
yield spec, f"synthetic_{size}", 0
277271

278272

@@ -348,7 +342,10 @@ def main():
348342
results.append(result.as_dict())
349343
if result.success:
350344
print(
351-
f" ✅ {result.median_s:.4f}s, {result.validations_per_sec:.2f} val/s"
345+
" ✅ {:.4f}s, {:.2f} val/s".format(
346+
result.median_s,
347+
result.validations_per_sec,
348+
)
352349
)
353350
else:
354351
print(f" ❌ Error: {result.error}")

tests/integration/test_shortcuts.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
from openapi_spec_validator import openapi_v2_spec_validator
88
from openapi_spec_validator import openapi_v30_spec_validator
99
from openapi_spec_validator import openapi_v32_spec_validator
10+
from openapi_spec_validator import shortcuts as shortcuts_module
1011
from openapi_spec_validator import validate
1112
from openapi_spec_validator import validate_spec
1213
from openapi_spec_validator import validate_spec_url
1314
from openapi_spec_validator import validate_url
15+
from openapi_spec_validator.config import RESOLVED_CACHE_MAXSIZE_DEFAULT
1416
from openapi_spec_validator.validation.exceptions import OpenAPIValidationError
1517
from openapi_spec_validator.validation.exceptions import ValidatorDetectError
1618

@@ -23,6 +25,56 @@ def test_spec_schema_version_not_detected(self):
2325
validate(spec)
2426

2527

28+
def test_validate_uses_resolved_cache_maxsize_env(monkeypatch):
29+
captured: dict[str, int] = {}
30+
original_from_dict = shortcuts_module.SchemaPath.from_dict
31+
spec = {
32+
"openapi": "3.0.0",
33+
"info": {"title": "Test API", "version": "0.0.1"},
34+
"paths": {},
35+
}
36+
37+
def fake_from_dict(cls, *args, **kwargs):
38+
captured["resolved_cache_maxsize"] = kwargs["resolved_cache_maxsize"]
39+
return original_from_dict(*args, **kwargs)
40+
41+
monkeypatch.setenv("OPENAPI_SPEC_VALIDATOR_RESOLVED_CACHE_MAXSIZE", "256")
42+
monkeypatch.setattr(
43+
shortcuts_module.SchemaPath,
44+
"from_dict",
45+
classmethod(fake_from_dict),
46+
)
47+
48+
validate(spec, cls=OpenAPIV30SpecValidator)
49+
50+
assert captured["resolved_cache_maxsize"] == 256
51+
52+
53+
def test_validate_uses_default_resolved_cache_on_invalid_env(monkeypatch):
54+
captured: dict[str, int] = {}
55+
original_from_dict = shortcuts_module.SchemaPath.from_dict
56+
spec = {
57+
"openapi": "3.0.0",
58+
"info": {"title": "Test API", "version": "0.0.1"},
59+
"paths": {},
60+
}
61+
62+
def fake_from_dict(cls, *args, **kwargs):
63+
captured["resolved_cache_maxsize"] = kwargs["resolved_cache_maxsize"]
64+
return original_from_dict(*args, **kwargs)
65+
66+
monkeypatch.setenv("OPENAPI_SPEC_VALIDATOR_RESOLVED_CACHE_MAXSIZE", "-1")
67+
monkeypatch.setattr(
68+
shortcuts_module.SchemaPath,
69+
"from_dict",
70+
classmethod(fake_from_dict),
71+
)
72+
73+
validate(spec, cls=OpenAPIV30SpecValidator)
74+
75+
assert captured["resolved_cache_maxsize"] == RESOLVED_CACHE_MAXSIZE_DEFAULT
76+
77+
2678
class TestLocalValidateSpecUrl:
2779
def test_spec_schema_version_not_detected(self, factory):
2880
spec_path = "data/empty.yaml"

tests/integration/validation/test_validators.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from openapi_spec_validator import OpenAPIV30SpecValidator
77
from openapi_spec_validator import OpenAPIV31SpecValidator
88
from openapi_spec_validator import OpenAPIV32SpecValidator
9+
from openapi_spec_validator.config import RESOLVED_CACHE_MAXSIZE_DEFAULT
10+
from openapi_spec_validator.validation import validators as validators_module
911
from openapi_spec_validator.validation.exceptions import OpenAPIValidationError
1012

1113

@@ -67,6 +69,60 @@ def test_ref_failed(self, factory, spec_file):
6769
OpenAPIV2SpecValidator(spec, base_uri=spec_url).validate()
6870

6971

72+
def test_spec_validator_uses_resolved_cache_maxsize_env(monkeypatch):
73+
captured: dict[str, int] = {}
74+
original_from_dict = validators_module.SchemaPath.from_dict
75+
76+
def fake_from_dict(cls, *args, **kwargs):
77+
captured["resolved_cache_maxsize"] = kwargs["resolved_cache_maxsize"]
78+
return original_from_dict(*args, **kwargs)
79+
80+
monkeypatch.setenv("OPENAPI_SPEC_VALIDATOR_RESOLVED_CACHE_MAXSIZE", "64")
81+
monkeypatch.setattr(
82+
validators_module.SchemaPath,
83+
"from_dict",
84+
classmethod(fake_from_dict),
85+
)
86+
87+
OpenAPIV30SpecValidator(
88+
{
89+
"openapi": "3.0.0",
90+
"info": {"title": "Test API", "version": "0.0.1"},
91+
"paths": {},
92+
}
93+
)
94+
95+
assert captured["resolved_cache_maxsize"] == 64
96+
97+
98+
def test_spec_validator_uses_default_resolved_cache_on_invalid_env(
99+
monkeypatch,
100+
):
101+
captured: dict[str, int] = {}
102+
original_from_dict = validators_module.SchemaPath.from_dict
103+
104+
def fake_from_dict(cls, *args, **kwargs):
105+
captured["resolved_cache_maxsize"] = kwargs["resolved_cache_maxsize"]
106+
return original_from_dict(*args, **kwargs)
107+
108+
monkeypatch.setenv("OPENAPI_SPEC_VALIDATOR_RESOLVED_CACHE_MAXSIZE", "bad")
109+
monkeypatch.setattr(
110+
validators_module.SchemaPath,
111+
"from_dict",
112+
classmethod(fake_from_dict),
113+
)
114+
115+
OpenAPIV30SpecValidator(
116+
{
117+
"openapi": "3.0.0",
118+
"info": {"title": "Test API", "version": "0.0.1"},
119+
"paths": {},
120+
}
121+
)
122+
123+
assert captured["resolved_cache_maxsize"] == RESOLVED_CACHE_MAXSIZE_DEFAULT
124+
125+
70126
class TestLocalOpenAPIv30Validator:
71127
LOCAL_SOURCE_DIRECTORY = "data/v3.0/"
72128

0 commit comments

Comments
 (0)