From b072fc7995a42098560a3ec49415dc2a848ce75b Mon Sep 17 00:00:00 2001 From: Nementon Date: Fri, 30 Apr 2021 15:23:23 +0200 Subject: [PATCH 01/11] templatize api,endpoint init files --- openapi_python_client/__init__.py | 21 ++++++++++++++++--- .../templates/api_init.py.jinja | 1 + .../templates/endpoint_init.py.jinja | 0 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 openapi_python_client/templates/api_init.py.jinja create mode 100644 openapi_python_client/templates/endpoint_init.py.jinja diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 2a7cf574b..35018d1cd 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -239,15 +239,30 @@ def _build_api(self) -> None: client_path.write_text(client_template.render(), encoding=self.file_encoding) # Generate endpoints + endpoint_collections_by_tag = self.openapi.endpoint_collections_by_tag.items() api_dir = self.package_dir / "api" api_dir.mkdir() - api_init = api_dir / "__init__.py" - api_init.write_text('""" Contains methods for accessing the API """', encoding=self.file_encoding) + api_init_path = api_dir / "__init__.py" + api_init_template = self.env.get_template("api_init.py.jinja") + api_init_path.write_text( + api_init_template.render( + package_name=self.package_name, + endpoint_collections_by_tag=endpoint_collections_by_tag, + ), + encoding=self.file_encoding, + ) endpoint_template = self.env.get_template("endpoint_module.py.jinja") - for tag, collection in self.openapi.endpoint_collections_by_tag.items(): + for tag, collection in endpoint_collections_by_tag: tag_dir = api_dir / tag tag_dir.mkdir() + + endpoint_init_path = tag_dir / "__init__.py" + endpoint_init_template = self.env.get_template("endpoint_init.py.jinja") + endpoint_init_path.write_text( + endpoint_init_template.render(package_name=self.package_name, tag=tag, endpoints=collection.endpoints), + encoding=self.file_encoding, + ) (tag_dir / "__init__.py").touch() for endpoint in collection.endpoints: diff --git a/openapi_python_client/templates/api_init.py.jinja b/openapi_python_client/templates/api_init.py.jinja new file mode 100644 index 000000000..dc035f4ce --- /dev/null +++ b/openapi_python_client/templates/api_init.py.jinja @@ -0,0 +1 @@ +""" Contains methods for accessing the API """ diff --git a/openapi_python_client/templates/endpoint_init.py.jinja b/openapi_python_client/templates/endpoint_init.py.jinja new file mode 100644 index 000000000..e69de29bb From e10ba94c09601c30f51b9430c978b20271cba4e5 Mon Sep 17 00:00:00 2001 From: p1-ra Date: Mon, 3 May 2021 15:35:14 +0200 Subject: [PATCH 02/11] e2e / add {api_init.py.jina, endpoint_init.py.jinja} custom templates tests --- .../custom-templates-golden-record/README.md | 1 + .../my_test_api_client/api/__init__.py | 17 +++ .../api/default/__init__.py | 16 +++ .../my_test_api_client/api/tests/__init__.py | 129 ++++++++++++++++++ .../test_custom_templates/api_init.py.jinja | 19 +++ .../endpoint_init.py.jinja | 37 +++++ end_to_end_tests/test_end_to_end.py | 70 +++++++--- 7 files changed, 270 insertions(+), 19 deletions(-) create mode 100644 end_to_end_tests/custom-templates-golden-record/README.md create mode 100644 end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py create mode 100644 end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py create mode 100644 end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py create mode 100644 end_to_end_tests/test_custom_templates/api_init.py.jinja create mode 100644 end_to_end_tests/test_custom_templates/endpoint_init.py.jinja diff --git a/end_to_end_tests/custom-templates-golden-record/README.md b/end_to_end_tests/custom-templates-golden-record/README.md new file mode 100644 index 000000000..e5106eea7 --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/README.md @@ -0,0 +1 @@ +my-test-api-client \ No newline at end of file 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 new file mode 100644 index 000000000..359d0b2dc --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/__init__.py @@ -0,0 +1,17 @@ +""" Contains methods for accessing the API """ + + +from typing import Type + +from my_test_api_client.api.default import DefaultEndpoints +from my_test_api_client.api.tests import TestsEndpoints + + +class MyTestApiClientApi: + @classmethod + def tests(cls) -> Type[TestsEndpoints]: + return TestsEndpoints + + @classmethod + def default(cls) -> Type[DefaultEndpoints]: + return DefaultEndpoints diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py new file mode 100644 index 000000000..5928666d9 --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py @@ -0,0 +1,16 @@ +""" Contains methods for accessing the API Endpoints """ + + +import types + +from my_test_api_client.api.default import get_common_parameters, post_common_parameters + + +class DefaultEndpoints: + @classmethod + def get_common_parameters(cls) -> types.ModuleType: + return get_common_parameters + + @classmethod + def post_common_parameters(cls) -> types.ModuleType: + return post_common_parameters diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py new file mode 100644 index 000000000..b92538c28 --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py @@ -0,0 +1,129 @@ +""" Contains methods for accessing the API Endpoints """ + + +import types + +from my_test_api_client.api.tests import ( + defaults_tests_defaults_post, + get_basic_list_of_booleans, + get_basic_list_of_floats, + get_basic_list_of_integers, + get_basic_list_of_strings, + get_user_list, + int_enum_tests_int_enum_post, + json_body_tests_json_body_post, + no_response_tests_no_response_get, + octet_stream_tests_octet_stream_get, + optional_value_tests_optional_query_param, + test_inline_objects, + token_with_cookie_auth_token_with_cookie_get, + unsupported_content_tests_unsupported_content_get, + upload_file_tests_upload_post, +) + + +class TestsEndpoints: + @classmethod + def get_user_list(cls) -> types.ModuleType: + """ + Get a list of things + """ + return get_user_list + + @classmethod + def get_basic_list_of_strings(cls) -> types.ModuleType: + """ + Get a list of strings + """ + return get_basic_list_of_strings + + @classmethod + def get_basic_list_of_integers(cls) -> types.ModuleType: + """ + Get a list of integers + """ + return get_basic_list_of_integers + + @classmethod + def get_basic_list_of_floats(cls) -> types.ModuleType: + """ + Get a list of floats + """ + return get_basic_list_of_floats + + @classmethod + def get_basic_list_of_booleans(cls) -> types.ModuleType: + """ + Get a list of booleans + """ + return get_basic_list_of_booleans + + @classmethod + def upload_file_tests_upload_post(cls) -> types.ModuleType: + """ + Upload a file + """ + return upload_file_tests_upload_post + + @classmethod + def json_body_tests_json_body_post(cls) -> types.ModuleType: + """ + Try sending a JSON body + """ + return json_body_tests_json_body_post + + @classmethod + def defaults_tests_defaults_post(cls) -> types.ModuleType: + """ + Defaults + """ + return defaults_tests_defaults_post + + @classmethod + def octet_stream_tests_octet_stream_get(cls) -> types.ModuleType: + """ + Octet Stream + """ + return octet_stream_tests_octet_stream_get + + @classmethod + def no_response_tests_no_response_get(cls) -> types.ModuleType: + """ + No Response + """ + return no_response_tests_no_response_get + + @classmethod + def unsupported_content_tests_unsupported_content_get(cls) -> types.ModuleType: + """ + Unsupported Content + """ + return unsupported_content_tests_unsupported_content_get + + @classmethod + def int_enum_tests_int_enum_post(cls) -> types.ModuleType: + """ + Int Enum + """ + return int_enum_tests_int_enum_post + + @classmethod + def test_inline_objects(cls) -> types.ModuleType: + """ + Test Inline Objects + """ + return test_inline_objects + + @classmethod + def optional_value_tests_optional_query_param(cls) -> types.ModuleType: + """ + Test optional query parameters + """ + return optional_value_tests_optional_query_param + + @classmethod + def token_with_cookie_auth_token_with_cookie_get(cls) -> types.ModuleType: + """ + Test optional cookie parameters + """ + return token_with_cookie_auth_token_with_cookie_get 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 new file mode 100644 index 000000000..06163930d --- /dev/null +++ b/end_to_end_tests/test_custom_templates/api_init.py.jinja @@ -0,0 +1,19 @@ +""" Contains methods for accessing the API """ + +{% macro snake_to_pascal_case(name) %} +{%- for part in name.split('_') -%} +{{ part | capitalize }} +{%- endfor -%} +{% endmacro %} + +from typing import Type +{% for tag, collection in endpoint_collections_by_tag %} +from {{ package_name }}.api.{{ tag }} import {{ snake_to_pascal_case(tag) }}Endpoints +{% endfor %} + +class {{ snake_to_pascal_case(package_name) }}Api: +{% for tag, collection in endpoint_collections_by_tag %} + @classmethod + def {{ tag }}(cls) -> Type[{{ snake_to_pascal_case(tag) }}Endpoints]: + return {{ snake_to_pascal_case(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 new file mode 100644 index 000000000..20612ba43 --- /dev/null +++ b/end_to_end_tests/test_custom_templates/endpoint_init.py.jinja @@ -0,0 +1,37 @@ +""" Contains methods for accessing the API Endpoints """ + +{% macro snake_to_pascal_case(name) %} +{%- for part in name.split('_') -%} +{{ part | capitalize }} +{%- endfor -%} +{% endmacro %} + +{% macro to_snake_name(name) %} +{% set last_snakize = {'at': 0} %} +{%- for part in name -%} +{{ '_' if not loop.first and part in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' and (loop.index - last_snakize.at) > 1 and not last_snakize.update({'at': loop.index}) }}{{ part | lower }} +{%- endfor -%} +{% endmacro %} + +import types +{% for endpoint in endpoints %} +from {{ package_name }}.api.{{ tag }} import {{ to_snake_name(endpoint.name) }} +{% endfor %} + +class {{ snake_to_pascal_case(tag) }}Endpoints: + +{% for endpoint in endpoints %} + + @classmethod + def {{ to_snake_name(endpoint.name) }}(cls) -> types.ModuleType: + {% if endpoint.description %} + """ + {{ endpoint.description }} + """ + {% elif endpoint.summary %} + """ + {{ endpoint.summary }} + """ + {% endif %} + return {{ to_snake_name(endpoint.name) }} +{% endfor %} diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index fa4d21598..ad552092a 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -12,7 +12,10 @@ def _compare_directories( record: Path, test_subject: Path, - expected_differences: Optional[Dict[str, str]] = None, + expected_differences: Optional[ + Dict[str, str] + ] = None, # key: path relative to generated directory, value: expected generated content + depth=0, ): first_printable = record.relative_to(Path.cwd()) second_printable = test_subject.relative_to(Path.cwd()) @@ -22,27 +25,41 @@ def _compare_directories( pytest.fail(f"{first_printable} or {second_printable} was missing: {missing_files}", pytrace=False) expected_differences = expected_differences or {} - _, mismatch, errors = cmpfiles(record, test_subject, dc.common_files, shallow=False) - mismatch = set(mismatch) - - for file_name in mismatch | set(expected_differences.keys()): - if file_name not in expected_differences: - continue - if file_name not in mismatch: - pytest.fail(f"Expected {file_name} to be different but it was not", pytrace=False) - generated = (test_subject / file_name).read_text() - assert generated == expected_differences[file_name], f"Unexpected output in {file_name}" - del expected_differences[file_name] - mismatch.remove(file_name) - - if mismatch: + _, mismatches, errors = cmpfiles(record, test_subject, dc.common_files, shallow=False) + mismatches = set(mismatches) + + expected_path_mismatches = [] + for file_name in mismatches: + + mismatch_file_path = test_subject.joinpath(file_name) + for expected_differences_path in expected_differences.keys(): + + if mismatch_file_path.match(str(expected_differences_path)): + + generated_content = (test_subject / file_name).read_text() + expected_content = expected_differences[expected_differences_path] + assert generated_content == expected_content, f"Unexpected output in {mismatch_file_path}" + expected_path_mismatches.append(expected_differences_path) + + for path_mismatch in expected_path_mismatches: + matched_file_name = path_mismatch.name + mismatches.remove(matched_file_name) + del expected_differences[path_mismatch] + + if mismatches: pytest.fail( - f"{first_printable} and {second_printable} had differing files: {mismatch}, and errors {errors}", + f"{first_printable} and {second_printable} had differing files: {mismatches}, and errors {errors}", pytrace=False, ) for sub_path in dc.common_dirs: - _compare_directories(record / sub_path, test_subject / sub_path, expected_differences=expected_differences) + _compare_directories( + record / sub_path, test_subject / sub_path, expected_differences=expected_differences, depth=depth + 1 + ) + + if depth == 0 and len(expected_differences.keys()) > 0: + failure = "\n".join([f"Expected {path} to be different but it was not" for path in expected_differences.keys()]) + pytest.fail(failure, pytrace=False) def run_e2e_test(extra_args=None, expected_differences=None): @@ -60,6 +77,7 @@ def run_e2e_test(extra_args=None, expected_differences=None): if result.exit_code != 0: raise result.exception + _compare_directories(gr_path, output_path, expected_differences=expected_differences) import mypy.api @@ -75,7 +93,21 @@ def test_end_to_end(): def test_custom_templates(): + expected_differences = {} # key: path relative to generated directory, value: expected generated content + expected_difference_paths = [ + Path("README.md"), + Path("my_test_api_client").joinpath("api", "__init__.py"), + Path("my_test_api_client").joinpath("api", "tests", "__init__.py"), + Path("my_test_api_client").joinpath("api", "default", "__init__.py"), + Path("my_test_api_client").joinpath("api", "parameters", "__init__.py"), + ] + + golden_tpls_root_dir = Path(__file__).parent.joinpath("custom-templates-golden-record") + for expected_difference_path in expected_difference_paths: + path = Path("my-test-api-client").joinpath(expected_difference_path) + expected_differences[path] = (golden_tpls_root_dir / expected_difference_path).read_text() + run_e2e_test( - extra_args=["--custom-template-path=end_to_end_tests/test_custom_templates"], - expected_differences={"README.md": "my-test-api-client"}, + extra_args=["--custom-template-path=end_to_end_tests/test_custom_templates/"], + expected_differences=expected_differences, ) From 842950da567f2440f1ef593953b667820f035c25 Mon Sep 17 00:00:00 2001 From: Nementon Date: Thu, 6 May 2021 08:52:13 +0200 Subject: [PATCH 03/11] expose utils module to jinja2 --- openapi_python_client/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 35018d1cd..43f813483 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -67,6 +67,7 @@ def __init__( else: loader = package_loader self.env: Environment = Environment(loader=loader, trim_blocks=True, lstrip_blocks=True) + self.env.globals.update(utils=utils) self.project_name: str = config.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client" self.project_dir: Path = Path.cwd() From 6c05320e70df9481f0fc42069d3f664913dcb034 Mon Sep 17 00:00:00 2001 From: Nementon Date: Thu, 6 May 2021 08:54:52 +0200 Subject: [PATCH 04/11] test_custom_templates / replace jinja2 marcro with call to utils module --- .../test_custom_templates/api_init.py.jinja | 14 ++++--------- .../endpoint_init.py.jinja | 21 ++++--------------- 2 files changed, 8 insertions(+), 27 deletions(-) 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 06163930d..3655db659 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 @@ -1,19 +1,13 @@ """ Contains methods for accessing the API """ -{% macro snake_to_pascal_case(name) %} -{%- for part in name.split('_') -%} -{{ part | capitalize }} -{%- endfor -%} -{% endmacro %} - from typing import Type {% for tag, collection in endpoint_collections_by_tag %} -from {{ package_name }}.api.{{ tag }} import {{ snake_to_pascal_case(tag) }}Endpoints +from {{ package_name }}.api.{{ tag }} import {{ utils.pascal_case(tag) }}Endpoints {% endfor %} -class {{ snake_to_pascal_case(package_name) }}Api: +class {{ utils.pascal_case(package_name) }}Api: {% for tag, collection in endpoint_collections_by_tag %} @classmethod - def {{ tag }}(cls) -> Type[{{ snake_to_pascal_case(tag) }}Endpoints]: - return {{ snake_to_pascal_case(tag) }}Endpoints + def {{ tag }}(cls) -> Type[{{ utils.pascal_case(tag) }}Endpoints]: + return {{ utils.pascal_case(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 20612ba43..fbbd40e4f 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 @@ -1,29 +1,16 @@ """ Contains methods for accessing the API Endpoints """ -{% macro snake_to_pascal_case(name) %} -{%- for part in name.split('_') -%} -{{ part | capitalize }} -{%- endfor -%} -{% endmacro %} - -{% macro to_snake_name(name) %} -{% set last_snakize = {'at': 0} %} -{%- for part in name -%} -{{ '_' if not loop.first and part in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' and (loop.index - last_snakize.at) > 1 and not last_snakize.update({'at': loop.index}) }}{{ part | lower }} -{%- endfor -%} -{% endmacro %} - import types {% for endpoint in endpoints %} -from {{ package_name }}.api.{{ tag }} import {{ to_snake_name(endpoint.name) }} +from {{ package_name }}.api.{{ tag }} import {{ utils.snake_case(endpoint.name) }} {% endfor %} -class {{ snake_to_pascal_case(tag) }}Endpoints: +class {{ utils.pascal_case(tag) }}Endpoints: {% for endpoint in endpoints %} @classmethod - def {{ to_snake_name(endpoint.name) }}(cls) -> types.ModuleType: + def {{ utils.snake_case(endpoint.name) }}(cls) -> types.ModuleType: {% if endpoint.description %} """ {{ endpoint.description }} @@ -33,5 +20,5 @@ class {{ snake_to_pascal_case(tag) }}Endpoints: {{ endpoint.summary }} """ {% endif %} - return {{ to_snake_name(endpoint.name) }} + return {{ utils.snake_case(endpoint.name) }} {% endfor %} From 89d464d9f10e65d89642013d1fd1391405b43ce5 Mon Sep 17 00:00:00 2001 From: p1-ra Date: Mon, 3 May 2021 16:24:49 +0200 Subject: [PATCH 05/11] task / regen / regen custom template golden record --- .../my_test_api_client/api/__init__.py | 6 +- .../api/default/__init__.py | 1 - .../api/parameters/__init__.py | 11 ++++ .../my_test_api_client/api/tests/__init__.py | 1 - end_to_end_tests/regen_golden_record.py | 55 ++++++++++++++++++- 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.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 359d0b2dc..3ee5dbaf0 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 @@ -1,9 +1,9 @@ """ Contains methods for accessing the API """ - from typing import Type from my_test_api_client.api.default import DefaultEndpoints +from my_test_api_client.api.parameters import ParametersEndpoints from my_test_api_client.api.tests import TestsEndpoints @@ -15,3 +15,7 @@ def tests(cls) -> Type[TestsEndpoints]: @classmethod def default(cls) -> Type[DefaultEndpoints]: return DefaultEndpoints + + @classmethod + def parameters(cls) -> Type[ParametersEndpoints]: + return ParametersEndpoints diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py index 5928666d9..4d0eb4fb5 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/default/__init__.py @@ -1,6 +1,5 @@ """ Contains methods for accessing the API Endpoints """ - import types from my_test_api_client.api.default import get_common_parameters, post_common_parameters diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py new file mode 100644 index 000000000..b92c6d96b --- /dev/null +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/parameters/__init__.py @@ -0,0 +1,11 @@ +""" Contains methods for accessing the API Endpoints """ + +import types + +from my_test_api_client.api.parameters import get_same_name_multiple_locations_param + + +class ParametersEndpoints: + @classmethod + def get_same_name_multiple_locations_param(cls) -> types.ModuleType: + return get_same_name_multiple_locations_param diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py index b92538c28..2cfd13809 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py @@ -1,6 +1,5 @@ """ Contains methods for accessing the API Endpoints """ - import types from my_test_api_client.api.tests import ( diff --git a/end_to_end_tests/regen_golden_record.py b/end_to_end_tests/regen_golden_record.py index aaa6aa850..1d4dc943d 100644 --- a/end_to_end_tests/regen_golden_record.py +++ b/end_to_end_tests/regen_golden_record.py @@ -1,12 +1,16 @@ """ Regenerate golden-record """ +import filecmp +import os import shutil +import tempfile from pathlib import Path from typer.testing import CliRunner from openapi_python_client.cli import app -if __name__ == "__main__": + +def regen_golden_record(): runner = CliRunner() openapi_path = Path(__file__).parent / "openapi.json" @@ -24,3 +28,52 @@ if result.exception: raise result.exception output_path.rename(gr_path) + + +def regen_custom_template_golden_record(): + runner = CliRunner() + openapi_path = Path(__file__).parent / "openapi.json" + tpl_dir = Path(__file__).parent / "test_custom_templates" + + gr_path = Path(__file__).parent / "golden-record" + tpl_gr_path = Path(__file__).parent / "custom-templates-golden-record" + + output_path = Path(tempfile.mkdtemp()) + config_path = Path(__file__).parent / "config.yml" + + shutil.rmtree(tpl_gr_path, ignore_errors=True) + + os.chdir(str(output_path.absolute())) + result = runner.invoke( + app, ["generate", f"--config={config_path}", f"--path={openapi_path}", f"--custom-template-path={tpl_dir}"] + ) + + if result.stdout: + generated_output_path = output_path / "my-test-api-client" + for f in generated_output_path.glob("**/*"): # nb: works for Windows and Unix + relative_to_generated = f.relative_to(generated_output_path) + gr_file = gr_path / relative_to_generated + if not gr_file.exists(): + print(f"{gr_file} does not exist, ignoring") + continue + + if not gr_file.is_file(): + continue + + if not filecmp.cmp(gr_file, f, shallow=False): + target_file = tpl_gr_path / relative_to_generated + target_dir = target_file.parent + + target_dir.mkdir(parents=True, exist_ok=True) + shutil.copy(f"{f}", f"{target_file}") + + shutil.rmtree(output_path, ignore_errors=True) + + if result.exception: + shutil.rmtree(output_path, ignore_errors=True) + raise result.exception + + +if __name__ == "__main__": + regen_golden_record() + regen_custom_template_golden_record() From 94bf2c446efdce8b86d3aaacf0748aee2b2bd998 Mon Sep 17 00:00:00 2001 From: p1-ra Date: Thu, 20 May 2021 12:10:10 +0200 Subject: [PATCH 06/11] test_end_to_end / correct typing --- end_to_end_tests/test_end_to_end.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index ad552092a..6bca500ab 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -13,7 +13,7 @@ def _compare_directories( record: Path, test_subject: Path, expected_differences: Optional[ - Dict[str, str] + Dict[Path, str] ] = None, # key: path relative to generated directory, value: expected generated content depth=0, ): From cbeaefc457c096bd137c8537fab556d88666d112 Mon Sep 17 00:00:00 2001 From: p1-ra Date: Thu, 20 May 2021 12:14:48 +0200 Subject: [PATCH 07/11] endoint __init__ template / factorize propagated vars --- end_to_end_tests/test_custom_templates/api_init.py.jinja | 4 ++-- .../test_custom_templates/endpoint_init.py.jinja | 8 ++++---- openapi_python_client/__init__.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) 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 3655db659..03c2a2f6f 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 @@ -1,12 +1,12 @@ """ Contains methods for accessing the API """ from typing import Type -{% for tag, collection in endpoint_collections_by_tag %} +{% for tag in endpoint_collections_by_tag.keys() %} from {{ package_name }}.api.{{ tag }} import {{ utils.pascal_case(tag) }}Endpoints {% endfor %} class {{ utils.pascal_case(package_name) }}Api: -{% for tag, collection in endpoint_collections_by_tag %} +{% for tag in endpoint_collections_by_tag.keys() %} @classmethod def {{ tag }}(cls) -> Type[{{ utils.pascal_case(tag) }}Endpoints]: return {{ utils.pascal_case(tag) }}Endpoints 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 fbbd40e4f..57e8ba124 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 @@ -1,13 +1,13 @@ """ Contains methods for accessing the API Endpoints """ import types -{% for endpoint in endpoints %} -from {{ package_name }}.api.{{ tag }} import {{ utils.snake_case(endpoint.name) }} +{% for endpoint in endpoint_collection.endpoints %} +from {{ package_name }}.api.{{ endpoint_collection.tag }} import {{ utils.snake_case(endpoint.name) }} {% endfor %} -class {{ utils.pascal_case(tag) }}Endpoints: +class {{ utils.pascal_case(endpoint_collection.tag) }}Endpoints: -{% for endpoint in endpoints %} +{% for endpoint in endpoint_collection.endpoints %} @classmethod def {{ utils.snake_case(endpoint.name) }}(cls) -> types.ModuleType: diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 43f813483..42e60e35d 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -240,7 +240,7 @@ def _build_api(self) -> None: client_path.write_text(client_template.render(), encoding=self.file_encoding) # Generate endpoints - endpoint_collections_by_tag = self.openapi.endpoint_collections_by_tag.items() + endpoint_collections_by_tag = self.openapi.endpoint_collections_by_tag api_dir = self.package_dir / "api" api_dir.mkdir() api_init_path = api_dir / "__init__.py" @@ -254,14 +254,14 @@ def _build_api(self) -> None: ) endpoint_template = self.env.get_template("endpoint_module.py.jinja") - for tag, collection in endpoint_collections_by_tag: + for tag, collection in endpoint_collections_by_tag.items(): tag_dir = api_dir / tag tag_dir.mkdir() endpoint_init_path = tag_dir / "__init__.py" endpoint_init_template = self.env.get_template("endpoint_init.py.jinja") endpoint_init_path.write_text( - endpoint_init_template.render(package_name=self.package_name, tag=tag, endpoints=collection.endpoints), + endpoint_init_template.render(package_name=self.package_name, endpoint_collection=collection), encoding=self.file_encoding, ) (tag_dir / "__init__.py").touch() From 31a9fb03c981820df1b7a6932ddfd6e36539faa3 Mon Sep 17 00:00:00 2001 From: p1-ra Date: Thu, 20 May 2021 12:18:58 +0200 Subject: [PATCH 08/11] templates / globaly propagate package metadata vars --- openapi_python_client/__init__.py | 35 ++++++++----------- .../templates/README.md.jinja | 2 +- .../templates/package_init.py.jinja | 2 +- .../templates/pyproject.toml.jinja | 4 +-- .../templates/setup.py.jinja | 4 +-- 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 42e60e35d..1819b69eb 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -67,7 +67,6 @@ def __init__( else: loader = package_loader self.env: Environment = Environment(loader=loader, trim_blocks=True, lstrip_blocks=True) - self.env.globals.update(utils=utils) self.project_name: str = config.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client" self.project_dir: Path = Path.cwd() @@ -82,6 +81,15 @@ def __init__( self.version: str = config.package_version_override or openapi.version self.env.filters.update(TEMPLATE_FILTERS) + self.env.globals.update( + utils=utils, + package_name=self.package_name, + package_dir=self.package_dir, + package_description=self.package_description, + package_version=self.version, + project_name=self.project_name, + project_dir=self.project_dir, + ) def build(self) -> Sequence[GeneratorError]: """Create the project from templates""" @@ -144,9 +152,7 @@ def _create_package(self) -> None: package_init = self.package_dir / "__init__.py" package_init_template = self.env.get_template("package_init.py.jinja") - package_init.write_text( - package_init_template.render(description=self.package_description), encoding=self.file_encoding - ) + package_init.write_text(package_init_template.render(), encoding=self.file_encoding) if self.meta != MetaType.NONE: pytyped = self.package_dir / "py.typed" @@ -168,9 +174,7 @@ def _build_metadata(self) -> None: readme = self.project_dir / "README.md" readme_template = self.env.get_template("README.md.jinja") readme.write_text( - readme_template.render( - project_name=self.project_name, description=self.package_description, package_name=self.package_name - ), + readme_template.render(), encoding=self.file_encoding, ) @@ -184,12 +188,7 @@ def _build_pyproject_toml(self, *, use_poetry: bool) -> None: pyproject_template = self.env.get_template(template) pyproject_path = self.project_dir / "pyproject.toml" pyproject_path.write_text( - pyproject_template.render( - project_name=self.project_name, - package_name=self.package_name, - version=self.version, - description=self.package_description, - ), + pyproject_template.render(), encoding=self.file_encoding, ) @@ -197,12 +196,7 @@ def _build_setup_py(self) -> None: template = self.env.get_template("setup.py.jinja") path = self.project_dir / "setup.py" path.write_text( - template.render( - project_name=self.project_name, - package_name=self.package_name, - version=self.version, - description=self.package_description, - ), + template.render(), encoding=self.file_encoding, ) @@ -247,7 +241,6 @@ def _build_api(self) -> None: api_init_template = self.env.get_template("api_init.py.jinja") api_init_path.write_text( api_init_template.render( - package_name=self.package_name, endpoint_collections_by_tag=endpoint_collections_by_tag, ), encoding=self.file_encoding, @@ -261,7 +254,7 @@ def _build_api(self) -> None: endpoint_init_path = tag_dir / "__init__.py" endpoint_init_template = self.env.get_template("endpoint_init.py.jinja") endpoint_init_path.write_text( - endpoint_init_template.render(package_name=self.package_name, endpoint_collection=collection), + endpoint_init_template.render(endpoint_collection=collection), encoding=self.file_encoding, ) (tag_dir / "__init__.py").touch() diff --git a/openapi_python_client/templates/README.md.jinja b/openapi_python_client/templates/README.md.jinja index 2a5d18d87..e6de0dda5 100644 --- a/openapi_python_client/templates/README.md.jinja +++ b/openapi_python_client/templates/README.md.jinja @@ -1,5 +1,5 @@ # {{ project_name }} -{{ description }} +{{ package_description }} ## Usage First, create a client: diff --git a/openapi_python_client/templates/package_init.py.jinja b/openapi_python_client/templates/package_init.py.jinja index 917cd7dde..f146549d0 100644 --- a/openapi_python_client/templates/package_init.py.jinja +++ b/openapi_python_client/templates/package_init.py.jinja @@ -1,2 +1,2 @@ -""" {{ description }} """ +""" {{ package_description }} """ from .client import AuthenticatedClient, Client diff --git a/openapi_python_client/templates/pyproject.toml.jinja b/openapi_python_client/templates/pyproject.toml.jinja index 9e311a1a8..695092f48 100644 --- a/openapi_python_client/templates/pyproject.toml.jinja +++ b/openapi_python_client/templates/pyproject.toml.jinja @@ -1,7 +1,7 @@ [tool.poetry] name = "{{ project_name }}" -version = "{{ version }}" -description = "{{ description }}" +version = "{{ package_version }}" +description = "{{ package_description }}" authors = [] diff --git a/openapi_python_client/templates/setup.py.jinja b/openapi_python_client/templates/setup.py.jinja index 0dd31d23b..027120ab9 100644 --- a/openapi_python_client/templates/setup.py.jinja +++ b/openapi_python_client/templates/setup.py.jinja @@ -7,8 +7,8 @@ long_description = (here / "README.md").read_text(encoding="utf-8") setup( name="{{ project_name }}", - version="{{ version }}", - description="{{ description }}", + version="{{ package_version }}", + description="{{ package_description }}", long_description=long_description, long_description_content_type="text/markdown", package_dir={"": "{{ package_name }}"}, From 20ce01c7cc34a57f418aa5f2e44e14995834bb61 Mon Sep 17 00:00:00 2001 From: p1-ra Date: Thu, 20 May 2021 14:24:19 +0200 Subject: [PATCH 09/11] test___init__.py / correct templates API call breaking changes --- tests/test___init__.py | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/tests/test___init__.py b/tests/test___init__.py index 0579e83f0..3e1efbd5c 100644 --- a/tests/test___init__.py +++ b/tests/test___init__.py @@ -403,11 +403,7 @@ def test__build_metadata_poetry(self, mocker): project._build_metadata() project.env.get_template.assert_has_calls([mocker.call("README.md.jinja"), mocker.call(".gitignore.jinja")]) - readme_template.render.assert_called_once_with( - description=project.package_description, - project_name=project.project_name, - package_name=project.package_name, - ) + readme_template.render.assert_called_once_with() readme_path.write_text.assert_called_once_with(readme_template.render(), encoding="utf-8") git_ignore_template.render.assert_called_once() git_ignore_path.write_text.assert_called_once_with(git_ignore_template.render(), encoding="utf-8") @@ -440,11 +436,7 @@ def test__build_metadata_setup(self, mocker): project._build_metadata() project.env.get_template.assert_has_calls([mocker.call("README.md.jinja"), mocker.call(".gitignore.jinja")]) - readme_template.render.assert_called_once_with( - description=project.package_description, - project_name=project.project_name, - package_name=project.package_name, - ) + readme_template.render.assert_called_once_with() readme_path.write_text.assert_called_once_with(readme_template.render(), encoding="utf-8") git_ignore_template.render.assert_called_once() git_ignore_path.write_text.assert_called_once_with(git_ignore_template.render(), encoding="utf-8") @@ -483,12 +475,7 @@ def test__build_pyproject_toml(self, mocker, use_poetry): project.env.get_template.assert_called_once_with(template_path) - pyproject_template.render.assert_called_once_with( - project_name=project.project_name, - package_name=project.package_name, - version=project.version, - description=project.package_description, - ) + pyproject_template.render.assert_called_once_with() pyproject_path.write_text.assert_called_once_with(pyproject_template.render(), encoding="utf-8") def test__build_setup_py(self, mocker): @@ -511,12 +498,7 @@ def test__build_setup_py(self, mocker): project.env.get_template.assert_called_once_with("setup.py.jinja") - setup_template.render.assert_called_once_with( - project_name=project.project_name, - package_name=project.package_name, - version=project.version, - description=project.package_description, - ) + setup_template.render.assert_called_once_with() setup_path.write_text.assert_called_once_with(setup_template.render(), encoding="utf-8") From 560cbba924b87517fae9a7995faf4e96a0ad291c Mon Sep 17 00:00:00 2001 From: p1-ra <18233250+p1-ra@users.noreply.github.com> Date: Wed, 9 Jun 2021 15:58:47 +0200 Subject: [PATCH 10/11] Update test_end_to_end.py,openapi_python_client/__init__.py : various imprv Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com> --- end_to_end_tests/test_end_to_end.py | 38 +++++++++++++++++------------ openapi_python_client/__init__.py | 1 - 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index 6bca500ab..bcc8b12e1 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -1,7 +1,7 @@ import shutil from filecmp import cmpfiles, dircmp from pathlib import Path -from typing import Dict, Optional +from typing import Dict, List, Optional import pytest from typer.testing import CliRunner @@ -12,11 +12,18 @@ def _compare_directories( record: Path, test_subject: Path, - expected_differences: Optional[ - Dict[Path, str] - ] = None, # key: path relative to generated directory, value: expected generated content + expected_differences: Dict[Path, str], depth=0, ): + """ + Compare two directories and assert that only expected_differences are different + + Args: + record: Path to the expected output + test_subject: Path to the generated code being checked + expected_differences: key: path relative to generated directory, value: expected generated content + depth: Used to track recursion + """ first_printable = record.relative_to(Path.cwd()) second_printable = test_subject.relative_to(Path.cwd()) dc = dircmp(record, test_subject) @@ -30,16 +37,14 @@ def _compare_directories( expected_path_mismatches = [] for file_name in mismatches: - mismatch_file_path = test_subject.joinpath(file_name) - for expected_differences_path in expected_differences.keys(): - - if mismatch_file_path.match(str(expected_differences_path)): + expected_content = expected_differences.get(mismatch_file_path) + if expected_content is None: + continue - generated_content = (test_subject / file_name).read_text() - expected_content = expected_differences[expected_differences_path] - assert generated_content == expected_content, f"Unexpected output in {mismatch_file_path}" - expected_path_mismatches.append(expected_differences_path) + generated_content = (test_subject / file_name).read_text() + assert generated_content == expected_content, f"Unexpected output in {mismatch_file_path}" + expected_path_mismatches.append(mismatch_file_path) for path_mismatch in expected_path_mismatches: matched_file_name = path_mismatch.name @@ -62,7 +67,7 @@ def _compare_directories( pytest.fail(failure, pytrace=False) -def run_e2e_test(extra_args=None, expected_differences=None): +def run_e2e_test(extra_args: List[str], expected_differences: Dict[Path, str]): runner = CliRunner() openapi_path = Path(__file__).parent / "openapi.json" config_path = Path(__file__).parent / "config.yml" @@ -78,6 +83,8 @@ def run_e2e_test(extra_args=None, expected_differences=None): if result.exit_code != 0: raise result.exception + # Use absolute paths for expected differences for easier comparisons + expected_differences = {output_path.joinpath(key): value for key, value in expected_differences.items()} _compare_directories(gr_path, output_path, expected_differences=expected_differences) import mypy.api @@ -89,7 +96,7 @@ def run_e2e_test(extra_args=None, expected_differences=None): def test_end_to_end(): - run_e2e_test() + run_e2e_test([], {}) def test_custom_templates(): @@ -104,8 +111,7 @@ def test_custom_templates(): golden_tpls_root_dir = Path(__file__).parent.joinpath("custom-templates-golden-record") for expected_difference_path in expected_difference_paths: - path = Path("my-test-api-client").joinpath(expected_difference_path) - expected_differences[path] = (golden_tpls_root_dir / expected_difference_path).read_text() + expected_differences[expected_difference_path] = (golden_tpls_root_dir / expected_difference_path).read_text() run_e2e_test( extra_args=["--custom-template-path=end_to_end_tests/test_custom_templates/"], diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 1819b69eb..b1458e1a4 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -257,7 +257,6 @@ def _build_api(self) -> None: endpoint_init_template.render(endpoint_collection=collection), encoding=self.file_encoding, ) - (tag_dir / "__init__.py").touch() for endpoint in collection.endpoints: module_path = tag_dir / f"{snake_case(endpoint.name)}.py" From 44537a14e2da8fa0ee9d0e1bab5b857877d9bfea Mon Sep 17 00:00:00 2001 From: p1-ra Date: Wed, 9 Jun 2021 16:14:21 +0200 Subject: [PATCH 11/11] regen templates golden records (rebased on main) --- .../my_test_api_client/api/tests/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py index 2cfd13809..dcb864fe9 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py @@ -14,6 +14,7 @@ no_response_tests_no_response_get, octet_stream_tests_octet_stream_get, optional_value_tests_optional_query_param, + post_form_data, test_inline_objects, token_with_cookie_auth_token_with_cookie_get, unsupported_content_tests_unsupported_content_get, @@ -57,6 +58,13 @@ def get_basic_list_of_booleans(cls) -> types.ModuleType: """ return get_basic_list_of_booleans + @classmethod + def post_form_data(cls) -> types.ModuleType: + """ + Post form data + """ + return post_form_data + @classmethod def upload_file_tests_upload_post(cls) -> types.ModuleType: """