Skip to content

Commit 3227f78

Browse files
authored
Allow overriding generated package/project names (#133)
* Added config options for overriding the generated library's project & package names Co-authored-by: Ethan Mann <[email protected]>
1 parent 9587f59 commit 3227f78

File tree

9 files changed

+143
-68
lines changed

9 files changed

+143
-68
lines changed

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77

88
## 0.5.2 - unreleased
9-
### Fixes
9+
### Additions
10+
- Added `project_name_override` and `package_name_override` config options to override the name of the generated project/package (#123)
1011
- The generated library's version is now the same as the OpenAPI doc's version (#134)
1112

13+
1214
## 0.5.1 - 2020-08-05
1315
### Fixes
1416
- Relative paths are now allowed in securitySchemes/OAuthFlow/tokenUrl (#130).
1517
- Schema validation errors will no longer print a stack trace (#131).
1618
- Invalid YAML/URL will no longer print stack trace (#128)
1719

20+
1821
## 0.5.0 - 2020-08-05
1922
### Changes
2023
- When encountering a problem, the generator will now differentiate between warnings (things it was able to skip past)

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ class_overrides:
8686
8787
The easiest way to find what needs to be overridden is probably to generate your client and go look at everything in the
8888
models folder.
89+
90+
### project_name_override and package_name_override
91+
Used to change the name of generated client library project/package. If the project name is changed but an override for the package name
92+
isn't provided, the package name will be converted from the project name using the standard convention (replacing `-`'s with `_`'s).
93+
94+
Example:
95+
```yaml
96+
project_name_override: my-special-project-name
97+
package_name_override: my_extra_special_package_name
98+
```
8999

90100

91101
[CHANGELOG.md]: CHANGELOG.md

mypy.ini

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ disallow_untyped_defs = True
44
warn_redundant_casts = True
55
strict_equality = True
66

7+
[mypy-importlib_metadata]
8+
ignore_missing_imports = True
9+
710
[mypy-stringcase]
811
ignore_missing_imports = True
912

openapi_python_client/__init__.py

+8-17
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,22 @@
1818
from .parser.errors import GeneratorError
1919

2020
if sys.version_info.minor == 7: # version did not exist in 3.7, need to use a backport
21-
from importlib_metadata import version # type: ignore
21+
from importlib_metadata import version
2222
else:
2323
from importlib.metadata import version # type: ignore
2424

2525

2626
__version__ = version(__package__)
2727

2828

29-
def _get_project_for_url_or_path(url: Optional[str], path: Optional[Path]) -> Union[_Project, GeneratorError]:
29+
def _get_project_for_url_or_path(url: Optional[str], path: Optional[Path]) -> Union[Project, GeneratorError]:
3030
data_dict = _get_document(url=url, path=path)
3131
if isinstance(data_dict, GeneratorError):
3232
return data_dict
3333
openapi = GeneratorData.from_dict(data_dict)
3434
if isinstance(openapi, GeneratorError):
3535
return openapi
36-
return _Project(openapi=openapi)
37-
38-
39-
def load_config(*, path: Path) -> None:
40-
""" Loads config from provided Path """
41-
config_data = yaml.safe_load(path.read_text())
42-
43-
if "class_overrides" in config_data:
44-
from .parser import reference
45-
46-
for class_name, class_data in config_data["class_overrides"].items():
47-
reference.class_overrides[class_name] = reference.Reference(**class_data)
36+
return Project(openapi=openapi)
4837

4938

5039
def create_new_client(*, url: Optional[str], path: Optional[Path]) -> Sequence[GeneratorError]:
@@ -93,17 +82,19 @@ def _get_document(*, url: Optional[str], path: Optional[Path]) -> Union[Dict[str
9382
return GeneratorError(header="Invalid YAML from provided source")
9483

9584

96-
class _Project:
85+
class Project:
9786
TEMPLATE_FILTERS = {"snakecase": utils.snake_case}
87+
project_name_override: Optional[str] = None
88+
package_name_override: Optional[str] = None
9889

9990
def __init__(self, *, openapi: GeneratorData) -> None:
10091
self.openapi: GeneratorData = openapi
10192
self.env: Environment = Environment(loader=PackageLoader(__package__), trim_blocks=True, lstrip_blocks=True)
10293

103-
self.project_name: str = f"{openapi.title.replace(' ', '-').lower()}-client"
94+
self.project_name: str = self.project_name_override or f"{openapi.title.replace(' ', '-').lower()}-client"
10495
self.project_dir: Path = Path.cwd() / self.project_name
10596

106-
self.package_name: str = self.project_name.replace("-", "_")
97+
self.package_name: str = self.package_name_override or self.project_name.replace("-", "_")
10798
self.package_dir: Path = self.project_dir / self.package_name
10899
self.package_description: str = f"A client library for accessing {self.openapi.title}"
109100
self.version: str = openapi.version

openapi_python_client/cli.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ def _version_callback(value: bool) -> None:
1818

1919

2020
def _process_config(path: Optional[pathlib.Path]) -> None:
21-
from openapi_python_client import load_config
21+
from .config import Config
2222

2323
if not path:
2424
return
2525

2626
try:
27-
load_config(path=path)
27+
Config.load_from_path(path=path)
2828
except: # noqa
2929
raise typer.BadParameter("Unable to parse config")
3030

openapi_python_client/config.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from pathlib import Path
2+
from typing import Dict, Optional
3+
4+
import yaml
5+
from pydantic import BaseModel
6+
7+
8+
class ClassOverride(BaseModel):
9+
class_name: str
10+
module_name: str
11+
12+
13+
class Config(BaseModel):
14+
class_overrides: Optional[Dict[str, ClassOverride]]
15+
project_name_override: Optional[str]
16+
package_name_override: Optional[str]
17+
18+
def load_config(self) -> None:
19+
""" Loads config from provided Path """
20+
21+
if self.class_overrides is not None:
22+
from .parser import reference
23+
24+
for class_name, class_data in self.class_overrides.items():
25+
reference.class_overrides[class_name] = reference.Reference(**dict(class_data))
26+
27+
from openapi_python_client import Project
28+
29+
Project.project_name_override = self.project_name_override
30+
Project.package_name_override = self.package_name_override
31+
32+
@staticmethod
33+
def load_from_path(path: Path) -> None:
34+
config_data = yaml.safe_load(path.read_text())
35+
Config(**config_data).load_config()

tests/test___init__.py

+41-45
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def test__get_project_for_url_or_path(mocker):
1313
_get_document = mocker.patch("openapi_python_client._get_document", return_value=data_dict)
1414
openapi = mocker.MagicMock()
1515
from_dict = mocker.patch("openapi_python_client.parser.GeneratorData.from_dict", return_value=openapi)
16-
_Project = mocker.patch("openapi_python_client._Project")
16+
_Project = mocker.patch("openapi_python_client.Project")
1717
url = mocker.MagicMock()
1818
path = mocker.MagicMock()
1919

@@ -32,7 +32,7 @@ def test__get_project_for_url_or_path_generator_error(mocker):
3232
_get_document = mocker.patch("openapi_python_client._get_document", return_value=data_dict)
3333
error = GeneratorError()
3434
from_dict = mocker.patch("openapi_python_client.parser.GeneratorData.from_dict", return_value=error)
35-
_Project = mocker.patch("openapi_python_client._Project")
35+
_Project = mocker.patch("openapi_python_client.Project")
3636
url = mocker.MagicMock()
3737
path = mocker.MagicMock()
3838

@@ -219,19 +219,36 @@ class TestProject:
219219
def test___init__(self, mocker):
220220
openapi = mocker.MagicMock(title="My Test API")
221221

222-
from openapi_python_client import _Project
222+
from openapi_python_client import Project
223223

224-
project = _Project(openapi=openapi)
224+
project = Project(openapi=openapi)
225225

226226
assert project.openapi == openapi
227227
assert project.project_name == "my-test-api-client"
228228
assert project.package_name == "my_test_api_client"
229229
assert project.package_description == "A client library for accessing My Test API"
230230

231+
def test_project_and_package_name_overrides(self, mocker):
232+
openapi = mocker.MagicMock(title="My Test API")
233+
234+
from openapi_python_client import Project
235+
236+
Project.project_name_override = "my-special-project-name"
237+
project = Project(openapi=openapi)
238+
239+
assert project.project_name == "my-special-project-name"
240+
assert project.package_name == "my_special_project_name"
241+
242+
Project.package_name_override = "my_special_package_name"
243+
project = Project(openapi=openapi)
244+
245+
assert project.project_name == "my-special-project-name"
246+
assert project.package_name == "my_special_package_name"
247+
231248
def test_build(self, mocker):
232-
from openapi_python_client import _Project
249+
from openapi_python_client import Project
233250

234-
project = _Project(openapi=mocker.MagicMock(title="My Test API"))
251+
project = Project(openapi=mocker.MagicMock(title="My Test API"))
235252
project.project_dir = mocker.MagicMock()
236253
project.package_dir = mocker.MagicMock()
237254
project._build_metadata = mocker.MagicMock()
@@ -253,9 +270,9 @@ def test_build(self, mocker):
253270
assert result == project._get_errors.return_value
254271

255272
def test_build_file_exists(self, mocker):
256-
from openapi_python_client import _Project
273+
from openapi_python_client import Project
257274

258-
project = _Project(openapi=mocker.MagicMock(title="My Test API"))
275+
project = Project(openapi=mocker.MagicMock(title="My Test API"))
259276
project.project_dir = mocker.MagicMock()
260277
project.project_dir.mkdir.side_effect = FileExistsError
261278
result = project.build()
@@ -265,10 +282,10 @@ def test_build_file_exists(self, mocker):
265282
assert result == [GeneratorError(detail="Directory already exists. Delete it or use the update command.")]
266283

267284
def test_update(self, mocker):
268-
from openapi_python_client import _Project, shutil
285+
from openapi_python_client import Project, shutil
269286

270287
rmtree = mocker.patch.object(shutil, "rmtree")
271-
project = _Project(openapi=mocker.MagicMock(title="My Test API"))
288+
project = Project(openapi=mocker.MagicMock(title="My Test API"))
272289
project.package_dir = mocker.MagicMock()
273290
project._build_metadata = mocker.MagicMock()
274291
project._build_models = mocker.MagicMock()
@@ -288,9 +305,9 @@ def test_update(self, mocker):
288305
assert result == project._get_errors.return_value
289306

290307
def test_update_missing_dir(self, mocker):
291-
from openapi_python_client import _Project
308+
from openapi_python_client import Project
292309

293-
project = _Project(openapi=mocker.MagicMock(title="My Test API"))
310+
project = Project(openapi=mocker.MagicMock(title="My Test API"))
294311
project.package_dir = mocker.MagicMock()
295312
project.package_dir.is_dir.return_value = False
296313
project._build_models = mocker.MagicMock()
@@ -302,9 +319,9 @@ def test_update_missing_dir(self, mocker):
302319
project._build_models.assert_not_called()
303320

304321
def test__create_package(self, mocker):
305-
from openapi_python_client import _Project
322+
from openapi_python_client import Project
306323

307-
project = _Project(openapi=mocker.MagicMock(title="My Test API"))
324+
project = Project(openapi=mocker.MagicMock(title="My Test API"))
308325
project.package_dir = mocker.MagicMock()
309326
package_init_template = mocker.MagicMock()
310327
project.env = mocker.MagicMock()
@@ -327,9 +344,9 @@ def test__create_package(self, mocker):
327344
pytyped_path.write_text.assert_called_once_with("# Marker file for PEP 561")
328345

329346
def test__build_metadata(self, mocker):
330-
from openapi_python_client import _Project
347+
from openapi_python_client import Project
331348

332-
project = _Project(openapi=mocker.MagicMock(title="My Test API"))
349+
project = Project(openapi=mocker.MagicMock(title="My Test API"))
333350
project.project_dir = mocker.MagicMock()
334351
pyproject_path = mocker.MagicMock(autospec=pathlib.Path)
335352
readme_path = mocker.MagicMock(autospec=pathlib.Path)
@@ -375,7 +392,7 @@ def test__build_metadata(self, mocker):
375392
git_ignore_path.write_text.assert_called_once_with(git_ignore_template.render())
376393

377394
def test__build_models(self, mocker):
378-
from openapi_python_client import GeneratorData, _Project
395+
from openapi_python_client import GeneratorData, Project
379396

380397
openapi = mocker.MagicMock(autospec=GeneratorData, title="My Test API")
381398
model_1 = mocker.MagicMock()
@@ -384,7 +401,7 @@ def test__build_models(self, mocker):
384401
enum_1 = mocker.MagicMock()
385402
enum_2 = mocker.MagicMock()
386403
openapi.enums = {"1": enum_1, "2": enum_2}
387-
project = _Project(openapi=openapi)
404+
project = Project(openapi=openapi)
388405
project.package_dir = mocker.MagicMock()
389406
models_init = mocker.MagicMock()
390407
types_py = mocker.MagicMock()
@@ -466,15 +483,15 @@ def test__build_api(self, mocker):
466483

467484
from jinja2 import Template
468485

469-
from openapi_python_client import GeneratorData, _Project
486+
from openapi_python_client import GeneratorData, Project
470487

471488
openapi = mocker.MagicMock(autospec=GeneratorData, title="My Test API")
472489
tag_1 = mocker.MagicMock(autospec=str)
473490
tag_2 = mocker.MagicMock(autospec=str)
474491
collection_1 = mocker.MagicMock()
475492
collection_2 = mocker.MagicMock()
476493
openapi.endpoint_collections_by_tag = {tag_1: collection_1, tag_2: collection_2}
477-
project = _Project(openapi=openapi)
494+
project = Project(openapi=openapi)
478495
project.package_dir = mocker.MagicMock()
479496
api_errors = mocker.MagicMock(autospec=pathlib.Path)
480497
client_path = mocker.MagicMock()
@@ -557,11 +574,11 @@ def test__build_api(self, mocker):
557574
def test__reformat(mocker):
558575
import subprocess
559576

560-
from openapi_python_client import GeneratorData, _Project
577+
from openapi_python_client import GeneratorData, Project
561578

562579
sub_run = mocker.patch("subprocess.run")
563580
openapi = mocker.MagicMock(autospec=GeneratorData, title="My Test API")
564-
project = _Project(openapi=openapi)
581+
project = Project(openapi=openapi)
565582
project.project_dir = mocker.MagicMock(autospec=pathlib.Path)
566583

567584
project._reformat()
@@ -577,7 +594,7 @@ def test__reformat(mocker):
577594

578595

579596
def test__get_errors(mocker):
580-
from openapi_python_client import GeneratorData, _Project
597+
from openapi_python_client import GeneratorData, Project
581598
from openapi_python_client.parser.openapi import EndpointCollection, Schemas
582599

583600
openapi = mocker.MagicMock(
@@ -589,27 +606,6 @@ def test__get_errors(mocker):
589606
},
590607
schemas=mocker.MagicMock(autospec=Schemas, errors=[3]),
591608
)
592-
project = _Project(openapi=openapi)
609+
project = Project(openapi=openapi)
593610

594611
assert project._get_errors() == [1, 2, 3]
595-
596-
597-
def test_load_config(mocker):
598-
my_data = {"class_overrides": {"_MyCLASSName": {"class_name": "MyClassName", "module_name": "my_module_name"}}}
599-
safe_load = mocker.patch("yaml.safe_load", return_value=my_data)
600-
fake_path = mocker.MagicMock(autospec=pathlib.Path)
601-
602-
from openapi_python_client import load_config
603-
from openapi_python_client.parser import reference
604-
605-
reference.class_overrides = {}
606-
607-
load_config(path=fake_path)
608-
609-
fake_path.read_text.assert_called_once()
610-
safe_load.assert_called_once_with(fake_path.read_text())
611-
from openapi_python_client.parser import reference
612-
613-
assert reference.class_overrides == {
614-
"_MyCLASSName": reference.Reference(class_name="MyClassName", module_name="my_module_name")
615-
}

tests/test_cli.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ def _create_new_client(mocker) -> MagicMock:
2525
return mocker.patch("openapi_python_client.create_new_client", return_value=[])
2626

2727

28-
def test_config(mocker, _create_new_client):
29-
load_config = mocker.patch("openapi_python_client.load_config")
28+
def test_config_arg(mocker, _create_new_client):
29+
load_config = mocker.patch("openapi_python_client.config.Config.load_from_path")
3030
from openapi_python_client.cli import app
3131

3232
config_path = "config/path"
@@ -40,7 +40,9 @@ def test_config(mocker, _create_new_client):
4040

4141

4242
def test_bad_config(mocker, _create_new_client):
43-
load_config = mocker.patch("openapi_python_client.load_config", side_effect=ValueError("Bad Config"))
43+
load_config = mocker.patch(
44+
"openapi_python_client.config.Config.load_from_path", side_effect=ValueError("Bad Config")
45+
)
4446
from openapi_python_client.cli import app
4547

4648
config_path = "config/path"

0 commit comments

Comments
 (0)