Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixes `Enum` deserialization when the value is `UNSET`.
- Add handling of application/vnd.api+json media type.
- Support passing models into query parameters (#316). Thanks @forest-benchling!
- New `--file-encoding`command line option, set encoding used when writing generated, default: utf-8 (#330).

### Changes

Expand Down
73 changes: 52 additions & 21 deletions openapi_python_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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"
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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():
Expand All @@ -241,46 +254,64 @@ 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):
return data_dict
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

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

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()
Expand Down
24 changes: 22 additions & 2 deletions openapi_python_client/cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import codecs
import pathlib
from pprint import pformat
from typing import Optional, Sequence
Expand Down Expand Up @@ -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 """
Expand All @@ -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)


Expand All @@ -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
Expand All @@ -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)
24 changes: 13 additions & 11 deletions tests/test___init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand All @@ -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]

Expand All @@ -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
Expand All @@ -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]

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Loading