Skip to content

Commit 20cfce8

Browse files
authored
Fix Ruff formatting when meta=none (#940)
Also, does a bunch of test refactoring to make future changes like this easier by: - Putting `Config` in a fixture, since most tests need it but don't care what's in it - Deleting more of the mocked tests, since they don't test much and are hard to read To get coverage back up, this means adding more functional e2e tests --------- Co-authored-by: Dylan Anthony <[email protected]>
1 parent 89dc670 commit 20cfce8

36 files changed

+515
-1023
lines changed

Diff for: .changeset/fix_ruff_formatting_for_metanone.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
default: patch
3+
---
4+
5+
# Fix Ruff formatting for `--meta=none`
6+
7+
PR #940 fixes issue #939. Thanks @satwell!
8+
9+
Due to the lack of `pyproject.toml`, Ruff was not getting configured properly when `--meta=none`.
10+
As a result, it didn't clean up common generation issues like duplicate imports, which would then cause errors from
11+
linters.
12+
13+
This is now fixed by changing the default `post_hook` to `ruff check . --fix --extend-select=I` when `--meta=none`.
14+
Using `generate --meta=none` should now be almost identical to the code generated by `update`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
default: minor
3+
---
4+
5+
# Include the `UP` rule for generated Ruff config
6+
7+
This enables [pyupgrade-like improvements](https://docs.astral.sh/ruff/rules/#pyupgrade-up) which should replace some
8+
`.format()` calls with f-strings.

Diff for: .github/check_for_changes.py

-11
This file was deleted.

Diff for: .github/workflows/checks.yml

-5
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,6 @@ jobs:
149149
pip install pdm
150150
python -m venv .venv
151151
pdm install
152-
- name: Regenerate Integration Client
153-
run: |
154-
pdm run openapi-python-client update --url http://localhost:3000/openapi.json --config integration-tests-config.yaml --meta pdm
155-
- name: Check for any file changes
156-
run: python .github/check_for_changes.py
157152
- name: Cache Generated Client Dependencies
158153
uses: actions/cache@v3
159154
with:

Diff for: end_to_end_tests/custom_post_hooks.config.yml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
post_hooks:
2+
- echo "this should fail" && exit 1

Diff for: end_to_end_tests/golden-record/my_test_api_client/api/parameter_references/get_parameter_references_path_param.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ def _get_kwargs(
3434

3535
_kwargs: Dict[str, Any] = {
3636
"method": "get",
37-
"url": "/parameter-references/{path_param}".format(
38-
path_param=path_param,
39-
),
37+
"url": f"/parameter-references/{path_param}",
4038
"params": params,
4139
"cookies": cookies,
4240
}

Diff for: end_to_end_tests/golden-record/my_test_api_client/api/parameters/delete_common_parameters_overriding_param.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ def _get_kwargs(
2121

2222
_kwargs: Dict[str, Any] = {
2323
"method": "delete",
24-
"url": "/common_parameters_overriding/{param}".format(
25-
param=param_path,
26-
),
24+
"url": f"/common_parameters_overriding/{param_path}",
2725
"params": params,
2826
}
2927

Diff for: end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_common_parameters_overriding_param.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ def _get_kwargs(
2121

2222
_kwargs: Dict[str, Any] = {
2323
"method": "get",
24-
"url": "/common_parameters_overriding/{param}".format(
25-
param=param_path,
26-
),
24+
"url": f"/common_parameters_overriding/{param_path}",
2725
"params": params,
2826
}
2927

Diff for: end_to_end_tests/golden-record/my_test_api_client/api/parameters/get_same_name_multiple_locations_param.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ def _get_kwargs(
3131

3232
_kwargs: Dict[str, Any] = {
3333
"method": "get",
34-
"url": "/same-name-multiple-locations/{param}".format(
35-
param=param_path,
36-
),
34+
"url": f"/same-name-multiple-locations/{param_path}",
3735
"params": params,
3836
"cookies": cookies,
3937
}

Diff for: end_to_end_tests/golden-record/my_test_api_client/api/parameters/multiple_path_parameters.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,7 @@ def _get_kwargs(
1616
) -> Dict[str, Any]:
1717
_kwargs: Dict[str, Any] = {
1818
"method": "get",
19-
"url": "/multiple-path-parameters/{param4}/something/{param2}/{param1}/{param3}".format(
20-
param4=param4,
21-
param2=param2,
22-
param1=param1,
23-
param3=param3,
24-
),
19+
"url": f"/multiple-path-parameters/{param4}/something/{param2}/{param1}/{param3}",
2520
}
2621

2722
return _kwargs

Diff for: end_to_end_tests/golden-record/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ requires = ["poetry-core>=1.0.0"]
2121
build-backend = "poetry.core.masonry.api"
2222

2323
[tool.ruff]
24-
select = ["F", "I"]
24+
select = ["F", "I", "UP"]
2525
line-length = 120

Diff for: end_to_end_tests/metadata_snapshots/pdm.pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ requires = ["pdm-backend"]
1919
build-backend = "pdm.backend"
2020

2121
[tool.ruff]
22-
select = ["F", "I"]
22+
select = ["F", "I", "UP"]
2323
line-length = 120

Diff for: end_to_end_tests/metadata_snapshots/poetry.pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ requires = ["poetry-core>=1.0.0"]
2121
build-backend = "poetry.core.masonry.api"
2222

2323
[tool.ruff]
24-
select = ["F", "I"]
24+
select = ["F", "I", "UP"]
2525
line-length = 120

Diff for: end_to_end_tests/regen_golden_record.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,11 @@ def regen_custom_template_golden_record():
7979
gr_path = Path(__file__).parent / "golden-record"
8080
tpl_gr_path = Path(__file__).parent / "custom-templates-golden-record"
8181

82-
output_path = Path(tempfile.mkdtemp())
82+
output_path = Path.cwd() / "my-test-api-client"
8383
config_path = Path(__file__).parent / "config.yml"
8484

8585
shutil.rmtree(tpl_gr_path, ignore_errors=True)
8686

87-
os.chdir(str(output_path.absolute()))
8887
result = runner.invoke(
8988
app,
9089
[
@@ -96,9 +95,8 @@ def regen_custom_template_golden_record():
9695
)
9796

9897
if result.stdout:
99-
generated_output_path = output_path / "my-test-api-client"
100-
for f in generated_output_path.glob("**/*"): # nb: works for Windows and Unix
101-
relative_to_generated = f.relative_to(generated_output_path)
98+
for f in output_path.glob("**/*"): # nb: works for Windows and Unix
99+
relative_to_generated = f.relative_to(output_path)
102100
gr_file = gr_path / relative_to_generated
103101
if not gr_file.exists():
104102
print(f"{gr_file} does not exist, ignoring")

Diff for: end_to_end_tests/test-3-1-golden-record/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ requires = ["poetry-core>=1.0.0"]
2121
build-backend = "poetry.core.masonry.api"
2222

2323
[tool.ruff]
24-
select = ["F", "I"]
24+
select = ["F", "I", "UP"]
2525
line-length = 120

Diff for: end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/api/const/post_const_path.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ def _get_kwargs(
2828

2929
_kwargs: Dict[str, Any] = {
3030
"method": "post",
31-
"url": "/const/{path}".format(
32-
path=path,
33-
),
31+
"url": f"/const/{path}",
3432
"params": params,
3533
}
3634

Diff for: end_to_end_tests/test_end_to_end.py

+112-20
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import shutil
22
from filecmp import cmpfiles, dircmp
33
from pathlib import Path
4-
from typing import Dict, List, Optional, Tuple
4+
from typing import Dict, List, Optional, Set
55

66
import pytest
7+
from click.testing import Result
78
from typer.testing import CliRunner
89

910
from openapi_python_client.cli import app
@@ -13,6 +14,7 @@ def _compare_directories(
1314
record: Path,
1415
test_subject: Path,
1516
expected_differences: Dict[Path, str],
17+
expected_missing: Optional[Set[str]] = None,
1618
ignore: List[str] = None,
1719
depth=0,
1820
):
@@ -28,7 +30,7 @@ def _compare_directories(
2830
first_printable = record.relative_to(Path.cwd())
2931
second_printable = test_subject.relative_to(Path.cwd())
3032
dc = dircmp(record, test_subject, ignore=[".ruff_cache", "__pycache__"] + (ignore or []))
31-
missing_files = dc.left_only + dc.right_only
33+
missing_files = set(dc.left_only + dc.right_only) - (expected_missing or set())
3234
if missing_files:
3335
pytest.fail(
3436
f"{first_printable} or {second_printable} was missing: {missing_files}",
@@ -77,21 +79,23 @@ def _compare_directories(
7779
def run_e2e_test(
7880
openapi_document: str,
7981
extra_args: List[str],
80-
expected_differences: Dict[Path, str],
82+
expected_differences: Optional[Dict[Path, str]] = None,
8183
golden_record_path: str = "golden-record",
8284
output_path: str = "my-test-api-client",
83-
):
85+
expected_missing: Optional[Set[str]] = None,
86+
) -> Result:
8487
output_path = Path.cwd() / output_path
8588
shutil.rmtree(output_path, ignore_errors=True)
86-
generate(extra_args, openapi_document)
89+
result = generate(extra_args, openapi_document)
8790
gr_path = Path(__file__).parent / golden_record_path
8891

92+
expected_differences = expected_differences or {}
8993
# Use absolute paths for expected differences for easier comparisons
9094
expected_differences = {
9195
output_path.joinpath(key): value for key, value in expected_differences.items()
9296
}
9397
_compare_directories(
94-
gr_path, output_path, expected_differences=expected_differences
98+
gr_path, output_path, expected_differences=expected_differences, expected_missing=expected_missing
9599
)
96100

97101
import mypy.api
@@ -100,19 +104,31 @@ def run_e2e_test(
100104
assert status == 0, f"Type checking client failed: {out}"
101105

102106
shutil.rmtree(output_path)
107+
return result
103108

104109

105-
def generate(extra_args: Optional[List[str]], openapi_document: str):
110+
def generate(extra_args: Optional[List[str]], openapi_document: str) -> Result:
111+
"""Generate a client from an OpenAPI document and return the path to the generated code"""
112+
_run_command("generate", extra_args, openapi_document)
113+
114+
115+
def _run_command(command: str, extra_args: Optional[List[str]] = None, openapi_document: Optional[str] = None, url: Optional[str] = None, config_path: Optional[Path] = None) -> Result:
106116
"""Generate a client from an OpenAPI document and return the path to the generated code"""
107117
runner = CliRunner()
108-
openapi_path = Path(__file__).parent / openapi_document
109-
config_path = Path(__file__).parent / "config.yml"
110-
args = ["generate", f"--config={config_path}", f"--path={openapi_path}"]
118+
if openapi_document is not None:
119+
openapi_path = Path(__file__).parent / openapi_document
120+
source_arg = f"--path={openapi_path}"
121+
else:
122+
source_arg = f"--url={url}"
123+
config_path = config_path or (Path(__file__).parent / "config.yml")
124+
args = [command, f"--config={config_path}", source_arg]
111125
if extra_args:
112126
args.extend(extra_args)
113127
result = runner.invoke(app, args)
114128
if result.exit_code != 0:
115-
raise result.exception
129+
raise Exception(result.stdout)
130+
return result
131+
116132

117133
def test_baseline_end_to_end_3_0():
118134
run_e2e_test("baseline_openapi_3.0.json", [], {})
@@ -147,19 +163,22 @@ def test_meta(meta: str, generated_file: Optional[str], expected_file: Optional[
147163

148164
if generated_file and expected_file:
149165
assert (output_path / generated_file).exists()
150-
assert (output_path / generated_file).read_text() == (Path(__file__).parent / "metadata_snapshots" / expected_file).read_text()
166+
assert (
167+
(output_path / generated_file).read_text() ==
168+
(Path(__file__).parent / "metadata_snapshots" / expected_file).read_text()
169+
)
151170

152171
shutil.rmtree(output_path)
153172

154173

155-
def test_no_meta():
156-
output_path = Path.cwd() / "test_3_1_features_client"
157-
shutil.rmtree(output_path, ignore_errors=True)
158-
generate([f"--meta=none"], "3.1_specific.openapi.yaml")
159-
160-
assert output_path.exists() # Has a different name than with-meta generation
161-
assert (output_path / "__init__.py").exists()
162-
shutil.rmtree(output_path)
174+
def test_none_meta():
175+
run_e2e_test(
176+
"3.1_specific.openapi.yaml",
177+
["--meta=none"],
178+
golden_record_path="test-3-1-golden-record/test_3_1_features_client",
179+
output_path="test_3_1_features_client",
180+
expected_missing={"py.typed"},
181+
)
163182

164183

165184
def test_custom_templates():
@@ -194,3 +213,76 @@ def test_custom_templates():
194213
extra_args=["--custom-template-path=end_to_end_tests/test_custom_templates/"],
195214
expected_differences=expected_differences,
196215
)
216+
217+
218+
@pytest.mark.parametrize(
219+
"command", ("generate", "update")
220+
)
221+
def test_bad_url(command: str):
222+
runner = CliRunner()
223+
result = runner.invoke(app, [command, "--url=not_a_url"])
224+
assert result.exit_code == 1
225+
assert "Could not get OpenAPI document from provided URL" in result.stdout
226+
227+
228+
def test_custom_post_hooks():
229+
shutil.rmtree(Path.cwd() / "my-test-api-client", ignore_errors=True)
230+
runner = CliRunner()
231+
openapi_document = Path(__file__).parent / "baseline_openapi_3.0.json"
232+
config_path = Path(__file__).parent / "custom_post_hooks.config.yml"
233+
result = runner.invoke(app, ["generate", f"--path={openapi_document}", f"--config={config_path}"])
234+
assert result.exit_code == 1
235+
assert "this should fail" in result.stdout
236+
shutil.rmtree(Path.cwd() / "my-test-api-client", ignore_errors=True)
237+
238+
239+
def test_generate_dir_already_exists():
240+
project_dir = Path.cwd() / "my-test-api-client"
241+
if not project_dir.exists():
242+
project_dir.mkdir()
243+
runner = CliRunner()
244+
openapi_document = Path(__file__).parent / "baseline_openapi_3.0.json"
245+
result = runner.invoke(app, ["generate", f"--path={openapi_document}"])
246+
assert result.exit_code == 1
247+
assert "Directory already exists" in result.stdout
248+
shutil.rmtree(Path.cwd() / "my-test-api-client", ignore_errors=True)
249+
250+
251+
def test_update_dir_not_found():
252+
project_dir = Path.cwd() / "my-test-api-client"
253+
shutil.rmtree(project_dir, ignore_errors=True)
254+
runner = CliRunner()
255+
openapi_document = Path(__file__).parent / "baseline_openapi_3.0.json"
256+
result = runner.invoke(app, ["update", f"--path={openapi_document}"])
257+
assert result.exit_code == 1
258+
assert str(project_dir) in result.stdout
259+
260+
261+
@pytest.mark.parametrize(
262+
("file_name", "content", "expected_error"),
263+
(
264+
("invalid_openapi.yaml", "not a valid openapi document", "Failed to parse OpenAPI document"),
265+
("invalid_json.json", "Invalid JSON", "Invalid JSON"),
266+
("invalid_yaml.yaml", "{", "Invalid YAML"),
267+
),
268+
ids=("invalid_openapi", "invalid_json", "invalid_yaml")
269+
)
270+
def test_invalid_openapi_document(file_name, content, expected_error):
271+
runner = CliRunner()
272+
openapi_document = Path.cwd() / file_name
273+
openapi_document.write_text(content)
274+
result = runner.invoke(app, ["generate", f"--path={openapi_document}"])
275+
assert result.exit_code == 1
276+
assert expected_error in result.stdout
277+
openapi_document.unlink()
278+
279+
280+
def test_update_integration_tests():
281+
url = "https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.json"
282+
source_path = Path(__file__).parent.parent / "integration-tests"
283+
project_path = Path.cwd() / "integration-tests"
284+
if source_path != project_path: # Just in case someone runs this from root dir
285+
shutil.copytree(source_path, project_path)
286+
config_path = project_path / "config.yaml"
287+
_run_command("update", url=url, config_path=config_path)
288+
_compare_directories(source_path, project_path, expected_differences={})

Diff for: integration-tests-config.yaml

-1
This file was deleted.

Diff for: integration-tests/config.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
project_name_override: integration-tests
2+
post_hooks:
3+
- ruff check . --fix
4+
- ruff format .
5+
- mypy . --strict

0 commit comments

Comments
 (0)