Skip to content

Commit 55b7252

Browse files
authored
feat(cli): Add configuration loading with --config (#45)
* feat(cli): Add configuration loading with `--config` This update introduces the ability to load configuration settings from a specified `pyproject.toml` file in the CLI commands. It enhances the flexibility of the application by allowing users to define their settings in a centralized manner, improving maintainability and usability. Implements: #38 Signed-off-by: Samuel Giffard <samuel@giffard.co> * refactor(graph): Simplify file read/write operations Updated file handling in cli.py to use `read_text` and `write_text` methods for better readability and performance. Improved error message formatting in graph.py for clarity. Implements: #38 Signed-off-by: Samuel Giffard <samuel.giffard@mytomorrows.com> * fix: Update `layout` parameter type to use Layouts enum Changed the layout parameter in multiple classes to use the Layouts enum for better type safety and clarity in configuration. Implements: #38 Signed-off-by: Samuel Giffard <samuel.giffard@mytomorrows.com> * feat(cli): Add default values for enum options in CLI This update introduces default values for the `--max-enum-members` and `--sort` options in the CLI, enhancing user experience by providing clearer guidance on expected input. Implements: #38 Signed-off-by: Samuel Giffard <samuel.giffard@mytomorrows.com> * docs: Update README to include `--config` Added information on using an alternative configuration file with the CLI by passing the `--config` flag. This enhances user flexibility in managing project settings. Implements: #38 Signed-off-by: Samuel Giffard <samuel.giffard@mytomorrows.com> --------- Signed-off-by: Samuel Giffard <samuel@giffard.co> Signed-off-by: Samuel Giffard <samuel.giffard@mytomorrows.com>
1 parent dc411ec commit 55b7252

File tree

14 files changed

+394
-145
lines changed

14 files changed

+394
-145
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ To create a PNG file:
233233

234234
### pyproject.toml
235235

236-
The settings for your project can be saved directly in the `pyprojects.toml` file of your project.
236+
Some of the settings for your project can be saved directly in the `pyprojects.toml` file of your project.
237237

238238
```toml
239239
[tool.paracelsus]
@@ -256,8 +256,19 @@ exclude_tables = [
256256
]
257257
column_sort = "preserve-order"
258258
omit_comments = false
259+
max_enum_members = 10
259260
```
260261

262+
### Alternative config files
263+
264+
It is possible to use an alternative configuration file for both `graph` and `inject` by passing the `--config` flag to the CLI.
265+
266+
```bash
267+
paracelsus graph --config path/to/alternative_pyproject.toml
268+
```
269+
270+
This file does not need to be named `pyproject.toml`, as long as it is a valid TOML file and contains a `[tool.paracelsus]` section.
271+
261272
## Sponsorship
262273

263274
This project is developed by [Robert Hafner](https://blog.tedivm.com) If you find this project useful please consider sponsoring me using Github!

paracelsus/cli.py

Lines changed: 107 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,58 @@
11
import re
22
import sys
3-
from enum import Enum
43
from pathlib import Path
5-
from typing import Any, Dict, List, Optional
4+
from textwrap import dedent
5+
from typing import List, Optional
66

77
import typer
88
from typing_extensions import Annotated
9-
9+
from dataclasses import asdict
10+
from paracelsus.config import (
11+
Formats,
12+
ColumnSorts,
13+
Layouts,
14+
ParacelsusSettingsForGraph,
15+
ParacelsusSettingsForInject,
16+
MAX_ENUM_MEMBERS_DEFAULT,
17+
SORT_DEFAULT,
18+
)
1019
from .graph import get_graph_string, transformers
1120
from .pyproject import get_pyproject_settings
1221

1322
app = typer.Typer()
1423

15-
PYPROJECT_SETTINGS = get_pyproject_settings()
16-
17-
18-
class Formats(str, Enum):
19-
mermaid = "mermaid"
20-
mmd = "mmd"
21-
dot = "dot"
22-
gv = "gv"
23-
24-
25-
class ColumnSorts(str, Enum):
26-
key_based = "key-based"
27-
preserve = "preserve-order"
28-
29-
30-
class Layouts(str, Enum):
31-
dagre = "dagre"
32-
elk = "elk"
33-
34-
35-
if "column_sort" in PYPROJECT_SETTINGS:
36-
SORT_DEFAULT = ColumnSorts(PYPROJECT_SETTINGS["column_sort"]).value
37-
else:
38-
SORT_DEFAULT = ColumnSorts.key_based.value
39-
40-
if "omit_comments" in PYPROJECT_SETTINGS:
41-
OMIT_COMMENTS_DEFAULT = PYPROJECT_SETTINGS["omit_comments"]
42-
else:
43-
OMIT_COMMENTS_DEFAULT = False
44-
45-
if "max_enum_members" in PYPROJECT_SETTINGS:
46-
MAX_ENUM_MEMBERS_DEFAULT = PYPROJECT_SETTINGS["max_enum_members"]
47-
else:
48-
MAX_ENUM_MEMBERS_DEFAULT = 3
49-
5024

51-
def get_base_class(base_class_path: str | None, settings: Dict[str, Any] | None) -> str:
25+
def get_base_class(base_class_path: str | None, base_from_config: str) -> str:
5226
if base_class_path:
5327
return base_class_path
54-
if not settings:
55-
raise ValueError("`base_class_path` argument must be passed if no pyproject.toml file is present.")
56-
if "base" not in settings:
57-
raise ValueError("`base_class_path` argument must be passed if not defined in pyproject.toml.")
58-
return settings["base"]
28+
if base_from_config:
29+
return base_from_config
30+
31+
raise ValueError(
32+
dedent(
33+
"""\
34+
Either provide `--base-class-path` argument or define `base` in the pyproject.toml file:
35+
[tool.paracelsus]
36+
base = "example.base:Base"
37+
"""
38+
)
39+
)
5940

6041

6142
@app.command(help="Create the graph structure and print it to stdout.")
6243
def graph(
44+
config: Annotated[
45+
Path,
46+
typer.Option(
47+
help="Path to a pyproject.toml file to load configuration from.",
48+
file_okay=True,
49+
dir_okay=False,
50+
resolve_path=True,
51+
exists=True,
52+
default_factory=lambda: Path.cwd() / "pyproject.toml",
53+
show_default=str(Path.cwd() / "pyproject.toml"),
54+
),
55+
],
6356
base_class_path: Annotated[
6457
Optional[str],
6558
typer.Argument(help="The SQLAlchemy base class used by the database to graph."),
@@ -92,58 +85,69 @@ def graph(
9285
Formats, typer.Option(help="The file format to output the generated graph to.")
9386
] = Formats.mermaid.value, # type: ignore # Typer will fail to render the help message, but this code works.
9487
column_sort: Annotated[
95-
ColumnSorts,
88+
Optional[ColumnSorts],
9689
typer.Option(
9790
help="Specifies the method of sorting columns in diagrams.",
91+
show_default=str(SORT_DEFAULT.value),
9892
),
99-
] = SORT_DEFAULT, # type: ignore # Typer will fail to render the help message, but this code works.
93+
] = None,
10094
omit_comments: Annotated[
101-
bool,
95+
Optional[bool],
10296
typer.Option(
10397
"--omit-comments",
10498
help="Omit SQLAlchemy column comments from the diagram.",
10599
),
106-
] = OMIT_COMMENTS_DEFAULT,
100+
] = None,
107101
max_enum_members: Annotated[
108-
int,
102+
Optional[int],
109103
typer.Option(
110104
"--max-enum-members",
111105
help="Maximum number of enum members to display in diagrams. 0 means no enum values are shown, any positive number limits the display.",
106+
show_default=str(MAX_ENUM_MEMBERS_DEFAULT),
112107
),
113-
] = MAX_ENUM_MEMBERS_DEFAULT,
108+
] = None,
114109
layout: Annotated[
115110
Optional[Layouts],
116111
typer.Option(
117112
help="Specifies the layout of the diagram. Only applicable for mermaid format.",
118113
),
119114
] = None,
120115
):
121-
settings = get_pyproject_settings()
122-
base_class = get_base_class(base_class_path, settings)
123-
124-
if "imports" in settings:
125-
import_module.extend(settings["imports"])
116+
settings = get_pyproject_settings(config_file=config)
126117

127-
if layout and format != Formats.mermaid:
128-
raise ValueError("The `layout` parameter can only be used with the `mermaid` format.")
118+
graph_settings = ParacelsusSettingsForGraph(
119+
base_class_path=get_base_class(base_class_path, settings.base),
120+
import_module=import_module + settings.imports,
121+
include_tables=set(include_tables + settings.include_tables),
122+
exclude_tables=set(exclude_tables + settings.exclude_tables),
123+
python_dir=python_dir,
124+
format=format,
125+
column_sort=column_sort if column_sort is not None else settings.column_sort,
126+
omit_comments=omit_comments if omit_comments is not None else settings.omit_comments,
127+
max_enum_members=max_enum_members if max_enum_members is not None else settings.max_enum_members,
128+
layout=layout,
129+
)
129130

130131
graph_string = get_graph_string(
131-
base_class_path=base_class,
132-
import_module=import_module,
133-
include_tables=set(include_tables + settings.get("include_tables", [])),
134-
exclude_tables=set(exclude_tables + settings.get("exclude_tables", [])),
135-
python_dir=python_dir,
136-
format=format.value,
137-
column_sort=column_sort,
138-
omit_comments=omit_comments,
139-
max_enum_members=max_enum_members,
140-
layout=layout.value if layout else None,
132+
**asdict(graph_settings),
141133
)
142134
typer.echo(graph_string, nl=not graph_string.endswith("\n"))
143135

144136

145137
@app.command(help="Create a graph and inject it as a code field into a markdown file.")
146138
def inject(
139+
config: Annotated[
140+
Path,
141+
typer.Option(
142+
help="Path to a pyproject.toml file to load configuration from.",
143+
file_okay=True,
144+
dir_okay=False,
145+
resolve_path=True,
146+
exists=True,
147+
default_factory=lambda: Path.cwd() / "pyproject.toml",
148+
show_default=str(Path.cwd() / "pyproject.toml"),
149+
),
150+
],
147151
file: Annotated[
148152
Path,
149153
typer.Argument(
@@ -201,74 +205,78 @@ def inject(
201205
),
202206
] = False,
203207
column_sort: Annotated[
204-
ColumnSorts,
208+
Optional[ColumnSorts],
205209
typer.Option(
206210
help="Specifies the method of sorting columns in diagrams.",
211+
show_default=str(SORT_DEFAULT.value),
207212
),
208-
] = SORT_DEFAULT, # type: ignore # Typer will fail to render the help message, but this code works.
213+
] = None,
209214
omit_comments: Annotated[
210-
bool,
215+
Optional[bool],
211216
typer.Option(
212217
"--omit-comments",
213218
help="Omit SQLAlchemy column comments from the diagram.",
214219
),
215-
] = OMIT_COMMENTS_DEFAULT,
220+
] = None,
216221
max_enum_members: Annotated[
217-
int,
222+
Optional[int],
218223
typer.Option(
219224
"--max-enum-members",
220225
help="Maximum number of enum members to display in diagrams. 0 means no enum values are shown, any positive number limits the display.",
226+
show_default=str(MAX_ENUM_MEMBERS_DEFAULT),
221227
),
222-
] = MAX_ENUM_MEMBERS_DEFAULT,
228+
] = None,
223229
layout: Annotated[
224230
Optional[Layouts],
225231
typer.Option(
226232
help="Specifies the layout of the diagram. Only applicable for mermaid format.",
227233
),
228234
] = None,
229235
):
230-
settings = get_pyproject_settings()
231-
base_class = get_base_class(base_class_path, settings)
232-
233-
if "imports" in settings:
234-
import_module.extend(settings["imports"])
235-
236-
if layout and format != Formats.mermaid:
237-
raise ValueError("The `layout` parameter can only be used with the `mermaid` format.")
236+
settings = get_pyproject_settings(config_file=config)
237+
238+
inject_settings = ParacelsusSettingsForInject(
239+
graph_settings=ParacelsusSettingsForGraph(
240+
base_class_path=get_base_class(base_class_path, settings.base),
241+
import_module=import_module + settings.imports,
242+
include_tables=set(include_tables + settings.include_tables),
243+
exclude_tables=set(exclude_tables + settings.exclude_tables),
244+
python_dir=python_dir,
245+
format=format,
246+
column_sort=column_sort if column_sort is not None else settings.column_sort,
247+
omit_comments=omit_comments if omit_comments is not None else settings.omit_comments,
248+
max_enum_members=max_enum_members if max_enum_members is not None else settings.max_enum_members,
249+
layout=layout,
250+
),
251+
file=file,
252+
replace_begin_tag=replace_begin_tag,
253+
replace_end_tag=replace_end_tag,
254+
check=check,
255+
)
238256

239257
# Generate Graph
240258
graph = get_graph_string(
241-
base_class_path=base_class,
242-
import_module=import_module,
243-
include_tables=set(include_tables + settings.get("include_tables", [])),
244-
exclude_tables=set(exclude_tables + settings.get("exclude_tables", [])),
245-
python_dir=python_dir,
246-
format=format.value,
247-
column_sort=column_sort,
248-
omit_comments=omit_comments,
249-
max_enum_members=max_enum_members,
250-
layout=layout.value if layout else None,
259+
**asdict(inject_settings.graph_settings),
251260
)
252261

253-
comment_format = transformers[format].comment_format # type: ignore
262+
comment_format = transformers[inject_settings.graph_settings.format].comment_format # type: ignore
254263

255264
# Convert Graph to Injection String
256-
graph_piece = f"""{replace_begin_tag}
265+
graph_piece = f"""{inject_settings.replace_begin_tag}
257266
```{comment_format}
258267
{graph}
259268
```
260-
{replace_end_tag}"""
269+
{inject_settings.replace_end_tag}"""
261270

262271
# Get content from current file.
263-
with open(file, "r") as fp:
264-
old_content = fp.read()
272+
old_content = inject_settings.file.read_text()
265273

266274
# Replace old content with newly generated content.
267-
pattern = re.escape(replace_begin_tag) + "(.*)" + re.escape(replace_end_tag)
275+
pattern = re.escape(inject_settings.replace_begin_tag) + "(.*)" + re.escape(inject_settings.replace_end_tag)
268276
new_content = re.sub(pattern, graph_piece, old_content, flags=re.MULTILINE | re.DOTALL)
269277

270278
# Return result depends on whether we're in check mode.
271-
if check:
279+
if inject_settings.check:
272280
if new_content == old_content:
273281
# If content is the same then we passed the test.
274282
typer.echo("No changes detected.")
@@ -279,8 +287,7 @@ def inject(
279287
sys.exit(1)
280288
else:
281289
# Dump newly generated contents back to file.
282-
with open(file, "w") as fp:
283-
fp.write(new_content)
290+
inject_settings.file.write_text(new_content)
284291

285292

286293
@app.command(help="Display the current installed version of paracelsus.")

0 commit comments

Comments
 (0)