diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index 61c8c56c3..8f468cf4e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py @@ -10,6 +10,7 @@ from .free_form_model import FreeFormModel from .http_validation_error import HTTPValidationError from .model_from_all_of import ModelFromAllOf +from .model_name import ModelName from .model_with_additional_properties_inlined import ModelWithAdditionalPropertiesInlined from .model_with_additional_properties_inlined_additional_property import ( ModelWithAdditionalPropertiesInlinedAdditionalProperty, @@ -19,6 +20,7 @@ from .model_with_any_json_properties_additional_property_type0 import ModelWithAnyJsonPropertiesAdditionalPropertyType0 from .model_with_primitive_additional_properties import ModelWithPrimitiveAdditionalProperties from .model_with_primitive_additional_properties_a_date_holder import ModelWithPrimitiveAdditionalPropertiesADateHolder +from .model_with_property_ref import ModelWithPropertyRef from .model_with_union_property import ModelWithUnionProperty from .model_with_union_property_inlined import ModelWithUnionPropertyInlined from .model_with_union_property_inlined_fruit_type0 import ModelWithUnionPropertyInlinedFruitType0 diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_name.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_name.py new file mode 100644 index 000000000..75b12f284 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_name.py @@ -0,0 +1,44 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +T = TypeVar("T", bound="ModelName") + + +@attr.s(auto_attribs=True) +class ModelName: + """ """ + + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + model_name = cls() + + model_name.additional_properties = d + return model_name + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py new file mode 100644 index 000000000..1553914ba --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_property_ref.py @@ -0,0 +1,60 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..models.model_name import ModelName +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ModelWithPropertyRef") + + +@attr.s(auto_attribs=True) +class ModelWithPropertyRef: + """ """ + + inner: Union[Unset, ModelName] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + inner: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.inner, Unset): + inner = self.inner.to_dict() + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if inner is not UNSET: + field_dict["inner"] = inner + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + inner: Union[Unset, ModelName] = UNSET + _inner = d.pop("inner", UNSET) + if not isinstance(_inner, Unset): + inner = ModelName.from_dict(_inner) + + model_with_property_ref = cls( + inner=inner, + ) + + model_with_property_ref.additional_properties = d + return model_with_property_ref + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index ed0de8918..9c1a3fcf3 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -829,10 +829,10 @@ "one_of_models": { "oneOf": [ { - "ref": "#components/schemas/FreeFormModel" + "ref": "#/components/schemas/FreeFormModel" }, { - "ref": "#components/schemas/ModelWithUnionProperty" + "ref": "#/components/schemas/ModelWithUnionProperty" } ], "nullable": false @@ -840,10 +840,10 @@ "nullable_one_of_models": { "oneOf": [ { - "ref": "#components/schemas/FreeFormModel" + "ref": "#/components/schemas/FreeFormModel" }, { - "ref": "#components/schemas/ModelWithUnionProperty" + "ref": "#/components/schemas/ModelWithUnionProperty" } ], "nullable": true @@ -851,10 +851,10 @@ "not_required_one_of_models": { "oneOf": [ { - "ref": "#components/schemas/FreeFormModel" + "ref": "#/components/schemas/FreeFormModel" }, { - "ref": "#components/schemas/ModelWithUnionProperty" + "ref": "#/components/schemas/ModelWithUnionProperty" } ], "nullable": false @@ -862,10 +862,10 @@ "not_required_nullable_one_of_models": { "oneOf": [ { - "ref": "#components/schemas/FreeFormModel" + "ref": "#/components/schemas/FreeFormModel" }, { - "ref": "#components/schemas/ModelWithUnionProperty" + "ref": "#/components/schemas/ModelWithUnionProperty" }, { "type": "string" @@ -1120,6 +1120,16 @@ "type": "string" } } + }, + "model_reference_doesnt_match": { + "title": "ModelName", + "type": "object" + }, + "ModelWithPropertyRef": { + "type": "object", + "properties": { + "inner": {"$ref": "#/components/schemas/model_reference_doesnt_match"} + } } } } diff --git a/end_to_end_tests/regen_golden_record.py b/end_to_end_tests/regen_golden_record.py index 5269f522d..350124c7e 100644 --- a/end_to_end_tests/regen_golden_record.py +++ b/end_to_end_tests/regen_golden_record.py @@ -18,7 +18,7 @@ shutil.rmtree(gr_path, ignore_errors=True) shutil.rmtree(output_path, ignore_errors=True) - result = runner.invoke(app, [f"--config={config_path}", "generate", f"--path={openapi_path}"]) + result = runner.invoke(app, ["generate", f"--config={config_path}", f"--path={openapi_path}"]) if result.stdout: print(result.stdout) diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index 9201d9746..fa4d21598 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -53,7 +53,7 @@ def run_e2e_test(extra_args=None, expected_differences=None): output_path = Path.cwd() / "my-test-api-client" shutil.rmtree(output_path, ignore_errors=True) - args = [f"--config={config_path}", "generate", f"--path={openapi_path}"] + args = ["generate", f"--config={config_path}", f"--path={openapi_path}"] if extra_args: args.extend(extra_args) result = runner.invoke(app, args) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index b5ad8afeb..16cc07000 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -14,7 +14,8 @@ from openapi_python_client import utils -from .parser import GeneratorData, import_string_from_reference +from .config import Config +from .parser import GeneratorData, import_string_from_class from .parser.errors import GeneratorError from .utils import snake_case @@ -41,15 +42,12 @@ class MetaType(str, Enum): class Project: - project_name_override: Optional[str] = None - package_name_override: Optional[str] = None - package_version_override: Optional[str] = None - def __init__( self, *, openapi: GeneratorData, meta: MetaType, + config: Config, custom_template_path: Optional[Path] = None, file_encoding: str = "utf-8", ) -> None: @@ -70,17 +68,17 @@ def __init__( loader = package_loader self.env: Environment = Environment(loader=loader, trim_blocks=True, lstrip_blocks=True) - self.project_name: str = self.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client" + self.project_name: str = config.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client" self.project_dir: Path = Path.cwd() if meta != MetaType.NONE: self.project_dir /= self.project_name - self.package_name: str = self.package_name_override or self.project_name.replace("-", "_") + self.package_name: str = config.package_name_override or self.project_name.replace("-", "_") self.package_dir: Path = self.project_dir / self.package_name self.package_description: str = utils.remove_string_escapes( f"A client library for accessing {self.openapi.title}" ) - self.version: str = self.package_version_override or openapi.version + self.version: str = config.package_version_override or openapi.version self.env.filters.update(TEMPLATE_FILTERS) @@ -215,21 +213,21 @@ def _build_models(self) -> None: imports = [] model_template = self.env.get_template("model.py.jinja") - for model in self.openapi.models.values(): - module_path = models_dir / f"{model.reference.module_name}.py" + for model in self.openapi.models: + module_path = models_dir / f"{model.class_info.module_name}.py" module_path.write_text(model_template.render(model=model), encoding=self.file_encoding) - imports.append(import_string_from_reference(model.reference)) + imports.append(import_string_from_class(model.class_info)) # Generate enums str_enum_template = self.env.get_template("str_enum.py.jinja") int_enum_template = self.env.get_template("int_enum.py.jinja") - for enum in self.openapi.enums.values(): - module_path = models_dir / f"{enum.reference.module_name}.py" + for enum in self.openapi.enums: + module_path = models_dir / f"{enum.class_info.module_name}.py" if enum.value_type is int: module_path.write_text(int_enum_template.render(enum=enum), encoding=self.file_encoding) else: module_path.write_text(str_enum_template.render(enum=enum), encoding=self.file_encoding) - imports.append(import_string_from_reference(enum.reference)) + imports.append(import_string_from_class(enum.class_info)) models_init_template = self.env.get_template("models_init.py.jinja") models_init.write_text(models_init_template.render(imports=imports), encoding=self.file_encoding) @@ -261,16 +259,23 @@ def _get_project_for_url_or_path( url: Optional[str], path: Optional[Path], meta: MetaType, + config: Config, custom_template_path: Optional[Path] = None, file_encoding: str = "utf-8", ) -> Union[Project, GeneratorError]: data_dict = _get_document(url=url, path=path) if isinstance(data_dict, GeneratorError): return data_dict - openapi = GeneratorData.from_dict(data_dict) + openapi = GeneratorData.from_dict(data_dict, config=config) if isinstance(openapi, GeneratorError): return openapi - return Project(openapi=openapi, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding) + return Project( + openapi=openapi, + custom_template_path=custom_template_path, + meta=meta, + file_encoding=file_encoding, + config=config, + ) def create_new_client( @@ -278,6 +283,7 @@ def create_new_client( url: Optional[str], path: Optional[Path], meta: MetaType, + config: Config, custom_template_path: Optional[Path] = None, file_encoding: str = "utf-8", ) -> Sequence[GeneratorError]: @@ -288,7 +294,12 @@ def create_new_client( A list containing any errors encountered when generating. """ project = _get_project_for_url_or_path( - url=url, path=path, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding + url=url, + path=path, + custom_template_path=custom_template_path, + meta=meta, + file_encoding=file_encoding, + config=config, ) if isinstance(project, GeneratorError): return [project] @@ -300,6 +311,7 @@ def update_existing_client( url: Optional[str], path: Optional[Path], meta: MetaType, + config: Config, custom_template_path: Optional[Path] = None, file_encoding: str = "utf-8", ) -> Sequence[GeneratorError]: @@ -310,7 +322,12 @@ def update_existing_client( A list containing any errors encountered when generating. """ project = _get_project_for_url_or_path( - url=url, path=path, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding + url=url, + path=path, + custom_template_path=custom_template_path, + meta=meta, + file_encoding=file_encoding, + config=config, ) if isinstance(project, GeneratorError): return [project] diff --git a/openapi_python_client/cli.py b/openapi_python_client/cli.py index 1f94f37ea..d84062fd4 100644 --- a/openapi_python_client/cli.py +++ b/openapi_python_client/cli.py @@ -6,6 +6,7 @@ import typer from openapi_python_client import MetaType +from openapi_python_client.config import Config from openapi_python_client.parser.errors import ErrorLevel, GeneratorError, ParseError app = typer.Typer() @@ -19,14 +20,13 @@ def _version_callback(value: bool) -> None: raise typer.Exit() -def _process_config(path: Optional[pathlib.Path]) -> None: - from .config import Config +def _process_config(path: Optional[pathlib.Path]) -> Config: if not path: - return + return Config() try: - Config.load_from_path(path=path) + return Config.load_from_path(path=path) except: # noqa raise typer.BadParameter("Unable to parse config") @@ -35,9 +35,6 @@ def _process_config(path: Optional[pathlib.Path]) -> None: @app.callback(name="openapi-python-client") def cli( version: bool = typer.Option(False, "--version", callback=_version_callback, help="Print the version and exit"), - config: Optional[pathlib.Path] = typer.Option( - None, callback=_process_config, help="Path to the config file to use" - ), ) -> None: """ Generate a Python client from an OpenAPI JSON document """ pass @@ -111,14 +108,17 @@ def handle_errors(errors: Sequence[GeneratorError]) -> None: help="The type of metadata you want to generate.", ) +CONFIG_OPTION = typer.Option(None, "--config", help="Path to the config file to use") + @app.command() def generate( url: Optional[str] = typer.Option(None, help="A URL to read the JSON from"), path: Optional[pathlib.Path] = typer.Option(None, help="A path to the JSON file"), custom_template_path: Optional[pathlib.Path] = typer.Option(None, **custom_template_path_options), # type: ignore - file_encoding: str = typer.Option("utf-8", help="Encoding used when writing generated"), meta: MetaType = _meta_option, + file_encoding: str = typer.Option("utf-8", help="Encoding used when writing generated"), + config_path: Optional[pathlib.Path] = CONFIG_OPTION, ) -> None: """ Generate a new OpenAPI Client library """ from . import create_new_client @@ -136,8 +136,14 @@ def generate( typer.secho("Unknown encoding : {}".format(file_encoding), fg=typer.colors.RED) raise typer.Exit(code=1) + config = _process_config(config_path) errors = create_new_client( - url=url, path=path, meta=meta, custom_template_path=custom_template_path, file_encoding=file_encoding + url=url, + path=path, + meta=meta, + custom_template_path=custom_template_path, + file_encoding=file_encoding, + config=config, ) handle_errors(errors) @@ -149,6 +155,7 @@ def update( custom_template_path: Optional[pathlib.Path] = typer.Option(None, **custom_template_path_options), # type: ignore meta: MetaType = _meta_option, file_encoding: str = typer.Option("utf-8", help="Encoding used when writing generated"), + config_path: Optional[pathlib.Path] = CONFIG_OPTION, ) -> None: """ Update an existing OpenAPI Client library """ from . import update_existing_client @@ -166,7 +173,13 @@ def update( typer.secho("Unknown encoding : {}".format(file_encoding), fg=typer.colors.RED) raise typer.Exit(code=1) + config = _process_config(config_path) errors = update_existing_client( - url=url, path=path, meta=meta, custom_template_path=custom_template_path, file_encoding=file_encoding + url=url, + path=path, + meta=meta, + custom_template_path=custom_template_path, + file_encoding=file_encoding, + config=config, ) handle_errors(errors) diff --git a/openapi_python_client/config.py b/openapi_python_client/config.py index b4fa787a9..273848e57 100644 --- a/openapi_python_client/config.py +++ b/openapi_python_client/config.py @@ -6,37 +6,23 @@ class ClassOverride(BaseModel): - class_name: str - module_name: str + class_name: Optional[str] = None + module_name: Optional[str] = None class Config(BaseModel): - class_overrides: Optional[Dict[str, ClassOverride]] + class_overrides: Dict[str, ClassOverride] = {} project_name_override: Optional[str] package_name_override: Optional[str] package_version_override: Optional[str] - field_prefix: Optional[str] - - def load_config(self) -> None: - """ Sets globals based on Config """ - from openapi_python_client import Project - - from . import utils - from .parser import reference - - if self.class_overrides is not None: - for class_name, class_data in self.class_overrides.items(): - reference.class_overrides[class_name] = reference.Reference(**dict(class_data)) - - Project.project_name_override = self.project_name_override - Project.package_name_override = self.package_name_override - Project.package_version_override = self.package_version_override - - if self.field_prefix is not None: - utils.FIELD_PREFIX = self.field_prefix + field_prefix: str = "field_" @staticmethod - def load_from_path(path: Path) -> None: + def load_from_path(path: Path) -> "Config": """ Creates a Config from provided JSON or YAML file and sets a bunch of globals from it """ + from . import utils + config_data = yaml.safe_load(path.read_text()) - Config(**config_data).load_config() + config = Config(**config_data) + utils.FIELD_PREFIX = config.field_prefix + return config diff --git a/openapi_python_client/parser/__init__.py b/openapi_python_client/parser/__init__.py index 65fd27df9..6c20f52d1 100644 --- a/openapi_python_client/parser/__init__.py +++ b/openapi_python_client/parser/__init__.py @@ -1,5 +1,5 @@ """ Classes representing the data in the OpenAPI schema """ -__all__ = ["GeneratorData", "import_string_from_reference"] +__all__ = ["GeneratorData", "import_string_from_class"] -from .openapi import GeneratorData, import_string_from_reference +from .openapi import GeneratorData, import_string_from_class diff --git a/openapi_python_client/parser/errors.py b/openapi_python_client/parser/errors.py index 5c49c7c78..9a5ced0f5 100644 --- a/openapi_python_client/parser/errors.py +++ b/openapi_python_client/parser/errors.py @@ -2,7 +2,7 @@ from enum import Enum from typing import Optional -__all__ = ["GeneratorError", "ParseError", "PropertyError", "ValidationError"] +__all__ = ["ErrorLevel", "GeneratorError", "ParseError", "PropertyError", "ValidationError"] from pydantic import BaseModel diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 283dd1948..09a24dad0 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -1,15 +1,15 @@ from copy import deepcopy from dataclasses import dataclass, field from enum import Enum -from typing import Any, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union from pydantic import ValidationError from .. import schema as oai from .. import utils +from ..config import Config from .errors import GeneratorError, ParseError, PropertyError -from .properties import EnumProperty, ModelProperty, Property, Schemas, build_schemas, property_from_data -from .reference import Reference +from .properties import Class, EnumProperty, ModelProperty, Property, Schemas, build_schemas, property_from_data from .responses import Response, response_from_data @@ -22,9 +22,9 @@ class ParameterLocation(str, Enum): COOKIE = "cookie" -def import_string_from_reference(reference: Reference, prefix: str = "") -> str: +def import_string_from_class(class_: Class, prefix: str = "") -> str: """ Create a string which is used to import a reference """ - return f"from {prefix}.{reference.module_name} import {reference.class_name}" + return f"from {prefix}.{class_.module_name} import {class_.name}" @dataclass @@ -37,7 +37,7 @@ class EndpointCollection: @staticmethod def from_data( - *, data: Dict[str, oai.PathItem], schemas: Schemas + *, data: Dict[str, oai.PathItem], schemas: Schemas, config: Config ) -> Tuple[Dict[str, "EndpointCollection"], Schemas]: """ Parse the openapi paths data to get EndpointCollections by tag """ endpoints_by_tag: Dict[str, EndpointCollection] = {} @@ -52,7 +52,7 @@ def from_data( tag = utils.snake_case((operation.tags or ["default"])[0]) collection = endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag)) endpoint, schemas = Endpoint.from_data( - data=operation, path=path, method=method, tag=tag, schemas=schemas + data=operation, path=path, method=method, tag=tag, schemas=schemas, config=config ) if isinstance(endpoint, ParseError): endpoint.header = ( @@ -96,32 +96,32 @@ class Endpoint: header_parameters: List[Property] = field(default_factory=list) cookie_parameters: List[Property] = field(default_factory=list) responses: List[Response] = field(default_factory=list) - form_body_reference: Optional[Reference] = None + form_body_class: Optional[Class] = None json_body: Optional[Property] = None - multipart_body_reference: Optional[Reference] = None + multipart_body_class: Optional[Class] = None errors: List[ParseError] = field(default_factory=list) @staticmethod - def parse_request_form_body(body: oai.RequestBody) -> Optional[Reference]: + def parse_request_form_body(*, body: oai.RequestBody, config: Config) -> Optional[Class]: """ Return form_body_reference """ body_content = body.content form_body = body_content.get("application/x-www-form-urlencoded") if form_body is not None and isinstance(form_body.media_type_schema, oai.Reference): - return Reference.from_ref(form_body.media_type_schema.ref) + return Class.from_string(string=form_body.media_type_schema.ref, config=config) return None @staticmethod - def parse_multipart_body(body: oai.RequestBody) -> Optional[Reference]: + def parse_multipart_body(*, body: oai.RequestBody, config: Config) -> Optional[Class]: """ Return form_body_reference """ body_content = body.content json_body = body_content.get("multipart/form-data") if json_body is not None and isinstance(json_body.media_type_schema, oai.Reference): - return Reference.from_ref(json_body.media_type_schema.ref) + return Class.from_string(string=json_body.media_type_schema.ref, config=config) return None @staticmethod def parse_request_json_body( - *, body: oai.RequestBody, schemas: Schemas, parent_name: str + *, body: oai.RequestBody, schemas: Schemas, parent_name: str, config: Config ) -> Tuple[Union[Property, PropertyError, None], Schemas]: """ Return json_body """ body_content = body.content @@ -133,42 +133,45 @@ def parse_request_json_body( data=json_body.media_type_schema, schemas=schemas, parent_name=parent_name, + config=config, ) return None, schemas @staticmethod def _add_body( - *, endpoint: "Endpoint", data: oai.Operation, schemas: Schemas + *, + endpoint: "Endpoint", + data: oai.Operation, + schemas: Schemas, + config: Config, ) -> Tuple[Union[ParseError, "Endpoint"], Schemas]: """ Adds form or JSON body to Endpoint if included in data """ endpoint = deepcopy(endpoint) if data.requestBody is None or isinstance(data.requestBody, oai.Reference): return endpoint, schemas - endpoint.form_body_reference = Endpoint.parse_request_form_body(data.requestBody) + endpoint.form_body_class = Endpoint.parse_request_form_body(body=data.requestBody, config=config) json_body, schemas = Endpoint.parse_request_json_body( - body=data.requestBody, schemas=schemas, parent_name=endpoint.name + body=data.requestBody, schemas=schemas, parent_name=endpoint.name, config=config ) if isinstance(json_body, ParseError): return ParseError(detail=f"cannot parse body of endpoint {endpoint.name}", data=json_body.data), schemas - endpoint.multipart_body_reference = Endpoint.parse_multipart_body(data.requestBody) + endpoint.multipart_body_class = Endpoint.parse_multipart_body(body=data.requestBody, config=config) - if endpoint.form_body_reference: - endpoint.relative_imports.add( - import_string_from_reference(endpoint.form_body_reference, prefix="...models") - ) - if endpoint.multipart_body_reference: - endpoint.relative_imports.add( - import_string_from_reference(endpoint.multipart_body_reference, prefix="...models") - ) + if endpoint.form_body_class: + endpoint.relative_imports.add(import_string_from_class(endpoint.form_body_class, prefix="...models")) + if endpoint.multipart_body_class: + endpoint.relative_imports.add(import_string_from_class(endpoint.multipart_body_class, prefix="...models")) if json_body is not None: endpoint.json_body = json_body endpoint.relative_imports.update(endpoint.json_body.get_imports(prefix="...")) return endpoint, schemas @staticmethod - def _add_responses(*, endpoint: "Endpoint", data: oai.Responses, schemas: Schemas) -> Tuple["Endpoint", Schemas]: + def _add_responses( + *, endpoint: "Endpoint", data: oai.Responses, schemas: Schemas, config: Config + ) -> Tuple["Endpoint", Schemas]: endpoint = deepcopy(endpoint) for code, response_data in data.items(): @@ -187,7 +190,7 @@ def _add_responses(*, endpoint: "Endpoint", data: oai.Responses, schemas: Schema continue response, schemas = response_from_data( - status_code=status_code, data=response_data, schemas=schemas, parent_name=endpoint.name + status_code=status_code, data=response_data, schemas=schemas, parent_name=endpoint.name, config=config ) if isinstance(response, ParseError): endpoint.errors.append( @@ -206,7 +209,7 @@ def _add_responses(*, endpoint: "Endpoint", data: oai.Responses, schemas: Schema @staticmethod def _add_parameters( - *, endpoint: "Endpoint", data: oai.Operation, schemas: Schemas + *, endpoint: "Endpoint", data: oai.Operation, schemas: Schemas, config: Config ) -> Tuple[Union["Endpoint", ParseError], Schemas]: endpoint = deepcopy(endpoint) if data.parameters is None: @@ -220,6 +223,7 @@ def _add_parameters( data=param.param_schema, schemas=schemas, parent_name=endpoint.name, + config=config, ) if isinstance(prop, ParseError): return ParseError(detail=f"cannot parse parameter of endpoint {endpoint.name}", data=prop.data), schemas @@ -239,7 +243,7 @@ def _add_parameters( @staticmethod def from_data( - *, data: oai.Operation, path: str, method: str, tag: str, schemas: Schemas + *, data: oai.Operation, path: str, method: str, tag: str, schemas: Schemas, config: Config ) -> Tuple[Union["Endpoint", ParseError], Schemas]: """ Construct an endpoint from the OpenAPI data """ @@ -257,11 +261,11 @@ def from_data( tag=tag, ) - result, schemas = Endpoint._add_parameters(endpoint=endpoint, data=data, schemas=schemas) + result, schemas = Endpoint._add_parameters(endpoint=endpoint, data=data, schemas=schemas, config=config) if isinstance(result, ParseError): return result, schemas - result, schemas = Endpoint._add_responses(endpoint=result, data=data.responses, schemas=schemas) - result, schemas = Endpoint._add_body(endpoint=result, data=data, schemas=schemas) + result, schemas = Endpoint._add_responses(endpoint=result, data=data.responses, schemas=schemas, config=config) + result, schemas = Endpoint._add_body(endpoint=result, data=data, schemas=schemas, config=config) return result, schemas @@ -282,13 +286,13 @@ class GeneratorData: title: str description: Optional[str] version: str - models: Dict[str, ModelProperty] + models: Iterator[ModelProperty] errors: List[ParseError] endpoint_collections_by_tag: Dict[str, EndpointCollection] - enums: Dict[str, EnumProperty] + enums: Iterator[EnumProperty] @staticmethod - def from_dict(d: Dict[str, Any]) -> Union["GeneratorData", GeneratorError]: + def from_dict(d: Dict[str, Any], *, config: Config) -> Union["GeneratorData", GeneratorError]: """ Create an OpenAPI from dict """ try: openapi = oai.OpenAPI.parse_obj(d) @@ -304,19 +308,22 @@ def from_dict(d: Dict[str, Any]) -> Union["GeneratorData", GeneratorError]: header="openapi-python-client only supports OpenAPI 3.x", detail=f"The version of the provided document was {openapi.openapi}", ) - if openapi.components is None or openapi.components.schemas is None: - schemas = Schemas() - else: - schemas = build_schemas(components=openapi.components.schemas) - endpoint_collections_by_tag, schemas = EndpointCollection.from_data(data=openapi.paths, schemas=schemas) - enums = schemas.enums + schemas = Schemas() + if openapi.components and openapi.components.schemas: + schemas = build_schemas(components=openapi.components.schemas, schemas=schemas, config=config) + endpoint_collections_by_tag, schemas = EndpointCollection.from_data( + data=openapi.paths, schemas=schemas, config=config + ) + + enums = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, EnumProperty)) + models = (prop for prop in schemas.classes_by_name.values() if isinstance(prop, ModelProperty)) return GeneratorData( title=openapi.info.title, description=openapi.info.description, version=openapi.info.version, endpoint_collections_by_tag=endpoint_collections_by_tag, - models=schemas.models, + models=models, errors=schemas.errors, enums=enums, ) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index e7ff2b137..b9f29557c 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -1,17 +1,28 @@ +__all__ = [ + "Class", + "EnumProperty", + "ModelProperty", + "NoneProperty", + "Property", + "Schemas", + "build_schemas", + "property_from_data", +] + from itertools import chain from typing import Any, ClassVar, Dict, Generic, Iterable, Iterator, List, Optional, Set, Tuple, TypeVar, Union import attr +from ... import Config from ... import schema as oai from ... import utils -from ..errors import PropertyError, ValidationError -from ..reference import Reference +from ..errors import ParseError, PropertyError, ValidationError from .converter import convert, convert_chain from .enum_property import EnumProperty from .model_property import ModelProperty, build_model_property from .property import Property -from .schemas import Schemas +from .schemas import Class, Schemas, parse_reference_path, update_schemas_with_data @attr.s(auto_attribs=True, frozen=True) @@ -267,6 +278,7 @@ def build_enum_property( schemas: Schemas, enum: List[Union[str, int]], parent_name: Optional[str], + config: Config, ) -> Tuple[Union[EnumProperty, PropertyError], Schemas]: """ Create an EnumProperty from schema data. @@ -278,6 +290,7 @@ def build_enum_property( schemas: The Schemas which have been defined so far (used to prevent naming collisions) enum: The enum from the provided data. Required separately here to prevent extra type checking. parent_name: The context in which this EnumProperty is defined, used to create more specific class names. + config: The global config for this run of the generator Returns: A tuple containing either the created property or a PropertyError describing what went wrong AND update schemas. @@ -286,15 +299,15 @@ def build_enum_property( class_name = data.title or name if parent_name: class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" - reference = Reference.from_ref(class_name) + class_info = Class.from_string(string=class_name, config=config) values = EnumProperty.values_from_list(enum) - if reference.class_name in schemas.enums: - existing = schemas.enums[reference.class_name] - if values != existing.values: + if class_info.name in schemas.classes_by_name: + existing = schemas.classes_by_name[class_info.name] + if not isinstance(existing, EnumProperty) or values != existing.values: return ( PropertyError( - detail=f"Found conflicting enums named {reference.class_name} with incompatible values.", data=data + detail=f"Found conflicting enums named {class_info.name} with incompatible values.", data=data ), schemas, ) @@ -309,12 +322,10 @@ def build_enum_property( if data.default is not None: inverse_values = {v: k for k, v in values.items()} try: - default = f"{reference.class_name}.{inverse_values[data.default]}" + default = f"{class_info.name}.{inverse_values[data.default]}" except KeyError: return ( - PropertyError( - detail=f"{data.default} is an invalid default for enum {reference.class_name}", data=data - ), + PropertyError(detail=f"{data.default} is an invalid default for enum {class_info.name}", data=data), schemas, ) @@ -323,21 +334,26 @@ def build_enum_property( required=required, default=default, nullable=data.nullable, - reference=reference, + class_info=class_info, values=values, value_type=value_type, ) - schemas = attr.evolve(schemas, enums={**schemas.enums, prop.reference.class_name: prop}) + schemas = attr.evolve(schemas, classes_by_name={**schemas.classes_by_name, class_info.name: prop}) return prop, schemas def build_union_property( - *, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str + *, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str, config: Config ) -> Tuple[Union[UnionProperty, PropertyError], Schemas]: sub_properties: List[Property] = [] for i, sub_prop_data in enumerate(chain(data.anyOf, data.oneOf)): sub_prop, schemas = property_from_data( - name=f"{name}_type{i}", required=required, data=sub_prop_data, schemas=schemas, parent_name=parent_name + name=f"{name}_type{i}", + required=required, + data=sub_prop_data, + schemas=schemas, + parent_name=parent_name, + config=config, ) if isinstance(sub_prop, PropertyError): return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), schemas @@ -357,12 +373,12 @@ def build_union_property( def build_list_property( - *, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str + *, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str, config: Config ) -> Tuple[Union[ListProperty[Any], PropertyError], Schemas]: if data.items is None: return PropertyError(data=data, detail="type array must have items defined"), schemas inner_prop, schemas = property_from_data( - name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name=parent_name + name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name=parent_name, config=config ) if isinstance(inner_prop, PropertyError): return PropertyError(data=inner_prop.data, detail=f"invalid data in items of array {name}"), schemas @@ -385,8 +401,10 @@ def _property_from_ref( data: oai.Reference, schemas: Schemas, ) -> Tuple[Union[Property, PropertyError], Schemas]: - reference = Reference.from_ref(data.ref) - existing = schemas.enums.get(reference.class_name) or schemas.models.get(reference.class_name) + ref_path = parse_reference_path(data.ref) + if isinstance(ref_path, ParseError): + return PropertyError(data=data, detail=ref_path.detail), schemas + existing = schemas.classes_by_reference.get(ref_path) if existing: return ( attr.evolve(existing, required=required, name=name, nullable=nullable), @@ -401,12 +419,14 @@ def _property_from_data( data: Union[oai.Reference, oai.Schema], schemas: Schemas, parent_name: str, + config: Config, ) -> Tuple[Union[Property, PropertyError], Schemas]: """ Generate a Property from the OpenAPI dictionary representation of it """ name = utils.remove_string_escapes(name) if isinstance(data, oai.Reference): return _property_from_ref(name=name, required=required, nullable=False, data=data, schemas=schemas) + # A union of a single reference should just be passed through to that reference (don't create copy class) sub_data = (data.allOf or []) + data.anyOf + data.oneOf if len(sub_data) == 1 and isinstance(sub_data[0], oai.Reference): return _property_from_ref( @@ -415,10 +435,18 @@ def _property_from_data( if data.enum: return build_enum_property( - data=data, name=name, required=required, schemas=schemas, enum=data.enum, parent_name=parent_name + data=data, + name=name, + required=required, + schemas=schemas, + enum=data.enum, + parent_name=parent_name, + config=config, ) if data.anyOf or data.oneOf: - return build_union_property(data=data, name=name, required=required, schemas=schemas, parent_name=parent_name) + return build_union_property( + data=data, name=name, required=required, schemas=schemas, parent_name=parent_name, config=config + ) if data.type == "string": return _string_based_property(name=name, required=required, data=data), schemas elif data.type == "number": @@ -452,9 +480,13 @@ def _property_from_data( schemas, ) elif data.type == "array": - return build_list_property(data=data, name=name, required=required, schemas=schemas, parent_name=parent_name) + return build_list_property( + data=data, name=name, required=required, schemas=schemas, parent_name=parent_name, config=config + ) elif data.type == "object" or data.allOf: - return build_model_property(data=data, name=name, schemas=schemas, required=required, parent_name=parent_name) + return build_model_property( + data=data, name=name, schemas=schemas, required=required, parent_name=parent_name, config=config + ) elif not data.type: return NoneProperty(name=name, required=required, nullable=False, default=None), schemas return PropertyError(data=data, detail=f"unknown type {data.type}"), schemas @@ -467,37 +499,50 @@ def property_from_data( data: Union[oai.Reference, oai.Schema], schemas: Schemas, parent_name: str, + config: Config, ) -> Tuple[Union[Property, PropertyError], Schemas]: - try: - return _property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name=parent_name) - except ValidationError: - return PropertyError(detail="Failed to validate default value", data=data), schemas + """ + Build a Property from an OpenAPI schema or reference. This Property represents a single input or output for a + generated API operation. + Args: + name: The name of the property, defined in OpenAPI as the key pointing at the schema. This is the parameter used + to send this data to an API or that the API will respond with. This will be used to generate a `python_name` + which is the name of the variable/attribute in generated Python. + required: Whether or not this property is required in whatever source is creating it. OpenAPI defines this by + including the property's name in the `required` list. If the property is required, `Unset` will not be + included in the generated code's available types. + data: The OpenAPI schema or reference that defines the details of this property (e.g. type, sub-properties). + schemas: A structure containing all of the parsed schemas so far that will become generated classes. This is + used to resolve references and to ensure that conflicting class names are not generated. + parent_name: The name of the thing above this property, prepended to generated class names to reduce the chance + of duplication. + config: Contains the parsed config that the user provided to tweak generation settings. Needed to apply class + name overrides for generated classes. -def update_schemas_with_data(name: str, data: oai.Schema, schemas: Schemas) -> Union[Schemas, PropertyError]: - prop: Union[PropertyError, ModelProperty, EnumProperty] - if data.enum is not None: - prop, schemas = build_enum_property( - data=data, name=name, required=True, schemas=schemas, enum=data.enum, parent_name=None + Returns: + A tuple containing either the parsed Property or a PropertyError (if something went wrong) and the updated + Schemas (including any new classes that should be generated). + """ + try: + return _property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name=parent_name, config=config ) - else: - prop, schemas = build_model_property(data=data, name=name, schemas=schemas, required=True, parent_name=None) - if isinstance(prop, PropertyError): - return prop - else: - return schemas + except ValidationError: + return PropertyError(detail="Failed to validate default value", data=data), schemas -def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) -> Schemas: +def build_schemas( + *, components: Dict[str, Union[oai.Reference, oai.Schema]], schemas: Schemas, config: Config +) -> Schemas: """ Get a list of Schemas from an OpenAPI dict """ - schemas = Schemas() to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Schema]]] = components.items() - processing = True + still_making_progress = True errors: List[PropertyError] = [] # References could have forward References so keep going as long as we are making progress - while processing: - processing = False + while still_making_progress: + still_making_progress = False errors = [] next_round = [] # Only accumulate errors from the last round, since we might fix some along the way @@ -505,13 +550,17 @@ def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) -> if isinstance(data, oai.Reference): schemas.errors.append(PropertyError(data=data, detail="Reference schemas are not supported.")) continue - schemas_or_err = update_schemas_with_data(name, data, schemas) + ref_path = parse_reference_path(f"#/components/schemas/{name}") + if isinstance(ref_path, ParseError): + schemas.errors.append(PropertyError(detail=ref_path.detail, data=data)) + continue + schemas_or_err = update_schemas_with_data(ref_path=ref_path, data=data, schemas=schemas, config=config) if isinstance(schemas_or_err, PropertyError): next_round.append((name, data)) errors.append(schemas_or_err) - else: - schemas = schemas_or_err - processing = True # We made some progress this round, do another after it's done + continue + schemas = schemas_or_err + still_making_progress = True to_process = next_round schemas.errors.extend(errors) diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py index fca4fc881..9e62643f1 100644 --- a/openapi_python_client/parser/properties/enum_property.py +++ b/openapi_python_client/parser/properties/enum_property.py @@ -5,8 +5,8 @@ import attr from ... import utils -from ..reference import Reference from .property import Property +from .schemas import Class ValueType = Union[str, int] @@ -16,14 +16,14 @@ class EnumProperty(Property): """ A property that should use an enum """ values: Dict[str, ValueType] - reference: Reference + class_info: Class value_type: Type[ValueType] default: Optional[Any] = attr.ib() template: ClassVar[str] = "enum_property.py.jinja" def get_base_type_string(self, json: bool = False) -> str: - return self.reference.class_name + return self.class_info.name def get_base_json_type_string(self, json: bool = False) -> str: return self.value_type.__name__ @@ -37,7 +37,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: back to the root of the generated client. """ imports = super().get_imports(prefix=prefix) - imports.add(f"from {prefix}models.{self.reference.module_name} import {self.reference.class_name}") + imports.add(f"from {prefix}models.{self.class_info.module_name} import {self.class_info.name}") return imports @staticmethod diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 35717e8ba..83d824d8a 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -3,19 +3,19 @@ import attr +from ... import Config from ... import schema as oai from ... import utils -from ..errors import PropertyError -from ..reference import Reference +from ..errors import ParseError, PropertyError from .property import Property -from .schemas import Schemas +from .schemas import Class, Schemas, parse_reference_path @attr.s(auto_attribs=True, frozen=True) class ModelProperty(Property): """ A property which refers to another Schema """ - reference: Reference + class_info: Class required_properties: List[Property] optional_properties: List[Property] description: str @@ -27,7 +27,7 @@ class ModelProperty(Property): json_is_dict: ClassVar[bool] = True def get_base_type_string(self, json: bool = False) -> str: - return self.reference.class_name + return self.class_info.name def get_imports(self, *, prefix: str) -> Set[str]: """ @@ -40,7 +40,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports = super().get_imports(prefix=prefix) imports.update( { - f"from {prefix}models.{self.reference.module_name} import {self.reference.class_name}", + f"from {prefix}models.{self.class_info.module_name} import {self.class_info.name}", "from typing import Dict", "from typing import cast", } @@ -67,7 +67,9 @@ class _PropertyData(NamedTuple): schemas: Schemas -def _process_properties(*, data: oai.Schema, schemas: Schemas, class_name: str) -> Union[_PropertyData, PropertyError]: +def _process_properties( + *, data: oai.Schema, schemas: Schemas, class_name: str, config: Config +) -> Union[_PropertyData, PropertyError]: from . import property_from_data properties: Dict[str, Property] = {} @@ -88,10 +90,14 @@ def _check_existing(prop: Property) -> Union[Property, PropertyError]: unprocessed_props = data.properties or {} for sub_prop in data.allOf or []: if isinstance(sub_prop, oai.Reference): - source_name = Reference.from_ref(sub_prop.ref).class_name - sub_model = schemas.models.get(source_name) + ref_path = parse_reference_path(sub_prop.ref) + if isinstance(ref_path, ParseError): + return PropertyError(detail=ref_path.detail, data=sub_prop) + sub_model = schemas.classes_by_reference.get(ref_path) if sub_model is None: return PropertyError(f"Reference {sub_prop.ref} not found") + if not isinstance(sub_model, ModelProperty): + return PropertyError("Cannot take allOf a non-object") for prop in chain(sub_model.required_properties, sub_model.optional_properties): prop_or_error = _check_existing(prop) if isinstance(prop_or_error, PropertyError): @@ -103,7 +109,7 @@ def _check_existing(prop: Property) -> Union[Property, PropertyError]: for key, value in unprocessed_props.items(): prop_required = key in required_set prop_or_error, schemas = property_from_data( - name=key, required=prop_required, data=value, schemas=schemas, parent_name=class_name + name=key, required=prop_required, data=value, schemas=schemas, parent_name=class_name, config=config ) if isinstance(prop_or_error, Property): prop_or_error = _check_existing(prop_or_error) @@ -130,7 +136,11 @@ def _check_existing(prop: Property) -> Union[Property, PropertyError]: def _get_additional_properties( - *, schema_additional: Union[None, bool, oai.Reference, oai.Schema], schemas: Schemas, class_name: str + *, + schema_additional: Union[None, bool, oai.Reference, oai.Schema], + schemas: Schemas, + class_name: str, + config: Config, ) -> Tuple[Union[bool, Property, PropertyError], Schemas]: from . import property_from_data @@ -150,12 +160,13 @@ def _get_additional_properties( data=schema_additional, schemas=schemas, parent_name=class_name, + config=config, ) return additional_properties, schemas def build_model_property( - *, data: oai.Schema, name: str, schemas: Schemas, required: bool, parent_name: Optional[str] + *, data: oai.Schema, name: str, schemas: Schemas, required: bool, parent_name: Optional[str], config: Config ) -> Tuple[Union[ModelProperty, PropertyError], Schemas]: """ A single ModelProperty from its OAI data @@ -167,19 +178,20 @@ def build_model_property( schemas: Existing Schemas which have already been processed (to check name conflicts) required: Whether or not this property is required by the parent (affects typing) parent_name: The name of the property that this property is inside of (affects class naming) + config: Config data for this run of the generator, used to modifying names """ class_name = data.title or name if parent_name: class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" - ref = Reference.from_ref(class_name) + class_info = Class.from_string(string=class_name, config=config) - property_data = _process_properties(data=data, schemas=schemas, class_name=class_name) + property_data = _process_properties(data=data, schemas=schemas, class_name=class_name, config=config) if isinstance(property_data, PropertyError): return property_data, schemas schemas = property_data.schemas additional_properties, schemas = _get_additional_properties( - schema_additional=data.additionalProperties, schemas=schemas, class_name=class_name + schema_additional=data.additionalProperties, schemas=schemas, class_name=class_name, config=config ) if isinstance(additional_properties, Property): property_data.relative_imports.update(additional_properties.get_imports(prefix="..")) @@ -187,7 +199,7 @@ def build_model_property( return additional_properties, schemas prop = ModelProperty( - reference=ref, + class_info=class_info, required_properties=property_data.required_props, optional_properties=property_data.optional_props, relative_imports=property_data.relative_imports, @@ -198,11 +210,9 @@ def build_model_property( name=name, additional_properties=additional_properties, ) - if prop.reference.class_name in schemas.models: - error = PropertyError( - data=data, detail=f'Attempted to generate duplicate models with name "{prop.reference.class_name}"' - ) + if class_info.name in schemas.classes_by_name: + error = PropertyError(data=data, detail=f'Attempted to generate duplicate models with name "{class_info.name}"') return error, schemas - schemas = attr.evolve(schemas, models={**schemas.models, prop.reference.class_name: prop}) + schemas = attr.evolve(schemas, classes_by_name={**schemas.classes_by_name, class_info.name: prop}) return prop, schemas diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index c30f6a059..b4e9140b4 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -1,10 +1,14 @@ -__all__ = ["Schemas"] +__all__ = ["Class", "Schemas", "parse_reference_path", "update_schemas_with_data"] -from typing import TYPE_CHECKING, Dict, List +from typing import TYPE_CHECKING, Dict, List, NewType, Union, cast +from urllib.parse import urlparse import attr -from ..errors import ParseError +from ... import Config +from ... import schema as oai +from ... import utils +from ..errors import ParseError, PropertyError if TYPE_CHECKING: # pragma: no cover from .enum_property import EnumProperty @@ -14,10 +18,66 @@ ModelProperty = "ModelProperty" +_ReferencePath = NewType("_ReferencePath", str) +_ClassName = NewType("_ClassName", str) + + +def parse_reference_path(ref_path_raw: str) -> Union[_ReferencePath, ParseError]: + parsed = urlparse(ref_path_raw) + if parsed.scheme or parsed.path: + return ParseError(detail=f"Remote references such as {ref_path_raw} are not supported yet.") + return cast(_ReferencePath, parsed.fragment) + + +@attr.s(auto_attribs=True, frozen=True) +class Class: + """ Represents Python class which will be generated from an OpenAPI schema """ + + name: _ClassName + module_name: str + + @staticmethod + def from_string(*, string: str, config: Config) -> "Class": + """ Get a Class from an arbitrary string """ + class_name = string.split("/")[-1] # Get rid of ref path stuff + class_name = utils.pascal_case(class_name) + override = config.class_overrides.get(class_name) + + if override is not None and override.class_name is not None: + class_name = override.class_name + + if override is not None and override.module_name is not None: + module_name = override.module_name + else: + module_name = utils.snake_case(class_name) + + return Class(name=cast(_ClassName, class_name), module_name=module_name) + + @attr.s(auto_attribs=True, frozen=True) class Schemas: - """ Structure for containing all defined, shareable, and resuabled schemas (attr classes and Enums) """ + """ Structure for containing all defined, shareable, and reusable schemas (attr classes and Enums) """ - enums: Dict[str, EnumProperty] = attr.ib(factory=dict) - models: Dict[str, ModelProperty] = attr.ib(factory=dict) + classes_by_reference: Dict[_ReferencePath, Union[EnumProperty, ModelProperty]] = attr.ib(factory=dict) + classes_by_name: Dict[_ClassName, Union[EnumProperty, ModelProperty]] = attr.ib(factory=dict) errors: List[ParseError] = attr.ib(factory=list) + + +def update_schemas_with_data( + *, ref_path: _ReferencePath, data: oai.Schema, schemas: Schemas, config: Config +) -> Union[Schemas, PropertyError]: + from . import build_enum_property, build_model_property + + prop: Union[PropertyError, ModelProperty, EnumProperty] + if data.enum is not None: + prop, schemas = build_enum_property( + data=data, name=ref_path, required=True, schemas=schemas, enum=data.enum, parent_name=None, config=config + ) + else: + prop, schemas = build_model_property( + data=data, name=ref_path, schemas=schemas, required=True, parent_name=None, config=config + ) + if isinstance(prop, PropertyError): + return prop + schemas = attr.evolve(schemas, classes_by_reference={ref_path: prop, **schemas.classes_by_reference}) + return schemas diff --git a/openapi_python_client/parser/reference.py b/openapi_python_client/parser/reference.py deleted file mode 100644 index 809d24441..000000000 --- a/openapi_python_client/parser/reference.py +++ /dev/null @@ -1,27 +0,0 @@ -""" A Reference is ultimately a Class which will be in models, usually defined in a body input or return type """ - -from dataclasses import dataclass -from typing import Dict - -from .. import utils - -class_overrides: Dict[str, "Reference"] = {} - - -@dataclass -class Reference: - """ A reference to a class which will be in models """ - - class_name: str - module_name: str - - @staticmethod - def from_ref(ref: str) -> "Reference": - """ Get a Reference from the openapi #/schemas/blahblah string """ - ref_value = ref.split("/")[-1] - class_name = utils.pascal_case(ref_value) - - if class_name in class_overrides: - return class_overrides[class_name] - - return Reference(class_name=class_name, module_name=utils.snake_case(class_name)) diff --git a/openapi_python_client/parser/responses.py b/openapi_python_client/parser/responses.py index 3d01a0eab..a7d5e89d7 100644 --- a/openapi_python_client/parser/responses.py +++ b/openapi_python_client/parser/responses.py @@ -4,6 +4,7 @@ import attr +from .. import Config from .. import schema as oai from .errors import ParseError, PropertyError from .properties import NoneProperty, Property, Schemas, property_from_data @@ -27,22 +28,28 @@ class Response: def empty_response(status_code: int, response_name: str) -> Response: + """ Return an empty response, for when no response type is defined """ return Response( status_code=status_code, - prop=NoneProperty(name=response_name, default=None, nullable=False, required=True), + prop=NoneProperty( + name=response_name, + default=None, + nullable=False, + required=True, + ), source="None", ) def response_from_data( - *, status_code: int, data: Union[oai.Response, oai.Reference], schemas: Schemas, parent_name: str + *, status_code: int, data: Union[oai.Response, oai.Reference], schemas: Schemas, parent_name: str, config: Config ) -> Tuple[Union[Response, ParseError], Schemas]: """ Generate a Response from the OpenAPI dictionary representation of it """ response_name = f"response_{status_code}" if isinstance(data, oai.Reference) or data.content is None: return ( - empty_response(status_code, response_name), + empty_response(status_code=status_code, response_name=response_name), schemas, ) @@ -67,6 +74,7 @@ def response_from_data( data=schema_data, schemas=schemas, parent_name=parent_name, + config=config, ) if isinstance(prop, PropertyError): diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index e822ff320..66d6209b3 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -85,12 +85,12 @@ client: Client, {{ parameter.to_string() }}, {% endfor %} {# Form data if any #} -{% if endpoint.form_body_reference %} -form_data: {{ endpoint.form_body_reference.class_name }}, +{% if endpoint.form_body_class %} +form_data: {{ endpoint.form_body_class.name }}, {% endif %} {# Multipart data if any #} -{% if endpoint.multipart_body_reference %} -multipart_data: {{ endpoint.multipart_body_reference.class_name }}, +{% if endpoint.multipart_body_class %} +multipart_data: {{ endpoint.multipart_body_class.name }}, {% endif %} {# JSON body if any #} {% if endpoint.json_body %} @@ -115,10 +115,10 @@ client=client, {% for parameter in endpoint.path_parameters %} {{ parameter.python_name }}={{ parameter.python_name }}, {% endfor %} -{% if endpoint.form_body_reference %} +{% if endpoint.form_body_class %} form_data=form_data, {% endif %} -{% if endpoint.multipart_body_reference %} +{% if endpoint.multipart_body_class %} multipart_data=multipart_data, {% endif %} {% if endpoint.json_body %} diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 174d67b9d..06876141a 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -4,7 +4,7 @@ import httpx from attr import asdict from ...client import AuthenticatedClient, Client -from ...types import Response, UNSET{% if endpoint.multipart_body_reference %}, File {% endif %} +from ...types import Response, UNSET{% if endpoint.multipart_body_class %}, File {% endif %} {% for relative in endpoint.relative_imports %} {{ relative }} @@ -36,7 +36,7 @@ def _get_kwargs( {{ json_body(endpoint) | indent(4) }} - {% if endpoint.multipart_body_reference %} + {% if endpoint.multipart_body_class %} files = {} data = {} for key, value in multipart_data.to_dict().items(): @@ -51,9 +51,9 @@ def _get_kwargs( "headers": headers, "cookies": cookies, "timeout": client.get_timeout(), - {% if endpoint.form_body_reference %} + {% if endpoint.form_body_class %} "data": asdict(form_data), - {% elif endpoint.multipart_body_reference %} + {% elif endpoint.multipart_body_class %} "files": files, "data": data, {% elif endpoint.json_body %} diff --git a/openapi_python_client/templates/int_enum.py.jinja b/openapi_python_client/templates/int_enum.py.jinja index 18d6066ae..a508f1c8e 100644 --- a/openapi_python_client/templates/int_enum.py.jinja +++ b/openapi_python_client/templates/int_enum.py.jinja @@ -1,6 +1,6 @@ from enum import IntEnum -class {{ enum.reference.class_name }}(IntEnum): +class {{ enum.class_info.name }}(IntEnum): {% for key, value in enum.values.items() %} {{ key }} = {{ value }} {% endfor %} diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index c286489e3..8541db32d 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -18,10 +18,13 @@ from ..types import UNSET, Unset {% set additional_property_type = 'Any' if model.additional_properties == True else model.additional_properties.get_type_string() %} {% endif %} -T = TypeVar("T", bound="{{ model.reference.class_name }}") +{% set class_name = model.class_info.name %} +{% set module_name = model.class_info.module_name %} + +T = TypeVar("T", bound="{{ class_name }}") @attr.s(auto_attribs=True) -class {{ model.reference.class_name }}: +class {{ class_name }}: """ {{ model.description }} """ {% for property in model.required_properties + model.optional_properties %} {% if property.default is none and property.required %} @@ -91,7 +94,7 @@ class {{ model.reference.class_name }}: {% endif %} {% endfor %} - {{model.reference.module_name}} = cls( + {{ module_name }} = cls( {% for property in model.required_properties + model.optional_properties %} {{ property.python_name }}={{ property.python_name }}, {% endfor %} @@ -105,12 +108,12 @@ class {{ model.reference.class_name }}: {{ construct(model.additional_properties, "prop_dict") | indent(12) }} additional_properties[prop_name] = {{ model.additional_properties.python_name }} - {{model.reference.module_name}}.additional_properties = additional_properties + {{ module_name }}.additional_properties = additional_properties {% else %} - {{model.reference.module_name}}.additional_properties = d + {{ module_name }}.additional_properties = d {% endif %} {% endif %} - return {{model.reference.module_name}} + return {{ module_name }} {% if model.additional_properties %} @property diff --git a/openapi_python_client/templates/property_templates/enum_property.py.jinja b/openapi_python_client/templates/property_templates/enum_property.py.jinja index 4916927bd..9dd051b38 100644 --- a/openapi_python_client/templates/property_templates/enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/enum_property.py.jinja @@ -1,5 +1,5 @@ {% macro construct_function(property, source) %} -{{ property.reference.class_name }}({{ source }}) +{{ property.class_info.name }}({{ source }}) {% endmacro %} {% from "property_templates/property_macros.py.jinja" import construct_template %} diff --git a/openapi_python_client/templates/property_templates/model_property.py.jinja b/openapi_python_client/templates/property_templates/model_property.py.jinja index 4e394cc34..2772918cf 100644 --- a/openapi_python_client/templates/property_templates/model_property.py.jinja +++ b/openapi_python_client/templates/property_templates/model_property.py.jinja @@ -1,5 +1,5 @@ {% macro construct_function(property, source) %} -{{ property.reference.class_name }}.from_dict({{ source }}) +{{ property.class_info.name }}.from_dict({{ source }}) {% endmacro %} {% from "property_templates/property_macros.py.jinja" import construct_template %} diff --git a/openapi_python_client/templates/str_enum.py.jinja b/openapi_python_client/templates/str_enum.py.jinja index 74dcd1de2..4a9ab384a 100644 --- a/openapi_python_client/templates/str_enum.py.jinja +++ b/openapi_python_client/templates/str_enum.py.jinja @@ -1,6 +1,6 @@ from enum import Enum -class {{ enum.reference.class_name }}(str, Enum): +class {{ enum.class_info.name }}(str, Enum): {% for key, value in enum.values.items() %} {{ key }} = "{{ value }}" {% endfor %} diff --git a/poetry.lock b/poetry.lock index d7352752e..4b92307fc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -687,6 +687,7 @@ autoflake = [ {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"}, ] black = [ + {file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"}, {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] certifi = [ @@ -821,39 +822,20 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mccabe = [ @@ -1028,12 +1010,6 @@ regex = [ {file = "regex-2020.9.27-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637"}, {file = "regex-2020.9.27-cp38-cp38-win32.whl", hash = "sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f"}, {file = "regex-2020.9.27-cp38-cp38-win_amd64.whl", hash = "sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c"}, - {file = "regex-2020.9.27-cp39-cp39-manylinux1_i686.whl", hash = "sha256:84cada8effefe9a9f53f9b0d2ba9b7b6f5edf8d2155f9fdbe34616e06ececf81"}, - {file = "regex-2020.9.27-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:816064fc915796ea1f26966163f6845de5af78923dfcecf6551e095f00983650"}, - {file = "regex-2020.9.27-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:5d892a4f1c999834eaa3c32bc9e8b976c5825116cde553928c4c8e7e48ebda67"}, - {file = "regex-2020.9.27-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c9443124c67b1515e4fe0bb0aa18df640965e1030f468a2a5dc2589b26d130ad"}, - {file = "regex-2020.9.27-cp39-cp39-win32.whl", hash = "sha256:49f23ebd5ac073765ecbcf046edc10d63dcab2f4ae2bce160982cb30df0c0302"}, - {file = "regex-2020.9.27-cp39-cp39-win_amd64.whl", hash = "sha256:3d20024a70b97b4f9546696cbf2fd30bae5f42229fbddf8661261b1eaff0deb7"}, {file = "regex-2020.9.27.tar.gz", hash = "sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d"}, ] requests = [ @@ -1079,28 +1055,19 @@ typed-ast = [ {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, - {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, - {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typer = [ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..967640ff2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,58 @@ +from typing import Callable + +import pytest + +from openapi_python_client.parser.properties import EnumProperty, ModelProperty + + +@pytest.fixture +def model_property_factory() -> Callable[..., ModelProperty]: + """ + This fixture surfaces in the test as a function which manufactures ModelProperties with defaults. + + You can pass the same params into this as the ModelProperty constructor to override defaults. + """ + from openapi_python_client.parser.properties import Class + + def _factory(**kwargs): + kwargs = { + "name": "", + "description": "", + "required": True, + "nullable": True, + "default": None, + "class_info": Class(name="", module_name=""), + "required_properties": [], + "optional_properties": [], + "relative_imports": set(), + "additional_properties": False, + **kwargs, + } + return ModelProperty(**kwargs) + + return _factory + + +@pytest.fixture +def enum_property_factory() -> Callable[..., EnumProperty]: + """ + This fixture surfaces in the test as a function which manufactures EnumProperties with defaults. + + You can pass the same params into this as the EnumProerty constructor to override defaults. + """ + from openapi_python_client.parser.properties import Class + + def _factory(**kwargs): + kwargs = { + "name": "test", + "required": True, + "nullable": False, + "default": None, + "class_info": Class(name="", module_name=""), + "values": {}, + "value_type": str, + **kwargs, + } + return EnumProperty(**kwargs) + + return _factory diff --git a/tests/test___init__.py b/tests/test___init__.py index 3d2547d89..0579e83f0 100644 --- a/tests/test___init__.py +++ b/tests/test___init__.py @@ -5,7 +5,7 @@ import pytest import yaml -from openapi_python_client import GeneratorError +from openapi_python_client import Config, GeneratorError def test__get_project_for_url_or_path(mocker): @@ -16,15 +16,16 @@ def test__get_project_for_url_or_path(mocker): _Project = mocker.patch("openapi_python_client.Project") url = mocker.MagicMock() path = mocker.MagicMock() + config = mocker.MagicMock() from openapi_python_client import MetaType, _get_project_for_url_or_path - project = _get_project_for_url_or_path(url=url, path=path, meta=MetaType.POETRY) + project = _get_project_for_url_or_path(url=url, path=path, meta=MetaType.POETRY, config=config) _get_document.assert_called_once_with(url=url, path=path) - from_dict.assert_called_once_with(data_dict) + from_dict.assert_called_once_with(data_dict, config=config) _Project.assert_called_once_with( - openapi=openapi, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + openapi=openapi, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8", config=config ) assert project == _Project.return_value @@ -37,13 +38,14 @@ def test__get_project_for_url_or_path_generator_error(mocker): _Project = mocker.patch("openapi_python_client.Project") url = mocker.MagicMock() path = mocker.MagicMock() + config = mocker.MagicMock() from openapi_python_client import MetaType, _get_project_for_url_or_path - project = _get_project_for_url_or_path(url=url, path=path, meta=MetaType.POETRY) + project = _get_project_for_url_or_path(url=url, path=path, meta=MetaType.POETRY, config=config) _get_document.assert_called_once_with(url=url, path=path) - from_dict.assert_called_once_with(data_dict) + from_dict.assert_called_once_with(data_dict, config=config) _Project.assert_not_called() assert project == error @@ -58,7 +60,7 @@ def test__get_project_for_url_or_path_document_error(mocker): from openapi_python_client import MetaType, _get_project_for_url_or_path - project = _get_project_for_url_or_path(url=url, path=path, meta=MetaType.POETRY) + project = _get_project_for_url_or_path(url=url, path=path, meta=MetaType.POETRY, config=Config()) _get_document.assert_called_once_with(url=url, path=path) from_dict.assert_not_called() @@ -72,13 +74,14 @@ def test_create_new_client(mocker): ) url = mocker.MagicMock() path = mocker.MagicMock() + config = mocker.MagicMock() from openapi_python_client import MetaType, create_new_client - result = create_new_client(url=url, path=path, meta=MetaType.POETRY) + result = create_new_client(url=url, path=path, meta=MetaType.POETRY, config=config) _get_project_for_url_or_path.assert_called_once_with( - url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8", config=config ) project.build.assert_called_once() assert result == project.build.return_value @@ -91,13 +94,14 @@ def test_create_new_client_project_error(mocker): ) url = mocker.MagicMock() path = mocker.MagicMock() + config = mocker.MagicMock() from openapi_python_client import MetaType, create_new_client - result = create_new_client(url=url, path=path, meta=MetaType.POETRY) + result = create_new_client(url=url, path=path, meta=MetaType.POETRY, config=config) _get_project_for_url_or_path.assert_called_once_with( - url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8", config=config ) assert result == [error] @@ -109,13 +113,14 @@ def test_update_existing_client(mocker): ) url = mocker.MagicMock() path = mocker.MagicMock() + config = mocker.MagicMock() from openapi_python_client import MetaType, update_existing_client - result = update_existing_client(url=url, path=path, meta=MetaType.POETRY) + result = update_existing_client(url=url, path=path, meta=MetaType.POETRY, config=config) _get_project_for_url_or_path.assert_called_once_with( - url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8", config=config ) project.update.assert_called_once() assert result == project.update.return_value @@ -128,13 +133,14 @@ def test_update_existing_client_project_error(mocker): ) url = mocker.MagicMock() path = mocker.MagicMock() + config = mocker.MagicMock() from openapi_python_client import MetaType, update_existing_client - result = update_existing_client(url=url, path=path, meta=MetaType.POETRY) + result = update_existing_client(url=url, path=path, meta=MetaType.POETRY, config=config) _get_project_for_url_or_path.assert_called_once_with( - url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8", config=config ) assert result == [error] @@ -225,13 +231,23 @@ def test__get_document_bad_yaml(self, mocker): assert result == GeneratorError(header="Invalid YAML from provided source") +def make_project(**kwargs): + from unittest.mock import MagicMock + + from openapi_python_client import MetaType, Project + + kwargs = {"openapi": MagicMock(title="My Test API"), "meta": MetaType.POETRY, "config": Config(), **kwargs} + + return Project(**kwargs) + + class TestProject: def test___init__(self, mocker): openapi = mocker.MagicMock(title="My Test API") from openapi_python_client import MetaType, Project - project = Project(openapi=openapi, meta=MetaType.POETRY) + project = Project(openapi=openapi, meta=MetaType.POETRY, config=Config()) assert project.openapi == openapi assert project.project_name == "my-test-api-client" @@ -246,37 +262,41 @@ def test___init___no_meta(self, mocker): from openapi_python_client import MetaType, Project - project = Project(openapi=openapi, meta=MetaType.NONE) + project = Project(openapi=openapi, meta=MetaType.NONE, config=Config()) assert project.openapi == openapi - assert project.project_name == "my-test-api-client" - assert project.package_name == "my_test_api_client" assert project.package_description == "A client library for accessing My Test API" assert project.meta == MetaType.NONE assert project.project_dir == pathlib.Path.cwd() assert project.package_dir == pathlib.Path.cwd() / project.package_name - def test_project_and_package_name_overrides(self, mocker): + @pytest.mark.parametrize( + "project_override, package_override, expected_project_name, expected_package_name", + ( + (None, None, "my-test-api-client", "my_test_api_client"), + ("custom-project", None, "custom-project", "custom_project"), + ("custom-project", "custom_package", "custom-project", "custom_package"), + (None, "custom_package", "my-test-api-client", "custom_package"), + ), + ) + def test_project_and_package_names( + self, mocker, project_override, package_override, expected_project_name, expected_package_name + ): openapi = mocker.MagicMock(title="My Test API") from openapi_python_client import MetaType, Project - Project.project_name_override = "my-special-project-name" - project = Project(openapi=openapi, meta=MetaType.POETRY) - - assert project.project_name == "my-special-project-name" - assert project.package_name == "my_special_project_name" - - Project.package_name_override = "my_special_package_name" - project = Project(openapi=openapi, meta=MetaType.POETRY) + project = Project( + openapi=openapi, + meta=MetaType.POETRY, + config=Config(project_name_override=project_override, package_name_override=package_override), + ) - assert project.project_name == "my-special-project-name" - assert project.package_name == "my_special_package_name" + assert project.project_name == expected_project_name + assert project.package_name == expected_package_name def test_build(self, mocker): - from openapi_python_client import MetaType, Project - - project = Project(openapi=mocker.MagicMock(title="My Test API"), meta=MetaType.POETRY) + project = make_project() project.project_dir = mocker.MagicMock() project.package_dir = mocker.MagicMock() project._build_metadata = mocker.MagicMock() @@ -298,9 +318,9 @@ def test_build(self, mocker): assert result == project._get_errors.return_value def test_build_no_meta(self, mocker): - from openapi_python_client import MetaType, Project + from openapi_python_client import MetaType - project = Project(openapi=mocker.MagicMock(title="My Test API"), meta=MetaType.NONE) + project = make_project(meta=MetaType.NONE) project.project_dir = mocker.MagicMock() project.package_dir = mocker.MagicMock() project._build_metadata = mocker.MagicMock() @@ -315,9 +335,7 @@ def test_build_no_meta(self, mocker): project.project_dir.mkdir.assert_not_called() def test_build_file_exists(self, mocker): - from openapi_python_client import MetaType, Project - - project = Project(openapi=mocker.MagicMock(title="My Test API"), meta=MetaType.POETRY) + project = make_project() project.project_dir = mocker.MagicMock() project.project_dir.mkdir.side_effect = FileExistsError result = project.build() @@ -327,10 +345,10 @@ def test_build_file_exists(self, mocker): assert result == [GeneratorError(detail="Directory already exists. Delete it or use the update command.")] def test_update(self, mocker): - from openapi_python_client import MetaType, Project, shutil + from openapi_python_client import shutil rmtree = mocker.patch.object(shutil, "rmtree") - project = Project(openapi=mocker.MagicMock(title="My Test API"), meta=MetaType.POETRY) + project = make_project() project.package_dir = mocker.MagicMock() project._build_metadata = mocker.MagicMock() project._build_models = mocker.MagicMock() @@ -350,9 +368,7 @@ def test_update(self, mocker): assert result == project._get_errors.return_value def test_update_missing_dir(self, mocker): - from openapi_python_client import MetaType, Project - - project = Project(openapi=mocker.MagicMock(title="My Test API"), meta=MetaType.POETRY) + project = make_project() project.package_dir = mocker.MagicMock() project.package_dir.is_dir.return_value = False project._build_models = mocker.MagicMock() @@ -364,9 +380,7 @@ def test_update_missing_dir(self, mocker): project._build_models.assert_not_called() def test__build_metadata_poetry(self, mocker): - from openapi_python_client import MetaType, Project - - project = Project(openapi=mocker.MagicMock(title="My Test API"), meta=MetaType.POETRY) + project = make_project() project._build_pyproject_toml = mocker.MagicMock() project.project_dir = mocker.MagicMock() readme_path = mocker.MagicMock(autospec=pathlib.Path) @@ -400,9 +414,9 @@ def test__build_metadata_poetry(self, mocker): project._build_pyproject_toml.assert_called_once_with(use_poetry=True) def test__build_metadata_setup(self, mocker): - from openapi_python_client import MetaType, Project + from openapi_python_client import MetaType - project = Project(openapi=mocker.MagicMock(title="My Test API"), meta=MetaType.SETUP) + project = make_project(meta=MetaType.SETUP) project._build_pyproject_toml = mocker.MagicMock() project._build_setup_py = mocker.MagicMock() project.project_dir = mocker.MagicMock() @@ -438,9 +452,9 @@ def test__build_metadata_setup(self, mocker): project._build_setup_py.assert_called_once() def test__build_metadata_none(self, mocker): - from openapi_python_client import MetaType, Project + from openapi_python_client import MetaType - project = Project(openapi=mocker.MagicMock(title="My Test API"), meta=MetaType.NONE) + project = make_project(meta=MetaType.NONE) project._build_pyproject_toml = mocker.MagicMock() project._build_metadata() @@ -449,9 +463,7 @@ def test__build_metadata_none(self, mocker): @pytest.mark.parametrize("use_poetry", [(True,), (False,)]) def test__build_pyproject_toml(self, mocker, use_poetry): - from openapi_python_client import MetaType, Project - - project = Project(openapi=mocker.MagicMock(title="My Test API"), meta=MetaType.POETRY) + project = make_project() project.project_dir = mocker.MagicMock() pyproject_path = mocker.MagicMock(autospec=pathlib.Path) paths = { @@ -480,9 +492,7 @@ def test__build_pyproject_toml(self, mocker, use_poetry): pyproject_path.write_text.assert_called_once_with(pyproject_template.render(), encoding="utf-8") def test__build_setup_py(self, mocker): - from openapi_python_client import MetaType, Project - - project = Project(openapi=mocker.MagicMock(title="My Test API"), meta=MetaType.SETUP) + project = make_project() project.project_dir = mocker.MagicMock() setup_path = mocker.MagicMock(autospec=pathlib.Path) paths = { @@ -513,11 +523,8 @@ def test__build_setup_py(self, mocker): def test__reformat(mocker): import subprocess - from openapi_python_client import GeneratorData, MetaType, Project - sub_run = mocker.patch("subprocess.run") - openapi = mocker.MagicMock(autospec=GeneratorData, title="My Test API") - project = Project(openapi=openapi, meta=MetaType.POETRY) + project = make_project() project.project_dir = mocker.MagicMock(autospec=pathlib.Path) project._reformat() @@ -556,30 +563,27 @@ def test__get_errors(mocker): }, errors=[3], ) - project = Project(openapi=openapi, meta=MetaType.POETRY) + project = Project(openapi=openapi, meta=MetaType.POETRY, config=Config()) assert project._get_errors() == [1, 2, 3] def test__custom_templates(mocker): from openapi_python_client import GeneratorData, MetaType, Project - from openapi_python_client.parser.openapi import EndpointCollection, Schemas openapi = mocker.MagicMock( autospec=GeneratorData, title="My Test API", - endpoint_collections_by_tag={ - "default": mocker.MagicMock(autospec=EndpointCollection, parse_errors=[1]), - "other": mocker.MagicMock(autospec=EndpointCollection, parse_errors=[2]), - }, - schemas=mocker.MagicMock(autospec=Schemas, errors=[3]), ) - project = Project(openapi=openapi, meta=MetaType.POETRY) + project = Project(openapi=openapi, meta=MetaType.POETRY, config=Config()) assert isinstance(project.env.loader, jinja2.PackageLoader) project = Project( - openapi=openapi, custom_template_path="../end_to_end_tests/test_custom_templates", meta=MetaType.POETRY + openapi=openapi, + custom_template_path="../end_to_end_tests/test_custom_templates", + meta=MetaType.POETRY, + config=Config(), ) assert isinstance(project.env.loader, jinja2.ChoiceLoader) assert len(project.env.loader.loaders) == 2 diff --git a/tests/test_cli.py b/tests/test_cli.py index d5bc2c69e..c551e80dd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,6 +4,7 @@ import pytest from typer.testing import CliRunner +from openapi_python_client import Config from openapi_python_client.parser.errors import GeneratorError, ParseError runner = CliRunner() @@ -35,14 +36,19 @@ def test_config_arg(mocker, _create_new_client): result = runner.invoke( app, - [f"--config={config_path}", "generate", f"--path={path}", f"--file-encoding={file_encoding}"], + ["generate", f"--config={config_path}", f"--path={path}", f"--file-encoding={file_encoding}"], catch_exceptions=False, ) assert result.exit_code == 0 load_config.assert_called_once_with(path=Path(config_path)) _create_new_client.assert_called_once_with( - url=None, path=Path(path), custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + url=None, + path=Path(path), + custom_template_path=None, + meta=MetaType.POETRY, + file_encoding="utf-8", + config=load_config.return_value, ) @@ -55,7 +61,7 @@ def test_bad_config(mocker, _create_new_client): config_path = "config/path" path = "cool/path" - result = runner.invoke(app, [f"--config={config_path}", "generate", f"--path={path}"]) + result = runner.invoke(app, ["generate", f"--config={config_path}", f"--path={path}"]) assert result.exit_code == 2 assert "Unable to parse config" in result.stdout @@ -82,47 +88,62 @@ def test_generate_url_and_path(self, _create_new_client): def test_generate_url(self, _create_new_client): url = "cool.url" - from openapi_python_client.cli import MetaType, app + from openapi_python_client.cli import Config, MetaType, app result = runner.invoke(app, ["generate", f"--url={url}"]) assert result.exit_code == 0 _create_new_client.assert_called_once_with( - url=url, path=None, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + url=url, path=None, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8", config=Config() ) def test_generate_path(self, _create_new_client): path = "cool/path" - from openapi_python_client.cli import MetaType, app + from openapi_python_client.cli import Config, MetaType, app result = runner.invoke(app, ["generate", f"--path={path}"]) assert result.exit_code == 0 _create_new_client.assert_called_once_with( - url=None, path=Path(path), custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + url=None, + path=Path(path), + custom_template_path=None, + meta=MetaType.POETRY, + file_encoding="utf-8", + config=Config(), ) def test_generate_meta(self, _create_new_client): path = "cool/path" - from openapi_python_client.cli import MetaType, app + from openapi_python_client.cli import Config, MetaType, app result = runner.invoke(app, ["generate", f"--path={path}", "--meta=none"]) assert result.exit_code == 0 _create_new_client.assert_called_once_with( - url=None, path=Path(path), custom_template_path=None, meta=MetaType.NONE, file_encoding="utf-8" + url=None, + path=Path(path), + custom_template_path=None, + meta=MetaType.NONE, + file_encoding="utf-8", + config=Config(), ) def test_generate_encoding(self, _create_new_client): path = "cool/path" file_encoding = "utf-8" - from openapi_python_client.cli import MetaType, app + from openapi_python_client.cli import Config, MetaType, app result = runner.invoke(app, ["generate", f"--path={path}", f"--file-encoding={file_encoding}"]) assert result.exit_code == 0 _create_new_client.assert_called_once_with( - url=None, path=Path(path), custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + url=None, + path=Path(path), + custom_template_path=None, + meta=MetaType.POETRY, + file_encoding="utf-8", + config=Config(), ) def test_generate_encoding_errors(self, _create_new_client): @@ -198,36 +219,46 @@ def test_update_url_and_path(self, _update_existing_client): def test_update_url(self, _update_existing_client): url = "cool.url" - from openapi_python_client.cli import MetaType, app + from openapi_python_client.cli import Config, MetaType, app result = runner.invoke(app, ["update", f"--url={url}"]) assert result.exit_code == 0 _update_existing_client.assert_called_once_with( - url=url, path=None, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + url=url, path=None, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8", config=Config() ) def test_update_path(self, _update_existing_client): path = "cool/path" - from openapi_python_client.cli import MetaType, app + from openapi_python_client.cli import Config, MetaType, app result = runner.invoke(app, ["update", f"--path={path}"]) assert result.exit_code == 0 _update_existing_client.assert_called_once_with( - url=None, path=Path(path), custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + url=None, + path=Path(path), + custom_template_path=None, + meta=MetaType.POETRY, + file_encoding="utf-8", + config=Config(), ) def test_update_encoding(self, _update_existing_client): path = "cool/path" file_encoding = "utf-8" - from openapi_python_client.cli import MetaType, app + from openapi_python_client.cli import Config, MetaType, app result = runner.invoke(app, ["update", f"--path={path}", f"--file-encoding={file_encoding}"]) assert result.exit_code == 0 _update_existing_client.assert_called_once_with( - url=None, path=Path(path), custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + url=None, + path=Path(path), + custom_template_path=None, + meta=MetaType.POETRY, + file_encoding="utf-8", + config=Config(), ) def test_update_encoding_errors(self, _update_existing_client): diff --git a/tests/test_config.py b/tests/test_config.py index 0ed3ace75..d0cdbcd2b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,46 +4,29 @@ def test_load_from_path(mocker): - safe_load = mocker.patch("yaml.safe_load", return_value={}) + from openapi_python_client import utils + + override1 = {"class_name": "ExampleClass", "module_name": "example_module"} + override2 = {"class_name": "DifferentClass", "module_name": "different_module"} + safe_load = mocker.patch( + "yaml.safe_load", + return_value={ + "field_prefix": "blah", + "class_overrides": {"Class1": override1, "Class2": override2}, + "project_name_override": "project-name", + "package_name_override": "package_name", + "package_version_override": "package_version", + }, + ) fake_path = mocker.MagicMock(autospec=pathlib.Path) - load_config = mocker.patch("openapi_python_client.config.Config.load_config") - Config.load_from_path(fake_path) + config = Config.load_from_path(fake_path) safe_load.assert_called() - load_config.assert_called() - - -class TestLoadConfig: - def test_class_overrides(self): - from openapi_python_client.parser import reference - - override1 = {"class_name": "ExampleClass", "module_name": "example_module"} - override2 = {"class_name": "DifferentClass", "module_name": "different_module"} - config = Config(class_overrides={"Class1": override1, "Class2": override2}) - config.load_config() - - assert reference.class_overrides["Class1"] == reference.Reference(**override1) - assert reference.class_overrides["Class2"] == reference.Reference(**override2) - - def test_project_and_package_name_and_package_version_overrides(self): - config = Config( - project_name_override="project-name", - package_name_override="package_name", - package_version_override="package_version", - ) - config.load_config() - - from openapi_python_client import Project - - assert Project.project_name_override == "project-name" - assert Project.package_name_override == "package_name" - assert Project.package_version_override == "package_version" - - def test_field_prefix(self): - Config(field_prefix="blah").load_config() - - from openapi_python_client import utils - - assert utils.FIELD_PREFIX == "blah" - - utils.FIELD_PREFIX = "field_" + assert utils.FIELD_PREFIX == "blah" + assert config.class_overrides["Class1"] == override1 + assert config.class_overrides["Class2"] == override2 + assert config.project_name_override == "project-name" + assert config.package_name_override == "package_name" + assert config.package_version_override == "package_version" + + utils.FIELD_PREFIX = "field_" diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index 5f0ecdc90..1b43041a4 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -10,51 +10,58 @@ class TestGeneratorData: - def test_from_dict(self, mocker): + def test_from_dict(self, mocker, model_property_factory, enum_property_factory): + from openapi_python_client.parser.properties import Schemas + build_schemas = mocker.patch(f"{MODULE_NAME}.build_schemas") EndpointCollection = mocker.patch(f"{MODULE_NAME}.EndpointCollection") schemas = mocker.MagicMock() + schemas.classes_by_name = { + "Model": model_property_factory(), + "Enum": enum_property_factory(), + } endpoints_collections_by_tag = mocker.MagicMock() EndpointCollection.from_data.return_value = (endpoints_collections_by_tag, schemas) OpenAPI = mocker.patch(f"{MODULE_NAME}.oai.OpenAPI") openapi = OpenAPI.parse_obj.return_value openapi.openapi = mocker.MagicMock(major=3) - + config = mocker.MagicMock() in_dict = mocker.MagicMock() from openapi_python_client.parser.openapi import GeneratorData - generator_data = GeneratorData.from_dict(in_dict) + generator_data = GeneratorData.from_dict(in_dict, config=config) OpenAPI.parse_obj.assert_called_once_with(in_dict) - build_schemas.assert_called_once_with(components=openapi.components.schemas) - EndpointCollection.from_data.assert_called_once_with(data=openapi.paths, schemas=build_schemas.return_value) - assert generator_data == GeneratorData( - title=openapi.info.title, - description=openapi.info.description, - version=openapi.info.version, - endpoint_collections_by_tag=endpoints_collections_by_tag, - errors=schemas.errors, - models=schemas.models, - enums=schemas.enums, - ) + build_schemas.assert_called_once_with(components=openapi.components.schemas, config=config, schemas=Schemas()) + EndpointCollection.from_data.assert_called_once_with( + data=openapi.paths, schemas=build_schemas.return_value, config=config + ) + assert generator_data.title == openapi.info.title + assert generator_data.description == openapi.info.description + assert generator_data.version == openapi.info.version + assert generator_data.endpoint_collections_by_tag == endpoints_collections_by_tag + assert generator_data.errors == schemas.errors + assert list(generator_data.models) == [schemas.classes_by_name["Model"]] + assert list(generator_data.enums) == [schemas.classes_by_name["Enum"]] # Test no components openapi.components = None build_schemas.reset_mock() - GeneratorData.from_dict(in_dict) + GeneratorData.from_dict(in_dict, config=config) build_schemas.assert_not_called() def test_from_dict_invalid_schema(self, mocker): Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") + config = mocker.MagicMock() in_dict = {} from openapi_python_client.parser.openapi import GeneratorData - generator_data = GeneratorData.from_dict(in_dict) + generator_data = GeneratorData.from_dict(in_dict, config=config) assert generator_data == GeneratorError( header="Failed to parse OpenAPI document", @@ -73,12 +80,13 @@ def test_from_dict_invalid_schema(self, mocker): def test_swagger_document_invalid_schema(self, mocker): Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") + config = mocker.MagicMock() in_dict = {"swagger": "2.0"} from openapi_python_client.parser.openapi import GeneratorData - generator_data = GeneratorData.from_dict(in_dict) + generator_data = GeneratorData.from_dict(in_dict, config=config) assert generator_data == GeneratorError( header="Failed to parse OpenAPI document", @@ -98,16 +106,15 @@ def test_swagger_document_invalid_schema(self, mocker): def test_from_dict_invalid_version(self, mocker): Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") - OpenAPI = mocker.patch(f"{MODULE_NAME}.oai.OpenAPI") openapi = OpenAPI.parse_obj.return_value openapi.openapi = oai.SemVer("2.1.3") - in_dict = mocker.MagicMock() + config = mocker.MagicMock() from openapi_python_client.parser.openapi import GeneratorData - generator_data = GeneratorData.from_dict(in_dict) + generator_data = GeneratorData.from_dict(in_dict, config=config) assert generator_data == GeneratorError( header="openapi-python-client only supports OpenAPI 3.x", @@ -140,21 +147,23 @@ def test_parse_request_form_body(self, mocker): ) } ) - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") + from_string = mocker.patch(f"{MODULE_NAME}.Class.from_string") + config = mocker.MagicMock() from openapi_python_client.parser.openapi import Endpoint - result = Endpoint.parse_request_form_body(body) + result = Endpoint.parse_request_form_body(body=body, config=config) - from_ref.assert_called_once_with(ref) - assert result == from_ref() + from_string.assert_called_once_with(string=ref, config=config) + assert result == from_string.return_value def test_parse_request_form_body_no_data(self): body = oai.RequestBody.construct(content={}) + config = MagicMock() from openapi_python_client.parser.openapi import Endpoint - result = Endpoint.parse_request_form_body(body) + result = Endpoint.parse_request_form_body(body=body, config=config) assert result is None @@ -163,21 +172,22 @@ def test_parse_multipart_body(self, mocker): body = oai.RequestBody.construct( content={"multipart/form-data": oai.MediaType.construct(media_type_schema=oai.Reference.construct(ref=ref))} ) - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") + from_string = mocker.patch(f"{MODULE_NAME}.Class.from_string") + config = MagicMock() from openapi_python_client.parser.openapi import Endpoint - result = Endpoint.parse_multipart_body(body) + result = Endpoint.parse_multipart_body(body=body, config=config) - from_ref.assert_called_once_with(ref) - assert result == from_ref() + from_string.assert_called_once_with(string=ref, config=config) + assert result == from_string.return_value def test_parse_multipart_body_no_data(self): body = oai.RequestBody.construct(content={}) from openapi_python_client.parser.openapi import Endpoint - result = Endpoint.parse_multipart_body(body) + result = Endpoint.parse_multipart_body(body=body, config=MagicMock()) assert result is None @@ -190,11 +200,12 @@ def test_parse_request_json_body(self, mocker): ) property_from_data = mocker.patch(f"{MODULE_NAME}.property_from_data") schemas = Schemas() + config = MagicMock() - result = Endpoint.parse_request_json_body(body=body, schemas=schemas, parent_name="parent") + result = Endpoint.parse_request_json_body(body=body, schemas=schemas, parent_name="parent", config=config) property_from_data.assert_called_once_with( - name="json_body", required=True, data=schema, schemas=schemas, parent_name="parent" + name="json_body", required=True, data=schema, schemas=schemas, parent_name="parent", config=config ) assert result == property_from_data.return_value @@ -204,7 +215,7 @@ def test_parse_request_json_body_no_data(self): body = oai.RequestBody.construct(content={}) schemas = Schemas() - result = Endpoint.parse_request_json_body(body=body, schemas=schemas, parent_name="parent") + result = Endpoint.parse_request_json_body(body=body, schemas=schemas, parent_name="parent", config=MagicMock()) assert result == (None, schemas) @@ -215,7 +226,7 @@ def test_add_body_no_data(self, mocker): endpoint = self.make_endpoint() schemas = Schemas() - Endpoint._add_body(endpoint=endpoint, data=oai.Operation.construct(), schemas=schemas) + Endpoint._add_body(endpoint=endpoint, data=oai.Operation.construct(), schemas=schemas, config=MagicMock()) parse_request_form_body.assert_not_called() @@ -231,7 +242,10 @@ def test_add_body_bad_data(self, mocker): schemas = Schemas() result = Endpoint._add_body( - endpoint=endpoint, data=oai.Operation.construct(requestBody=request_body), schemas=schemas + endpoint=endpoint, + data=oai.Operation.construct(requestBody=request_body), + schemas=schemas, + config=MagicMock(), ) assert result == ( @@ -240,18 +254,15 @@ def test_add_body_bad_data(self, mocker): ) def test_add_body_happy(self, mocker): - from openapi_python_client.parser.openapi import Endpoint, Reference, Schemas + from openapi_python_client.parser.openapi import Class, Endpoint from openapi_python_client.parser.properties import Property request_body = mocker.MagicMock() - form_body_reference = Reference.from_ref(ref="a") - multipart_body_reference = Reference.from_ref(ref="b") - parse_request_form_body = mocker.patch.object( - Endpoint, "parse_request_form_body", return_value=form_body_reference - ) - parse_multipart_body = mocker.patch.object( - Endpoint, "parse_multipart_body", return_value=multipart_body_reference - ) + config = mocker.MagicMock() + form_body_class = Class(name="A", module_name="a") + multipart_body_class = Class(name="B", module_name="b") + parse_request_form_body = mocker.patch.object(Endpoint, "parse_request_form_body", return_value=form_body_class) + parse_multipart_body = mocker.patch.object(Endpoint, "parse_multipart_body", return_value=multipart_body_class) json_body = mocker.MagicMock(autospec=Property) json_body_imports = mocker.MagicMock() @@ -260,53 +271,59 @@ def test_add_body_happy(self, mocker): parse_request_json_body = mocker.patch.object( Endpoint, "parse_request_json_body", return_value=(json_body, parsed_schemas) ) - import_string_from_reference = mocker.patch( - f"{MODULE_NAME}.import_string_from_reference", side_effect=["import_1", "import_2"] + import_string_from_class = mocker.patch( + f"{MODULE_NAME}.import_string_from_class", side_effect=["import_1", "import_2"] ) endpoint = self.make_endpoint() initial_schemas = mocker.MagicMock() (endpoint, response_schemas) = Endpoint._add_body( - endpoint=endpoint, data=oai.Operation.construct(requestBody=request_body), schemas=initial_schemas + endpoint=endpoint, + data=oai.Operation.construct(requestBody=request_body), + schemas=initial_schemas, + config=config, ) assert response_schemas == parsed_schemas - parse_request_form_body.assert_called_once_with(request_body) - parse_request_json_body.assert_called_once_with(body=request_body, schemas=initial_schemas, parent_name="name") - parse_multipart_body.assert_called_once_with(request_body) - import_string_from_reference.assert_has_calls( + parse_request_form_body.assert_called_once_with(body=request_body, config=config) + parse_request_json_body.assert_called_once_with( + body=request_body, schemas=initial_schemas, parent_name="name", config=config + ) + parse_multipart_body.assert_called_once_with(body=request_body, config=config) + import_string_from_class.assert_has_calls( [ - mocker.call(form_body_reference, prefix="...models"), - mocker.call(multipart_body_reference, prefix="...models"), + mocker.call(form_body_class, prefix="...models"), + mocker.call(multipart_body_class, prefix="...models"), ] ) json_body.get_imports.assert_called_once_with(prefix="...") assert endpoint.relative_imports == {"import_1", "import_2", "import_3", json_body_imports} assert endpoint.json_body == json_body - assert endpoint.form_body_reference == form_body_reference - assert endpoint.multipart_body_reference == multipart_body_reference + assert endpoint.form_body_class == form_body_class + assert endpoint.multipart_body_class == multipart_body_class def test__add_responses_status_code_error(self, mocker): from openapi_python_client.parser.openapi import Endpoint, Schemas schemas = Schemas() response_1_data = mocker.MagicMock() - response_2_data = mocker.MagicMock() data = { "not_a_number": response_1_data, } endpoint = self.make_endpoint() parse_error = ParseError(data=mocker.MagicMock()) response_from_data = mocker.patch(f"{MODULE_NAME}.response_from_data", return_value=(parse_error, schemas)) + config = MagicMock() - response, schemas = Endpoint._add_responses(endpoint=endpoint, data=data, schemas=schemas) + response, schemas = Endpoint._add_responses(endpoint=endpoint, data=data, schemas=schemas, config=config) assert response.errors == [ ParseError( detail=f"Invalid response status code not_a_number (not a number), response will be ommitted from generated client" ) ] + response_from_data.assert_not_called() def test__add_responses_error(self, mocker): from openapi_python_client.parser.openapi import Endpoint, Schemas @@ -321,13 +338,14 @@ def test__add_responses_error(self, mocker): endpoint = self.make_endpoint() parse_error = ParseError(data=mocker.MagicMock()) response_from_data = mocker.patch(f"{MODULE_NAME}.response_from_data", return_value=(parse_error, schemas)) + config = MagicMock() - response, schemas = Endpoint._add_responses(endpoint=endpoint, data=data, schemas=schemas) + response, schemas = Endpoint._add_responses(endpoint=endpoint, data=data, schemas=schemas, config=config) response_from_data.assert_has_calls( [ - mocker.call(status_code=200, data=response_1_data, schemas=schemas, parent_name="name"), - mocker.call(status_code=404, data=response_2_data, schemas=schemas, parent_name="name"), + mocker.call(status_code=200, data=response_1_data, schemas=schemas, parent_name="name", config=config), + mocker.call(status_code=404, data=response_2_data, schemas=schemas, parent_name="name", config=config), ] ) assert response.errors == [ @@ -368,13 +386,18 @@ def test__add_responses(self, mocker): response_from_data = mocker.patch( f"{MODULE_NAME}.response_from_data", side_effect=[(response_1, schemas_1), (response_2, schemas_2)] ) + config = MagicMock() - endpoint, response_schemas = Endpoint._add_responses(endpoint=endpoint, data=data, schemas=schemas) + endpoint, response_schemas = Endpoint._add_responses( + endpoint=endpoint, data=data, schemas=schemas, config=config + ) response_from_data.assert_has_calls( [ - mocker.call(status_code=200, data=response_1_data, schemas=schemas, parent_name="name"), - mocker.call(status_code=404, data=response_2_data, schemas=schemas_1, parent_name="name"), + mocker.call(status_code=200, data=response_1_data, schemas=schemas, parent_name="name", config=config), + mocker.call( + status_code=404, data=response_2_data, schemas=schemas_1, parent_name="name", config=config + ), ] ) assert endpoint.responses == [response_1, response_2] @@ -391,8 +414,12 @@ def test__add_parameters_handles_no_params(self): endpoint = self.make_endpoint() schemas = Schemas() + config = MagicMock() + # Just checking there's no exception here - assert Endpoint._add_parameters(endpoint=endpoint, data=oai.Operation.construct(), schemas=schemas) == ( + assert Endpoint._add_parameters( + endpoint=endpoint, data=oai.Operation.construct(), schemas=schemas, config=config + ) == ( endpoint, schemas, ) @@ -406,9 +433,10 @@ def test__add_parameters_parse_error(self, mocker): property_schemas = mocker.MagicMock() mocker.patch(f"{MODULE_NAME}.property_from_data", return_value=(parse_error, property_schemas)) param = oai.Parameter.construct(name="test", required=True, param_schema=mocker.MagicMock(), param_in="cookie") + config = MagicMock() result = Endpoint._add_parameters( - endpoint=endpoint, data=oai.Operation.construct(parameters=[param]), schemas=initial_schemas + endpoint=endpoint, data=oai.Operation.construct(parameters=[param]), schemas=initial_schemas, config=config ) assert result == ( ParseError(data=parse_error.data, detail=f"cannot parse parameter of endpoint {endpoint.name}"), @@ -425,9 +453,10 @@ def test__add_parameters_fail_loudly_when_location_not_supported(self, mocker): name="test", required=True, param_schema=mocker.MagicMock(), param_in="error_location" ) schemas = Schemas() + config = MagicMock() result = Endpoint._add_parameters( - endpoint=endpoint, data=oai.Operation.construct(parameters=[param]), schemas=schemas + endpoint=endpoint, data=oai.Operation.construct(parameters=[param]), schemas=schemas, config=config ) assert result == (ParseError(data=param, detail="Parameter must be declared in path or query"), parsed_schemas) @@ -471,19 +500,37 @@ def test__add_parameters_happy(self, mocker): ] ) initial_schemas = mocker.MagicMock() + config = MagicMock() - (endpoint, schemas) = Endpoint._add_parameters(endpoint=endpoint, data=data, schemas=initial_schemas) + (endpoint, schemas) = Endpoint._add_parameters( + endpoint=endpoint, data=data, schemas=initial_schemas, config=config + ) property_from_data.assert_has_calls( [ mocker.call( - name="path_prop_name", required=True, data=path_schema, schemas=initial_schemas, parent_name="name" + name="path_prop_name", + required=True, + data=path_schema, + schemas=initial_schemas, + parent_name="name", + config=config, ), mocker.call( - name="query_prop_name", required=False, data=query_schema, schemas=schemas_1, parent_name="name" + name="query_prop_name", + required=False, + data=query_schema, + schemas=schemas_1, + parent_name="name", + config=config, ), mocker.call( - name="header_prop_name", required=False, data=header_schema, schemas=schemas_2, parent_name="name" + name="header_prop_name", + required=False, + data=header_schema, + schemas=schemas_2, + parent_name="name", + config=config, ), ] ) @@ -511,8 +558,11 @@ def test_from_data_bad_params(self, mocker): responses=mocker.MagicMock(), ) inital_schemas = mocker.MagicMock() + config = MagicMock() - result = Endpoint.from_data(data=data, path=path, method=method, tag="default", schemas=inital_schemas) + result = Endpoint.from_data( + data=data, path=path, method=method, tag="default", schemas=inital_schemas, config=config + ) assert result == (parse_error, return_schemas) @@ -535,8 +585,11 @@ def test_from_data_bad_responses(self, mocker): responses=mocker.MagicMock(), ) initial_schemas = mocker.MagicMock() + config = MagicMock() - result = Endpoint.from_data(data=data, path=path, method=method, tag="default", schemas=initial_schemas) + result = Endpoint.from_data( + data=data, path=path, method=method, tag="default", schemas=initial_schemas, config=config + ) assert result == (parse_error, response_schemas) @@ -563,10 +616,13 @@ def test_from_data_standard(self, mocker): responses=mocker.MagicMock(), ) initial_schemas = mocker.MagicMock() + config = MagicMock() mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) - endpoint = Endpoint.from_data(data=data, path=path, method=method, tag="default", schemas=initial_schemas) + endpoint = Endpoint.from_data( + data=data, path=path, method=method, tag="default", schemas=initial_schemas, config=config + ) assert endpoint == _add_body.return_value @@ -581,9 +637,14 @@ def test_from_data_standard(self, mocker): ), data=data, schemas=initial_schemas, + config=config, + ) + _add_responses.assert_called_once_with( + endpoint=param_endpoint, data=data.responses, schemas=param_schemas, config=config + ) + _add_body.assert_called_once_with( + endpoint=response_endpoint, data=data, schemas=response_schemas, config=config ) - _add_responses.assert_called_once_with(endpoint=param_endpoint, data=data.responses, schemas=param_schemas) - _add_body.assert_called_once_with(endpoint=response_endpoint, data=data, schemas=response_schemas) def test_from_data_no_operation_id(self, mocker): from openapi_python_client.parser.openapi import Endpoint @@ -605,8 +666,9 @@ def test_from_data_no_operation_id(self, mocker): ) schemas = mocker.MagicMock() mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) + config = MagicMock() - result = Endpoint.from_data(data=data, path=path, method=method, tag="default", schemas=schemas) + result = Endpoint.from_data(data=data, path=path, method=method, tag="default", schemas=schemas, config=config) assert result == _add_body.return_value @@ -621,12 +683,16 @@ def test_from_data_no_operation_id(self, mocker): ), data=data, schemas=schemas, + config=config, ) _add_responses.assert_called_once_with( - endpoint=_add_parameters.return_value[0], data=data.responses, schemas=_add_parameters.return_value[1] + endpoint=_add_parameters.return_value[0], + data=data.responses, + schemas=_add_parameters.return_value[1], + config=config, ) _add_body.assert_called_once_with( - endpoint=_add_responses.return_value[0], data=data, schemas=_add_responses.return_value[1] + endpoint=_add_responses.return_value[0], data=data, schemas=_add_responses.return_value[1], config=config ) def test_from_data_no_security(self, mocker): @@ -649,8 +715,9 @@ def test_from_data_no_security(self, mocker): method = mocker.MagicMock() mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) schemas = mocker.MagicMock() + config = MagicMock() - Endpoint.from_data(data=data, path=path, method=method, tag="a", schemas=schemas) + Endpoint.from_data(data=data, path=path, method=method, tag="a", schemas=schemas, config=config) _add_parameters.assert_called_once_with( endpoint=Endpoint( @@ -663,12 +730,16 @@ def test_from_data_no_security(self, mocker): ), data=data, schemas=schemas, + config=config, ) _add_responses.assert_called_once_with( - endpoint=_add_parameters.return_value[0], data=data.responses, schemas=_add_parameters.return_value[1] + endpoint=_add_parameters.return_value[0], + data=data.responses, + schemas=_add_parameters.return_value[1], + config=config, ) _add_body.assert_called_once_with( - endpoint=_add_responses.return_value[0], data=data, schemas=_add_responses.return_value[1] + endpoint=_add_responses.return_value[0], data=data, schemas=_add_responses.return_value[1], config=config ) @pytest.mark.parametrize( @@ -687,23 +758,23 @@ def test_response_type(self, response_types, expected): class TestImportStringFromReference: def test_import_string_from_reference_no_prefix(self, mocker): - from openapi_python_client.parser.openapi import import_string_from_reference - from openapi_python_client.parser.reference import Reference + from openapi_python_client.parser.openapi import import_string_from_class + from openapi_python_client.parser.properties import Class - reference = mocker.MagicMock(autospec=Reference) - result = import_string_from_reference(reference) + class_ = mocker.MagicMock(autospec=Class) + result = import_string_from_class(class_) - assert result == f"from .{reference.module_name} import {reference.class_name}" + assert result == f"from .{class_.module_name} import {class_.name}" def test_import_string_from_reference_with_prefix(self, mocker): - from openapi_python_client.parser.openapi import import_string_from_reference - from openapi_python_client.parser.reference import Reference + from openapi_python_client.parser.openapi import import_string_from_class + from openapi_python_client.parser.properties import Class prefix = mocker.MagicMock(autospec=str) - reference = mocker.MagicMock(autospec=Reference) - result = import_string_from_reference(reference=reference, prefix=prefix) + class_ = mocker.MagicMock(autospec=Class) + result = import_string_from_class(class_=class_, prefix=prefix) - assert result == f"from {prefix}.{reference.module_name} import {reference.class_name}" + assert result == f"from {prefix}.{class_.module_name} import {class_.name}" class TestEndpointCollection: @@ -729,14 +800,21 @@ def test_from_data(self, mocker): side_effect=[(endpoint_1, schemas_1), (endpoint_2, schemas_2), (endpoint_3, schemas_3)], ) schemas = mocker.MagicMock() + config = MagicMock() - result = EndpointCollection.from_data(data=data, schemas=schemas) + result = EndpointCollection.from_data(data=data, schemas=schemas, config=config) endpoint_from_data.assert_has_calls( [ - mocker.call(data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas), - mocker.call(data=path_1_post, path="path_1", method="post", tag="tag_2", schemas=schemas_1), - mocker.call(data=path_2_get, path="path_2", method="get", tag="default", schemas=schemas_2), + mocker.call( + data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas, config=config + ), + mocker.call( + data=path_1_post, path="path_1", method="post", tag="tag_2", schemas=schemas_1, config=config + ), + mocker.call( + data=path_2_get, path="path_2", method="get", tag="default", schemas=schemas_2, config=config + ), ], ) assert result == ( @@ -770,14 +848,21 @@ def test_from_data_errors(self, mocker): ], ) schemas = mocker.MagicMock() + config = MagicMock() - result, result_schemas = EndpointCollection.from_data(data=data, schemas=schemas) + result, result_schemas = EndpointCollection.from_data(data=data, schemas=schemas, config=config) endpoint_from_data.assert_has_calls( [ - mocker.call(data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas), - mocker.call(data=path_1_post, path="path_1", method="post", tag="tag_2", schemas=schemas_1), - mocker.call(data=path_2_get, path="path_2", method="get", tag="default", schemas=schemas_2), + mocker.call( + data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas, config=config + ), + mocker.call( + data=path_1_post, path="path_1", method="post", tag="tag_2", schemas=schemas_1, config=config + ), + mocker.call( + data=path_2_get, path="path_2", method="get", tag="default", schemas=schemas_2, config=config + ), ], ) assert result["default"].parse_errors[0].data == "1" @@ -807,20 +892,26 @@ def test_from_data_tags_snake_case_sanitizer(self, mocker): side_effect=[(endpoint_1, schemas_1), (endpoint_2, schemas_2), (endpoint_3, schemas_3)], ) schemas = mocker.MagicMock() + config = MagicMock() - result = EndpointCollection.from_data(data=data, schemas=schemas) + result = EndpointCollection.from_data(data=data, schemas=schemas, config=config) endpoint_from_data.assert_has_calls( [ - mocker.call(data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas), + mocker.call( + data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas, config=config + ), mocker.call( data=path_1_post, path="path_1", method="post", tag="amf_subscription_info_document", schemas=schemas_1, + config=config, + ), + mocker.call( + data=path_2_get, path="path_2", method="get", tag="default", schemas=schemas_2, config=config ), - mocker.call(data=path_2_get, path="path_2", method="get", tag="default", schemas=schemas_2), ], ) assert result == ( diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 749db4d44..fdb1af343 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -1,6 +1,10 @@ +from unittest.mock import MagicMock, call + +import attr import pytest import openapi_python_client.schema as oai +from openapi_python_client import Config from openapi_python_client.parser.errors import PropertyError, ValidationError from openapi_python_client.parser.properties import BooleanProperty, FloatProperty, IntProperty @@ -398,110 +402,35 @@ def test_get_imports(self, mocker): class TestEnumProperty: - def test_get_type_string(self, mocker): - fake_reference = mocker.MagicMock(class_name="MyTestEnum") - - from openapi_python_client.parser import properties - - p = properties.EnumProperty( - name="test", - required=True, - default=None, - values={}, - nullable=False, - reference=fake_reference, - value_type=str, - ) + @pytest.mark.parametrize( + "required, nullable, expected", + ( + (False, False, "Union[Unset, {}]"), + (True, False, "{}"), + (False, True, "Union[Unset, None, {}]"), + (True, True, "Optional[{}]"), + ), + ) + def test_get_type_string(self, mocker, enum_property_factory, required, nullable, expected): + fake_class = mocker.MagicMock() + fake_class.name = "MyTestEnum" - base_type_string = f"MyTestEnum" + p = enum_property_factory(class_info=fake_class, required=required, nullable=nullable) - assert p.get_type_string() == base_type_string - assert p.get_type_string(json=True) == "str" + assert p.get_type_string() == expected.format(fake_class.name) + assert p.get_type_string(no_optional=True) == fake_class.name + assert p.get_type_string(json=True) == expected.format("str") - p = properties.EnumProperty( - name="test", - required=True, - default=None, - values={}, - nullable=True, - reference=fake_reference, - value_type=str, - ) - assert p.get_type_string() == f"Optional[{base_type_string}]" - assert p.get_type_string(no_optional=True) == base_type_string - - p = properties.EnumProperty( - name="test", - required=False, - default=None, - values={}, - nullable=True, - reference=fake_reference, - value_type=str, - ) - assert p.get_type_string() == f"Union[Unset, None, {base_type_string}]" - assert p.get_type_string(no_optional=True) == base_type_string - - p = properties.EnumProperty( - name="test", - required=False, - default=None, - values={}, - nullable=False, - reference=fake_reference, - value_type=str, - ) - assert p.get_type_string() == f"Union[Unset, {base_type_string}]" - assert p.get_type_string(no_optional=True) == base_type_string - - def test_get_imports(self, mocker): - fake_reference = mocker.MagicMock(class_name="MyTestEnum", module_name="my_test_enum") + def test_get_imports(self, mocker, enum_property_factory): + fake_class = mocker.MagicMock(module_name="my_test_enum") + fake_class.name = "MyTestEnum" prefix = "..." - from openapi_python_client.parser import properties - - enum_property = properties.EnumProperty( - name="test", - required=True, - default=None, - values={}, - nullable=False, - reference=fake_reference, - value_type=str, - ) - - assert enum_property.get_imports(prefix=prefix) == { - f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - } + enum_property = enum_property_factory(class_info=fake_class, required=False) - enum_property = properties.EnumProperty( - name="test", - required=False, - default=None, - values={}, - nullable=False, - reference=fake_reference, - value_type=str, - ) assert enum_property.get_imports(prefix=prefix) == { - f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import Union", - "from ...types import UNSET, Unset", - } - - enum_property = properties.EnumProperty( - name="test", - required=False, - default=None, - values={}, - nullable=True, - reference=fake_reference, - value_type=str, - ) - assert enum_property.get_imports(prefix=prefix) == { - f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import Union", - "from typing import Optional", + f"from {prefix}models.{fake_class.module_name} import {fake_class.name}", + "from typing import Union", # Makes sure unset is handled via base class "from ...types import UNSET, Unset", } @@ -534,7 +463,7 @@ def test_values_from_list_duplicate(self): class TestPropertyFromData: def test_property_from_data_str_enum(self, mocker): - from openapi_python_client.parser.properties import EnumProperty, Reference + from openapi_python_client.parser.properties import Class, EnumProperty from openapi_python_client.schema import Schema data = Schema(title="AnEnum", enum=["A", "B", "C"], nullable=False, default="B") @@ -543,10 +472,10 @@ def test_property_from_data_str_enum(self, mocker): from openapi_python_client.parser.properties import Schemas, property_from_data - schemas = Schemas(enums={"AnEnum": mocker.MagicMock()}) + schemas = Schemas(classes_by_name={"AnEnum": mocker.MagicMock()}) prop, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent" + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=Config() ) assert prop == EnumProperty( @@ -554,18 +483,18 @@ def test_property_from_data_str_enum(self, mocker): required=True, nullable=False, values={"A": "A", "B": "B", "C": "C"}, - reference=Reference(class_name="ParentAnEnum", module_name="parent_an_enum"), + class_info=Class(name="ParentAnEnum", module_name="parent_an_enum"), value_type=str, default="ParentAnEnum.B", ) assert schemas != new_schemas, "Provided Schemas was mutated" - assert new_schemas.enums == { - "AnEnum": schemas.enums["AnEnum"], + assert new_schemas.classes_by_name == { + "AnEnum": schemas.classes_by_name["AnEnum"], "ParentAnEnum": prop, } def test_property_from_data_int_enum(self, mocker): - from openapi_python_client.parser.properties import EnumProperty, Reference + from openapi_python_client.parser.properties import Class, EnumProperty from openapi_python_client.schema import Schema data = Schema.construct(title="anEnum", enum=[1, 2, 3], nullable=False, default=3) @@ -574,10 +503,10 @@ def test_property_from_data_int_enum(self, mocker): from openapi_python_client.parser.properties import Schemas, property_from_data - schemas = Schemas(enums={"AnEnum": mocker.MagicMock()}) + schemas = Schemas(classes_by_name={"AnEnum": mocker.MagicMock()}) prop, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent" + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=Config() ) assert prop == EnumProperty( @@ -585,21 +514,21 @@ def test_property_from_data_int_enum(self, mocker): required=True, nullable=False, values={"VALUE_1": 1, "VALUE_2": 2, "VALUE_3": 3}, - reference=Reference(class_name="ParentAnEnum", module_name="parent_an_enum"), + class_info=Class(name="ParentAnEnum", module_name="parent_an_enum"), value_type=int, default="ParentAnEnum.VALUE_3", ) assert schemas != new_schemas, "Provided Schemas was mutated" - assert new_schemas.enums == { - "AnEnum": schemas.enums["AnEnum"], + assert new_schemas.classes_by_name == { + "AnEnum": schemas.classes_by_name["AnEnum"], "ParentAnEnum": prop, } def test_property_from_data_ref_enum(self): - from openapi_python_client.parser.properties import EnumProperty, Reference, Schemas, property_from_data + from openapi_python_client.parser.properties import Class, EnumProperty, Schemas, property_from_data name = "some_enum" - data = oai.Reference.construct(ref="MyEnum") + data = oai.Reference.construct(ref="#/components/schemas/MyEnum") existing_enum = EnumProperty( name="an_enum", required=True, @@ -607,11 +536,13 @@ def test_property_from_data_ref_enum(self): default=None, values={"A": "a"}, value_type=str, - reference=Reference(class_name="MyEnum", module_name="my_enum"), + class_info=Class(name="MyEnum", module_name="my_enum"), ) - schemas = Schemas(enums={"MyEnum": existing_enum}) + schemas = Schemas(classes_by_reference={"/components/schemas/MyEnum": existing_enum}) - prop, new_schemas = property_from_data(name=name, required=False, data=data, schemas=schemas, parent_name="") + prop, new_schemas = property_from_data( + name=name, required=False, data=data, schemas=schemas, parent_name="", config=Config() + ) assert prop == EnumProperty( name="some_enum", @@ -620,39 +551,41 @@ def test_property_from_data_ref_enum(self): default=None, values={"A": "a"}, value_type=str, - reference=Reference(class_name="MyEnum", module_name="my_enum"), + class_info=Class(name="MyEnum", module_name="my_enum"), ) assert schemas == new_schemas def test_property_from_data_ref_model(self): - from openapi_python_client.parser.properties import ModelProperty, Reference, Schemas, property_from_data + from openapi_python_client.parser.properties import Class, ModelProperty, Schemas, property_from_data name = "new_name" required = False class_name = "MyModel" - data = oai.Reference.construct(ref=class_name) + data = oai.Reference.construct(ref=f"#/components/schemas/{class_name}") existing_model = ModelProperty( name="old_name", required=True, nullable=False, default=None, - reference=Reference(class_name=class_name, module_name="my_model"), + class_info=Class(name=class_name, module_name="my_model"), required_properties=[], optional_properties=[], description="", relative_imports=set(), additional_properties=False, ) - schemas = Schemas(models={class_name: existing_model}) + schemas = Schemas(classes_by_reference={f"/components/schemas/{class_name}": existing_model}) - prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="") + prop, new_schemas = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="", config=Config() + ) assert prop == ModelProperty( name=name, required=required, nullable=False, default=None, - reference=Reference(class_name=class_name, module_name="my_model"), + class_info=Class(name=class_name, module_name="my_model"), required_properties=[], optional_properties=[], description="", @@ -667,18 +600,37 @@ def test_property_from_data_ref_not_found(self, mocker): name = mocker.MagicMock() required = mocker.MagicMock() data = oai.Reference.construct(ref=mocker.MagicMock()) - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") + parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) schemas = Schemas() prop, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent" + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=mocker.MagicMock() ) - from_ref.assert_called_once_with(data.ref) + parse_reference_path.assert_called_once_with(data.ref) assert prop == PropertyError(data=data, detail="Could not find reference in parsed models or enums") assert schemas == new_schemas + def test_property_from_data_invalid_ref(self, mocker): + from openapi_python_client.parser.properties import PropertyError, Schemas, property_from_data + + name = mocker.MagicMock() + required = mocker.MagicMock() + data = oai.Reference.construct(ref=mocker.MagicMock()) + parse_reference_path = mocker.patch( + f"{MODULE_NAME}.parse_reference_path", return_value=PropertyError(detail="bad stuff") + ) + schemas = Schemas() + + prop, new_schemas = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=mocker.MagicMock() + ) + + parse_reference_path.assert_called_once_with(data.ref) + assert prop == PropertyError(data=data, detail="bad stuff") + assert schemas == new_schemas + def test_property_from_data_string(self, mocker): from openapi_python_client.parser.properties import Schemas, property_from_data @@ -690,7 +642,7 @@ def test_property_from_data_string(self, mocker): schemas = Schemas() p, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent" + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=mocker.MagicMock() ) assert p == _string_based_property.return_value @@ -714,7 +666,7 @@ def test_property_from_data_simple_types(self, openapi_type, prop_type, python_t schemas = Schemas() p, new_schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent" + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=MagicMock() ) assert p == prop_type(name=name, required=required, default=python_type(data.default), nullable=False) @@ -724,12 +676,16 @@ def test_property_from_data_simple_types(self, openapi_type, prop_type, python_t data.default = 0 data.nullable = True - p, _ = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="parent") + p, _ = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=MagicMock() + ) assert p == prop_type(name=name, required=required, default=python_type(data.default), nullable=True) # Test bad default value data.default = "a" - p, _ = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="parent") + p, _ = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=MagicMock() + ) assert python_type is bool or isinstance(p, PropertyError) def test_property_from_data_array(self, mocker): @@ -744,12 +700,15 @@ def test_property_from_data_array(self, mocker): build_list_property = mocker.patch(f"{MODULE_NAME}.build_list_property") mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) schemas = Schemas() + config = MagicMock() - response = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="parent") + response = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config + ) assert response == build_list_property.return_value build_list_property.assert_called_once_with( - data=data, name=name, required=required, schemas=schemas, parent_name="parent" + data=data, name=name, required=required, schemas=schemas, parent_name="parent", config=config ) def test_property_from_data_object(self, mocker): @@ -763,12 +722,15 @@ def test_property_from_data_object(self, mocker): build_model_property = mocker.patch(f"{MODULE_NAME}.build_model_property") mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) schemas = Schemas() + config = MagicMock() - response = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="parent") + response = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config + ) assert response == build_model_property.return_value build_model_property.assert_called_once_with( - data=data, name=name, required=required, schemas=schemas, parent_name="parent" + data=data, name=name, required=required, schemas=schemas, parent_name="parent", config=config ) def test_property_from_data_union(self, mocker): @@ -785,57 +747,37 @@ def test_property_from_data_union(self, mocker): build_union_property = mocker.patch(f"{MODULE_NAME}.build_union_property") mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) schemas = Schemas() + config = MagicMock() - response = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="parent") + response = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config + ) assert response == build_union_property.return_value build_union_property.assert_called_once_with( - data=data, name=name, required=required, schemas=schemas, parent_name="parent" + data=data, name=name, required=required, schemas=schemas, parent_name="parent", config=config ) - def test_property_from_data_union_of_one_element(self, mocker): - from openapi_python_client.parser.properties import ModelProperty, Reference, Schemas, property_from_data + def test_property_from_data_union_of_one_element(self, mocker, model_property_factory): + from openapi_python_client.parser.properties import Class, ModelProperty, Schemas, property_from_data name = "new_name" required = False class_name = "MyModel" - existing_model = ModelProperty( - name="old_name", - required=True, - nullable=False, - default=None, - reference=Reference(class_name=class_name, module_name="my_model"), - required_properties=[], - optional_properties=[], - description="", - relative_imports=set(), - additional_properties=False, - ) - schemas = Schemas(models={class_name: existing_model}) + existing_model = model_property_factory() + schemas = Schemas(classes_by_reference={f"/{class_name}": existing_model}) data = oai.Schema.construct( - allOf=[oai.Reference.construct(ref=class_name)], + allOf=[oai.Reference.construct(ref=f"#/{class_name}")], nullable=True, ) build_union_property = mocker.patch(f"{MODULE_NAME}.build_union_property") - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) prop, schemas = property_from_data( - name=name, required=required, data=data, schemas=schemas, parent_name="parent" + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=Config() ) - assert prop == ModelProperty( - name=name, - required=required, - nullable=True, - default=None, - reference=Reference(class_name=class_name, module_name="my_model"), - required_properties=[], - optional_properties=[], - description="", - relative_imports=set(), - additional_properties=False, - ) + assert prop == attr.evolve(existing_model, name=name, required=required) build_union_property.assert_not_called() def test_property_from_data_unsupported_type(self, mocker): @@ -846,7 +788,9 @@ def test_property_from_data_unsupported_type(self, mocker): from openapi_python_client.parser.errors import PropertyError from openapi_python_client.parser.properties import Schemas, property_from_data - assert property_from_data(name=name, required=required, data=data, schemas=Schemas(), parent_name="parent") == ( + assert property_from_data( + name=name, required=required, data=data, schemas=Schemas(), parent_name="parent", config=MagicMock() + ) == ( PropertyError(data=data, detail=f"unknown type {data.type}"), Schemas(), ) @@ -858,7 +802,7 @@ def test_property_from_data_no_valid_props_in_data(self): data = oai.Schema() prop, new_schemas = property_from_data( - name="blah", required=True, data=data, schemas=schemas, parent_name="parent" + name="blah", required=True, data=data, schemas=schemas, parent_name="parent", config=MagicMock() ) assert prop == NoneProperty(name="blah", required=True, nullable=False, default=None) @@ -873,7 +817,7 @@ def test_property_from_data_validation_error(self, mocker): data = oai.Schema() err, new_schemas = property_from_data( - name="blah", required=True, data=data, schemas=schemas, parent_name="parent" + name="blah", required=True, data=data, schemas=schemas, parent_name="parent", config=MagicMock() ) assert err == PropertyError(detail="Failed to validate default value", data=data) assert new_schemas == schemas @@ -890,7 +834,7 @@ def test_build_list_property_no_items(self, mocker): schemas = properties.Schemas() p, new_schemas = properties.build_list_property( - name=name, required=required, data=data, schemas=schemas, parent_name="parent" + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=MagicMock() ) assert p == PropertyError(data=data, detail="type array must have items defined") @@ -911,16 +855,17 @@ def test_build_list_property_invalid_items(self, mocker): property_from_data = mocker.patch.object( properties, "property_from_data", return_value=(properties.PropertyError(data="blah"), second_schemas) ) + config = MagicMock() p, new_schemas = properties.build_list_property( - name=name, required=required, data=data, schemas=schemas, parent_name="parent" + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config ) assert p == PropertyError(data="blah", detail=f"invalid data in items of array {name}") assert new_schemas == second_schemas assert schemas != new_schemas, "Schema was mutated" property_from_data.assert_called_once_with( - name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name="parent" + name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name="parent", config=config ) def test_build_list_property(self, mocker): @@ -939,9 +884,10 @@ def test_build_list_property(self, mocker): ) mocker.patch("openapi_python_client.utils.snake_case", return_value=name) mocker.patch("openapi_python_client.utils.to_valid_python_identifier", return_value=name) + config = MagicMock() p, new_schemas = properties.build_list_property( - name=name, required=required, data=data, schemas=schemas, parent_name="parent" + name=name, required=required, data=data, schemas=schemas, parent_name="parent", config=config ) assert isinstance(p, properties.ListProperty) @@ -949,7 +895,7 @@ def test_build_list_property(self, mocker): assert new_schemas == second_schemas assert schemas != new_schemas, "Schema was mutated" property_from_data.assert_called_once_with( - name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name="parent" + name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name="parent", config=config ) @@ -970,7 +916,9 @@ def test_property_from_data_union(self, mocker): from openapi_python_client.parser.properties import Schemas, property_from_data - p, s = property_from_data(name=name, required=required, data=data, schemas=Schemas(), parent_name="parent") + p, s = property_from_data( + name=name, required=required, data=data, schemas=Schemas(), parent_name="parent", config=MagicMock() + ) FloatProperty.assert_called_once_with(name=name, required=required, default=0.0, nullable=False) IntProperty.assert_called_once_with(name=name, required=required, default=0, nullable=False) @@ -992,7 +940,9 @@ def test_property_from_data_union_bad_type(self, mocker): from openapi_python_client.parser.properties import Schemas, property_from_data - p, s = property_from_data(name=name, required=required, data=data, schemas=Schemas(), parent_name="parent") + p, s = property_from_data( + name=name, required=required, data=data, schemas=Schemas(), parent_name="parent", config=MagicMock() + ) assert p == PropertyError(detail=f"Invalid property in union {name}", data=oai.Schema(type="garbage")) @@ -1081,72 +1031,86 @@ def test__string_based_property_unsupported_format(self, mocker): assert p == StringProperty(name=name, required=required, nullable=True, default=None) -def test_build_schemas(mocker): - build_model_property = mocker.patch(f"{MODULE_NAME}.build_model_property") - in_data = {"1": mocker.MagicMock(enum=None), "2": mocker.MagicMock(enum=None), "3": mocker.MagicMock(enum=None)} - model_1 = mocker.MagicMock() - schemas_1 = mocker.MagicMock() - model_2 = mocker.MagicMock() - schemas_2 = mocker.MagicMock(errors=[]) - error = PropertyError() - schemas_3 = mocker.MagicMock() - - # This loops through one for each, then again to retry the error - build_model_property.side_effect = [ - (model_1, schemas_1), - (model_2, schemas_2), - (error, schemas_3), - (error, schemas_3), - ] - - from openapi_python_client.parser.properties import Schemas, build_schemas - - result = build_schemas(components=in_data) - - build_model_property.assert_has_calls( - [ - mocker.call(data=in_data["1"], name="1", schemas=Schemas(), required=True, parent_name=None), - mocker.call(data=in_data["2"], name="2", schemas=schemas_1, required=True, parent_name=None), - mocker.call(data=in_data["3"], name="3", schemas=schemas_2, required=True, parent_name=None), - mocker.call(data=in_data["3"], name="3", schemas=schemas_2, required=True, parent_name=None), - ] - ) - # schemas_3 was the last to come back from build_model_property, but it should be ignored because it's an error - assert result == schemas_2 - assert result.errors == [error] - - -def test_build_parse_error_on_reference(): - from openapi_python_client.parser.openapi import build_schemas - - ref_schema = oai.Reference.construct() - in_data = {"1": ref_schema} - result = build_schemas(components=in_data) - assert result.errors[0] == PropertyError(data=ref_schema, detail="Reference schemas are not supported.") - +class TestBuildSchemas: + def test_skips_references_and_keeps_going(self, mocker): + from openapi_python_client.parser.properties import Schemas, build_schemas + from openapi_python_client.schema import Reference, Schema + + components = {"a_ref": Reference.construct(), "a_schema": Schema.construct()} + update_schemas_with_data = mocker.patch(f"{MODULE_NAME}.update_schemas_with_data") + parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") + config = Config() + + result = build_schemas(components=components, schemas=Schemas(), config=config) + # Should not even try to parse a path for the Reference + parse_reference_path.assert_called_once_with("#/components/schemas/a_schema") + update_schemas_with_data.assert_called_once_with( + ref_path=parse_reference_path.return_value, + config=config, + data=components["a_schema"], + schemas=Schemas( + errors=[PropertyError(detail="Reference schemas are not supported.", data=components["a_ref"])] + ), + ) + assert result == update_schemas_with_data.return_value -def test_build_enums(mocker): - from openapi_python_client.parser.openapi import build_schemas + def test_records_bad_uris_and_keeps_going(self, mocker): + from openapi_python_client.parser.properties import Schemas, build_schemas + from openapi_python_client.schema import Schema - build_model_property = mocker.patch(f"{MODULE_NAME}.build_model_property") - schemas = mocker.MagicMock() - build_enum_property = mocker.patch(f"{MODULE_NAME}.build_enum_property", return_value=(mocker.MagicMock(), schemas)) - in_data = {"1": mocker.MagicMock(enum=["val1", "val2", "val3"])} + components = {"first": Schema.construct(), "second": Schema.construct()} + update_schemas_with_data = mocker.patch(f"{MODULE_NAME}.update_schemas_with_data") + parse_reference_path = mocker.patch( + f"{MODULE_NAME}.parse_reference_path", side_effect=[PropertyError(detail="some details"), "a_path"] + ) + config = Config() + + result = build_schemas(components=components, schemas=Schemas(), config=config) + parse_reference_path.assert_has_calls( + [ + call("#/components/schemas/first"), + call("#/components/schemas/second"), + ] + ) + update_schemas_with_data.assert_called_once_with( + ref_path="a_path", + config=config, + data=components["second"], + schemas=Schemas(errors=[PropertyError(detail="some details", data=components["first"])]), + ) + assert result == update_schemas_with_data.return_value - build_schemas(components=in_data) + def test_retries_failing_properties_while_making_progress(self, mocker): + from openapi_python_client.parser.properties import Schemas, build_schemas + from openapi_python_client.schema import Schema - build_enum_property.assert_called() - build_model_property.assert_not_called() + components = {"first": Schema.construct(), "second": Schema.construct()} + update_schemas_with_data = mocker.patch( + f"{MODULE_NAME}.update_schemas_with_data", side_effect=[PropertyError(), Schemas(), PropertyError()] + ) + parse_reference_path = mocker.patch(f"{MODULE_NAME}.parse_reference_path") + config = Config() + + result = build_schemas(components=components, schemas=Schemas(), config=config) + parse_reference_path.assert_has_calls( + [ + call("#/components/schemas/first"), + call("#/components/schemas/second"), + call("#/components/schemas/first"), + ] + ) + assert update_schemas_with_data.call_count == 3 + assert result.errors == [PropertyError()] def test_build_enum_property_conflict(mocker): from openapi_python_client.parser.properties import Schemas, build_enum_property data = oai.Schema() - schemas = Schemas(enums={"Existing": mocker.MagicMock()}) + schemas = Schemas(classes_by_name={"Existing": mocker.MagicMock()}) err, schemas = build_enum_property( - data=data, name="Existing", required=True, schemas=schemas, enum=[], parent_name=None + data=data, name="Existing", required=True, schemas=schemas, enum=[], parent_name=None, config=Config() ) assert schemas == schemas @@ -1160,7 +1124,7 @@ def test_build_enum_property_no_values(): schemas = Schemas() err, schemas = build_enum_property( - data=data, name="Existing", required=True, schemas=schemas, enum=[], parent_name=None + data=data, name="Existing", required=True, schemas=schemas, enum=[], parent_name=None, config=Config() ) assert schemas == schemas @@ -1174,7 +1138,7 @@ def test_build_enum_property_bad_default(): schemas = Schemas() err, schemas = build_enum_property( - data=data, name="Existing", required=True, schemas=schemas, enum=["A"], parent_name=None + data=data, name="Existing", required=True, schemas=schemas, enum=["A"], parent_name=None, config=Config() ) assert schemas == schemas diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 9a856c190..b366c2333 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -1,11 +1,18 @@ from typing import Callable +from unittest.mock import MagicMock import pytest import openapi_python_client.schema as oai +from openapi_python_client import Config from openapi_python_client.parser.errors import PropertyError from openapi_python_client.parser.properties import DateTimeProperty, ModelProperty, StringProperty -from openapi_python_client.parser.reference import Reference + + +def get_class(): + from openapi_python_client.parser.properties import Class + + return Class(name="MyClass", module_name="my_module") @pytest.mark.parametrize( @@ -23,14 +30,14 @@ ], ) def test_get_type_string(no_optional, nullable, required, json, expected): - from openapi_python_client.parser.properties import ModelProperty, Reference + from openapi_python_client.parser.properties import ModelProperty prop = ModelProperty( name="prop", required=required, nullable=nullable, default=None, - reference=Reference(class_name="MyClass", module_name="my_module"), + class_info=get_class(), description="", optional_properties=[], required_properties=[], @@ -42,14 +49,14 @@ def test_get_type_string(no_optional, nullable, required, json, expected): def test_get_imports(): - from openapi_python_client.parser.properties import ModelProperty, Reference + from openapi_python_client.parser.properties import ModelProperty prop = ModelProperty( name="prop", required=False, nullable=True, default=None, - reference=Reference(class_name="MyClass", module_name="my_module"), + class_info=get_class(), description="", optional_properties=[], required_properties=[], @@ -89,17 +96,13 @@ def test_additional_schemas(self, additional_properties_schema, expected_additio ) model, _ = build_model_property( - data=data, - name="prop", - schemas=Schemas(), - required=True, - parent_name="parent", + data=data, name="prop", schemas=Schemas(), required=True, parent_name="parent", config=MagicMock() ) assert model.additional_properties == expected_additional_properties def test_happy_path(self): - from openapi_python_client.parser.properties import Schemas, build_model_property + from openapi_python_client.parser.properties import Class, Schemas, build_model_property data = oai.Schema.construct( required=["req"], @@ -111,27 +114,26 @@ def test_happy_path(self): description="A class called MyModel", nullable=False, ) - schemas = Schemas(models={"OtherModel": None}) + schemas = Schemas(classes_by_reference={"OtherModel": None}, classes_by_name={"OtherModel": None}) model, new_schemas = build_model_property( - data=data, - name="prop", - schemas=schemas, - required=True, - parent_name="parent", + data=data, name="prop", schemas=schemas, required=True, parent_name="parent", config=Config() ) assert new_schemas != schemas - assert new_schemas.models == { + assert new_schemas.classes_by_name == { "OtherModel": None, "ParentMyModel": model, } + assert new_schemas.classes_by_reference == { + "OtherModel": None, + } assert model == ModelProperty( name="prop", required=True, nullable=False, default=None, - reference=Reference(class_name="ParentMyModel", module_name="parent_my_model"), + class_info=Class(name="ParentMyModel", module_name="parent_my_model"), required_properties=[StringProperty(name="req", required=True, nullable=False, default=None)], optional_properties=[DateTimeProperty(name="opt", required=False, nullable=False, default=None)], description=data.description, @@ -149,14 +151,10 @@ def test_model_name_conflict(self): from openapi_python_client.parser.properties import Schemas, build_model_property data = oai.Schema.construct() - schemas = Schemas(models={"OtherModel": None}) + schemas = Schemas(classes_by_name={"OtherModel": None}) err, new_schemas = build_model_property( - data=data, - name="OtherModel", - schemas=schemas, - required=True, - parent_name=None, + data=data, name="OtherModel", schemas=schemas, required=True, parent_name=None, config=Config() ) assert new_schemas == schemas @@ -173,11 +171,7 @@ def test_bad_props_return_error(self): schemas = Schemas() err, new_schemas = build_model_property( - data=data, - name="prop", - schemas=schemas, - required=True, - parent_name=None, + data=data, name="prop", schemas=schemas, required=True, parent_name=None, config=MagicMock() ) assert new_schemas == schemas @@ -196,40 +190,13 @@ def test_bad_additional_props_return_error(self): schemas = Schemas() err, new_schemas = build_model_property( - data=data, - name="prop", - schemas=schemas, - required=True, - parent_name=None, + data=data, name="prop", schemas=schemas, required=True, parent_name=None, config=MagicMock() ) assert new_schemas == schemas assert err == PropertyError(detail="unknown type not_real", data=oai.Schema(type="not_real")) -@pytest.fixture -def model_property() -> Callable[..., ModelProperty]: - from openapi_python_client.parser.reference import Reference - - def _factory(**kwargs): - kwargs = { - "name": "", - "description": "", - "required": True, - "nullable": True, - "default": None, - "reference": Reference(class_name="", module_name=""), - "required_properties": [], - "optional_properties": [], - "relative_imports": set(), - "additional_properties": False, - **kwargs, - } - return ModelProperty(**kwargs) - - return _factory - - def string_property(**kwargs) -> StringProperty: kwargs = { "name": "", @@ -242,56 +209,88 @@ def string_property(**kwargs) -> StringProperty: class TestProcessProperties: - def test_conflicting_properties_different_types(self, model_property): + def test_conflicting_properties_different_types(self, model_property_factory): from openapi_python_client.parser.properties import Schemas from openapi_python_client.parser.properties.model_property import _process_properties - data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="First"), oai.Reference.construct(ref="Second")]) + data = oai.Schema.construct( + allOf=[oai.Reference.construct(ref="#/First"), oai.Reference.construct(ref="#/Second")] + ) schemas = Schemas( - models={ - "First": model_property( + classes_by_reference={ + "/First": model_property_factory( optional_properties=[StringProperty(name="prop", required=True, nullable=True, default=None)] ), - "Second": model_property( + "/Second": model_property_factory( optional_properties=[DateTimeProperty(name="prop", required=True, nullable=True, default=None)] ), } ) - result = _process_properties(data=data, schemas=schemas, class_name="") + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) assert isinstance(result, PropertyError) - def test_conflicting_properties_same_types(self, model_property): + def test_invalid_reference(self, model_property_factory): from openapi_python_client.parser.properties import Schemas from openapi_python_client.parser.properties.model_property import _process_properties - data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="First"), oai.Reference.construct(ref="Second")]) + data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="ThisIsNotGood")]) + schemas = Schemas() + + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + + assert isinstance(result, PropertyError) + + def test_non_model_reference(self, enum_property_factory): + from openapi_python_client.parser.properties import Schemas + from openapi_python_client.parser.properties.model_property import _process_properties + + data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="#/First")]) + schemas = Schemas( + classes_by_reference={ + "/First": enum_property_factory(), + } + ) + + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) + + assert isinstance(result, PropertyError) + + def test_conflicting_properties_same_types(self, model_property_factory): + from openapi_python_client.parser.properties import Schemas + from openapi_python_client.parser.properties.model_property import _process_properties + + data = oai.Schema.construct( + allOf=[oai.Reference.construct(ref="#/First"), oai.Reference.construct(ref="#/Second")] + ) schemas = Schemas( - models={ - "First": model_property(optional_properties=[string_property(default="abc")]), - "Second": model_property(optional_properties=[string_property()]), + classes_by_reference={ + "/First": model_property_factory(optional_properties=[string_property(default="abc")]), + "/Second": model_property_factory(optional_properties=[string_property()]), } ) - result = _process_properties(data=data, schemas=schemas, class_name="") + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) assert isinstance(result, PropertyError) - def test_duplicate_properties(self, model_property): + def test_duplicate_properties(self, model_property_factory): from openapi_python_client.parser.properties import Schemas from openapi_python_client.parser.properties.model_property import _process_properties - data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="First"), oai.Reference.construct(ref="Second")]) + data = oai.Schema.construct( + allOf=[oai.Reference.construct(ref="#/First"), oai.Reference.construct(ref="#/Second")] + ) prop = string_property() schemas = Schemas( - models={ - "First": model_property(optional_properties=[prop]), - "Second": model_property(optional_properties=[prop]), + classes_by_reference={ + "/First": model_property_factory(optional_properties=[prop]), + "/Second": model_property_factory(optional_properties=[prop]), } ) - result = _process_properties(data=data, schemas=schemas, class_name="") + result = _process_properties(data=data, schemas=schemas, class_name="", config=Config()) assert result.optional_props == [prop], "There should only be one copy of duplicate properties" @@ -299,23 +298,27 @@ def test_duplicate_properties(self, model_property): @pytest.mark.parametrize("second_nullable", [True, False]) @pytest.mark.parametrize("first_required", [True, False]) @pytest.mark.parametrize("second_required", [True, False]) - def test_mixed_requirements(self, model_property, first_nullable, second_nullable, first_required, second_required): + def test_mixed_requirements( + self, model_property_factory, first_nullable, second_nullable, first_required, second_required + ): from openapi_python_client.parser.properties import Schemas from openapi_python_client.parser.properties.model_property import _process_properties - data = oai.Schema.construct(allOf=[oai.Reference.construct(ref="First"), oai.Reference.construct(ref="Second")]) + data = oai.Schema.construct( + allOf=[oai.Reference.construct(ref="#/First"), oai.Reference.construct(ref="#/Second")] + ) schemas = Schemas( - models={ - "First": model_property( + classes_by_reference={ + "/First": model_property_factory( optional_properties=[string_property(required=first_required, nullable=first_nullable)] ), - "Second": model_property( + "/Second": model_property_factory( optional_properties=[string_property(required=second_required, nullable=second_nullable)] ), } ) - result = _process_properties(data=data, schemas=schemas, class_name="") + result = _process_properties(data=data, schemas=schemas, class_name="", config=MagicMock()) nullable = first_nullable and second_nullable required = first_required or second_required @@ -346,7 +349,7 @@ def test_direct_properties_non_ref(self): ) schemas = Schemas() - result = _process_properties(data=data, schemas=schemas, class_name="") + result = _process_properties(data=data, schemas=schemas, class_name="", config=MagicMock()) assert result.optional_props == [string_property(name="second", required=False, nullable=False)] assert result.required_props == [string_property(name="first", required=True, nullable=False)] diff --git a/tests/test_parser/test_properties/test_schemas.py b/tests/test_parser/test_properties/test_schemas.py new file mode 100644 index 000000000..7d961f802 --- /dev/null +++ b/tests/test_parser/test_properties/test_schemas.py @@ -0,0 +1,34 @@ +import pytest + + +def test_class_from_string_default_config(): + from openapi_python_client import Config + from openapi_python_client.parser.properties import Class + + class_ = Class.from_string(string="#/components/schemas/PingResponse", config=Config()) + + assert class_.name == "PingResponse" + assert class_.module_name == "ping_response" + + +@pytest.mark.parametrize( + "class_override, module_override, expected_class, expected_module", + ( + (None, None, "_MyResponse", "_my_response"), + ("MyClass", None, "MyClass", "my_class"), + ("MyClass", "some_module", "MyClass", "some_module"), + (None, "some_module", "_MyResponse", "some_module"), + ), +) +def test_class_from_string(class_override, module_override, expected_class, expected_module): + from openapi_python_client.config import ClassOverride, Config + from openapi_python_client.parser.properties import Class + + ref = "#/components/schemas/_MyResponse" + config = Config( + class_overrides={"_MyResponse": ClassOverride(class_name=class_override, module_name=module_override)} + ) + + result = Class.from_string(string=ref, config=config) + assert result.name == expected_class + assert result.module_name == expected_module diff --git a/tests/test_parser/test_reference.py b/tests/test_parser/test_reference.py deleted file mode 100644 index 3660a1c35..000000000 --- a/tests/test_parser/test_reference.py +++ /dev/null @@ -1,16 +0,0 @@ -def test_from_ref(): - from openapi_python_client.parser.reference import Reference - - r = Reference.from_ref("#/components/schemas/PingResponse") - - assert r.class_name == "PingResponse" - assert r.module_name == "ping_response" - - -def test_from_ref_class_overrides(): - from openapi_python_client.parser.reference import Reference, class_overrides - - ref = "#/components/schemas/_MyResponse" - class_overrides["_MyResponse"] = Reference(class_name="MyResponse", module_name="my_response") - - assert Reference.from_ref(ref) == class_overrides["_MyResponse"] diff --git a/tests/test_parser/test_responses.py b/tests/test_parser/test_responses.py index eb20fb338..025172388 100644 --- a/tests/test_parser/test_responses.py +++ b/tests/test_parser/test_responses.py @@ -1,3 +1,5 @@ +from unittest.mock import MagicMock + import openapi_python_client.schema as oai from openapi_python_client.parser.errors import ParseError, PropertyError from openapi_python_client.parser.properties import NoneProperty, Schemas, StringProperty @@ -9,7 +11,11 @@ def test_response_from_data_no_content(): from openapi_python_client.parser.responses import Response, response_from_data response, schemas = response_from_data( - status_code=200, data=oai.Response.construct(description=""), schemas=Schemas(), parent_name="parent" + status_code=200, + data=oai.Response.construct(description=""), + schemas=Schemas(), + parent_name="parent", + config=MagicMock(), ) assert response == Response( @@ -23,7 +29,9 @@ def test_response_from_data_unsupported_content_type(): from openapi_python_client.parser.responses import response_from_data data = oai.Response.construct(description="", content={"blah": None}) - response, schemas = response_from_data(status_code=200, data=data, schemas=Schemas(), parent_name="parent") + response, schemas = response_from_data( + status_code=200, data=data, schemas=Schemas(), parent_name="parent", config=MagicMock() + ) assert response == ParseError(data=data, detail="Unsupported content_type {'blah': None}") @@ -32,7 +40,9 @@ def test_response_from_data_no_content_schema(): from openapi_python_client.parser.responses import Response, response_from_data data = oai.Response.construct(description="", content={"application/json": oai.MediaType.construct()}) - response, schemas = response_from_data(status_code=200, data=data, schemas=Schemas(), parent_name="parent") + response, schemas = response_from_data( + status_code=200, data=data, schemas=Schemas(), parent_name="parent", config=MagicMock() + ) assert response == Response( status_code=200, @@ -48,13 +58,15 @@ def test_response_from_data_property_error(mocker): data = oai.Response.construct( description="", content={"application/json": oai.MediaType.construct(media_type_schema="something")} ) + config = MagicMock() + response, schemas = responses.response_from_data( - status_code=400, data=data, schemas=Schemas(), parent_name="parent" + status_code=400, data=data, schemas=Schemas(), parent_name="parent", config=config ) assert response == PropertyError() property_from_data.assert_called_once_with( - name="response_400", required=True, data="something", schemas=Schemas(), parent_name="parent" + name="response_400", required=True, data="something", schemas=Schemas(), parent_name="parent", config=config ) @@ -66,8 +78,10 @@ def test_response_from_data_property(mocker): data = oai.Response.construct( description="", content={"application/json": oai.MediaType.construct(media_type_schema="something")} ) + config = MagicMock() + response, schemas = responses.response_from_data( - status_code=400, data=data, schemas=Schemas(), parent_name="parent" + status_code=400, data=data, schemas=Schemas(), parent_name="parent", config=config ) assert response == responses.Response( @@ -76,5 +90,5 @@ def test_response_from_data_property(mocker): source="response.json()", ) property_from_data.assert_called_once_with( - name="response_400", required=True, data="something", schemas=Schemas(), parent_name="parent" + name="response_400", required=True, data="something", schemas=Schemas(), parent_name="parent", config=config ) diff --git a/usage.md b/usage.md index 4890e0e37..709534d05 100644 --- a/usage.md +++ b/usage.md @@ -1,6 +1,6 @@ # `openapi-python-client` -Generate a Python client from an OpenAPI JSON document +Generate a Python client from an OpenAPI JSON document **Usage**: @@ -10,20 +10,19 @@ $ openapi-python-client [OPTIONS] COMMAND [ARGS]... **Options**: -* `--version`: Print the version and exit [default: False] -* `--config PATH`: Path to the config file to use -* `--install-completion`: Install completion for the current shell. -* `--show-completion`: Show completion for the current shell, to copy it or customize the installation. -* `--help`: Show this message and exit. +- `--version`: Print the version and exit [default: False] +- `--install-completion`: Install completion for the current shell. +- `--show-completion`: Show completion for the current shell, to copy it or customize the installation. +- `--help`: Show this message and exit. **Commands**: -* `generate`: Generate a new OpenAPI Client library -* `update`: Update an existing OpenAPI Client library +- `generate`: Generate a new OpenAPI Client library +- `update`: Update an existing OpenAPI Client library ## `openapi-python-client generate` -Generate a new OpenAPI Client library +Generate a new OpenAPI Client library **Usage**: @@ -33,15 +32,17 @@ $ openapi-python-client generate [OPTIONS] **Options**: -* `--url TEXT`: A URL to read the JSON from -* `--path PATH`: A path to the JSON file -* `--custom-template-path DIRECTORY`: A path to a directory containing custom template(s) -* `--meta [none|poetry|setup]`: The type of metadata you want to generate. [default: poetry] -* `--help`: Show this message and exit. +- `--url TEXT`: A URL to read the JSON from +- `--path PATH`: A path to the JSON file +- `--custom-template-path DIRECTORY`: A path to a directory containing custom template(s) +- `--meta [none|poetry|setup]`: The type of metadata you want to generate. [default: poetry] +- `--file-encoding TEXT`: Encoding used when writing generated [default: utf-8] +- `--config PATH`: Path to the config file to use +- `--help`: Show this message and exit. ## `openapi-python-client update` -Update an existing OpenAPI Client library +Update an existing OpenAPI Client library **Usage**: @@ -51,9 +52,10 @@ $ openapi-python-client update [OPTIONS] **Options**: -* `--url TEXT`: A URL to read the JSON from -* `--path PATH`: A path to the JSON file -* `--custom-template-path DIRECTORY`: A path to a directory containing custom template(s) -* `--meta [none|poetry|setup]`: The type of metadata you want to generate. [default: poetry] -* `--help`: Show this message and exit. - +- `--url TEXT`: A URL to read the JSON from +- `--path PATH`: A path to the JSON file +- `--custom-template-path DIRECTORY`: A path to a directory containing custom template(s) +- `--meta [none|poetry|setup]`: The type of metadata you want to generate. [default: poetry] +- `--file-encoding TEXT`: Encoding used when writing generated [default: utf-8] +- `--config PATH`: Path to the config file to use +- `--help`: Show this message and exit.