diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index cb4c1915..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,12 +0,0 @@ -# Contributing - -This package is open to contributions. To contribute, please follow these steps: - -1. Fork the upstream drf-openapi-tester repository into a personal account. -2. Install [poetry](https://python-poetry.org/), and install dev dependencies using `poetry install` -3. Install [pre-commit](https://pre-commit.com/) (for project linting) by running `pre-commit install` -4. Create a new branch for your changes, and make sure to add tests! -5. Run `poetry run pytest` to ensure all tests are passing -6. Run `pre-commit run --all-files` locally to ensure proper linting -7. Push the topic branch to your personal fork -8. Create a pull request to the drf-openapi-tester repository with an explanation of your changes diff --git a/manage.py b/manage.py index 2042a40c..8049aa24 100644 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import sys -def main(): +def main() -> None: os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") try: from django.core.management import execute_from_command_line diff --git a/openapi_tester/loaders.py b/openapi_tester/loaders.py index 18b5f3e0..35cb2446 100644 --- a/openapi_tester/loaders.py +++ b/openapi_tester/loaders.py @@ -18,7 +18,7 @@ from rest_framework.views import APIView if TYPE_CHECKING: - from typing import Callable + from typing import Any, Callable from urllib.parse import ParseResult from django.urls import ResolverMatch @@ -74,7 +74,7 @@ def get_schema(self) -> dict: return self.get_schema() def de_reference_schema(self, schema: dict) -> dict: - url = schema["basePath"] if "basePath" in schema else self.base_path + url = schema.get("basePath", self.base_path) recursion_handler = handle_recursion_limit(schema) resolver = RefResolver( schema, @@ -138,7 +138,7 @@ def resolve_path(self, endpoint_path: str, method: str) -> tuple[str, ResolverMa for key, value in reversed(list(resolved_route.kwargs.items())): index = path.rfind(str(value)) path = f"{path[:index]}{{{key}}}{path[index + len(str(value)):]}" - if "{pk}" in path and api_settings.SCHEMA_COERCE_PATH_PK: + if "{pk}" in path and api_settings.SCHEMA_COERCE_PATH_PK: # noqa: FS003 path, resolved_route = self.handle_pk_parameter( resolved_route=resolved_route, path=path, method=method ) @@ -182,7 +182,7 @@ def load_schema(self) -> dict: Loads generated schema from drf-yasg and returns it as a dict. """ odict_schema = self.schema_generator.get_schema(None, True) - return loads(dumps(odict_schema.as_odict())) + return cast(dict, loads(dumps(odict_schema.as_odict()))) def resolve_path(self, endpoint_path: str, method: str) -> tuple[str, ResolverMatch]: de_parameterized_path, resolved_path = super().resolve_path(endpoint_path=endpoint_path, method=method) @@ -206,7 +206,7 @@ def load_schema(self) -> dict: """ Loads generated schema from drf_spectacular and returns it as a dict. """ - return loads(dumps(self.schema_generator.get_schema(public=True))) + return cast(dict, loads(dumps(self.schema_generator.get_schema(public=True)))) def resolve_path(self, endpoint_path: str, method: str) -> tuple[str, ResolverMatch]: from drf_spectacular.settings import spectacular_settings @@ -227,7 +227,7 @@ def __init__(self, path: str, field_key_map: dict[str, str] | None = None): super().__init__(field_key_map=field_key_map) self.path = path if not isinstance(path, pathlib.PosixPath) else str(path) - def load_schema(self) -> dict: + def load_schema(self) -> dict[str, Any]: """ Loads a static OpenAPI schema from file, and parses it to a python dict. @@ -236,4 +236,6 @@ def load_schema(self) -> dict: """ with open(self.path, encoding="utf-8") as file: content = file.read() - return json.loads(content) if ".json" in self.path else yaml.load(content, Loader=yaml.FullLoader) + return cast( + dict, json.loads(content) if ".json" in self.path else yaml.load(content, Loader=yaml.FullLoader) + ) diff --git a/openapi_tester/schema_tester.py b/openapi_tester/schema_tester.py index 98bea5fa..1ad08949 100644 --- a/openapi_tester/schema_tester.py +++ b/openapi_tester/schema_tester.py @@ -2,7 +2,7 @@ from __future__ import annotations from itertools import chain -from typing import TYPE_CHECKING, Callable, List, cast +from typing import TYPE_CHECKING, Any, Callable, List, Optional, cast from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -38,7 +38,6 @@ ) if TYPE_CHECKING: - from typing import Any from rest_framework.response import Response @@ -79,7 +78,7 @@ def __init__( raise ImproperlyConfigured(INIT_ERROR) @staticmethod - def get_key_value(schema: dict, key: str, error_addon: str = "") -> dict: + def get_key_value(schema: dict[str, dict], key: str, error_addon: str = "") -> dict: """ Returns the value of a given key """ @@ -91,7 +90,7 @@ def get_key_value(schema: dict, key: str, error_addon: str = "") -> dict: ) from e @staticmethod - def get_status_code(schema: dict, status_code: str | int, error_addon: str = "") -> dict: + def get_status_code(schema: dict[str | int, dict], status_code: str | int, error_addon: str = "") -> dict: """ Returns the status code section of a schema, handles both str and int status codes """ @@ -104,7 +103,7 @@ def get_status_code(schema: dict, status_code: str | int, error_addon: str = "") ) @staticmethod - def get_schema_type(schema: dict) -> str | None: + def get_schema_type(schema: dict[str, str]) -> str | None: if "type" in schema: return schema["type"] if "properties" in schema or "additionalProperties" in schema: @@ -132,14 +131,16 @@ def get_response_schema_section(self, response: Response) -> dict[str, Any]: method_object = self.get_key_value( route_object, response_method, - f"\n\nUndocumented method: {response_method}.\n\nDocumented methods: {[method.lower() for method in route_object.keys() if method.lower() != 'parameters']}.", + f"\n\nUndocumented method: {response_method}.\n\nDocumented methods: " + f"{[method.lower() for method in route_object.keys() if method.lower() != 'parameters']}.", ) responses_object = self.get_key_value(method_object, "responses") status_code_object = self.get_status_code( responses_object, response.status_code, - f"\n\nUndocumented status code: {response.status_code}.\n\nDocumented status codes: {list(responses_object.keys())}. ", + f"\n\nUndocumented status code: {response.status_code}.\n\n" + f"Documented status codes: {list(responses_object.keys())}. ", ) if "openapi" not in schema: # pylint: disable=E1135 @@ -155,7 +156,8 @@ def get_response_schema_section(self, response: Response) -> dict[str, Any]: json_object = self.get_key_value( content_object, "application/json", - f"\n\nNo `application/json` responses documented for method: {response_method}, path: {parameterized_path}", + f"\n\nNo `application/json` responses documented for method: " + f"{response_method}, path: {parameterized_path}", ) return self.get_key_value(json_object, "schema") @@ -163,12 +165,13 @@ def get_response_schema_section(self, response: Response) -> dict[str, Any]: raise UndocumentedSchemaSectionError( UNDOCUMENTED_SCHEMA_SECTION_ERROR.format( key="content", - error_addon=f"\n\nNo `content` defined for this response: {response_method}, path: {parameterized_path}", + error_addon=f"\n\nNo `content` defined for this response: " + f"{response_method}, path: {parameterized_path}", ) ) return {} - def handle_one_of(self, schema_section: dict, data: Any, reference: str, **kwargs: Any): + def handle_one_of(self, schema_section: dict, data: Any, reference: str, **kwargs: Any) -> None: matches = 0 passed_schema_section_formats = set() for option in schema_section["oneOf"]: @@ -186,7 +189,7 @@ def handle_one_of(self, schema_section: dict, data: Any, reference: str, **kwarg if matches != 1: raise DocumentationError(f"{VALIDATE_ONE_OF_ERROR.format(matches=matches)}\n\nReference: {reference}.oneOf") - def handle_any_of(self, schema_section: dict, data: Any, reference: str, **kwargs: Any): + def handle_any_of(self, schema_section: dict, data: Any, reference: str, **kwargs: Any) -> None: any_of: list[dict[str, Any]] = schema_section.get("anyOf", []) for schema in chain(any_of, lazy_combinations(any_of)): try: @@ -257,7 +260,7 @@ def test_schema_section( if not schema_section_type: return combined_validators = cast( - List[Callable], + List[Callable[[dict, Any], Optional[str]]], [ validate_type, validate_format, @@ -349,7 +352,7 @@ def test_openapi_object( ignore_case=ignore_case, ) - def test_openapi_array(self, schema_section: dict, data: dict, reference: str, **kwargs: Any) -> None: + def test_openapi_array(self, schema_section: dict[str, Any], data: dict, reference: str, **kwargs: Any) -> None: for datum in data: self.test_schema_section( # the items keyword is required in arrays @@ -364,8 +367,8 @@ def validate_response( response: Response, case_tester: Callable[[str], None] | None = None, ignore_case: list[str] | None = None, - validators: list[Callable[[dict, Any], str | None]] | None = None, - ): + validators: list[Callable[[dict[str, Any], Any], str | None]] | None = None, + ) -> None: """ Verifies that an OpenAPI schema definition matches an API response. diff --git a/openapi_tester/utils.py b/openapi_tester/utils.py index 128aa0a7..e07c7517 100644 --- a/openapi_tester/utils.py +++ b/openapi_tester/utils.py @@ -1,4 +1,6 @@ -""" Utils Module - this file contains utility functions used in multiple places """ +""" +Utils Module - this file contains utility functions used in multiple places. +""" from __future__ import annotations from copy import deepcopy @@ -10,7 +12,9 @@ def merge_objects(dictionaries: Sequence[dict[str, Any]]) -> dict[str, Any]: - """helper function to deep merge objects""" + """ + Deeply merge objects. + """ output: dict[str, Any] = {} for dictionary in dictionaries: for key, value in dictionary.items(): @@ -27,8 +31,10 @@ def merge_objects(dictionaries: Sequence[dict[str, Any]]) -> dict[str, Any]: return output -def normalize_schema_section(schema_section: dict) -> dict: - """helper method to remove allOf and handle edge uses of oneOf""" +def normalize_schema_section(schema_section: dict[str, Any]) -> dict[str, Any]: + """ + Remove allOf and handle edge uses of oneOf. + """ output: dict[str, Any] = deepcopy(schema_section) if output.get("allOf"): all_of = output.pop("allOf") @@ -46,7 +52,9 @@ def normalize_schema_section(schema_section: dict) -> dict: def lazy_combinations(options_list: Sequence[dict[str, Any]]) -> Iterator[dict]: - """helper to lazy evaluate possible permutations of possible combinations""" + """ + Lazily evaluate possible combinations. + """ for i in range(2, len(options_list) + 1): for combination in combinations(options_list, i): yield merge_objects(combination) diff --git a/openapi_tester/validators.py b/openapi_tester/validators.py index 03f68f82..1f4ec559 100644 --- a/openapi_tester/validators.py +++ b/openapi_tester/validators.py @@ -33,8 +33,8 @@ from typing import Any, Callable -def create_validator(validation_fn: Callable, wrap_as_validator: bool = False) -> Callable: - def wrapped(value: Any): +def create_validator(validation_fn: Callable, wrap_as_validator: bool = False) -> Callable[[Any], bool]: + def wrapped(value: Any) -> bool: try: return bool(validation_fn(value)) or not wrap_as_validator except (ValueError, ValidationError): diff --git a/pyproject.toml b/pyproject.toml index 4a08b66a..7171c2ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,15 +56,19 @@ prance = "*" pyYAML = "*" [tool.poetry.dev-dependencies] +coverage = { extras = ["toml"], version = "^6"} drf-spectacular = "*" drf-yasg = "*" Faker = "*" pre-commit = "*" -pytest = "*" -pytest-django = "*" pylint = "*" -coverage = { extras = ["toml"], version = "6.1"} +pytest = "*" pytest-cov = "*" +pytest-django = "*" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" [tool.black] line-length = 120 @@ -72,15 +76,7 @@ include = '\.pyi?$' [tool.isort] profile = "black" -multi_line_output = 3 -include_trailing_comma = true line_length = 120 -known_third_party = ["django", "drf_spectacular", "drf_yasg", "faker", "inflection", "openapi_spec_validator", "prance", "pytest", "rest_framework", "yaml"] -known_first_party = ["openapi_tester"] - -[build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" [tool.pylint.FORMAT] max-line-length = 120 @@ -106,9 +102,8 @@ max-locals = 20 good-names = "_,e,i" [tool.coverage.run] -source = ["openapi_tester/*"] +source = ["openapi_tester"] omit = [ - "openapi_tester/type_declarations.py", "manage.py", "test_project/*", ] diff --git a/setup.cfg b/setup.cfg index dabe27d1..2407e508 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,5 @@ [flake8] ignore= - # E501: Line length - E501 # Docstring at the top of a public module D100 # Docstring at the top of a public class (method is enough) @@ -9,35 +7,17 @@ ignore= # Make docstrings one line if it can fit. D200 D210 - # Imperative docstring declarations - D401 - # Type annotation for `self` - TYP101 - TYP102 # for cls - ANN101 # Missing docstring in __init__ D107 # Missing docstring in public package D104 - # Missing type annotations for `**kwargs` - TYP003 # Whitespace before ':'. Black formats code this way. E203 # 1 blank line required between summary line and description D205 - # First line should end with a period - here we have a few cases where the first line is too long, and - # this issue can't be fixed without using noqa notation - D400 # Line break before binary operator. W504 will be hit when this is excluded. W503 - # Missing type annotation for *args - TYP002 - ANN002 - # Missing type annotation for **kwargs - ANN003 - # f-string missing prefix (too many false positives) - FS003 - # Handle error-cases first + # Handle error cases first SIM106 enable-extensions = enable-extensions = TC, TC1 @@ -49,9 +29,13 @@ exclude = manage.py, .venv max-complexity = 16 +max-line-length = 120 +per-file-ignores = + openapi_tester/constants.py:FS003 + tests/*:FS003 + test_project/*:FS003 [mypy] -python_version = 3.10 show_column_numbers = True show_error_context = False ignore_missing_imports = True diff --git a/tests/test_schema_tester.py b/tests/test_schema_tester.py index 66b6d9f2..ba0bb7b0 100644 --- a/tests/test_schema_tester.py +++ b/tests/test_schema_tester.py @@ -201,7 +201,8 @@ def test_validate_response_failure_scenario_undocumented_content(client, monkeyp response = client.get(de_parameterized_path) with pytest.raises( UndocumentedSchemaSectionError, - match=f"Error: Unsuccessfully tried to index the OpenAPI schema by `content`. \n\nNo `content` defined for this response: {method}, path: {parameterized_path}", + match=f"Error: Unsuccessfully tried to index the OpenAPI schema by `content`. \n\n" + f"No `content` defined for this response: {method}, path: {parameterized_path}", ): tester.validate_response(response)