Skip to content

Commit 7f8d10e

Browse files
feat: Add --file-encoding CLI option (#330)
* feat: Add new client parameters: encoding * fix: Modify the parameter name encoding to file_encoding, and set the default is UTF-8 * fix: test coverage (openapi_python_client/cli.py) * fix: Modify the parameter name encoding to file_encoding, * docs: Add CHANGELOG entry for #330 * Update CHANGELOG.md Co-authored-by: Ethan Mann <[email protected]>
1 parent 2d305dd commit 7f8d10e

File tree

5 files changed

+146
-41
lines changed

5 files changed

+146
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- Add handling of application/vnd.api+json media type.
2424
- Support passing models into query parameters (#316). Thanks @forest-benchling!
2525
- Add support for cookie parameters (#326).
26+
- New `--file-encoding` command line option (#330). Sets the encoding used when writing generated files (defaults to utf-8). Thanks @dongfangtianyu!
2627

2728
### Changes
2829

openapi_python_client/__init__.py

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,17 @@ class Project:
4545
package_name_override: Optional[str] = None
4646
package_version_override: Optional[str] = None
4747

48-
def __init__(self, *, openapi: GeneratorData, meta: MetaType, custom_template_path: Optional[Path] = None) -> None:
48+
def __init__(
49+
self,
50+
*,
51+
openapi: GeneratorData,
52+
meta: MetaType,
53+
custom_template_path: Optional[Path] = None,
54+
file_encoding: str = "utf-8",
55+
) -> None:
4956
self.openapi: GeneratorData = openapi
5057
self.meta: MetaType = meta
58+
self.file_encoding = file_encoding
5159

5260
package_loader = PackageLoader(__package__)
5361
loader: BaseLoader
@@ -137,15 +145,17 @@ def _create_package(self) -> None:
137145
package_init = self.package_dir / "__init__.py"
138146

139147
package_init_template = self.env.get_template("package_init.py.jinja")
140-
package_init.write_text(package_init_template.render(description=self.package_description))
148+
package_init.write_text(
149+
package_init_template.render(description=self.package_description), encoding=self.file_encoding
150+
)
141151

142152
if self.meta != MetaType.NONE:
143153
pytyped = self.package_dir / "py.typed"
144-
pytyped.write_text("# Marker file for PEP 561")
154+
pytyped.write_text("# Marker file for PEP 561", encoding=self.file_encoding)
145155

146156
types_template = self.env.get_template("types.py.jinja")
147157
types_path = self.package_dir / "types.py"
148-
types_path.write_text(types_template.render())
158+
types_path.write_text(types_template.render(), encoding=self.file_encoding)
149159

150160
def _build_metadata(self) -> None:
151161
if self.meta == MetaType.NONE:
@@ -161,13 +171,14 @@ def _build_metadata(self) -> None:
161171
readme.write_text(
162172
readme_template.render(
163173
project_name=self.project_name, description=self.package_description, package_name=self.package_name
164-
)
174+
),
175+
encoding=self.file_encoding,
165176
)
166177

167178
# .gitignore
168179
git_ignore_path = self.project_dir / ".gitignore"
169180
git_ignore_template = self.env.get_template(".gitignore.jinja")
170-
git_ignore_path.write_text(git_ignore_template.render())
181+
git_ignore_path.write_text(git_ignore_template.render(), encoding=self.file_encoding)
171182

172183
def _build_pyproject_toml(self, *, use_poetry: bool) -> None:
173184
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:
179190
package_name=self.package_name,
180191
version=self.version,
181192
description=self.package_description,
182-
)
193+
),
194+
encoding=self.file_encoding,
183195
)
184196

185197
def _build_setup_py(self) -> None:
@@ -191,7 +203,8 @@ def _build_setup_py(self) -> None:
191203
package_name=self.package_name,
192204
version=self.version,
193205
description=self.package_description,
194-
)
206+
),
207+
encoding=self.file_encoding,
195208
)
196209

197210
def _build_models(self) -> None:
@@ -204,7 +217,7 @@ def _build_models(self) -> None:
204217
model_template = self.env.get_template("model.py.jinja")
205218
for model in self.openapi.models.values():
206219
module_path = models_dir / f"{model.reference.module_name}.py"
207-
module_path.write_text(model_template.render(model=model))
220+
module_path.write_text(model_template.render(model=model), encoding=self.file_encoding)
208221
imports.append(import_string_from_reference(model.reference))
209222

210223
# Generate enums
@@ -213,25 +226,25 @@ def _build_models(self) -> None:
213226
for enum in self.openapi.enums.values():
214227
module_path = models_dir / f"{enum.reference.module_name}.py"
215228
if enum.value_type is int:
216-
module_path.write_text(int_enum_template.render(enum=enum))
229+
module_path.write_text(int_enum_template.render(enum=enum), encoding=self.file_encoding)
217230
else:
218-
module_path.write_text(str_enum_template.render(enum=enum))
231+
module_path.write_text(str_enum_template.render(enum=enum), encoding=self.file_encoding)
219232
imports.append(import_string_from_reference(enum.reference))
220233

221234
models_init_template = self.env.get_template("models_init.py.jinja")
222-
models_init.write_text(models_init_template.render(imports=imports))
235+
models_init.write_text(models_init_template.render(imports=imports), encoding=self.file_encoding)
223236

224237
def _build_api(self) -> None:
225238
# Generate Client
226239
client_path = self.package_dir / "client.py"
227240
client_template = self.env.get_template("client.py.jinja")
228-
client_path.write_text(client_template.render())
241+
client_path.write_text(client_template.render(), encoding=self.file_encoding)
229242

230243
# Generate endpoints
231244
api_dir = self.package_dir / "api"
232245
api_dir.mkdir()
233246
api_init = api_dir / "__init__.py"
234-
api_init.write_text('""" Contains methods for accessing the API """')
247+
api_init.write_text('""" Contains methods for accessing the API """', encoding=self.file_encoding)
235248

236249
endpoint_template = self.env.get_template("endpoint_module.py.jinja")
237250
for tag, collection in self.openapi.endpoint_collections_by_tag.items():
@@ -241,46 +254,64 @@ def _build_api(self) -> None:
241254

242255
for endpoint in collection.endpoints:
243256
module_path = tag_dir / f"{snake_case(endpoint.name)}.py"
244-
module_path.write_text(endpoint_template.render(endpoint=endpoint))
257+
module_path.write_text(endpoint_template.render(endpoint=endpoint), encoding=self.file_encoding)
245258

246259

247260
def _get_project_for_url_or_path(
248-
url: Optional[str], path: Optional[Path], meta: MetaType, custom_template_path: Optional[Path] = None
261+
url: Optional[str],
262+
path: Optional[Path],
263+
meta: MetaType,
264+
custom_template_path: Optional[Path] = None,
265+
file_encoding: str = "utf-8",
249266
) -> Union[Project, GeneratorError]:
250267
data_dict = _get_document(url=url, path=path)
251268
if isinstance(data_dict, GeneratorError):
252269
return data_dict
253270
openapi = GeneratorData.from_dict(data_dict)
254271
if isinstance(openapi, GeneratorError):
255272
return openapi
256-
return Project(openapi=openapi, custom_template_path=custom_template_path, meta=meta)
273+
return Project(openapi=openapi, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding)
257274

258275

259276
def create_new_client(
260-
*, url: Optional[str], path: Optional[Path], meta: MetaType, custom_template_path: Optional[Path] = None
277+
*,
278+
url: Optional[str],
279+
path: Optional[Path],
280+
meta: MetaType,
281+
custom_template_path: Optional[Path] = None,
282+
file_encoding: str = "utf-8",
261283
) -> Sequence[GeneratorError]:
262284
"""
263285
Generate the client library
264286
265287
Returns:
266288
A list containing any errors encountered when generating.
267289
"""
268-
project = _get_project_for_url_or_path(url=url, path=path, custom_template_path=custom_template_path, meta=meta)
290+
project = _get_project_for_url_or_path(
291+
url=url, path=path, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding
292+
)
269293
if isinstance(project, GeneratorError):
270294
return [project]
271295
return project.build()
272296

273297

274298
def update_existing_client(
275-
*, url: Optional[str], path: Optional[Path], meta: MetaType, custom_template_path: Optional[Path] = None
299+
*,
300+
url: Optional[str],
301+
path: Optional[Path],
302+
meta: MetaType,
303+
custom_template_path: Optional[Path] = None,
304+
file_encoding: str = "utf-8",
276305
) -> Sequence[GeneratorError]:
277306
"""
278307
Update an existing client library
279308
280309
Returns:
281310
A list containing any errors encountered when generating.
282311
"""
283-
project = _get_project_for_url_or_path(url=url, path=path, custom_template_path=custom_template_path, meta=meta)
312+
project = _get_project_for_url_or_path(
313+
url=url, path=path, custom_template_path=custom_template_path, meta=meta, file_encoding=file_encoding
314+
)
284315
if isinstance(project, GeneratorError):
285316
return [project]
286317
return project.update()

openapi_python_client/cli.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import codecs
12
import pathlib
23
from pprint import pformat
34
from typing import Optional, Sequence
@@ -116,6 +117,7 @@ def generate(
116117
url: Optional[str] = typer.Option(None, help="A URL to read the JSON from"),
117118
path: Optional[pathlib.Path] = typer.Option(None, help="A path to the JSON file"),
118119
custom_template_path: Optional[pathlib.Path] = typer.Option(None, **custom_template_path_options), # type: ignore
120+
file_encoding: str = typer.Option("utf-8", help="Encoding used when writing generated"),
119121
meta: MetaType = _meta_option,
120122
) -> None:
121123
""" Generate a new OpenAPI Client library """
@@ -127,7 +129,16 @@ def generate(
127129
if url and path:
128130
typer.secho("Provide either --url or --path, not both", fg=typer.colors.RED)
129131
raise typer.Exit(code=1)
130-
errors = create_new_client(url=url, path=path, meta=meta, custom_template_path=custom_template_path)
132+
133+
try:
134+
codecs.getencoder(file_encoding)
135+
except LookupError:
136+
typer.secho("Unknown encoding : {}".format(file_encoding), fg=typer.colors.RED)
137+
raise typer.Exit(code=1)
138+
139+
errors = create_new_client(
140+
url=url, path=path, meta=meta, custom_template_path=custom_template_path, file_encoding=file_encoding
141+
)
131142
handle_errors(errors)
132143

133144

@@ -137,6 +148,7 @@ def update(
137148
path: Optional[pathlib.Path] = typer.Option(None, help="A path to the JSON file"),
138149
custom_template_path: Optional[pathlib.Path] = typer.Option(None, **custom_template_path_options), # type: ignore
139150
meta: MetaType = _meta_option,
151+
file_encoding: str = typer.Option("utf-8", help="Encoding used when writing generated"),
140152
) -> None:
141153
""" Update an existing OpenAPI Client library """
142154
from . import update_existing_client
@@ -148,5 +160,13 @@ def update(
148160
typer.secho("Provide either --url or --path, not both", fg=typer.colors.RED)
149161
raise typer.Exit(code=1)
150162

151-
errors = update_existing_client(url=url, path=path, meta=meta, custom_template_path=custom_template_path)
163+
try:
164+
codecs.getencoder(file_encoding)
165+
except LookupError:
166+
typer.secho("Unknown encoding : {}".format(file_encoding), fg=typer.colors.RED)
167+
raise typer.Exit(code=1)
168+
169+
errors = update_existing_client(
170+
url=url, path=path, meta=meta, custom_template_path=custom_template_path, file_encoding=file_encoding
171+
)
152172
handle_errors(errors)

tests/test___init__.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ def test__get_project_for_url_or_path(mocker):
2323

2424
_get_document.assert_called_once_with(url=url, path=path)
2525
from_dict.assert_called_once_with(data_dict)
26-
_Project.assert_called_once_with(openapi=openapi, custom_template_path=None, meta=MetaType.POETRY)
26+
_Project.assert_called_once_with(
27+
openapi=openapi, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8"
28+
)
2729
assert project == _Project.return_value
2830

2931

@@ -76,7 +78,7 @@ def test_create_new_client(mocker):
7678
result = create_new_client(url=url, path=path, meta=MetaType.POETRY)
7779

7880
_get_project_for_url_or_path.assert_called_once_with(
79-
url=url, path=path, custom_template_path=None, meta=MetaType.POETRY
81+
url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8"
8082
)
8183
project.build.assert_called_once()
8284
assert result == project.build.return_value
@@ -95,7 +97,7 @@ def test_create_new_client_project_error(mocker):
9597
result = create_new_client(url=url, path=path, meta=MetaType.POETRY)
9698

9799
_get_project_for_url_or_path.assert_called_once_with(
98-
url=url, path=path, custom_template_path=None, meta=MetaType.POETRY
100+
url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8"
99101
)
100102
assert result == [error]
101103

@@ -113,7 +115,7 @@ def test_update_existing_client(mocker):
113115
result = update_existing_client(url=url, path=path, meta=MetaType.POETRY)
114116

115117
_get_project_for_url_or_path.assert_called_once_with(
116-
url=url, path=path, custom_template_path=None, meta=MetaType.POETRY
118+
url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8"
117119
)
118120
project.update.assert_called_once()
119121
assert result == project.update.return_value
@@ -132,7 +134,7 @@ def test_update_existing_client_project_error(mocker):
132134
result = update_existing_client(url=url, path=path, meta=MetaType.POETRY)
133135

134136
_get_project_for_url_or_path.assert_called_once_with(
135-
url=url, path=path, custom_template_path=None, meta=MetaType.POETRY
137+
url=url, path=path, custom_template_path=None, meta=MetaType.POETRY, file_encoding="utf-8"
136138
)
137139
assert result == [error]
138140

@@ -392,9 +394,9 @@ def test__build_metadata_poetry(self, mocker):
392394
project_name=project.project_name,
393395
package_name=project.package_name,
394396
)
395-
readme_path.write_text.assert_called_once_with(readme_template.render())
397+
readme_path.write_text.assert_called_once_with(readme_template.render(), encoding="utf-8")
396398
git_ignore_template.render.assert_called_once()
397-
git_ignore_path.write_text.assert_called_once_with(git_ignore_template.render())
399+
git_ignore_path.write_text.assert_called_once_with(git_ignore_template.render(), encoding="utf-8")
398400
project._build_pyproject_toml.assert_called_once_with(use_poetry=True)
399401

400402
def test__build_metadata_setup(self, mocker):
@@ -429,9 +431,9 @@ def test__build_metadata_setup(self, mocker):
429431
project_name=project.project_name,
430432
package_name=project.package_name,
431433
)
432-
readme_path.write_text.assert_called_once_with(readme_template.render())
434+
readme_path.write_text.assert_called_once_with(readme_template.render(), encoding="utf-8")
433435
git_ignore_template.render.assert_called_once()
434-
git_ignore_path.write_text.assert_called_once_with(git_ignore_template.render())
436+
git_ignore_path.write_text.assert_called_once_with(git_ignore_template.render(), encoding="utf-8")
435437
project._build_pyproject_toml.assert_called_once_with(use_poetry=False)
436438
project._build_setup_py.assert_called_once()
437439

@@ -475,7 +477,7 @@ def test__build_pyproject_toml(self, mocker, use_poetry):
475477
version=project.version,
476478
description=project.package_description,
477479
)
478-
pyproject_path.write_text.assert_called_once_with(pyproject_template.render())
480+
pyproject_path.write_text.assert_called_once_with(pyproject_template.render(), encoding="utf-8")
479481

480482
def test__build_setup_py(self, mocker):
481483
from openapi_python_client import MetaType, Project
@@ -505,7 +507,7 @@ def test__build_setup_py(self, mocker):
505507
version=project.version,
506508
description=project.package_description,
507509
)
508-
setup_path.write_text.assert_called_once_with(setup_template.render())
510+
setup_path.write_text.assert_called_once_with(setup_template.render(), encoding="utf-8")
509511

510512

511513
def test__reformat(mocker):

0 commit comments

Comments
 (0)