Skip to content

Commit a5a99e1

Browse files
authored
Remove update, add --overwrite and --output-path (#1052)
* Removes `update` * Adds `--output-path` to `generate` * Adds `--overwrite` to `generate` Also removing Safety from CI because it's currently failing in CI, and the current command is about to be removed and instead require an account. --------- Co-authored-by: Dylan Anthony <dbanty@users.noreply.github.com>
1 parent 73f92ea commit a5a99e1

File tree

15 files changed

+189
-594
lines changed

15 files changed

+189
-594
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
default: minor
3+
---
4+
5+
# Added an `--output-path` option to `generate`
6+
7+
Rather than changing directories before running `generate` you can now specify an output directory with `--output-path`.
8+
Note that the project name will _not_ be appended to the `--output-path`, whatever path you specify is where the
9+
generated code will be placed.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
default: minor
3+
---
4+
5+
# Added an `--overwrite` flag to `generate`
6+
7+
You can now tell `openapi-python-client` to overwrite an existing directory, rather than deleting it yourself before
8+
running `generate`.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
default: major
3+
---
4+
5+
# Removed the `update` command
6+
7+
The `update` command is no more, you can (mostly) replace its usage with some new flags on the `generate` command.
8+
9+
If you had a package named `my-api-client` in the current working directory, the `update` command previously would update the `my_api_client` module within it. You can now _almost_ perfectly replicate this behavior using `openapi-python-client generate --meta=none --output-path=my-api-client/my_api_client --overwrite`.
10+
11+
The only difference is that `my-api-client` would have run `post_hooks` in the `my-api-client` directory,
12+
but `generate` will run `post_hooks` in the `output-path` directory.
13+
14+
Alternatively, you can now also run `openapi-python-client generate --meta=<your-meta-type> --overwrite` to regenerate
15+
the entire client, if you don't care about keeping any changes you've made to the generated client.
16+
17+
Please comment on [discussion #824](https://github.com/openapi-generators/openapi-python-client/discussions/824)
18+
(or a new discussion, as appropriate) to aid in designing future features that fill any gaps this leaves for you.

.github/workflows/checks.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,6 @@ jobs:
4242
- name: Check formatting
4343
run: pdm run ruff format . --check
4444

45-
- name: Run safety
46-
run: pdm safety_check
47-
4845
- name: Run mypy
4946
run: pdm mypy --show-error-codes
5047

end_to_end_tests/invalid_openapi.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
openapi: "3.1.0"
2+
info:
3+
title: "There's something wrong with me"
4+
version: "0.1.0"
5+
paths:
6+
"/{optional}":
7+
get:
8+
parameters:
9+
- in: "path"
10+
name: "optional"
11+
schema:
12+
type: "string"
13+
responses:
14+
"200":
15+
description: "Successful Response"
16+
content:
17+
"application/json":
18+
schema:
19+
const: "Why have a fixed response? I dunno"

end_to_end_tests/test_end_to_end.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -215,16 +215,21 @@ def test_custom_templates():
215215
)
216216

217217

218-
@pytest.mark.parametrize(
219-
"command", ("generate", "update")
220-
)
221-
def test_bad_url(command: str):
218+
def test_bad_url():
222219
runner = CliRunner()
223-
result = runner.invoke(app, [command, "--url=not_a_url"])
220+
result = runner.invoke(app, ["generate", "--url=not_a_url"])
224221
assert result.exit_code == 1
225222
assert "Could not get OpenAPI document from provided URL" in result.stdout
226223

227224

225+
def test_invalid_document():
226+
runner = CliRunner()
227+
path = Path(__file__).parent / "invalid_openapi.yaml"
228+
result = runner.invoke(app, ["generate", f"--path={path}", "--fail-on-warning"])
229+
assert result.exit_code == 1
230+
assert "Warning(s) encountered while generating" in result.stdout
231+
232+
228233
def test_custom_post_hooks():
229234
shutil.rmtree(Path.cwd() / "my-test-api-client", ignore_errors=True)
230235
runner = CliRunner()
@@ -248,16 +253,6 @@ def test_generate_dir_already_exists():
248253
shutil.rmtree(Path.cwd() / "my-test-api-client", ignore_errors=True)
249254

250255

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-
261256
@pytest.mark.parametrize(
262257
("file_name", "content", "expected_error"),
263258
(
@@ -280,9 +275,19 @@ def test_invalid_openapi_document(file_name, content, expected_error):
280275
def test_update_integration_tests():
281276
url = "https://raw.githubusercontent.com/openapi-generators/openapi-test-server/main/openapi.json"
282277
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={})
278+
temp_dir = Path.cwd() / "test_update_integration_tests"
279+
shutil.rmtree(temp_dir, ignore_errors=True)
280+
shutil.copytree(source_path, temp_dir)
281+
config_path = source_path / "config.yaml"
282+
_run_command(
283+
"generate",
284+
extra_args=["--meta=none", "--overwrite", f"--output-path={source_path / 'integration_tests'}"],
285+
url=url,
286+
config_path=config_path
287+
)
288+
_compare_directories(temp_dir, source_path, expected_differences={})
289+
import mypy.api
290+
291+
out, err, status = mypy.api.run([str(temp_dir), "--strict"])
292+
assert status == 0, f"Type checking client failed: {out}"
293+
shutil.rmtree(temp_dir)

integration-tests/config.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
project_name_override: integration-tests
22
post_hooks:
33
- ruff check . --fix
4-
- ruff format .
5-
- mypy . --strict
4+
- ruff format .

integration-tests/integration_tests/py.typed

Lines changed: 0 additions & 1 deletion
This file was deleted.

openapi_python_client/__init__.py

Lines changed: 27 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,22 @@ def __init__(
6565
)
6666

6767
self.project_name: str = config.project_name_override or f"{utils.kebab_case(openapi.title).lower()}-client"
68-
self.project_dir: Path = Path.cwd()
69-
if config.meta_type != MetaType.NONE:
70-
self.project_dir /= self.project_name
71-
7268
self.package_name: str = config.package_name_override or self.project_name.replace("-", "_")
73-
self.package_dir: Path = self.project_dir / self.package_name
69+
self.project_dir: Path # Where the generated code will be placed
70+
self.package_dir: Path # Where the generated Python module will be placed (same as project_dir if no meta)
71+
72+
if config.output_path is not None:
73+
self.project_dir = config.output_path
74+
elif config.meta_type == MetaType.NONE:
75+
self.project_dir = Path.cwd() / self.package_name
76+
else:
77+
self.project_dir = Path.cwd() / self.project_name
78+
79+
if config.meta_type == MetaType.NONE:
80+
self.package_dir = self.project_dir
81+
else:
82+
self.package_dir = self.project_dir / self.package_name
83+
7484
self.package_description: str = utils.remove_string_escapes(
7585
f"A client library for accessing {self.openapi.title}"
7686
)
@@ -95,29 +105,16 @@ def __init__(
95105
def build(self) -> Sequence[GeneratorError]:
96106
"""Create the project from templates"""
97107

98-
if self.config.meta_type == MetaType.NONE:
99-
print(f"Generating {self.package_name}")
100-
else:
101-
print(f"Generating {self.project_name}")
102-
try:
103-
self.project_dir.mkdir()
104-
except FileExistsError:
105-
return [GeneratorError(detail="Directory already exists. Delete it or use the update command.")]
106-
self._create_package()
107-
self._build_metadata()
108-
self._build_models()
109-
self._build_api()
110-
self._run_post_hooks()
111-
return self._get_errors()
112-
113-
def update(self) -> Sequence[GeneratorError]:
114-
"""Update an existing project"""
108+
print(f"Generating {self.project_dir}")
109+
if self.config.overwrite:
110+
shutil.rmtree(self.project_dir, ignore_errors=True)
115111

116-
if not self.package_dir.is_dir():
117-
return [GeneratorError(detail=f"Directory {self.package_dir} not found")]
118-
print(f"Updating {self.package_name}")
119-
shutil.rmtree(self.package_dir)
112+
try:
113+
self.project_dir.mkdir()
114+
except FileExistsError:
115+
return [GeneratorError(detail="Directory already exists. Delete it or use the --overwrite option.")]
120116
self._create_package()
117+
self._build_metadata()
121118
self._build_models()
122119
self._build_api()
123120
self._run_post_hooks()
@@ -138,7 +135,7 @@ def _run_command(self, cmd: str) -> None:
138135
)
139136
return
140137
try:
141-
cwd = self.package_dir if self.config.meta_type == MetaType.NONE else self.project_dir
138+
cwd = self.project_dir
142139
subprocess.run(cmd, cwd=cwd, shell=True, capture_output=True, check=True)
143140
except CalledProcessError as err:
144141
self.errors.append(
@@ -158,7 +155,8 @@ def _get_errors(self) -> List[GeneratorError]:
158155
return errors
159156

160157
def _create_package(self) -> None:
161-
self.package_dir.mkdir()
158+
if self.package_dir != self.project_dir:
159+
self.package_dir.mkdir()
162160
# Package __init__.py
163161
package_init = self.package_dir / "__init__.py"
164162

@@ -303,7 +301,7 @@ def _get_project_for_url_or_path(
303301
)
304302

305303

306-
def create_new_client(
304+
def generate(
307305
*,
308306
config: Config,
309307
custom_template_path: Optional[Path] = None,
@@ -323,26 +321,6 @@ def create_new_client(
323321
return project.build()
324322

325323

326-
def update_existing_client(
327-
*,
328-
config: Config,
329-
custom_template_path: Optional[Path] = None,
330-
) -> Sequence[GeneratorError]:
331-
"""
332-
Update an existing client library
333-
334-
Returns:
335-
A list containing any errors encountered when generating.
336-
"""
337-
project = _get_project_for_url_or_path(
338-
custom_template_path=custom_template_path,
339-
config=config,
340-
)
341-
if isinstance(project, GeneratorError):
342-
return [project]
343-
return project.update()
344-
345-
346324
def _load_yaml_or_json(data: bytes, content_type: Optional[str]) -> Union[Dict[str, Any], GeneratorError]:
347325
if content_type == "application/json":
348326
try:

openapi_python_client/cli.py

Lines changed: 42 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,14 @@ def _version_callback(value: bool) -> None:
2121

2222

2323
def _process_config(
24-
*, url: Optional[str], path: Optional[Path], config_path: Optional[Path], meta_type: MetaType, file_encoding: str
24+
*,
25+
url: Optional[str],
26+
path: Optional[Path],
27+
config_path: Optional[Path],
28+
meta_type: MetaType,
29+
file_encoding: str,
30+
overwrite: bool,
31+
output_path: Optional[Path],
2532
) -> Config:
2633
source: Union[Path, str]
2734
if url and not path:
@@ -49,7 +56,7 @@ def _process_config(
4956
except Exception as err:
5057
raise typer.BadParameter("Unable to parse config") from err
5158

52-
return Config.from_sources(config_file, meta_type, source, file_encoding)
59+
return Config.from_sources(config_file, meta_type, source, file_encoding, overwrite, output_path=output_path)
5360

5461

5562
# noinspection PyUnusedLocal
@@ -117,62 +124,46 @@ def handle_errors(errors: Sequence[GeneratorError], fail_on_warning: bool = Fals
117124
raise typer.Exit(code=1)
118125

119126

120-
custom_template_path_options = {
121-
"help": "A path to a directory containing custom template(s)",
122-
"file_okay": False,
123-
"dir_okay": True,
124-
"readable": True,
125-
"resolve_path": True,
126-
}
127-
128-
_meta_option = typer.Option(
129-
MetaType.POETRY,
130-
help="The type of metadata you want to generate.",
131-
)
132-
133-
CONFIG_OPTION = typer.Option(None, "--config", help="Path to the config file to use")
134-
135-
136127
@app.command()
137128
def generate(
138-
url: Optional[str] = typer.Option(None, help="A URL to read the JSON from"),
139-
path: Optional[Path] = typer.Option(None, help="A path to the JSON file"),
140-
custom_template_path: Optional[Path] = typer.Option(None, **custom_template_path_options), # type: ignore
141-
meta: MetaType = _meta_option,
129+
url: Optional[str] = typer.Option(None, help="A URL to read the OpenAPI document from"),
130+
path: Optional[Path] = typer.Option(None, help="A path to the OpenAPI document"),
131+
custom_template_path: Optional[Path] = typer.Option(
132+
None,
133+
help="A path to a directory containing custom template(s)",
134+
file_okay=False,
135+
dir_okay=True,
136+
readable=True,
137+
resolve_path=True,
138+
), # type: ignore
139+
meta: MetaType = typer.Option(
140+
MetaType.POETRY,
141+
help="The type of metadata you want to generate.",
142+
),
142143
file_encoding: str = typer.Option("utf-8", help="Encoding used when writing generated"),
143-
config_path: Optional[Path] = CONFIG_OPTION,
144+
config_path: Optional[Path] = typer.Option(None, "--config", help="Path to the config file to use"),
144145
fail_on_warning: bool = False,
146+
overwrite: bool = typer.Option(False, help="Overwrite the existing client if it exists"),
147+
output_path: Optional[Path] = typer.Option(
148+
None,
149+
help="Path to write the generated code to. "
150+
"Defaults to the OpenAPI document title converted to kebab or snake case (depending on meta type). "
151+
"Can also be overridden with `project_name_override` or `package_name_override` in config.",
152+
),
145153
) -> None:
146154
"""Generate a new OpenAPI Client library"""
147-
from . import create_new_client
148-
149-
config = _process_config(url=url, path=path, config_path=config_path, meta_type=meta, file_encoding=file_encoding)
150-
errors = create_new_client(
151-
custom_template_path=custom_template_path,
152-
config=config,
155+
from . import generate
156+
157+
config = _process_config(
158+
url=url,
159+
path=path,
160+
config_path=config_path,
161+
meta_type=meta,
162+
file_encoding=file_encoding,
163+
overwrite=overwrite,
164+
output_path=output_path,
153165
)
154-
handle_errors(errors, fail_on_warning)
155-
156-
157-
@app.command()
158-
def update(
159-
url: Optional[str] = typer.Option(None, help="A URL to read the JSON from"),
160-
path: Optional[Path] = typer.Option(None, help="A path to the JSON file"),
161-
custom_template_path: Optional[Path] = typer.Option(None, **custom_template_path_options), # type: ignore
162-
meta: MetaType = _meta_option,
163-
file_encoding: str = typer.Option("utf-8", help="Encoding used when writing generated"),
164-
config_path: Optional[Path] = CONFIG_OPTION,
165-
fail_on_warning: bool = False,
166-
) -> None:
167-
"""Update an existing OpenAPI Client library
168-
169-
The update command performs the same operations as generate except it does not overwrite specific metadata for the
170-
generated client such as the README.md, .gitignore, and pyproject.toml.
171-
"""
172-
from . import update_existing_client
173-
174-
config = _process_config(config_path=config_path, meta_type=meta, url=url, path=path, file_encoding=file_encoding)
175-
errors = update_existing_client(
166+
errors = generate(
176167
custom_template_path=custom_template_path,
177168
config=config,
178169
)

0 commit comments

Comments
 (0)