diff --git a/CHANGELOG.md b/CHANGELOG.md index 3680a407d..7747fa9fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add handling of application/vnd.api+json media type. - Support passing models into query parameters (#316). Thanks @forest-benchling! - Add support for cookie parameters (#326). +- New `--file-encoding` command line option (#330). Sets the encoding used when writing generated files (defaults to utf-8). Thanks @dongfangtianyu! ### Changes diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 8c97d45e9..b5ad8afeb 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -45,9 +45,17 @@ class Project: package_name_override: Optional[str] = None package_version_override: Optional[str] = None - def __init__(self, *, openapi: GeneratorData, meta: MetaType, custom_template_path: Optional[Path] = None) -> None: + def __init__( + self, + *, + openapi: GeneratorData, + meta: MetaType, + custom_template_path: Optional[Path] = None, + file_encoding: str = "utf-8", + ) -> None: self.openapi: GeneratorData = openapi self.meta: MetaType = meta + self.file_encoding = file_encoding package_loader = PackageLoader(__package__) loader: BaseLoader @@ -137,15 +145,17 @@ 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)) + package_init.write_text( + package_init_template.render(description=self.package_description), encoding=self.file_encoding + ) if self.meta != MetaType.NONE: pytyped = self.package_dir / "py.typed" - pytyped.write_text("# Marker file for PEP 561") + pytyped.write_text("# Marker file for PEP 561", encoding=self.file_encoding) types_template = self.env.get_template("types.py.jinja") types_path = self.package_dir / "types.py" - types_path.write_text(types_template.render()) + types_path.write_text(types_template.render(), encoding=self.file_encoding) def _build_metadata(self) -> None: if self.meta == MetaType.NONE: @@ -161,13 +171,14 @@ def _build_metadata(self) -> None: readme.write_text( readme_template.render( project_name=self.project_name, description=self.package_description, package_name=self.package_name - ) + ), + encoding=self.file_encoding, ) # .gitignore git_ignore_path = self.project_dir / ".gitignore" git_ignore_template = self.env.get_template(".gitignore.jinja") - git_ignore_path.write_text(git_ignore_template.render()) + git_ignore_path.write_text(git_ignore_template.render(), encoding=self.file_encoding) def _build_pyproject_toml(self, *, use_poetry: bool) -> None: template = "pyproject.toml.jinja" if use_poetry else "pyproject_no_poetry.toml.jinja" @@ -179,7 +190,8 @@ def _build_pyproject_toml(self, *, use_poetry: bool) -> None: package_name=self.package_name, version=self.version, description=self.package_description, - ) + ), + encoding=self.file_encoding, ) def _build_setup_py(self) -> None: @@ -191,7 +203,8 @@ def _build_setup_py(self) -> None: package_name=self.package_name, version=self.version, description=self.package_description, - ) + ), + encoding=self.file_encoding, ) def _build_models(self) -> None: @@ -204,7 +217,7 @@ def _build_models(self) -> None: 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" - module_path.write_text(model_template.render(model=model)) + module_path.write_text(model_template.render(model=model), encoding=self.file_encoding) imports.append(import_string_from_reference(model.reference)) # Generate enums @@ -213,25 +226,25 @@ def _build_models(self) -> None: for enum in self.openapi.enums.values(): module_path = models_dir / f"{enum.reference.module_name}.py" if enum.value_type is int: - module_path.write_text(int_enum_template.render(enum=enum)) + 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)) + module_path.write_text(str_enum_template.render(enum=enum), encoding=self.file_encoding) imports.append(import_string_from_reference(enum.reference)) models_init_template = self.env.get_template("models_init.py.jinja") - models_init.write_text(models_init_template.render(imports=imports)) + models_init.write_text(models_init_template.render(imports=imports), encoding=self.file_encoding) def _build_api(self) -> None: # Generate Client client_path = self.package_dir / "client.py" client_template = self.env.get_template("client.py.jinja") - client_path.write_text(client_template.render()) + client_path.write_text(client_template.render(), encoding=self.file_encoding) # Generate endpoints 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 """') + api_init.write_text('""" Contains methods for accessing the API """', 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(): @@ -241,11 +254,15 @@ def _build_api(self) -> None: for endpoint in collection.endpoints: module_path = tag_dir / f"{snake_case(endpoint.name)}.py" - module_path.write_text(endpoint_template.render(endpoint=endpoint)) + module_path.write_text(endpoint_template.render(endpoint=endpoint), encoding=self.file_encoding) def _get_project_for_url_or_path( - url: Optional[str], path: Optional[Path], meta: MetaType, custom_template_path: Optional[Path] = None + url: Optional[str], + path: Optional[Path], + meta: MetaType, + 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): @@ -253,11 +270,16 @@ def _get_project_for_url_or_path( openapi = GeneratorData.from_dict(data_dict) if isinstance(openapi, GeneratorError): return openapi - return Project(openapi=openapi, custom_template_path=custom_template_path, meta=meta) + return Project(openapi=openapi, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding) def create_new_client( - *, url: Optional[str], path: Optional[Path], meta: MetaType, custom_template_path: Optional[Path] = None + *, + url: Optional[str], + path: Optional[Path], + meta: MetaType, + custom_template_path: Optional[Path] = None, + file_encoding: str = "utf-8", ) -> Sequence[GeneratorError]: """ Generate the client library @@ -265,14 +287,21 @@ def create_new_client( Returns: 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) + project = _get_project_for_url_or_path( + url=url, path=path, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding + ) if isinstance(project, GeneratorError): return [project] return project.build() def update_existing_client( - *, url: Optional[str], path: Optional[Path], meta: MetaType, custom_template_path: Optional[Path] = None + *, + url: Optional[str], + path: Optional[Path], + meta: MetaType, + custom_template_path: Optional[Path] = None, + file_encoding: str = "utf-8", ) -> Sequence[GeneratorError]: """ Update an existing client library @@ -280,7 +309,9 @@ def update_existing_client( Returns: 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) + project = _get_project_for_url_or_path( + url=url, path=path, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding + ) if isinstance(project, GeneratorError): return [project] return project.update() diff --git a/openapi_python_client/cli.py b/openapi_python_client/cli.py index ca8565691..1f94f37ea 100644 --- a/openapi_python_client/cli.py +++ b/openapi_python_client/cli.py @@ -1,3 +1,4 @@ +import codecs import pathlib from pprint import pformat from typing import Optional, Sequence @@ -116,6 +117,7 @@ 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, ) -> None: """ Generate a new OpenAPI Client library """ @@ -127,7 +129,16 @@ def generate( if url and path: typer.secho("Provide either --url or --path, not both", fg=typer.colors.RED) raise typer.Exit(code=1) - errors = create_new_client(url=url, path=path, meta=meta, custom_template_path=custom_template_path) + + try: + codecs.getencoder(file_encoding) + except LookupError: + typer.secho("Unknown encoding : {}".format(file_encoding), fg=typer.colors.RED) + raise typer.Exit(code=1) + + errors = create_new_client( + url=url, path=path, meta=meta, custom_template_path=custom_template_path, file_encoding=file_encoding + ) handle_errors(errors) @@ -137,6 +148,7 @@ def update( 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 meta: MetaType = _meta_option, + file_encoding: str = typer.Option("utf-8", help="Encoding used when writing generated"), ) -> None: """ Update an existing OpenAPI Client library """ from . import update_existing_client @@ -148,5 +160,13 @@ def update( typer.secho("Provide either --url or --path, not both", fg=typer.colors.RED) raise typer.Exit(code=1) - errors = update_existing_client(url=url, path=path, meta=meta, custom_template_path=custom_template_path) + try: + codecs.getencoder(file_encoding) + except LookupError: + typer.secho("Unknown encoding : {}".format(file_encoding), fg=typer.colors.RED) + raise typer.Exit(code=1) + + errors = update_existing_client( + url=url, path=path, meta=meta, custom_template_path=custom_template_path, file_encoding=file_encoding + ) handle_errors(errors) diff --git a/tests/test___init__.py b/tests/test___init__.py index d45108181..3d2547d89 100644 --- a/tests/test___init__.py +++ b/tests/test___init__.py @@ -23,7 +23,9 @@ def test__get_project_for_url_or_path(mocker): _get_document.assert_called_once_with(url=url, path=path) from_dict.assert_called_once_with(data_dict) - _Project.assert_called_once_with(openapi=openapi, custom_template_path=None, meta=MetaType.POETRY) + _Project.assert_called_once_with( + openapi=openapi, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + ) assert project == _Project.return_value @@ -76,7 +78,7 @@ def test_create_new_client(mocker): result = create_new_client(url=url, path=path, meta=MetaType.POETRY) _get_project_for_url_or_path.assert_called_once_with( - url=url, path=path, custom_template_path=None, meta=MetaType.POETRY + url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" ) project.build.assert_called_once() assert result == project.build.return_value @@ -95,7 +97,7 @@ def test_create_new_client_project_error(mocker): result = create_new_client(url=url, path=path, meta=MetaType.POETRY) _get_project_for_url_or_path.assert_called_once_with( - url=url, path=path, custom_template_path=None, meta=MetaType.POETRY + url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" ) assert result == [error] @@ -113,7 +115,7 @@ def test_update_existing_client(mocker): result = update_existing_client(url=url, path=path, meta=MetaType.POETRY) _get_project_for_url_or_path.assert_called_once_with( - url=url, path=path, custom_template_path=None, meta=MetaType.POETRY + url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" ) project.update.assert_called_once() assert result == project.update.return_value @@ -132,7 +134,7 @@ def test_update_existing_client_project_error(mocker): result = update_existing_client(url=url, path=path, meta=MetaType.POETRY) _get_project_for_url_or_path.assert_called_once_with( - url=url, path=path, custom_template_path=None, meta=MetaType.POETRY + url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" ) assert result == [error] @@ -392,9 +394,9 @@ def test__build_metadata_poetry(self, mocker): project_name=project.project_name, package_name=project.package_name, ) - readme_path.write_text.assert_called_once_with(readme_template.render()) + 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()) + git_ignore_path.write_text.assert_called_once_with(git_ignore_template.render(), encoding="utf-8") project._build_pyproject_toml.assert_called_once_with(use_poetry=True) def test__build_metadata_setup(self, mocker): @@ -429,9 +431,9 @@ def test__build_metadata_setup(self, mocker): project_name=project.project_name, package_name=project.package_name, ) - readme_path.write_text.assert_called_once_with(readme_template.render()) + 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()) + git_ignore_path.write_text.assert_called_once_with(git_ignore_template.render(), encoding="utf-8") project._build_pyproject_toml.assert_called_once_with(use_poetry=False) project._build_setup_py.assert_called_once() @@ -475,7 +477,7 @@ def test__build_pyproject_toml(self, mocker, use_poetry): version=project.version, description=project.package_description, ) - pyproject_path.write_text.assert_called_once_with(pyproject_template.render()) + 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 @@ -505,7 +507,7 @@ def test__build_setup_py(self, mocker): version=project.version, description=project.package_description, ) - setup_path.write_text.assert_called_once_with(setup_template.render()) + setup_path.write_text.assert_called_once_with(setup_template.render(), encoding="utf-8") def test__reformat(mocker): diff --git a/tests/test_cli.py b/tests/test_cli.py index 1c1619a28..d5bc2c69e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -31,13 +31,18 @@ def test_config_arg(mocker, _create_new_client): config_path = "config/path" path = "cool/path" + file_encoding = "utf-8" - result = runner.invoke(app, [f"--config={config_path}", "generate", f"--path={path}"], catch_exceptions=False) + result = runner.invoke( + app, + [f"--config={config_path}", "generate", 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 + url=None, path=Path(path), custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" ) @@ -82,7 +87,9 @@ def test_generate_url(self, _create_new_client): 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) + _create_new_client.assert_called_once_with( + url=url, path=None, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" + ) def test_generate_path(self, _create_new_client): path = "cool/path" @@ -92,7 +99,7 @@ def test_generate_path(self, _create_new_client): assert result.exit_code == 0 _create_new_client.assert_called_once_with( - url=None, path=Path(path), custom_template_path=None, meta=MetaType.POETRY + url=None, path=Path(path), custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" ) def test_generate_meta(self, _create_new_client): @@ -103,9 +110,31 @@ def test_generate_meta(self, _create_new_client): assert result.exit_code == 0 _create_new_client.assert_called_once_with( - url=None, path=Path(path), custom_template_path=None, meta=MetaType.NONE + url=None, path=Path(path), custom_template_path=None, meta=MetaType.NONE, file_encoding="utf-8" + ) + + def test_generate_encoding(self, _create_new_client): + path = "cool/path" + file_encoding = "utf-8" + from openapi_python_client.cli import 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" ) + def test_generate_encoding_errors(self, _create_new_client): + path = "cool/path" + file_encoding = "error-file-encoding" + from openapi_python_client.cli import MetaType, app + + result = runner.invoke(app, ["generate", f"--path={path}", f"--file-encoding={file_encoding}"]) + + assert result.exit_code == 1 + assert result.output == "Unknown encoding : {}\n".format(file_encoding) + def test_generate_handle_errors(self, _create_new_client): _create_new_client.return_value = [GeneratorError(detail="this is a message")] path = "cool/path" @@ -175,7 +204,7 @@ def test_update_url(self, _update_existing_client): assert result.exit_code == 0 _update_existing_client.assert_called_once_with( - url=url, path=None, custom_template_path=None, meta=MetaType.POETRY + url=url, path=None, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" ) def test_update_path(self, _update_existing_client): @@ -186,5 +215,27 @@ def test_update_path(self, _update_existing_client): assert result.exit_code == 0 _update_existing_client.assert_called_once_with( - url=None, path=Path(path), custom_template_path=None, meta=MetaType.POETRY + url=None, path=Path(path), custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8" ) + + def test_update_encoding(self, _update_existing_client): + path = "cool/path" + file_encoding = "utf-8" + from openapi_python_client.cli import 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" + ) + + def test_update_encoding_errors(self, _update_existing_client): + path = "cool/path" + file_encoding = "error-file-encoding" + from openapi_python_client.cli import MetaType, app + + result = runner.invoke(app, ["update", f"--path={path}", f"--file-encoding={file_encoding}"]) + + assert result.exit_code == 1 + assert result.output == "Unknown encoding : {}\n".format(file_encoding)