From 909682a4b35c58825b48bdf21bc4402711b1a562 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 16 Aug 2021 12:13:41 -0600 Subject: [PATCH 1/4] fix: Properly replace reserved words in class and module names [#475, #476]. Thanks @mtovts! --- .../my_test_api_client/api/__init__.py | 5 ++ .../my_test_api_client/api/naming/__init__.py | 11 +++ .../my_test_api_client/api/naming/__init__.py | 0 .../api/naming/get_naming_keywords.py | 72 +++++++++++++++++++ .../my_test_api_client/models/__init__.py | 2 + .../my_test_api_client/models/import_.py | 44 ++++++++++++ .../my_test_api_client/models/none.py | 44 ++++++++++++ end_to_end_tests/openapi.json | 26 +++++++ .../parser/properties/schemas.py | 18 ++--- openapi_python_client/utils.py | 17 ++++- .../test_properties/test_model_property.py | 4 +- tests/test_utils.py | 17 +++++ 12 files changed, 248 insertions(+), 12 deletions(-) create mode 100644 end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/naming/__init__.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/naming/get_naming_keywords.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/import_.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/none.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py index a71148e05..34369178e 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py @@ -4,6 +4,7 @@ from .default import DefaultEndpoints from .location import LocationEndpoints +from .naming import NamingEndpoints from .parameters import ParametersEndpoints from .tag1 import Tag1Endpoints from .tests import TestsEndpoints @@ -29,3 +30,7 @@ def tag1(cls) -> Type[Tag1Endpoints]: @classmethod def location(cls) -> Type[LocationEndpoints]: return LocationEndpoints + + @classmethod + def naming(cls) -> Type[NamingEndpoints]: + return NamingEndpoints diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py new file mode 100644 index 000000000..f75d0b642 --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py @@ -0,0 +1,11 @@ +""" Contains methods for accessing the API Endpoints """ + +import types + +from . import get_naming_keywords + + +class NamingEndpoints: + @classmethod + def get_naming_keywords(cls) -> types.ModuleType: + return get_naming_keywords diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/naming/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/get_naming_keywords.py b/end_to_end_tests/golden-record/my_test_api_client/api/naming/get_naming_keywords.py new file mode 100644 index 000000000..fc26c3179 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/naming/get_naming_keywords.py @@ -0,0 +1,72 @@ +from typing import Any, Dict + +import httpx + +from ...client import Client +from ...types import UNSET, Response + + +def _get_kwargs( + *, + client: Client, + import_: str, +) -> Dict[str, Any]: + url = "{}/naming/keywords".format(client.base_url) + + headers: Dict[str, Any] = client.get_headers() + cookies: Dict[str, Any] = client.get_cookies() + + params: Dict[str, Any] = { + "import": import_, + } + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + return { + "url": url, + "headers": headers, + "cookies": cookies, + "timeout": client.get_timeout(), + "params": params, + } + + +def _build_response(*, response: httpx.Response) -> Response[Any]: + return Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=None, + ) + + +def sync_detailed( + *, + client: Client, + import_: str, +) -> Response[Any]: + kwargs = _get_kwargs( + client=client, + import_=import_, + ) + + response = httpx.get( + **kwargs, + ) + + return _build_response(response=response) + + +async def asyncio_detailed( + *, + client: Client, + import_: str, +) -> Response[Any]: + kwargs = _get_kwargs( + client=client, + import_=import_, + ) + + async with httpx.AsyncClient() as _client: + response = await _client.get(**kwargs) + + return _build_response(response=response) 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 51d0dd02c..63c5dd10d 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 @@ -19,6 +19,7 @@ from .different_enum import DifferentEnum from .free_form_model import FreeFormModel from .http_validation_error import HTTPValidationError +from .import_ import Import from .model_from_all_of import ModelFromAllOf from .model_name import ModelName from .model_with_additional_properties_inlined import ModelWithAdditionalPropertiesInlined @@ -35,6 +36,7 @@ from .model_with_union_property_inlined import ModelWithUnionPropertyInlined from .model_with_union_property_inlined_fruit_type_0 import ModelWithUnionPropertyInlinedFruitType0 from .model_with_union_property_inlined_fruit_type_1 import ModelWithUnionPropertyInlinedFruitType1 +from .none import None_ from .test_inline_objects_json_body import TestInlineObjectsJsonBody from .test_inline_objects_response_200 import TestInlineObjectsResponse200 from .validation_error import ValidationError diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/import_.py b/end_to_end_tests/golden-record/my_test_api_client/models/import_.py new file mode 100644 index 000000000..276a4f21b --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/import_.py @@ -0,0 +1,44 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +T = TypeVar("T", bound="Import") + + +@attr.s(auto_attribs=True) +class Import: + """ """ + + 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() + import_ = cls() + + import_.additional_properties = d + return import_ + + @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/none.py b/end_to_end_tests/golden-record/my_test_api_client/models/none.py new file mode 100644 index 000000000..e1722f094 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/none.py @@ -0,0 +1,44 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +T = TypeVar("T", bound="None_") + + +@attr.s(auto_attribs=True) +class None_: + """ """ + + 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() + none = cls() + + none.additional_properties = d + return none + + @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 0ee32fd34..bd33a3b14 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -933,6 +933,26 @@ ], "responses": {} } + }, + "/naming/keywords": { + "description": "Ensure that Python keywords are renamed properly.", + "get": { + "tags": [ + "naming" + ], + "parameters": [ + { + "name": "import", + "required": true, + "schema": { + "type": "string", + "nullable": false + }, + "in": "query" + } + ], + "responses": {} + } } }, "components": { @@ -1761,6 +1781,12 @@ "AByteStream": { "type": "string", "format": "byte" + }, + "import": { + "type": "object" + }, + "None": { + "type": "object" } } } diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index d1cfd63ba..9aa688879 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -7,7 +7,7 @@ from ... import Config from ... import schema as oai -from ... import utils +from ...utils import ClassName, PythonIdentifier from ..errors import ParseError, PropertyError if TYPE_CHECKING: # pragma: no cover @@ -17,7 +17,6 @@ _ReferencePath = NewType("_ReferencePath", str) -_ClassName = NewType("_ClassName", str) def parse_reference_path(ref_path_raw: str) -> Union[_ReferencePath, ParseError]: @@ -38,25 +37,26 @@ def parse_reference_path(ref_path_raw: str) -> Union[_ReferencePath, ParseError] class Class: """Represents Python class which will be generated from an OpenAPI schema""" - name: _ClassName - module_name: str + name: ClassName + module_name: PythonIdentifier @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) + class_name = ClassName(class_name, config.field_prefix) override = config.class_overrides.get(class_name) if override is not None and override.class_name is not None: - class_name = override.class_name + class_name = ClassName(override.class_name, config.field_prefix) 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) + module_name = class_name + module_name = PythonIdentifier(module_name, config.field_prefix) - return Class(name=cast(_ClassName, class_name), module_name=module_name) + return Class(name=class_name, module_name=module_name) @attr.s(auto_attribs=True, frozen=True) @@ -64,7 +64,7 @@ class Schemas: """Structure for containing all defined, shareable, and reusable schemas (attr classes and Enums)""" classes_by_reference: Dict[_ReferencePath, Property] = attr.ib(factory=dict) - classes_by_name: Dict[_ClassName, Property] = attr.ib(factory=dict) + classes_by_name: Dict[ClassName, Property] = attr.ib(factory=dict) errors: List[ParseError] = attr.ib(factory=list) diff --git a/openapi_python_client/utils.py b/openapi_python_client/utils.py index c74598b70..1f1465e88 100644 --- a/openapi_python_client/utils.py +++ b/openapi_python_client/utils.py @@ -7,7 +7,7 @@ class PythonIdentifier(str): - """A string which has been validated / transformed into a valid identifier for Python""" + """A snake_case string which has been validated / transformed into a valid identifier for Python""" def __new__(cls, value: str, prefix: str) -> "PythonIdentifier": new_value = fix_reserved_words(snake_case(sanitize(value))) @@ -20,6 +20,21 @@ def __deepcopy__(self, _: Any) -> "PythonIdentifier": return self +class ClassName(str): + """A PascalCase string which has been validated / transformed into a valid class name for Python""" + + def __new__(cls, value: str, prefix: str) -> "ClassName": + new_value = fix_reserved_words(pascal_case(sanitize(value))) + + if not new_value.isidentifier(): + value = f"{prefix}{new_value}" + new_value = fix_reserved_words(pascal_case(sanitize(value))) + return str.__new__(cls, new_value) + + def __deepcopy__(self, _: Any) -> "ClassName": + return self + + def sanitize(value: str) -> str: """Removes every character that isn't 0-9, A-Z, a-z, or a known delimiter""" return re.sub(rf"[^\w{DELIMITERS}]+", "", value) diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 0b5a729d1..5f179eab2 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -73,7 +73,7 @@ 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", config=MagicMock() + data=data, name="prop", schemas=Schemas(), required=True, parent_name="parent", config=Config() ) assert model.additional_properties == expected_additional_properties @@ -151,7 +151,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, config=MagicMock() + data=data, name="prop", schemas=schemas, required=True, parent_name=None, config=Config() ) assert new_schemas == schemas diff --git a/tests/test_utils.py b/tests/test_utils.py index c50c8d0cc..2345aaee8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -20,6 +20,23 @@ def test_empty_is_prefixed(self): assert utils.PythonIdentifier(value="", prefix="something") == "something" +class TestClassName: + def test_valid_is_not_changed(self): + assert utils.ClassName(value="ValidClass", prefix="field") == "ValidClass" + + def test_numbers_are_prefixed(self): + assert utils.ClassName(value="1", prefix="field") == "Field1" + + def test_invalid_symbols_are_stripped(self): + assert utils.ClassName(value="$abc", prefix="prefix") == "Abc" + + def test_keywords_are_postfixed(self): + assert utils.ClassName(value="none", prefix="prefix") == "None_" + + def test_empty_is_prefixed(self): + assert utils.ClassName(value="", prefix="something") == "Something" + + @pytest.mark.parametrize( "before, after", [ From 57dc38afdb7c385fafd354931a93cd0c8c084db3 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 16 Aug 2021 12:33:41 -0600 Subject: [PATCH 2/4] feat: Expose `python_identifier` and `class_name` functions to custom templates to rename with the same behavior as the parser. --- .../my_test_api_client/api/__init__.py | 6 +- .../my_test_api_client/api/naming/__init__.py | 11 --- .../my_test_api_client/api/naming/__init__.py | 0 .../api/naming/get_naming_keywords.py | 72 ------------------- .../test_custom_templates/api_init.py.jinja | 8 +-- .../endpoint_init.py.jinja | 8 +-- openapi_python_client/__init__.py | 3 + 7 files changed, 14 insertions(+), 94 deletions(-) delete mode 100644 end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py delete mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/naming/__init__.py delete mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/naming/get_naming_keywords.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py index 34369178e..3295d210c 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py @@ -4,10 +4,10 @@ from .default import DefaultEndpoints from .location import LocationEndpoints -from .naming import NamingEndpoints from .parameters import ParametersEndpoints from .tag1 import Tag1Endpoints from .tests import TestsEndpoints +from .true_ import True_Endpoints class MyTestApiClientApi: @@ -32,5 +32,5 @@ def location(cls) -> Type[LocationEndpoints]: return LocationEndpoints @classmethod - def naming(cls) -> Type[NamingEndpoints]: - return NamingEndpoints + def true_(cls) -> Type[True_Endpoints]: + return True_Endpoints diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py deleted file mode 100644 index f75d0b642..000000000 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/naming/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" Contains methods for accessing the API Endpoints """ - -import types - -from . import get_naming_keywords - - -class NamingEndpoints: - @classmethod - def get_naming_keywords(cls) -> types.ModuleType: - return get_naming_keywords diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/naming/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/naming/get_naming_keywords.py b/end_to_end_tests/golden-record/my_test_api_client/api/naming/get_naming_keywords.py deleted file mode 100644 index fc26c3179..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/api/naming/get_naming_keywords.py +++ /dev/null @@ -1,72 +0,0 @@ -from typing import Any, Dict - -import httpx - -from ...client import Client -from ...types import UNSET, Response - - -def _get_kwargs( - *, - client: Client, - import_: str, -) -> Dict[str, Any]: - url = "{}/naming/keywords".format(client.base_url) - - headers: Dict[str, Any] = client.get_headers() - cookies: Dict[str, Any] = client.get_cookies() - - params: Dict[str, Any] = { - "import": import_, - } - params = {k: v for k, v in params.items() if v is not UNSET and v is not None} - - return { - "url": url, - "headers": headers, - "cookies": cookies, - "timeout": client.get_timeout(), - "params": params, - } - - -def _build_response(*, response: httpx.Response) -> Response[Any]: - return Response( - status_code=response.status_code, - content=response.content, - headers=response.headers, - parsed=None, - ) - - -def sync_detailed( - *, - client: Client, - import_: str, -) -> Response[Any]: - kwargs = _get_kwargs( - client=client, - import_=import_, - ) - - response = httpx.get( - **kwargs, - ) - - return _build_response(response=response) - - -async def asyncio_detailed( - *, - client: Client, - import_: str, -) -> Response[Any]: - kwargs = _get_kwargs( - client=client, - import_=import_, - ) - - async with httpx.AsyncClient() as _client: - response = await _client.get(**kwargs) - - return _build_response(response=response) diff --git a/end_to_end_tests/test_custom_templates/api_init.py.jinja b/end_to_end_tests/test_custom_templates/api_init.py.jinja index 5f6a23523..0c5720bf2 100644 --- a/end_to_end_tests/test_custom_templates/api_init.py.jinja +++ b/end_to_end_tests/test_custom_templates/api_init.py.jinja @@ -2,12 +2,12 @@ from typing import Type {% for tag in endpoint_collections_by_tag.keys() %} -from .{{ tag }} import {{ utils.pascal_case(tag) }}Endpoints +from .{{ tag }} import {{ class_name(tag) }}Endpoints {% endfor %} -class {{ utils.pascal_case(package_name) }}Api: +class {{ class_name(package_name) }}Api: {% for tag in endpoint_collections_by_tag.keys() %} @classmethod - def {{ tag }}(cls) -> Type[{{ utils.pascal_case(tag) }}Endpoints]: - return {{ utils.pascal_case(tag) }}Endpoints + def {{ tag }}(cls) -> Type[{{ class_name(tag) }}Endpoints]: + return {{ class_name(tag) }}Endpoints {% endfor %} diff --git a/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja b/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja index ca9851679..dd64d942c 100644 --- a/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja +++ b/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja @@ -2,15 +2,15 @@ import types {% for endpoint in endpoint_collection.endpoints %} -from . import {{ utils.snake_case(endpoint.name) }} +from . import {{ python_identifier(endpoint.name) }} {% endfor %} -class {{ utils.pascal_case(endpoint_collection.tag) }}Endpoints: +class {{ class_name(endpoint_collection.tag) }}Endpoints: {% for endpoint in endpoint_collection.endpoints %} @classmethod - def {{ utils.snake_case(endpoint.name) }}(cls) -> types.ModuleType: + def {{ python_identifier(endpoint.name) }}(cls) -> types.ModuleType: {% if endpoint.description %} """ {{ endpoint.description }} @@ -20,5 +20,5 @@ class {{ utils.pascal_case(endpoint_collection.tag) }}Endpoints: {{ endpoint.summary }} """ {% endif %} - return {{ utils.snake_case(endpoint.name) }} + return {{ python_identifier(endpoint.name) }} {% endfor %} diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index ebf6c3fda..72d43abd1 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -58,6 +58,7 @@ def __init__( self.openapi: GeneratorData = openapi self.meta: MetaType = meta self.file_encoding = file_encoding + self.config = config package_loader = PackageLoader(__package__) loader: BaseLoader @@ -87,6 +88,8 @@ def __init__( self.env.filters.update(TEMPLATE_FILTERS) self.env.globals.update( utils=utils, + python_identifier=lambda x: utils.PythonIdentifier(x, config.field_prefix), + class_name=lambda x: utils.ClassName(x, config.field_prefix), package_name=self.package_name, package_dir=self.package_dir, package_description=self.package_description, From ba65079f2620f5c00f93016ab85fbfe2bd2ee5aa Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 16 Aug 2021 12:34:37 -0600 Subject: [PATCH 3/4] fix: Prevent generating Python files named the same as reserved / key words. --- openapi_python_client/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 72d43abd1..78dc41faf 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -17,7 +17,6 @@ from .config import Config from .parser import GeneratorData, import_string_from_class from .parser.errors import GeneratorError -from .utils import snake_case if sys.version_info.minor < 8: # version did not exist before 3.8, need to use a backport from importlib_metadata import version @@ -270,7 +269,7 @@ def _build_api(self) -> None: ) for endpoint in collection.endpoints: - module_path = tag_dir / f"{snake_case(endpoint.name)}.py" + module_path = tag_dir / f"{utils.PythonIdentifier(endpoint.name, self.config.field_prefix)}.py" module_path.write_text(endpoint_template.render(endpoint=endpoint), encoding=self.file_encoding) From 7e4fd7d2a2ffe4f123b2eda4748f8314f99f3b21 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Mon, 16 Aug 2021 12:35:08 -0600 Subject: [PATCH 4/4] fix: Treat `true` and `false` as reserved words. --- .../my_test_api_client/api/true_/__init__.py | 11 +++ .../my_test_api_client/api/true_/__init__.py | 0 .../my_test_api_client/api/true_/false_.py | 72 +++++++++++++++++++ end_to_end_tests/openapi.json | 3 +- openapi_python_client/utils.py | 2 +- 5 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/true_/__init__.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/true_/__init__.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/true_/false_.py diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/true_/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/true_/__init__.py new file mode 100644 index 000000000..9c2a0566f --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/true_/__init__.py @@ -0,0 +1,11 @@ +""" Contains methods for accessing the API Endpoints """ + +import types + +from . import false_ + + +class True_Endpoints: + @classmethod + def false_(cls) -> types.ModuleType: + return false_ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/true_/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/true_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/true_/false_.py b/end_to_end_tests/golden-record/my_test_api_client/api/true_/false_.py new file mode 100644 index 000000000..fc26c3179 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/true_/false_.py @@ -0,0 +1,72 @@ +from typing import Any, Dict + +import httpx + +from ...client import Client +from ...types import UNSET, Response + + +def _get_kwargs( + *, + client: Client, + import_: str, +) -> Dict[str, Any]: + url = "{}/naming/keywords".format(client.base_url) + + headers: Dict[str, Any] = client.get_headers() + cookies: Dict[str, Any] = client.get_cookies() + + params: Dict[str, Any] = { + "import": import_, + } + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + return { + "url": url, + "headers": headers, + "cookies": cookies, + "timeout": client.get_timeout(), + "params": params, + } + + +def _build_response(*, response: httpx.Response) -> Response[Any]: + return Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=None, + ) + + +def sync_detailed( + *, + client: Client, + import_: str, +) -> Response[Any]: + kwargs = _get_kwargs( + client=client, + import_=import_, + ) + + response = httpx.get( + **kwargs, + ) + + return _build_response(response=response) + + +async def asyncio_detailed( + *, + client: Client, + import_: str, +) -> Response[Any]: + kwargs = _get_kwargs( + client=client, + import_=import_, + ) + + async with httpx.AsyncClient() as _client: + response = await _client.get(**kwargs) + + return _build_response(response=response) diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index bd33a3b14..7a4a593c6 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -938,8 +938,9 @@ "description": "Ensure that Python keywords are renamed properly.", "get": { "tags": [ - "naming" + "true" ], + "operationId": "false", "parameters": [ { "name": "import", diff --git a/openapi_python_client/utils.py b/openapi_python_client/utils.py index 1f1465e88..4c47855d2 100644 --- a/openapi_python_client/utils.py +++ b/openapi_python_client/utils.py @@ -48,7 +48,7 @@ def split_words(value: str) -> List[str]: return re.findall(rf"[^{DELIMITERS}]+", value) -RESERVED_WORDS = (set(dir(builtins)) | {"self"}) - {"type", "id"} +RESERVED_WORDS = (set(dir(builtins)) | {"self", "true", "false"}) - {"type", "id"} def fix_reserved_words(value: str) -> str: