From f457a9bf2a40e04e804fee1cd35c1fdf3fd8cc9a Mon Sep 17 00:00:00 2001 From: p1c2u Date: Thu, 1 Sep 2022 12:35:27 +0100 Subject: [PATCH] Mypy static type check --- .github/workflows/python-test.yml | 3 + .pre-commit-config.yaml | 39 ++ MANIFEST.in | 1 + openapi_spec_validator/__init__.py | 56 ++- openapi_spec_validator/__main__.py | 57 +-- openapi_spec_validator/py.typed | 0 openapi_spec_validator/readers.py | 25 +- openapi_spec_validator/schemas/__init__.py | 6 +- openapi_spec_validator/schemas/utils.py | 11 +- openapi_spec_validator/shortcuts.py | 41 +- openapi_spec_validator/validation/__init__.py | 23 +- .../validation/decorators.py | 18 +- .../validation/exceptions.py | 2 +- .../validation/validators.py | 193 +++++---- poetry.lock | 127 +++++- pyproject.toml | 12 + tests/integration/conftest.py | 4 +- tests/integration/test_main.py | 70 ++-- tests/integration/test_shortcuts.py | 117 +++--- .../integration/validation/test_exceptions.py | 393 +++++++++--------- .../integration/validation/test_validators.py | 113 ++--- 21 files changed, 800 insertions(+), 511 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 openapi_spec_validator/py.typed diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index d511ca9..94eb53f 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -57,5 +57,8 @@ jobs: PYTEST_ADDOPTS: "--color=yes" run: poetry run pytest + - name: Static type check + run: poetry run mypy + - name: Upload coverage uses: codecov/codecov-action@v1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2068ea6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +--- +default_stages: [commit, push] +default_language_version: + # force all unspecified python hooks to run python3 + python: python3 +minimum_pre_commit_version: "1.20.0" +repos: + - repo: meta + hooks: + - id: check-hooks-apply + + - repo: https://github.com/asottile/pyupgrade + rev: v2.19.0 + 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 + language: system + require_serial: true + types: [python] + + - id: isort + name: isort + entry: isort + args: ['--filter-files'] + language: system + require_serial: true + types: [python] diff --git a/MANIFEST.in b/MANIFEST.in index 33b678f..14f5ea3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include README.md include requirements.txt include requirements_dev.txt +include openapi_spec_validator/py.typed include openapi_spec_validator/resources/schemas/*/* include LICENSE diff --git a/openapi_spec_validator/__init__.py b/openapi_spec_validator/__init__.py index b104f5f..23b8d36 100644 --- a/openapi_spec_validator/__init__.py +++ b/openapi_spec_validator/__init__.py @@ -1,62 +1,60 @@ -# -*- coding: utf-8 -*- from jsonschema_spec.handlers import default_handlers from openapi_spec_validator.shortcuts import validate_spec_detect_factory -from openapi_spec_validator.shortcuts import validate_spec_url_detect_factory from openapi_spec_validator.shortcuts import validate_spec_factory +from openapi_spec_validator.shortcuts import validate_spec_url_detect_factory from openapi_spec_validator.shortcuts import validate_spec_url_factory from openapi_spec_validator.validation import openapi_v2_spec_validator from openapi_spec_validator.validation import openapi_v3_spec_validator from openapi_spec_validator.validation import openapi_v30_spec_validator from openapi_spec_validator.validation import openapi_v31_spec_validator -__author__ = 'Artur Maciag' -__email__ = 'maciag.artur@gmail.com' -__version__ = '0.5.0a3' -__url__ = 'https://github.com/p1c2u/openapi-spec-validator' -__license__ = 'Apache License, Version 2.0' +__author__ = "Artur Maciag" +__email__ = "maciag.artur@gmail.com" +__version__ = "0.5.0a3" +__url__ = "https://github.com/p1c2u/openapi-spec-validator" +__license__ = "Apache License, Version 2.0" __all__ = [ - 'openapi_v2_spec_validator', - 'openapi_v3_spec_validator', - 'openapi_v30_spec_validator', - 'openapi_v31_spec_validator', - 'validate_v2_spec', - 'validate_v3_spec', - 'validate_v30_spec', - 'validate_v31_spec', - 'validate_spec', - 'validate_v2_spec_url', - 'validate_v3_spec_url', - 'validate_v30_spec_url', - 'validate_v31_spec_url', - 'validate_spec_url', + "openapi_v2_spec_validator", + "openapi_v3_spec_validator", + "openapi_v30_spec_validator", + "openapi_v31_spec_validator", + "validate_v2_spec", + "validate_v3_spec", + "validate_v30_spec", + "validate_v31_spec", + "validate_spec", + "validate_v2_spec_url", + "validate_v3_spec_url", + "validate_v30_spec_url", + "validate_v31_spec_url", + "validate_spec_url", ] # shortcuts -validate_spec = validate_spec_detect_factory({ +validate_spec = validate_spec_detect_factory( + { ("swagger", "2.0"): openapi_v2_spec_validator, ("openapi", "3.0"): openapi_v30_spec_validator, ("openapi", "3.1"): openapi_v31_spec_validator, }, ) -validate_spec_url = validate_spec_url_detect_factory({ +validate_spec_url = validate_spec_url_detect_factory( + { ("swagger", "2.0"): openapi_v2_spec_validator, ("openapi", "3.0"): openapi_v30_spec_validator, ("openapi", "3.1"): openapi_v31_spec_validator, }, ) validate_v2_spec = validate_spec_factory(openapi_v2_spec_validator) -validate_v2_spec_url = validate_spec_url_factory( - openapi_v2_spec_validator) +validate_v2_spec_url = validate_spec_url_factory(openapi_v2_spec_validator) validate_v30_spec = validate_spec_factory(openapi_v30_spec_validator) -validate_v30_spec_url = validate_spec_url_factory( - openapi_v30_spec_validator) +validate_v30_spec_url = validate_spec_url_factory(openapi_v30_spec_validator) validate_v31_spec = validate_spec_factory(openapi_v31_spec_validator) -validate_v31_spec_url = validate_spec_url_factory( - openapi_v31_spec_validator) +validate_v31_spec_url = validate_spec_url_factory(openapi_v31_spec_validator) # aliases to the latest v3 version validate_v3_spec = validate_v31_spec diff --git a/openapi_spec_validator/__main__.py b/openapi_spec_validator/__main__.py index 0fa2cc5..f534268 100644 --- a/openapi_spec_validator/__main__.py +++ b/openapi_spec_validator/__main__.py @@ -1,25 +1,26 @@ +from argparse import ArgumentParser import logging -import argparse import sys +from typing import Optional +from typing import Sequence from jsonschema.exceptions import best_match +from jsonschema.exceptions import ValidationError -from openapi_spec_validator import ( - openapi_v2_spec_validator, - openapi_v30_spec_validator, - openapi_v31_spec_validator, -) -from openapi_spec_validator.validation.exceptions import ValidationError -from openapi_spec_validator.readers import read_from_stdin, read_from_filename +from openapi_spec_validator import openapi_v2_spec_validator +from openapi_spec_validator import openapi_v30_spec_validator +from openapi_spec_validator import openapi_v31_spec_validator +from openapi_spec_validator.readers import read_from_filename +from openapi_spec_validator.readers import read_from_stdin logger = logging.getLogger(__name__) logging.basicConfig( - format='%(asctime)s %(levelname)s %(name)s %(message)s', - level=logging.WARNING + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.WARNING, ) -def print_validationerror(exc, errors="best-match"): +def print_validationerror(exc: ValidationError, errors: str = "best-match") -> None: print("# Validation Error\n") print(exc) if exc.cause: @@ -35,14 +36,14 @@ def print_validationerror(exc, errors="best-match"): print("## " + str(best_match(exc.context))) if len(exc.context) > 1: print( - "\n({} more subschemas errors,".format(len(exc.context) - 1), + f"\n({len(exc.context) - 1} more subschemas errors,", "use --errors=all to see them.)", ) -def main(args=None): - parser = argparse.ArgumentParser() - parser.add_argument('filename', help="Absolute or relative path to file") +def main(args: Optional[Sequence[str]] = None) -> None: + parser = ArgumentParser() + parser.add_argument("filename", help="Absolute or relative path to file") parser.add_argument( "--errors", choices=("best-match", "all"), @@ -51,46 +52,46 @@ def main(args=None): """use "all" to get all subschema errors.""", ) parser.add_argument( - '--schema', + "--schema", help="OpenAPI schema (default: 3.1.0)", type=str, - choices=['2.0', '3.0.0', '3.1.0'], - default='3.1.0' + choices=["2.0", "3.0.0", "3.1.0"], + default="3.1.0", ) - args = parser.parse_args(args) + args_parsed = parser.parse_args(args) # choose source reader = read_from_filename - if args.filename in ['-', '/-']: + if args_parsed.filename in ["-", "/-"]: reader = read_from_stdin # read source try: - spec, spec_url = reader(args.filename) + spec, spec_url = reader(args_parsed.filename) except Exception as exc: print(exc) sys.exit(1) # choose the validator validators = { - '2.0': openapi_v2_spec_validator, - '3.0.0': openapi_v30_spec_validator, - '3.1.0': openapi_v31_spec_validator, + "2.0": openapi_v2_spec_validator, + "3.0.0": openapi_v30_spec_validator, + "3.1.0": openapi_v31_spec_validator, } - validator = validators[args.schema] + validator = validators[args_parsed.schema] # validate try: validator.validate(spec, spec_url=spec_url) except ValidationError as exc: - print_validationerror(exc, args.errors) + print_validationerror(exc, args_parsed.errors) sys.exit(1) except Exception as exc: print(exc) sys.exit(2) else: - print('OK') + print("OK") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/openapi_spec_validator/py.typed b/openapi_spec_validator/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/openapi_spec_validator/readers.py b/openapi_spec_validator/readers.py index 2fc3ab9..d46c3aa 100644 --- a/openapi_spec_validator/readers.py +++ b/openapi_spec_validator/readers.py @@ -1,18 +1,23 @@ -import os -import pathlib +from os import path +from pathlib import Path import sys +from typing import Any +from typing import Hashable +from typing import Mapping +from typing import Tuple -from jsonschema_spec.handlers import file_handler, all_urls_handler +from jsonschema_spec.handlers import all_urls_handler +from jsonschema_spec.handlers import file_handler -def read_from_stdin(filename): - return file_handler(sys.stdin), '' +def read_from_stdin(filename: str) -> Tuple[Mapping[Hashable, Any], str]: + return file_handler(sys.stdin), "" # type: ignore -def read_from_filename(filename): - if not os.path.isfile(filename): - raise IOError("No such file: {0}".format(filename)) +def read_from_filename(filename: str) -> Tuple[Mapping[Hashable, Any], str]: + if not path.isfile(filename): + raise OSError(f"No such file: {filename}") - filename = os.path.abspath(filename) - uri = pathlib.Path(filename).as_uri() + filename = path.abspath(filename) + uri = Path(filename).as_uri() return all_urls_handler(uri), uri diff --git a/openapi_spec_validator/schemas/__init__.py b/openapi_spec_validator/schemas/__init__.py index 8858b78..6c72b45 100644 --- a/openapi_spec_validator/schemas/__init__.py +++ b/openapi_spec_validator/schemas/__init__.py @@ -3,9 +3,9 @@ __all__ = ["schema_v2", "schema_v3", "schema_v30", "schema_v31"] -schema_v2, _ = get_schema('2.0') -schema_v30, _ = get_schema('3.0') -schema_v31, _ = get_schema('3.1') +schema_v2, _ = get_schema("2.0") +schema_v30, _ = get_schema("3.0") +schema_v31, _ = get_schema("3.1") # alias to the latest v3 version schema_v3 = schema_v31 diff --git a/openapi_spec_validator/schemas/utils.py b/openapi_spec_validator/schemas/utils.py index d7666de..e101f07 100644 --- a/openapi_spec_validator/schemas/utils.py +++ b/openapi_spec_validator/schemas/utils.py @@ -1,14 +1,17 @@ """OpenAIP spec validator schemas utils module.""" from os import path +from typing import Any +from typing import Hashable +from typing import Mapping +from typing import Tuple import importlib_resources - from jsonschema_spec.readers import FilePathReader -def get_schema(version): - schema_path = 'resources/schemas/v{0}/schema.json'.format(version) - ref = importlib_resources.files('openapi_spec_validator') / schema_path +def get_schema(version: str) -> Tuple[Mapping[Hashable, Any], str]: + schema_path = f"resources/schemas/v{version}/schema.json" + ref = importlib_resources.files("openapi_spec_validator") / schema_path with importlib_resources.as_file(ref) as resource_path: schema_path_full = path.join(path.dirname(__file__), resource_path) return FilePathReader(schema_path_full).read() diff --git a/openapi_spec_validator/shortcuts.py b/openapi_spec_validator/shortcuts.py index 878b5c6..acca20b 100644 --- a/openapi_spec_validator/shortcuts.py +++ b/openapi_spec_validator/shortcuts.py @@ -1,41 +1,50 @@ """OpenAPI spec validator shortcuts module.""" -import urllib.parse +from typing import Any +from typing import Callable +from typing import Hashable +from typing import Mapping +from typing import Tuple from jsonschema_spec.handlers import all_urls_handler from openapi_spec_validator.exceptions import ValidatorDetectError +from openapi_spec_validator.validation.validators import SpecValidator -def detect_validator(choices, spec): +def detect_validator(choices: Mapping[Tuple[str, str], SpecValidator], spec: Mapping[Hashable, Any]) -> SpecValidator: for (key, value), validator in choices.items(): if key in spec and spec[key].startswith(value): return validator raise ValidatorDetectError("Spec schema version not detected") -def validate_spec_detect_factory(choices): - def validate(spec, spec_url=''): - validator_class = detect_validator(choices, spec) - return validator_class.validate(spec, spec_url=spec_url) +def validate_spec_detect_factory(choices: Mapping[Tuple[str, str], SpecValidator]) -> Callable[[Mapping[Hashable, Any], str], None]: + def validate(spec: Mapping[Hashable, Any], spec_url: str = "") -> None: + validator = detect_validator(choices, spec) + return validator.validate(spec, spec_url=spec_url) + return validate -def validate_spec_factory(validator_class): - def validate(spec, spec_url=''): - return validator_class.validate(spec, spec_url=spec_url) +def validate_spec_factory(validator: SpecValidator) -> Callable[[Mapping[Hashable, Any], str], None]: + def validate(spec: Mapping[Hashable, Any], spec_url: str = "") -> None: + return validator.validate(spec, spec_url=spec_url) + return validate -def validate_spec_url_detect_factory(choices): - def validate(spec_url): +def validate_spec_url_detect_factory(choices: Mapping[Tuple[str, str], SpecValidator]) -> Callable[[str], None]: + def validate(spec_url: str) -> None: spec = all_urls_handler(spec_url) - validator_class = detect_validator(choices, spec) - return validator_class.validate(spec, spec_url=spec_url) + validator = detect_validator(choices, spec) + return validator.validate(spec, spec_url=spec_url) + return validate -def validate_spec_url_factory(validator_class): - def validate(spec_url): +def validate_spec_url_factory(validator: SpecValidator) -> Callable[[str], None]: + def validate(spec_url: str) -> None: spec = all_urls_handler(spec_url) - return validator_class.validate(spec, spec_url=spec_url) + return validator.validate(spec, spec_url=spec_url) + return validate diff --git a/openapi_spec_validator/validation/__init__.py b/openapi_spec_validator/validation/__init__.py index 13bc218..28ea552 100644 --- a/openapi_spec_validator/validation/__init__.py +++ b/openapi_spec_validator/validation/__init__.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- -from jsonschema.validators import Draft202012Validator from jsonschema.validators import Draft4Validator +from jsonschema.validators import Draft202012Validator from jsonschema_spec.handlers import default_handlers from openapi_schema_validator import oas30_format_checker from openapi_schema_validator import oas31_format_checker @@ -13,30 +12,36 @@ from openapi_spec_validator.validation.validators import SpecValidator __all__ = [ - 'openapi_v2_spec_validator', - 'openapi_v3_spec_validator', - 'openapi_v30_spec_validator', - 'openapi_v31_spec_validator', + "openapi_v2_spec_validator", + "openapi_v3_spec_validator", + "openapi_v30_spec_validator", + "openapi_v31_spec_validator", ] # v2.0 spec openapi_v2_schema_validator = Draft4Validator(schema_v2) openapi_v2_spec_validator = SpecValidator( - openapi_v2_schema_validator, OAS30Validator, oas30_format_checker, + openapi_v2_schema_validator, + OAS30Validator, + oas30_format_checker, resolver_handlers=default_handlers, ) # v3.0 spec openapi_v30_schema_validator = Draft4Validator(schema_v30) openapi_v30_spec_validator = SpecValidator( - openapi_v30_schema_validator, OAS30Validator, oas30_format_checker, + openapi_v30_schema_validator, + OAS30Validator, + oas30_format_checker, resolver_handlers=default_handlers, ) # v3.1 spec openapi_v31_schema_validator = Draft202012Validator(schema_v31) openapi_v31_spec_validator = SpecValidator( - openapi_v31_schema_validator, OAS31Validator, oas31_format_checker, + openapi_v31_schema_validator, + OAS31Validator, + oas31_format_checker, resolver_handlers=default_handlers, ) diff --git a/openapi_spec_validator/validation/decorators.py b/openapi_spec_validator/validation/decorators.py index 3f4744a..988b3b8 100644 --- a/openapi_spec_validator/validation/decorators.py +++ b/openapi_spec_validator/validation/decorators.py @@ -1,18 +1,23 @@ """OpenAPI spec validator validation decorators module.""" -from functools import wraps import logging +from functools import wraps +from typing import Any +from typing import Callable +from typing import Iterator +from typing import Type -log = logging.getLogger(__name__) +from jsonschema.exceptions import ValidationError +log = logging.getLogger(__name__) -class ValidationErrorWrapper(object): - def __init__(self, error_class): +class ValidationErrorWrapper: + def __init__(self, error_class: Type[ValidationError]): self.error_class = error_class - def __call__(self, f): + def __call__(self, f: Callable[..., Any]) -> Callable[..., Any]: @wraps(f) - def wrapper(*args, **kwds): + def wrapper(*args: Any, **kwds: Any) -> Iterator[ValidationError]: errors = f(*args, **kwds) for err in errors: if not isinstance(err, self.error_class): @@ -20,4 +25,5 @@ def wrapper(*args, **kwds): yield self.error_class.create_from(err) else: yield err + return wrapper diff --git a/openapi_spec_validator/validation/exceptions.py b/openapi_spec_validator/validation/exceptions.py index bb44ce0..f081aa7 100644 --- a/openapi_spec_validator/validation/exceptions.py +++ b/openapi_spec_validator/validation/exceptions.py @@ -1,7 +1,7 @@ from jsonschema.exceptions import ValidationError -class OpenAPIValidationError(ValidationError): +class OpenAPIValidationError(ValidationError): # type: ignore pass diff --git a/openapi_spec_validator/validation/validators.py b/openapi_spec_validator/validation/validators.py index 5b3cde1..5e5238e 100644 --- a/openapi_spec_validator/validation/validators.py +++ b/openapi_spec_validator/validation/validators.py @@ -1,47 +1,78 @@ """OpenAPI spec validator validation validators module.""" import logging import string - +from typing import Any +from typing import Callable +from typing import Hashable +from typing import Iterator +from typing import List +from typing import Mapping +from typing import Optional +from typing import Type + +from jsonschema._format import FormatChecker +from jsonschema.exceptions import ValidationError +from jsonschema.protocols import Validator from jsonschema.validators import RefResolver from jsonschema_spec.accessors import SpecAccessor from jsonschema_spec.paths import Spec +from openapi_spec_validator.validation.decorators import ValidationErrorWrapper from openapi_spec_validator.validation.exceptions import ( - ParameterDuplicateError, ExtraParametersError, UnresolvableParameterError, - OpenAPIValidationError, DuplicateOperationIDError, + DuplicateOperationIDError, +) +from openapi_spec_validator.validation.exceptions import ExtraParametersError +from openapi_spec_validator.validation.exceptions import OpenAPIValidationError +from openapi_spec_validator.validation.exceptions import ( + ParameterDuplicateError, +) +from openapi_spec_validator.validation.exceptions import ( + UnresolvableParameterError, ) -from openapi_spec_validator.validation.decorators import ValidationErrorWrapper log = logging.getLogger(__name__) wraps_errors = ValidationErrorWrapper(OpenAPIValidationError) -def is_ref(spec): - return isinstance(spec, dict) and '$ref' in spec +def is_ref(spec: Any) -> bool: + return isinstance(spec, dict) and "$ref" in spec -class SpecValidator(object): +class SpecValidator: OPERATIONS = [ - 'get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace', + "get", + "put", + "post", + "delete", + "options", + "head", + "patch", + "trace", ] - def __init__(self, schema_validator, value_validator_class, value_validator_format_checker, resolver_handlers=None): + def __init__( + self, + schema_validator: Validator, + value_validator_class: Type[Validator], + value_validator_format_checker: FormatChecker, + resolver_handlers: Optional[Mapping[str, Callable[[str], Any]]] = None, + ): self.schema_validator = schema_validator self.value_validator_class = value_validator_class self.value_validator_format_checker = value_validator_format_checker self.resolver_handlers = resolver_handlers - self.operation_ids_registry = None + self.operation_ids_registry: Optional[List[str]] = None self.resolver = None - def validate(self, spec, spec_url=''): - for err in self.iter_errors(spec, spec_url=spec_url): + def validate(self, spec_dict: Mapping[Hashable, Any], spec_url: str = "") -> None: + for err in self.iter_errors(spec_dict, spec_url=spec_url): raise err @wraps_errors - def iter_errors(self, spec_dict, spec_url=''): + def iter_errors(self, spec_dict: Mapping[Hashable, Any], spec_url: str = "") -> Iterator[ValidationError]: self.operation_ids_registry = [] self.resolver = self._get_resolver(spec_url, spec_dict) @@ -49,95 +80,103 @@ def iter_errors(self, spec_dict, spec_url=''): accessor = SpecAccessor(spec_dict, self.resolver) spec = Spec(accessor) - paths = spec.get('paths', {}) - yield from self._iter_paths_errors(paths) + if "paths" in spec: + paths = spec / "paths" + yield from self._iter_paths_errors(paths) - components = spec.get('components', {}) - yield from self._iter_components_errors(components) + if "components" in spec: + components = spec / "components" + yield from self._iter_components_errors(components) - def _get_resolver(self, base_uri, referrer): - return RefResolver( - base_uri, referrer, handlers=self.resolver_handlers) + def _get_resolver(self, base_uri: str, referrer: Mapping[Hashable, Any]) -> RefResolver: + return RefResolver(base_uri, referrer, handlers=self.resolver_handlers) - def _iter_paths_errors(self, paths): + def _iter_paths_errors(self, paths: Spec) -> Iterator[ValidationError]: for url, path_item in paths.items(): yield from self._iter_path_errors(url, path_item) - def _iter_path_errors(self, url, path_item): - yield from self._iter_path_item_errors(url, path_item) - - def _iter_path_item_errors(self, url, path_item): - parameters = path_item.get('parameters', []) - yield from self._iter_parameters_errors(parameters) + def _iter_path_errors(self, url: str, path_item: Spec) -> Iterator[ValidationError]: + parameters = None + if "parameters" in path_item: + parameters = path_item / "parameters" + yield from self._iter_parameters_errors(parameters) for field_name, operation in path_item.items(): if field_name not in self.OPERATIONS: continue yield from self._iter_operation_errors( - url, field_name, operation, parameters) + url, field_name, operation, parameters + ) - def _iter_operation_errors(self, url, name, operation, path_parameters): - path_parameters = path_parameters or [] + def _iter_operation_errors(self, url: str, name: str, operation: Spec, path_parameters: Optional[Spec]) -> Iterator[ValidationError]: + assert self.operation_ids_registry is not None - operation_id = operation.getkey('operationId') - if operation_id is not None and operation_id in self.operation_ids_registry: + operation_id = operation.getkey("operationId") + if ( + operation_id is not None + and operation_id in self.operation_ids_registry + ): yield DuplicateOperationIDError( - "Operation ID '{0}' for '{1}' in '{2}' is not unique".format( - operation_id, name, url) + f"Operation ID '{operation_id}' for '{name}' in '{url}' is not unique" ) self.operation_ids_registry.append(operation_id) - parameters = operation.get('parameters', []) - yield from self._iter_parameters_errors(parameters) + names = [] + + parameters = None + if "parameters" in operation: + parameters = operation / "parameters" + yield from self._iter_parameters_errors(parameters) + names += list(self._get_path_param_names(parameters)) + + if path_parameters is not None: + names += list(self._get_path_param_names(path_parameters)) - all_params = list(set( - list(self._get_path_param_names(path_parameters)) + - list(self._get_path_param_names(parameters)) - )) + all_params = list(set(names)) for path in self._get_path_params_from_url(url): if path not in all_params: yield UnresolvableParameterError( - "Path parameter '{0}' for '{1}' operation in '{2}' " + "Path parameter '{}' for '{}' operation in '{}' " "was not resolved".format(path, name, url) ) return - def _get_path_param_names(self, params): + def _get_path_param_names(self, params: Spec) -> Iterator[str]: for param in params: - if param['in'] == 'path': - yield param['name'] + if param["in"] == "path": + yield param["name"] - def _get_path_params_from_url(self, url): + def _get_path_params_from_url(self, url: str) -> Iterator[str]: formatter = string.Formatter() path_params = [item[1] for item in formatter.parse(url)] return filter(None, path_params) - def _iter_parameters_errors(self, parameters): + def _iter_parameters_errors(self, parameters: Spec) -> Iterator[ValidationError]: seen = set() for parameter in parameters: yield from self._iter_parameter_errors(parameter) - key = (parameter['name'], parameter['in']) + key = (parameter["name"], parameter["in"]) if key in seen: yield ParameterDuplicateError( - "Duplicate parameter `{0}`".format(parameter['name']) + f"Duplicate parameter `{parameter['name']}`" ) seen.add(key) - def _iter_parameter_errors(self, parameter): - if 'schema' in parameter: - schema = parameter / 'schema' + def _iter_parameter_errors(self, parameter: Spec) -> Iterator[ValidationError]: + if "schema" in parameter: + schema = parameter / "schema" yield from self._iter_schema_errors(schema) - if 'default' in parameter: + if "default" in parameter: # only possible in swagger 2.0 - default = parameter.getkey('default') + default = parameter.getkey("default") if default is not None: yield from self._iter_value_errors(parameter, default) - def _iter_value_errors(self, schema, value): + def _iter_value_errors(self, schema: Spec, value: Any) -> Iterator[ValidationError]: with schema.open() as content: validator = self.value_validator_class( content, @@ -146,23 +185,25 @@ def _iter_value_errors(self, schema, value): ) yield from validator.iter_errors(value) - def _iter_schema_errors(self, schema, require_properties=True): + def _iter_schema_errors(self, schema: Spec, require_properties: bool = True) -> Iterator[ValidationError]: if not hasattr(schema.content(), "__getitem__"): return nested_properties = [] - if 'allOf' in schema: + if "allOf" in schema: all_of = schema / "allOf" for inner_schema in all_of: yield from self._iter_schema_errors( inner_schema, require_properties=False, ) - inner_schema_props = inner_schema.get("properties", {}) + if "properties" not in inner_schema: + continue + inner_schema_props = inner_schema / "properties" inner_schema_props_keys = inner_schema_props.keys() - nested_properties = nested_properties + list(inner_schema_props_keys) + nested_properties += list(inner_schema_props_keys) - if 'anyOf' in schema: + if "anyOf" in schema: any_of = schema / "anyOf" for inner_schema in any_of: yield from self._iter_schema_errors( @@ -170,52 +211,52 @@ def _iter_schema_errors(self, schema, require_properties=True): require_properties=False, ) - if 'oneOf' in schema: + if "oneOf" in schema: one_of = schema / "oneOf" for inner_schema in one_of: yield from self._iter_schema_errors( inner_schema, require_properties=False, ) - - if 'not' in schema: + + if "not" in schema: not_schema = schema / "not" yield from self._iter_schema_errors( not_schema, require_properties=False, ) - if 'items' in schema: + if "items" in schema: array_schema = schema / "items" yield from self._iter_schema_errors( array_schema, require_properties=False, ) - required = schema.getkey('required', []) - properties = schema.get('properties', {}).keys() - if 'allOf' in schema: - extra_properties = list(set(required) - set(properties) - set(nested_properties)) + required = schema.getkey("required", []) + properties = schema.get("properties", {}).keys() + if "allOf" in schema: + extra_properties = list( + set(required) - set(properties) - set(nested_properties) + ) else: extra_properties = list(set(required) - set(properties)) if extra_properties and require_properties: yield ExtraParametersError( - "Required list has not defined properties: {0}".format( - extra_properties - ) + f"Required list has not defined properties: {extra_properties}" ) - if 'default' in schema: - default = schema['default'] - nullable = schema.get('nullable', False) + if "default" in schema: + default = schema["default"] + nullable = schema.get("nullable", False) if default is not None or nullable is not True: yield from self._iter_value_errors(schema, default) - def _iter_components_errors(self, components): - schemas = components.get('schemas', {}) + def _iter_components_errors(self, components: Spec) -> Iterator[ValidationError]: + schemas = components.get("schemas", {}) yield from self._iter_schemas_errors(schemas) - def _iter_schemas_errors(self, schemas): + def _iter_schemas_errors(self, schemas: Spec) -> Iterator[ValidationError]: for _, schema in schemas.items(): yield from self._iter_schema_errors(schema) diff --git a/poetry.lock b/poetry.lock index ca0ce77..cd90f29 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,11 @@ +[[package]] +name = "astor" +version = "0.8.1" +description = "Read/rewrite/write Python ASTs" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + [[package]] name = "atomicwrites" version = "1.4.1" @@ -20,6 +28,29 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +[[package]] +name = "black" +version = "22.8.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2022.6.15" @@ -47,6 +78,18 @@ python-versions = ">=3.6.0" [package.extras] unicode_backport = ["unicodedata2"] +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + [[package]] name = "colorama" version = "0.4.5" @@ -103,6 +146,18 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" +[[package]] +name = "flynt" +version = "0.76" +description = "CLI tool to convert a python project's %-formatted strings to f-strings." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +astor = "*" +tomli = ">=1.1.0" + [[package]] name = "identify" version = "2.5.3" @@ -162,6 +217,20 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + [[package]] name = "jsonschema" version = "4.15.0" @@ -204,6 +273,33 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "mypy" +version = "0.971" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "nodeenv" version = "1.7.0" @@ -214,7 +310,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.* [[package]] name = "openapi-schema-validator" -version = "0.3.2" +version = "0.3.3" description = "OpenAPI schema validation for Python" category = "main" optional = false @@ -242,12 +338,20 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pathable" -version = "0.4.2" +version = "0.4.3" description = "Object-oriented paths" category = "main" optional = false python-versions = ">=3.7.0,<4.0.0" +[[package]] +name = "pathspec" +version = "0.10.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" + [[package]] name = "pkgutil-resolve-name" version = "1.3.10" @@ -465,6 +569,14 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] +[[package]] +name = "typed-ast" +version = "1.5.4" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "typing-extensions" version = "4.3.0" @@ -523,31 +635,39 @@ requests = ["requests"] [metadata] lock-version = "1.1" python-versions = "^3.7.0" -content-hash = "ae65829150610ff64c89427f0659e03bac83c36b00e8d98216f6aeb2d691d3ee" +content-hash = "ef6f3182b5cadd5b5771b0d10e16211bd7724a06c09112e70ade2a20866ed339" [metadata.files] +astor = [] atomicwrites = [] attrs = [] +black = [] certifi = [] cfgv = [] charset-normalizer = [] +click = [] colorama = [] coverage = [] distlib = [] filelock = [] flake8 = [] +flynt = [] identify = [] idna = [] importlib-metadata = [] importlib-resources = [] iniconfig = [] +isort = [] jsonschema = [] jsonschema-spec = [] mccabe = [] +mypy = [] +mypy-extensions = [] nodeenv = [] openapi-schema-validator = [] packaging = [] pathable = [] +pathspec = [] pkgutil-resolve-name = [] platformdirs = [] pluggy = [] @@ -566,6 +686,7 @@ six = [] toml = [] tomli = [] tox = [] +typed-ast = [] typing-extensions = [] urllib3 = [] virtualenv = [] diff --git a/pyproject.toml b/pyproject.toml index d17b4c8..8216ca2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,14 @@ source =["openapi_spec_validator"] [tool.coverage.xml] output = "reports/coverage.xml" +[tool.mypy] +files = "openapi_spec_validator" +strict = true + +[[tool.mypy.overrides]] +module = "jsonschema.*" +ignore_missing_imports = true + [tool.poetry] name = "openapi-spec-validator" version = "0.5.0a3" @@ -50,6 +58,10 @@ pytest = "^6.2.5" pytest-flake8 = "=1.0.7" pytest-cov = "^3.0.0" tox = "*" +mypy = "^0.971" +isort = "^5.10.1" +black = "^22.8.0" +flynt = "^0.76" [tool.poetry.scripts] openapi-spec-validator = "openapi_spec_validator.__main__:main" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index daf3661..2657e76 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -2,16 +2,16 @@ from pathlib import PurePath from urllib.parse import urlunparse +import pytest from jsonschema_spec.handlers.file import FilePathHandler from jsonschema_spec.handlers.urllib import UrllibHandler -import pytest from openapi_spec_validator import openapi_v2_spec_validator from openapi_spec_validator import openapi_v30_spec_validator from openapi_spec_validator import openapi_v31_spec_validator -def spec_file_url(spec_file, schema='file'): +def spec_file_url(spec_file, schema="file"): directory = path.abspath(path.dirname(__file__)) full_path = path.join(directory, spec_file) return urlunparse((schema, None, full_path, None, None, None)) diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index a99f4f7..1879bda 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -1,43 +1,52 @@ -import pytest from io import StringIO +from unittest import mock -from openapi_spec_validator.__main__ import main +import pytest -from unittest import mock +from openapi_spec_validator.__main__ import main def test_schema_default(): """Test default schema is 3.1.0""" - testargs = ['./tests/integration/data/v3.1/petstore.yaml'] + testargs = ["./tests/integration/data/v3.1/petstore.yaml"] main(testargs) def test_schema_v31(): """No errors when calling proper v3.1 file.""" - testargs = ['--schema', '3.1.0', - './tests/integration/data/v3.1/petstore.yaml'] + testargs = [ + "--schema", + "3.1.0", + "./tests/integration/data/v3.1/petstore.yaml", + ] main(testargs) def test_schema_v30(): """No errors when calling proper v3.0 file.""" - testargs = ['--schema', '3.0.0', - './tests/integration/data/v3.0/petstore.yaml'] + testargs = [ + "--schema", + "3.0.0", + "./tests/integration/data/v3.0/petstore.yaml", + ] main(testargs) def test_schema_v2(): """No errors when calling with proper v2 file.""" - testargs = ['--schema', '2.0', - './tests/integration/data/v2.0/petstore.yaml'] + testargs = [ + "--schema", + "2.0", + "./tests/integration/data/v2.0/petstore.yaml", + ] main(testargs) def test_errors_on_missing_description_best(capsys): """An error is obviously printed given an empty schema.""" testargs = [ - './tests/integration/data/v3.0/missing-description.yaml', - '--schema=3.0.0' + "./tests/integration/data/v3.0/missing-description.yaml", + "--schema=3.0.0", ] with pytest.raises(SystemExit): main(testargs) @@ -45,7 +54,7 @@ def test_errors_on_missing_description_best(capsys): assert "Failed validating" in out assert "'description' is a required property" in out assert "'$ref' is a required property" not in out - assert '1 more subschemas errors' in out + assert "1 more subschemas errors" in out def test_errors_on_missing_description_full(capsys): @@ -61,51 +70,60 @@ def test_errors_on_missing_description_full(capsys): assert "Failed validating" in out assert "'description' is a required property" in out assert "'$ref' is a required property" in out - assert '1 more subschema error' not in out + assert "1 more subschema error" not in out def test_schema_unknown(): """Errors on running with unknown schema.""" - testargs = ['--schema', 'x.x', - './tests/integration/data/v2.0/petstore.yaml'] + testargs = [ + "--schema", + "x.x", + "./tests/integration/data/v2.0/petstore.yaml", + ] with pytest.raises(SystemExit): main(testargs) def test_validation_error(): """SystemExit on running with ValidationError.""" - testargs = ['--schema', '3.0.0', - './tests/integration/data/v2.0/petstore.yaml'] + testargs = [ + "--schema", + "3.0.0", + "./tests/integration/data/v2.0/petstore.yaml", + ] with pytest.raises(SystemExit): main(testargs) @mock.patch( - 'openapi_spec_validator.__main__.openapi_v30_spec_validator.validate', + "openapi_spec_validator.__main__.openapi_v30_spec_validator.validate", side_effect=Exception, ) def test_unknown_error(m_validate): """SystemExit on running with unknown error.""" - testargs = ['--schema', '3.0.0', - './tests/integration/data/v2.0/petstore.yaml'] + testargs = [ + "--schema", + "3.0.0", + "./tests/integration/data/v2.0/petstore.yaml", + ] with pytest.raises(SystemExit): main(testargs) def test_nonexisting_file(): """Calling with non-existing file should sys.exit.""" - testargs = ['i_dont_exist.yaml'] + testargs = ["i_dont_exist.yaml"] with pytest.raises(SystemExit): main(testargs) def test_schema_stdin(): """Test schema from STDIN""" - spes_path = './tests/integration/data/v3.0/petstore.yaml' - with open(spes_path, 'r') as spec_file: + spes_path = "./tests/integration/data/v3.0/petstore.yaml" + with open(spes_path) as spec_file: spec_lines = spec_file.readlines() spec_io = StringIO("".join(spec_lines)) - testargs = ['--schema', '3.0.0', '-'] - with mock.patch('openapi_spec_validator.__main__.sys.stdin', spec_io): + testargs = ["--schema", "3.0.0", "-"] + with mock.patch("openapi_spec_validator.__main__.sys.stdin", spec_io): main(testargs) diff --git a/tests/integration/test_shortcuts.py b/tests/integration/test_shortcuts.py index 2256ce5..ce97340 100644 --- a/tests/integration/test_shortcuts.py +++ b/tests/integration/test_shortcuts.py @@ -1,18 +1,20 @@ import pytest -from openapi_spec_validator import ( - validate_spec, validate_spec_url, - validate_v2_spec, validate_v2_spec_url, - validate_spec_factory, validate_spec_url_factory, - openapi_v2_spec_validator, openapi_v30_spec_validator, - validate_v30_spec_url, validate_v30_spec, -) +from openapi_spec_validator import openapi_v2_spec_validator +from openapi_spec_validator import openapi_v30_spec_validator +from openapi_spec_validator import validate_spec +from openapi_spec_validator import validate_spec_factory +from openapi_spec_validator import validate_spec_url +from openapi_spec_validator import validate_spec_url_factory +from openapi_spec_validator import validate_v2_spec +from openapi_spec_validator import validate_v2_spec_url +from openapi_spec_validator import validate_v30_spec +from openapi_spec_validator import validate_v30_spec_url from openapi_spec_validator.exceptions import ValidatorDetectError from openapi_spec_validator.validation.exceptions import OpenAPIValidationError class TestValidateSpec: - def test_spec_schema_version_not_detected(self): spec = {} @@ -21,7 +23,6 @@ def test_spec_schema_version_not_detected(self): class TestValidateSpecUrl: - def test_spec_schema_version_not_detected(self, factory): spec_path = "data/empty.yaml" spec_url = factory.spec_file_url(spec_path) @@ -35,11 +36,14 @@ class TestValidatev2Spec: LOCAL_SOURCE_DIRECTORY = "data/v2.0/" def local_test_suite_file_path(self, test_file): - return "{}{}".format(self.LOCAL_SOURCE_DIRECTORY, test_file) + return f"{self.LOCAL_SOURCE_DIRECTORY}{test_file}" - @pytest.mark.parametrize('spec_file', [ - "petstore.yaml", - ]) + @pytest.mark.parametrize( + "spec_file", + [ + "petstore.yaml", + ], + ) def test_valid(self, factory, spec_file): spec_path = self.local_test_suite_file_path(spec_file) spec = factory.spec_from_file(spec_path) @@ -48,12 +52,14 @@ def test_valid(self, factory, spec_file): validate_spec(spec) validate_v2_spec(spec) - validate_spec_factory( - openapi_v2_spec_validator)(spec, spec_url) + validate_spec_factory(openapi_v2_spec_validator)(spec, spec_url) - @pytest.mark.parametrize('spec_file', [ - "empty.yaml", - ]) + @pytest.mark.parametrize( + "spec_file", + [ + "empty.yaml", + ], + ) def test_falied(self, factory, spec_file): spec_path = self.local_test_suite_file_path(spec_file) spec = factory.spec_from_file(spec_path) @@ -67,11 +73,14 @@ class TestValidatev30Spec: LOCAL_SOURCE_DIRECTORY = "data/v3.0/" def local_test_suite_file_path(self, test_file): - return "{}{}".format(self.LOCAL_SOURCE_DIRECTORY, test_file) + return f"{self.LOCAL_SOURCE_DIRECTORY}{test_file}" - @pytest.mark.parametrize('spec_file', [ - "petstore.yaml", - ]) + @pytest.mark.parametrize( + "spec_file", + [ + "petstore.yaml", + ], + ) def test_valid(self, factory, spec_file): spec_path = self.local_test_suite_file_path(spec_file) spec = factory.spec_from_file(spec_path) @@ -80,12 +89,14 @@ def test_valid(self, factory, spec_file): validate_spec(spec) validate_v30_spec(spec) - validate_spec_factory( - openapi_v30_spec_validator)(spec, spec_url) + validate_spec_factory(openapi_v30_spec_validator)(spec, spec_url) - @pytest.mark.parametrize('spec_file', [ - "empty.yaml", - ]) + @pytest.mark.parametrize( + "spec_file", + [ + "empty.yaml", + ], + ) def test_falied(self, factory, spec_file): spec_path = self.local_test_suite_file_path(spec_file) spec = factory.spec_from_file(spec_path) @@ -101,24 +112,26 @@ class TestValidatev2SpecUrl: ) def remote_test_suite_file_path(self, test_file): - return "{}{}".format(self.REMOTE_SOURCE_URL, test_file) - - @pytest.mark.parametrize('spec_file', [ - 'f25a1d44cff9669703257173e562376cc5bd0ec6/examples/v2.0/' - 'yaml/petstore.yaml', - 'f25a1d44cff9669703257173e562376cc5bd0ec6/examples/v2.0/' - 'yaml/api-with-examples.yaml', - 'f25a1d44cff9669703257173e562376cc5bd0ec6/examples/v2.0/' - 'yaml/petstore-expanded.yaml', - ]) + return f"{self.REMOTE_SOURCE_URL}{test_file}" + + @pytest.mark.parametrize( + "spec_file", + [ + "f25a1d44cff9669703257173e562376cc5bd0ec6/examples/v2.0/" + "yaml/petstore.yaml", + "f25a1d44cff9669703257173e562376cc5bd0ec6/examples/v2.0/" + "yaml/api-with-examples.yaml", + "f25a1d44cff9669703257173e562376cc5bd0ec6/examples/v2.0/" + "yaml/petstore-expanded.yaml", + ], + ) def test_valid(self, spec_file): spec_url = self.remote_test_suite_file_path(spec_file) validate_spec_url(spec_url) validate_v2_spec_url(spec_url) - validate_spec_url_factory( - openapi_v2_spec_validator)(spec_url) + validate_spec_url_factory(openapi_v2_spec_validator)(spec_url) class TestValidatev30SpecUrl: @@ -128,21 +141,23 @@ class TestValidatev30SpecUrl: ) def remote_test_suite_file_path(self, test_file): - return "{}{}".format(self.REMOTE_SOURCE_URL, test_file) - - @pytest.mark.parametrize('spec_file', [ - 'f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/' - 'petstore.yaml', - 'f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/' - 'api-with-examples.yaml', - '970566d5ca236a5ce1a02fb7d617fdbd07df88db/examples/v3.0/' - 'api-with-examples.yaml' - ]) + return f"{self.REMOTE_SOURCE_URL}{test_file}" + + @pytest.mark.parametrize( + "spec_file", + [ + "f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/" + "petstore.yaml", + "f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/" + "api-with-examples.yaml", + "970566d5ca236a5ce1a02fb7d617fdbd07df88db/examples/v3.0/" + "api-with-examples.yaml", + ], + ) def test_valid(self, spec_file): spec_url = self.remote_test_suite_file_path(spec_file) validate_spec_url(spec_url) validate_v30_spec_url(spec_url) - validate_spec_url_factory( - openapi_v30_spec_validator)(spec_url) + validate_spec_url_factory(openapi_v30_spec_validator)(spec_url) diff --git a/tests/integration/validation/test_exceptions.py b/tests/integration/validation/test_exceptions.py index 0841aa8..1b67570 100644 --- a/tests/integration/validation/test_exceptions.py +++ b/tests/integration/validation/test_exceptions.py @@ -1,11 +1,14 @@ from openapi_spec_validator.validation.exceptions import ( - ExtraParametersError, UnresolvableParameterError, OpenAPIValidationError, DuplicateOperationIDError, ) +from openapi_spec_validator.validation.exceptions import ExtraParametersError +from openapi_spec_validator.validation.exceptions import OpenAPIValidationError +from openapi_spec_validator.validation.exceptions import ( + UnresolvableParameterError, +) -class TestSpecValidatorIterErrors(object): - +class TestSpecValidatorIterErrors: def test_empty(self, validator_v30): spec = {} @@ -21,9 +24,9 @@ def test_empty(self, validator_v30): def test_info_empty(self, validator_v30): spec = { - 'openapi': '3.0.0', - 'info': {}, - 'paths': {}, + "openapi": "3.0.0", + "info": {}, + "paths": {}, } errors = validator_v30.iter_errors(spec) @@ -34,12 +37,12 @@ def test_info_empty(self, validator_v30): def test_minimalistic(self, validator_v30): spec = { - 'openapi': '3.0.0', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', + "openapi": "3.0.0", + "info": { + "title": "Test Api", + "version": "0.0.1", }, - 'paths': {}, + "paths": {}, } errors = validator_v30.iter_errors(spec) @@ -49,28 +52,28 @@ def test_minimalistic(self, validator_v30): def test_same_parameters_names(self, validator_v30): spec = { - 'openapi': '3.0.0', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', + "openapi": "3.0.0", + "info": { + "title": "Test Api", + "version": "0.0.1", }, - 'paths': { - '/test/{param1}': { - 'parameters': [ + "paths": { + "/test/{param1}": { + "parameters": [ { - 'name': 'param1', - 'in': 'query', - 'schema': { - 'type': 'integer', + "name": "param1", + "in": "query", + "schema": { + "type": "integer", }, }, { - 'name': 'param1', - 'in': 'path', - 'schema': { - 'type': 'integer', + "name": "param1", + "in": "path", + "schema": { + "type": "integer", }, - 'required': True, + "required": True, }, ], }, @@ -84,36 +87,36 @@ def test_same_parameters_names(self, validator_v30): def test_same_operation_ids(self, validator_v30): spec = { - 'openapi': '3.0.0', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', + "openapi": "3.0.0", + "info": { + "title": "Test Api", + "version": "0.0.1", }, - 'paths': { - '/test': { - 'get': { - 'operationId': 'operation1', - 'responses': { - 'default': { - 'description': 'default response', + "paths": { + "/test": { + "get": { + "operationId": "operation1", + "responses": { + "default": { + "description": "default response", }, }, }, - 'post': { - 'operationId': 'operation1', - 'responses': { - 'default': { - 'description': 'default response', + "post": { + "operationId": "operation1", + "responses": { + "default": { + "description": "default response", }, }, }, }, - '/test2': { - 'get': { - 'operationId': 'operation1', - 'responses': { - 'default': { - 'description': 'default response', + "/test2": { + "get": { + "operationId": "operation1", + "responses": { + "default": { + "description": "default response", }, }, }, @@ -130,30 +133,26 @@ def test_same_operation_ids(self, validator_v30): def test_allow_allof_required_no_properties(self, validator_v30): spec = { - 'openapi': '3.0.0', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', + "openapi": "3.0.0", + "info": { + "title": "Test Api", + "version": "0.0.1", }, - 'paths': {}, - 'components': { - 'schemas': { - 'Credit': { - 'type': 'object', - 'properties': { - 'clientId': {'type': 'string'}, - } + "paths": {}, + "components": { + "schemas": { + "Credit": { + "type": "object", + "properties": { + "clientId": {"type": "string"}, + }, }, - 'CreditCreate': { - 'allOf': [ - { - '$ref': '#/components/schemas/Credit' - }, - { - 'required': ['clientId'] - } + "CreditCreate": { + "allOf": [ + {"$ref": "#/components/schemas/Credit"}, + {"required": ["clientId"]}, ] - } + }, }, }, } @@ -162,46 +161,41 @@ def test_allow_allof_required_no_properties(self, validator_v30): errors_list = list(errors) assert errors_list == [] - def test_allow_allof_when_required_is_linked_to_the_parent_object(self, validator_v30): + def test_allow_allof_when_required_is_linked_to_the_parent_object( + self, validator_v30 + ): spec = { - 'openapi': '3.0.1', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', + "openapi": "3.0.1", + "info": { + "title": "Test Api", + "version": "0.0.1", }, - 'paths': {}, - 'components': { - 'schemas': { - 'Address': { - 'type': 'object', - 'properties': { - 'SubdivisionCode': { - 'type': 'string', - 'description': 'State or region' + "paths": {}, + "components": { + "schemas": { + "Address": { + "type": "object", + "properties": { + "SubdivisionCode": { + "type": "string", + "description": "State or region", + }, + "Town": { + "type": "string", + "description": "Town or city", }, - 'Town': { - 'type': 'string', - 'description': 'Town or city' + "CountryCode": { + "type": "string", }, - 'CountryCode': { - 'type': 'string', - } - } + }, + }, + "AddressCreation": { + "required": ["CountryCode", "Town"], + "type": "object", + "allOf": [{"$ref": "#/components/schemas/Address"}], }, - 'AddressCreation': { - 'required': [ - 'CountryCode', - 'Town' - ], - 'type': 'object', - 'allOf': [ - { - '$ref': '#/components/schemas/Address' - } - ] - } } - } + }, } errors = validator_v30.iter_errors(spec) @@ -210,19 +204,19 @@ def test_allow_allof_when_required_is_linked_to_the_parent_object(self, validato def test_extra_parameters_in_required(self, validator_v30): spec = { - 'openapi': '3.0.0', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', + "openapi": "3.0.0", + "info": { + "title": "Test Api", + "version": "0.0.1", }, - 'paths': {}, - 'components': { - 'schemas': { - 'testSchema': { - 'type': 'object', - 'required': [ - 'testparam1', - ] + "paths": {}, + "components": { + "schemas": { + "testSchema": { + "type": "object", + "required": [ + "testparam1", + ], } }, }, @@ -238,28 +232,28 @@ def test_extra_parameters_in_required(self, validator_v30): def test_undocumented_parameter(self, validator_v30): spec = { - 'openapi': '3.0.0', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', + "openapi": "3.0.0", + "info": { + "title": "Test Api", + "version": "0.0.1", }, - 'paths': { - '/test/{param1}/{param2}': { - 'get': { - 'responses': { - 'default': { - 'description': 'default response', + "paths": { + "/test/{param1}/{param2}": { + "get": { + "responses": { + "default": { + "description": "default response", }, }, }, - 'parameters': [ + "parameters": [ { - 'name': 'param1', - 'in': 'path', - 'schema': { - 'type': 'integer', + "name": "param1", + "in": "path", + "schema": { + "type": "integer", }, - 'required': True, + "required": True, }, ], }, @@ -277,17 +271,17 @@ def test_undocumented_parameter(self, validator_v30): def test_default_value_wrong_type(self, validator_v30): spec = { - 'openapi': '3.0.0', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', + "openapi": "3.0.0", + "info": { + "title": "Test Api", + "version": "0.0.1", }, - 'paths': {}, - 'components': { - 'schemas': { - 'test': { - 'type': 'integer', - 'default': 'invaldtype', + "paths": {}, + "components": { + "schemas": { + "test": { + "type": "integer", + "default": "invaldtype", }, }, }, @@ -304,29 +298,29 @@ def test_default_value_wrong_type(self, validator_v30): def test_parameter_default_value_wrong_type(self, validator_v30): spec = { - 'openapi': '3.0.0', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', + "openapi": "3.0.0", + "info": { + "title": "Test Api", + "version": "0.0.1", }, - 'paths': { - '/test/{param1}': { - 'get': { - 'responses': { - 'default': { - 'description': 'default response', + "paths": { + "/test/{param1}": { + "get": { + "responses": { + "default": { + "description": "default response", }, }, }, - 'parameters': [ + "parameters": [ { - 'name': 'param1', - 'in': 'path', - 'schema': { - 'type': 'integer', - 'default': 'invaldtype', + "name": "param1", + "in": "path", + "schema": { + "type": "integer", + "default": "invaldtype", }, - 'required': True, + "required": True, }, ], }, @@ -342,29 +336,28 @@ def test_parameter_default_value_wrong_type(self, validator_v30): "'invaldtype' is not of type 'integer'" ) - def test_parameter_default_value_wrong_type_swagger(self, - validator_v2): + def test_parameter_default_value_wrong_type_swagger(self, validator_v2): spec = { - 'swagger': '2.0', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', + "swagger": "2.0", + "info": { + "title": "Test Api", + "version": "0.0.1", }, - 'paths': { - '/test/': { - 'get': { - 'responses': { - '200': { - 'description': 'OK', - 'schema': {'type': 'object'}, + "paths": { + "/test/": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": {"type": "object"}, }, }, - 'parameters': [ + "parameters": [ { - 'name': 'param1', - 'in': 'query', - 'type': 'integer', - 'default': 'invaldtype', + "name": "param1", + "in": "query", + "type": "integer", + "default": "invaldtype", }, ], }, @@ -383,38 +376,40 @@ def test_parameter_default_value_wrong_type_swagger(self, def test_parameter_default_value_with_reference(self, validator_v30): spec = { - 'openapi': '3.0.0', - 'info': { - 'title': 'Test Api', - 'version': '0.0.1', + "openapi": "3.0.0", + "info": { + "title": "Test Api", + "version": "0.0.1", }, - 'paths': { - '/test/': { - 'get': { - 'responses': { - 'default': { - 'description': 'default response', + "paths": { + "/test/": { + "get": { + "responses": { + "default": { + "description": "default response", }, }, - 'parameters': [ + "parameters": [ { - 'name': 'param1', - 'in': 'query', - 'schema': { - 'allOf': [{ - '$ref': '#/components/schemas/type', - }], - 'default': 1, + "name": "param1", + "in": "query", + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/type", + } + ], + "default": 1, }, }, ], }, }, }, - 'components': { - 'schemas': { - 'type': { - 'type': 'integer', + "components": { + "schemas": { + "type": { + "type": "integer", } }, }, diff --git a/tests/integration/validation/test_validators.py b/tests/integration/validation/test_validators.py index 4cee021..ca7264c 100644 --- a/tests/integration/validation/test_validators.py +++ b/tests/integration/validation/test_validators.py @@ -8,13 +8,16 @@ class TestLocalOpenAPIv30Validator: LOCAL_SOURCE_DIRECTORY = "data/v3.0/" def local_test_suite_file_path(self, test_file): - return "{}{}".format(self.LOCAL_SOURCE_DIRECTORY, test_file) - - @pytest.mark.parametrize('spec_file', [ - "petstore.yaml", - "petstore-separate/spec/openapi.yaml", - "parent-reference/openapi.yaml", - ]) + return f"{self.LOCAL_SOURCE_DIRECTORY}{test_file}" + + @pytest.mark.parametrize( + "spec_file", + [ + "petstore.yaml", + "petstore-separate/spec/openapi.yaml", + "parent-reference/openapi.yaml", + ], + ) def test_valid(self, factory, validator_v30, spec_file): spec_path = self.local_test_suite_file_path(spec_file) spec = factory.spec_from_file(spec_path) @@ -22,9 +25,12 @@ def test_valid(self, factory, validator_v30, spec_file): return validator_v30.validate(spec, spec_url=spec_url) - @pytest.mark.parametrize('spec_file', [ - "empty.yaml", - ]) + @pytest.mark.parametrize( + "spec_file", + [ + "empty.yaml", + ], + ) def test_falied(self, factory, validator_v30, spec_file): spec_path = self.local_test_suite_file_path(spec_file) spec = factory.spec_from_file(spec_path) @@ -41,16 +47,19 @@ class TestRemoteOpenAPIv30Validator: ) def remote_test_suite_file_path(self, test_file): - return "{}{}".format(self.REMOTE_SOURCE_URL, test_file) - - @pytest.mark.parametrize('spec_file', [ - 'f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/' - 'petstore.yaml', - 'f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/' - 'api-with-examples.yaml', - '970566d5ca236a5ce1a02fb7d617fdbd07df88db/examples/v3.0/' - 'api-with-examples.yaml' - ]) + return f"{self.REMOTE_SOURCE_URL}{test_file}" + + @pytest.mark.parametrize( + "spec_file", + [ + "f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/" + "petstore.yaml", + "f75f8486a1aae1a7ceef92fbc63692cb2556c0cd/examples/v3.0/" + "api-with-examples.yaml", + "970566d5ca236a5ce1a02fb7d617fdbd07df88db/examples/v3.0/" + "api-with-examples.yaml", + ], + ) def test_valid(self, factory, validator_v30, spec_file): spec_url = self.remote_test_suite_file_path(spec_file) spec = factory.spec_from_url(spec_url) @@ -60,45 +69,53 @@ def test_valid(self, factory, validator_v30, spec_file): class TestRemoteOpeAPIv31Validator: - REMOTE_SOURCE_URL = 'https://raw.githubusercontent.com/' \ - 'OAI/OpenAPI-Specification/' \ - 'd9ac75b00c8bf405c2c90cfa9f20370564371dec/' + REMOTE_SOURCE_URL = ( + "https://raw.githubusercontent.com/" + "OAI/OpenAPI-Specification/" + "d9ac75b00c8bf405c2c90cfa9f20370564371dec/" + ) def remote_test_suite_file_path(self, test_file): - return "{}{}".format(self.REMOTE_SOURCE_URL, test_file) - - @pytest.mark.parametrize('spec_file', [ - 'comp_pathitems.yaml', - 'info_summary.yaml', - 'license_identifier.yaml', - 'mega.yaml', - 'minimal_comp.yaml', - 'minimal_hooks.yaml', - 'minimal_paths.yaml', - 'path_no_response.yaml', - 'path_var_empty_pathitem.yaml', - 'schema.yaml', - 'servers.yaml', - 'valid_schema_types.yaml', - ]) + return f"{self.REMOTE_SOURCE_URL}{test_file}" + + @pytest.mark.parametrize( + "spec_file", + [ + "comp_pathitems.yaml", + "info_summary.yaml", + "license_identifier.yaml", + "mega.yaml", + "minimal_comp.yaml", + "minimal_hooks.yaml", + "minimal_paths.yaml", + "path_no_response.yaml", + "path_var_empty_pathitem.yaml", + "schema.yaml", + "servers.yaml", + "valid_schema_types.yaml", + ], + ) def test_valid(self, factory, validator_v31, spec_file): spec_url = self.remote_test_suite_file_path( - '{}{}'.format('tests/v3.1/pass/', spec_file) + f"tests/v3.1/pass/{spec_file}" ) spec = factory.spec_from_url(spec_url) return validator_v31.validate(spec, spec_url=spec_url) - @pytest.mark.parametrize('spec_file', [ - 'invalid_schema_types.yaml', - 'no_containers.yaml', - 'server_enum_empty.yaml', - 'servers.yaml', - 'unknown_container.yaml', - ]) + @pytest.mark.parametrize( + "spec_file", + [ + "invalid_schema_types.yaml", + "no_containers.yaml", + "server_enum_empty.yaml", + "servers.yaml", + "unknown_container.yaml", + ], + ) def test_failed(self, factory, validator_v31, spec_file): spec_url = self.remote_test_suite_file_path( - '{}{}'.format('tests/v3.1/fail/', spec_file) + f"tests/v3.1/fail/{spec_file}" ) spec = factory.spec_from_url(spec_url)