Skip to content

Commit ed71f2b

Browse files
committed
Fix memory leak in SpecValidator.iter_errors by caching on instance
1 parent 2121137 commit ed71f2b

2 files changed

Lines changed: 40 additions & 4 deletions

File tree

openapi_spec_validator/validation/validators.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import logging
44
import warnings
5+
from collections.abc import Iterable
56
from collections.abc import Iterator
67
from collections.abc import Mapping
7-
from functools import lru_cache
88
from typing import cast
99

1010
from jsonschema.exceptions import ValidationError
@@ -19,6 +19,7 @@
1919
from openapi_spec_validator.schemas.types import AnySchema
2020
from openapi_spec_validator.settings import OpenAPISpecValidatorSettings
2121
from openapi_spec_validator.validation import keywords
22+
from openapi_spec_validator.validation.caches import CachedIterable
2223
from openapi_spec_validator.validation.decorators import unwraps_iter
2324
from openapi_spec_validator.validation.decorators import wraps_cached_iter
2425
from openapi_spec_validator.validation.decorators import wraps_errors
@@ -67,6 +68,7 @@ def __init__(
6768
self.keyword_validators_registry = KeywordValidatorRegistry(
6869
self.keyword_validators
6970
)
71+
self._cached_errors: CachedIterable[ValidationError] | None = None
7072

7173
def validate(self) -> None:
7274
for err in self.iter_errors():
@@ -83,15 +85,19 @@ def root_validator(self) -> keywords.RootValidator:
8385
self.keyword_validators_registry["__root__"],
8486
)
8587

86-
@unwraps_iter
87-
@lru_cache(maxsize=None)
8888
@wraps_cached_iter
8989
@wraps_errors
90-
def iter_errors(self) -> Iterator[ValidationError]:
90+
def _iter_errors(self) -> Iterator[ValidationError]:
9191
yield from self.schema_validator.iter_errors(self.schema)
9292

9393
yield from self.root_validator(self.schema_path)
9494

95+
@unwraps_iter
96+
def iter_errors(self) -> Iterable[ValidationError]:
97+
if getattr(self, "_cached_errors", None) is None:
98+
self._cached_errors = self._iter_errors()
99+
return self._cached_errors
100+
95101

96102
class OpenAPIV2SpecValidator(SpecValidator):
97103
schema_validator = openapi_v2_schema_validator

tests/integration/validation/test_validators.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,3 +665,33 @@ def test_failed(self, factory, spec_file):
665665

666666
with pytest.raises(OpenAPIValidationError):
667667
OpenAPIV31SpecValidator(spec, base_uri=spec_url).validate()
668+
669+
670+
def test_validator_iter_errors_instance_caching():
671+
spec = {
672+
"openapi": "3.0.0",
673+
"info": {"title": "Sample", "version": "1.0.0"},
674+
"paths": {},
675+
}
676+
validator = OpenAPIV30SpecValidator(spec)
677+
errors_1 = list(validator.iter_errors())
678+
errors_2 = list(validator.iter_errors())
679+
assert errors_1 == errors_2 == []
680+
681+
682+
def test_validator_instance_garbage_collected():
683+
import gc
684+
import weakref
685+
686+
spec = {
687+
"openapi": "3.0.0",
688+
"info": {"title": "Sample", "version": "1.0.0"},
689+
"paths": {},
690+
}
691+
validator = OpenAPIV30SpecValidator(spec)
692+
list(validator.iter_errors())
693+
ref = weakref.ref(validator)
694+
del validator
695+
gc.collect()
696+
697+
assert ref() is None

0 commit comments

Comments
 (0)