Skip to content

Commit 7c979af

Browse files
Merge pull request #86 from an23rd/main
feat: Adding an option to the CLI to disable automated formatting.
2 parents 2cf9112 + c1a7ee0 commit 7c979af

File tree

5 files changed

+142
-30
lines changed

5 files changed

+142
-30
lines changed

docs/references/index.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Arguments:
1616
Options:
1717
```console
1818
--library [httpx|requests|aiohttp]
19-
HTTP library to use in the generation of the client.
19+
HTTP library to use in the generation of the client.
2020
Defaults to 'httpx'.
2121

2222
--env-token-name TEXT Name of the environment variable that contains the token.
@@ -36,6 +36,10 @@ Options:
3636
Pydantic version to use for generated models.
3737
Defaults to 'v2'.
3838

39+
--formatter [black|none]
40+
Option to choose which auto formatter is applied.
41+
Defaults to 'black'.
42+
3943
--version Show the version and exit.
4044
-h, --help Show this help message and exit.
4145
```

src/openapi_python_generator/__main__.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from typing import Optional
2-
from enum import Enum
32

43
import click
54

65
from openapi_python_generator import __version__
7-
from openapi_python_generator.common import HTTPLibrary, PydanticVersion
6+
from openapi_python_generator.common import Formatter, HTTPLibrary, PydanticVersion
87
from openapi_python_generator.generate_data import generate_data
98

109
@click.command()
@@ -45,6 +44,13 @@
4544
show_default=True,
4645
help="Pydantic version to use for generated models.",
4746
)
47+
@click.option(
48+
"--formatter",
49+
type=click.Choice(["black", "none"]),
50+
default="black",
51+
show_default=True,
52+
help="Option to choose which auto formatter is applied.",
53+
)
4854
@click.version_option(version=__version__)
4955
def main(
5056
source: str,
@@ -54,6 +60,7 @@ def main(
5460
use_orjson: bool = False,
5561
custom_template_path: Optional[str] = None,
5662
pydantic_version: PydanticVersion = PydanticVersion.V2,
63+
formatter: Formatter = Formatter.BLACK,
5764
) -> None:
5865
"""
5966
Generate Python code from an OpenAPI 3.0 specification.
@@ -62,7 +69,7 @@ def main(
6269
an OUTPUT path, where the resulting client is created.
6370
"""
6471
generate_data(
65-
source, output, library, env_token_name, use_orjson, custom_template_path,pydantic_version
72+
source, output, library, env_token_name, use_orjson, custom_template_path, pydantic_version, formatter
6673
)
6774

6875

src/openapi_python_generator/common.py

+14
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,25 @@ class HTTPLibrary(str, Enum):
1414
requests = "requests"
1515
aiohttp = "aiohttp"
1616

17+
1718
class PydanticVersion(str, Enum):
1819
V1 = "v1"
1920
V2 = "v2"
2021

2122

23+
class Formatter(str, Enum):
24+
"""
25+
Enum for the available code formatters.
26+
"""
27+
28+
BLACK = "black"
29+
NONE = "none"
30+
31+
class FormatOptions:
32+
skip_validation: bool = False
33+
line_length: int = 120
34+
35+
2236
library_config_dict: Dict[Optional[HTTPLibrary], LibraryConfig] = {
2337
HTTPLibrary.httpx: LibraryConfig(
2438
name="httpx",

src/openapi_python_generator/generate_data.py

+33-23
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from pathlib import Path
2+
from typing import List
23
from typing import Optional
34
from typing import Union
45

@@ -14,34 +15,39 @@
1415
from openapi_pydantic.v3.v3_0 import OpenAPI
1516
from pydantic import ValidationError
1617

17-
from .common import HTTPLibrary, PydanticVersion
18+
from .common import FormatOptions, Formatter, HTTPLibrary, PydanticVersion
1819
from .common import library_config_dict
1920
from .language_converters.python.generator import generator
2021
from .language_converters.python.jinja_config import SERVICE_TEMPLATE
2122
from .language_converters.python.jinja_config import create_jinja_env
2223
from .models import ConversionResult
2324

2425

25-
def write_code(path: Path, content) -> None:
26+
def write_code(path: Path, content: str, formatter: Formatter) -> None:
2627
"""
2728
Write the content to the file at the given path.
28-
:param autoformat: The autoformat applied to the code written.
2929
:param path: The path to the file.
3030
:param content: The content to write.
31+
:param formatter: The formatter applied to the code written.
3132
"""
32-
try:
33-
with open(path, "w") as f:
34-
try:
35-
formatted_contend = black.format_file_contents(
36-
content, fast=False, mode=black.FileMode(line_length=120)
37-
)
33+
if formatter == Formatter.BLACK:
34+
formatted_contend = format_using_black(content)
35+
elif formatter == Formatter.NONE:
36+
formatted_contend = content
37+
else:
38+
raise NotImplementedError(f"Missing implementation for formatter {formatter!r}.")
39+
with open(path, "w") as f:
40+
f.write(formatted_contend)
41+
3842

39-
except NothingChanged:
40-
formatted_contend = content
41-
formatted_contend = isort.code(formatted_contend, line_length=120)
42-
f.write(formatted_contend)
43-
except Exception as e:
44-
raise e
43+
def format_using_black(content: str) -> str:
44+
try:
45+
formatted_contend = black.format_file_contents(
46+
content, fast=FormatOptions.skip_validation, mode=black.FileMode(line_length=FormatOptions.line_length)
47+
)
48+
except NothingChanged:
49+
return content
50+
return isort.code(formatted_contend, line_length=FormatOptions.line_length)
4551

4652

4753
def get_open_api(source: Union[str, Path]) -> OpenAPI:
@@ -105,14 +111,14 @@ def get_open_api(source: Union[str, Path]) -> OpenAPI:
105111
raise
106112

107113

108-
def write_data(data: ConversionResult, output: Union[str, Path]) -> None:
114+
def write_data(data: ConversionResult, output: Union[str, Path], formatter: Formatter) -> None:
109115
"""
110-
This function will firstly create the folderstrucutre of output, if it doesn't exist. Then it will create the
116+
This function will firstly create the folder structure of output, if it doesn't exist. Then it will create the
111117
models from data.models into the models sub module of the output folder. After this, the services will be created
112118
into the services sub module of the output folder.
113-
:param autoformat: The autoformat applied to the code written.
114119
:param data: The data to write.
115120
:param output: The path to the output folder.
121+
:param formatter: The formatter applied to the code written.
116122
"""
117123

118124
# Create the folder structure of the output folder.
@@ -126,17 +132,18 @@ def write_data(data: ConversionResult, output: Union[str, Path]) -> None:
126132
services_path = Path(output) / "services"
127133
services_path.mkdir(parents=True, exist_ok=True)
128134

129-
files = []
135+
files: List[str] = []
130136

131137
# Write the models.
132138
for model in data.models:
133139
files.append(model.file_name)
134-
write_code(models_path / f"{model.file_name}.py", model.content)
140+
write_code(models_path / f"{model.file_name}.py", model.content, formatter)
135141

136142
# Create models.__init__.py file containing imports to all models.
137143
write_code(
138144
models_path / "__init__.py",
139145
"\n".join([f"from .{file} import *" for file in files]),
146+
formatter,
140147
)
141148

142149
files = []
@@ -150,18 +157,20 @@ def write_data(data: ConversionResult, output: Union[str, Path]) -> None:
150157
write_code(
151158
services_path / f"{service.file_name}.py",
152159
jinja_env.get_template(SERVICE_TEMPLATE).render(**service.dict()),
160+
formatter,
153161
)
154162

155163
# Create services.__init__.py file containing imports to all services.
156-
write_code(services_path / "__init__.py", "")
164+
write_code(services_path / "__init__.py", "", formatter)
157165

158166
# Write the api_config.py file.
159-
write_code(Path(output) / "api_config.py", data.api_config.content)
167+
write_code(Path(output) / "api_config.py", data.api_config.content, formatter)
160168

161169
# Write the __init__.py file.
162170
write_code(
163171
Path(output) / "__init__.py",
164172
"from .models import *\nfrom .services import *\nfrom .api_config import *",
173+
formatter,
165174
)
166175

167176

@@ -173,6 +182,7 @@ def generate_data(
173182
use_orjson: bool = False,
174183
custom_template_path: Optional[str] = None,
175184
pydantic_version: PydanticVersion = PydanticVersion.V2,
185+
formatter: Formatter = Formatter.BLACK,
176186
) -> None:
177187
"""
178188
Generate Python code from an OpenAPI 3.0 specification.
@@ -189,4 +199,4 @@ def generate_data(
189199
pydantic_version,
190200
)
191201

192-
write_data(result, output)
202+
write_data(result, output, formatter)

tests/test_generate_data.py

+80-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
from pathlib import Path
12
import shutil
3+
import subprocess
24

35
import pytest
46
import yaml
57
from httpx import ConnectError
68
from orjson import orjson
79
from pydantic import ValidationError
810

9-
from openapi_python_generator.common import HTTPLibrary
11+
from openapi_python_generator.common import FormatOptions, Formatter, HTTPLibrary
1012
from openapi_python_generator.common import library_config_dict
1113
from openapi_python_generator.generate_data import generate_data
1214
from openapi_python_generator.generate_data import get_open_api
@@ -66,7 +68,7 @@ def test_generate_data(model_data_with_cleanup):
6668

6769
def test_write_data(model_data_with_cleanup):
6870
result = generator(model_data_with_cleanup, library_config_dict[HTTPLibrary.httpx])
69-
write_data(result, test_result_path)
71+
write_data(result, test_result_path, Formatter.BLACK)
7072

7173
assert test_result_path.exists()
7274
assert test_result_path.is_dir()
@@ -90,7 +92,7 @@ def test_write_data(model_data_with_cleanup):
9092
model_data_copy.paths = None
9193

9294
result = generator(model_data_copy, library_config_dict[HTTPLibrary.httpx])
93-
write_data(result, test_result_path)
95+
write_data(result, test_result_path, Formatter.BLACK)
9496

9597
assert test_result_path.exists()
9698
assert test_result_path.is_dir()
@@ -105,3 +107,78 @@ def test_write_data(model_data_with_cleanup):
105107
assert (test_result_path / "models" / "__init__.py").is_file()
106108
assert (test_result_path / "__init__.py").exists()
107109
assert (test_result_path / "__init__.py").is_file()
110+
111+
def test_write_formatted_data(model_data_with_cleanup):
112+
result = generator(model_data_with_cleanup, library_config_dict[HTTPLibrary.httpx])
113+
114+
# First write code without formatter
115+
write_data(result, test_result_path, Formatter.NONE)
116+
117+
assert test_result_path.exists()
118+
assert test_result_path.is_dir()
119+
assert (test_result_path / "api_config.py").exists()
120+
assert (test_result_path / "models").exists()
121+
assert (test_result_path / "models").is_dir()
122+
assert (test_result_path / "services").exists()
123+
assert (test_result_path / "services").is_dir()
124+
assert (test_result_path / "models" / "__init__.py").exists()
125+
assert (test_result_path / "services" / "__init__.py").exists()
126+
assert (test_result_path / "services" / "__init__.py").is_file()
127+
assert (test_result_path / "models" / "__init__.py").is_file()
128+
assert (test_result_path / "__init__.py").exists()
129+
assert (test_result_path / "__init__.py").is_file()
130+
131+
assert not files_are_black_formatted(test_result_path)
132+
133+
# delete test_result_path folder
134+
shutil.rmtree(test_result_path)
135+
136+
model_data_copy = model_data_with_cleanup.copy()
137+
model_data_copy.components = None
138+
model_data_copy.paths = None
139+
140+
result = generator(model_data_copy, library_config_dict[HTTPLibrary.httpx])
141+
write_data(result, test_result_path, Formatter.BLACK)
142+
143+
assert test_result_path.exists()
144+
assert test_result_path.is_dir()
145+
assert (test_result_path / "api_config.py").exists()
146+
assert (test_result_path / "models").exists()
147+
assert (test_result_path / "models").is_dir()
148+
assert (test_result_path / "services").exists()
149+
assert (test_result_path / "services").is_dir()
150+
assert (test_result_path / "models" / "__init__.py").exists()
151+
assert (test_result_path / "services" / "__init__.py").exists()
152+
assert (test_result_path / "services" / "__init__.py").is_file()
153+
assert (test_result_path / "models" / "__init__.py").is_file()
154+
assert (test_result_path / "__init__.py").exists()
155+
assert (test_result_path / "__init__.py").is_file()
156+
157+
assert files_are_black_formatted(test_result_path)
158+
159+
def files_are_black_formatted(test_result_path: Path) -> bool:
160+
# Run the `black --check` command on all files. This does not write any file.
161+
result = subprocess.run([
162+
"black",
163+
"--check",
164+
# Overwrite any exclusion due to a .gitignore.
165+
"--exclude", "''",
166+
# Settings also used when formatting the code when writing it
167+
"--fast" if FormatOptions.skip_validation else "--safe",
168+
"--line-length", str(FormatOptions.line_length),
169+
# The source directory
170+
str(test_result_path.absolute())
171+
],
172+
capture_output=True,
173+
text=True
174+
)
175+
176+
# With `--check` the return status has the following meaning:
177+
# - Return code 0 means nothing would change.
178+
# - Return code 1 means some files would be reformatted.
179+
# - Return code 123 means there was an internal error.
180+
181+
if result.returncode == 123:
182+
result.check_returncode # raise the error
183+
184+
return result.returncode == 0

0 commit comments

Comments
 (0)