Skip to content

Commit dac9b25

Browse files
committed
Added config file option
- Allows overriding class names, closing #9
1 parent 87105b3 commit dac9b25

File tree

19 files changed

+149
-25
lines changed

19 files changed

+149
-25
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,37 @@ get an error.
5151
for endpoints without a tag. Each of these modules in turn contains one function for calling each endpoint.
5252
1. A `models` module which has all the classes defined by the various schemas in your OpenAPI spec
5353

54+
For a full example you can look at tests/test_end_to_end which has a declared [FastAPI](https://fastapi.tiangolo.com/)
55+
server and the resulting openapi.json file in the "fastapi" directory. "golden-master" is the generated client from that
56+
OpenAPI document.
57+
5458
## OpenAPI features supported
5559
1. All HTTP Methods
5660
1. JSON and form bodies, path and query parameters
5761
1. float, string, int, datetimes, string enums, and custom schemas or lists containing any of those
5862
1. html/text or application/json responses containing any of the previous types
5963
1. Bearer token security
6064

65+
## Configuration
66+
You can pass a YAML (or JSON) file to openapi-python-client in order to change some behavior. The following parameters
67+
are supported:
68+
69+
### class_overrides
70+
Used to change the name of generated model classes, especially useful if you have a name like ABCModel which, when
71+
converted to snake case for module naming will be a_b_c_model. This param should be a mapping of existing class name
72+
(usually a key in the "schemas" section of your OpenAPI document) to class_name and module_name.
73+
74+
Example:
75+
```yaml
76+
class_overrides:
77+
ABCModel:
78+
class_name: ABCModel
79+
module_name: abc_model
80+
```
81+
82+
The easiest way to find what needs to be overridden is probably to generate your client and go look at everything in the
83+
models folder.
84+
6185
6286
## Contributors
6387
- Dylan Anthony <[email protected]> (Owner)

openapi_python_client/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Any, Dict, Optional
1010

1111
import httpx
12+
import yaml
1213
from jinja2 import Environment, PackageLoader
1314

1415
from .openapi_parser import OpenAPI, import_string_from_reference
@@ -22,6 +23,17 @@ def _get_project_for_url_or_path(url: Optional[str], path: Optional[Path]) -> _P
2223
return _Project(openapi=openapi)
2324

2425

26+
def load_config(*, path: Path) -> None:
27+
""" Loads config from provided Path """
28+
config_data = yaml.safe_load(path.read_text())
29+
30+
if "class_overrides" in config_data:
31+
from .openapi_parser import reference
32+
33+
for class_name, class_data in config_data["class_overrides"].items():
34+
reference.class_overrides[class_name] = reference.Reference(**class_data)
35+
36+
2537
def create_new_client(*, url: Optional[str], path: Optional[Path]) -> None:
2638
""" Generate the client library """
2739
project = _get_project_for_url_or_path(url=url, path=path)

openapi_python_client/cli.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,25 @@ def _version_callback(value: bool) -> None:
1414
raise typer.Exit()
1515

1616

17+
def _process_config(path: Optional[pathlib.Path]) -> None:
18+
from openapi_python_client import load_config
19+
20+
if not path:
21+
return
22+
23+
try:
24+
load_config(path=path)
25+
except:
26+
raise typer.BadParameter("Unable to parse config")
27+
28+
1729
# noinspection PyUnusedLocal
1830
@app.callback(name="openapi-python-client")
1931
def cli(
2032
version: bool = typer.Option(False, "--version", callback=_version_callback, help="Print the version and exit"),
33+
config: Optional[pathlib.Path] = typer.Option(
34+
None, callback=_process_config, help="Path to the config file to use"
35+
),
2136
) -> None:
2237
""" Generate a Python client from an OpenAPI JSON document """
2338
pass

openapi_python_client/openapi_parser/reference.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass
6+
from typing import Dict
67

78
import stringcase
89

10+
class_overrides: Dict[str, Reference] = {}
11+
912

1013
@dataclass
1114
class Reference:
@@ -18,4 +21,9 @@ class Reference:
1821
def from_ref(ref: str) -> Reference:
1922
""" Get a Reference from the openapi #/schemas/blahblah string """
2023
ref_value = ref.split("/")[-1]
21-
return Reference(class_name=stringcase.pascalcase(ref_value), module_name=stringcase.snakecase(ref_value),)
24+
class_name = stringcase.pascalcase(ref_value)
25+
26+
if class_name in class_overrides:
27+
return class_overrides[class_name]
28+
29+
return Reference(class_name=class_name, module_name=stringcase.snakecase(ref_value),)

poetry.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ shellingham = "^1.3.2"
2424
httpx = "^0.12.1"
2525
black = "^19.10b0"
2626
isort = "^4.3.21"
27+
pyyaml = "^5.3.1"
2728

2829
[tool.poetry.scripts]
2930
openapi-python-client = "openapi_python_client.cli:app"

tests/test___init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,21 @@ def test__reformat(mocker):
437437
mocker.call("black .", cwd=project.project_dir, shell=True),
438438
]
439439
)
440+
441+
442+
def test_load_config(mocker):
443+
my_data = {"class_overrides": {"_MyCLASSName": {"class_name": "MyClassName", "module_name": "my_module_name"}}}
444+
safe_load = mocker.patch("yaml.safe_load", return_value=my_data)
445+
fake_path = mocker.MagicMock(autospec=pathlib.Path)
446+
447+
from openapi_python_client import load_config
448+
449+
load_config(path=fake_path)
450+
451+
fake_path.read_text.assert_called_once()
452+
safe_load.assert_called_once_with(fake_path.read_text())
453+
from openapi_python_client.openapi_parser import reference
454+
455+
assert reference.class_overrides == {
456+
"_MyCLASSName": reference.Reference(class_name="MyClassName", module_name="my_module_name")
457+
}

tests/test_cli.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from pathlib import PosixPath
2+
from unittest.mock import MagicMock
23

34
import pytest
45
from typer.testing import CliRunner
@@ -18,17 +19,46 @@ def test_version(mocker):
1819

1920

2021
@pytest.fixture
21-
def _create_new_client(mocker):
22+
def _create_new_client(mocker) -> MagicMock:
2223
return mocker.patch("openapi_python_client.create_new_client")
2324

2425

26+
def test_config(mocker, _create_new_client):
27+
load_config = mocker.patch("openapi_python_client.load_config")
28+
from openapi_python_client.cli import app
29+
30+
config_path = "config/path"
31+
path = "cool/path"
32+
33+
result = runner.invoke(app, [f"--config={config_path}", "generate", f"--path={path}"], catch_exceptions=False)
34+
35+
assert result.exit_code == 0
36+
load_config.assert_called_once_with(path=PosixPath(config_path))
37+
_create_new_client.assert_called_once_with(url=None, path=PosixPath(path))
38+
39+
40+
def test_bad_config(mocker, _create_new_client):
41+
load_config = mocker.patch("openapi_python_client.load_config", side_effect=ValueError("Bad Config"))
42+
from openapi_python_client.cli import app
43+
44+
config_path = "config/path"
45+
path = "cool/path"
46+
47+
result = runner.invoke(app, [f"--config={config_path}", "generate", f"--path={path}"])
48+
49+
assert result.exit_code == 2
50+
assert "Unable to parse config" in result.stdout
51+
load_config.assert_called_once_with(path=PosixPath(config_path))
52+
_create_new_client.assert_not_called()
53+
54+
2555
class TestGenerate:
2656
def test_generate_no_params(self, _create_new_client):
2757
from openapi_python_client.cli import app
2858

2959
result = runner.invoke(app, ["generate"])
3060

31-
assert result.exit_code == 1
61+
assert result.exit_code == 1, result.output
3262
_create_new_client.assert_not_called()
3363

3464
def test_generate_url_and_path(self, _create_new_client):

tests/test_end_to_end/config.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class_overrides:
2+
_ABCResponse:
3+
class_name: ABCResponse
4+
module_name: abc_response

tests/test_end_to_end/fastapi/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
app = FastAPI(title="My Test API", description="An API for testing openapi-python-client",)
99

1010

11-
class PingResponse(BaseModel):
11+
class _ABCResponse(BaseModel):
1212
success: bool
1313

1414

15-
@app.get("/ping", response_model=PingResponse)
15+
@app.get("/ping", response_model=_ABCResponse)
1616
async def ping():
1717
""" A quick check to see if the system is running """
1818
return {"success": True}

0 commit comments

Comments
 (0)