Skip to content

Commit c29e483

Browse files
feat: Read config from config file or pyproject.toml
Issue-8: #8 PR-10: #10
1 parent fcb1a7e commit c29e483

9 files changed

Lines changed: 332 additions & 60 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,20 @@ Use the `-o`, `--output` option to write to a file instead of standard output:
3333
```bash
3434
griffe2md markdown -o markdown.md
3535
```
36+
37+
`griffe2md` can be configured in either `pyproject.toml` or a `griffe2md.toml` file. The latter can be placed in a `.config` or `config` directory in the project root.
38+
39+
`griffe2md.toml` file is structured as a simple key-value dictionary, e.g.:
40+
41+
```toml
42+
docstring_style = "sphinx"
43+
```
44+
45+
If you configure it in `pyproject.toml`, the configuration should go under the `tool.griffe2md` key:
46+
47+
```toml
48+
[tool.griffe2md]
49+
docstring_style = "sphinx"
50+
```
51+
52+
See [the documentation](https://mkdocstrings.github.io/griffe2md/reference/griffe2md/config/#griffe2md.config.ConfigDict) for reference.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ dependencies = [
3232
"griffe>=0.49",
3333
"jinja2>=3.1.2",
3434
"mdformat>=0.7.16",
35+
# YORE: EOL 3.10: Remove line.
36+
"tomli>=2.0; python_version < '3.11'",
3537
]
3638

3739
[project.urls]

src/griffe2md/cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from typing import Any
1919

2020
from griffe2md import debug
21+
from griffe2md.config import load_config
2122
from griffe2md.main import write_package_docs
2223

2324

@@ -57,5 +58,7 @@ def main(args: list[str] | None = None) -> int:
5758
"""
5859
parser = get_parser()
5960
opts = parser.parse_args(args=args)
60-
write_package_docs(opts.package, output=opts.output)
61+
config = load_config()
62+
63+
write_package_docs(opts.package, config, opts.output)
6164
return 0

src/griffe2md/config.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
"""Load configuration."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
import sys
7+
from pathlib import Path
8+
from typing import TYPE_CHECKING, Literal, TypedDict, cast
9+
10+
# YORE: EOL 3.10: Replace block with line 2.
11+
if sys.version_info >= (3, 11):
12+
import tomllib
13+
else:
14+
import tomli as tomllib
15+
16+
if TYPE_CHECKING:
17+
from re import Pattern
18+
19+
logger = logging.getLogger(__name__)
20+
21+
CONFIG_FILE_PATHS = (
22+
Path(".config/griffe2md.toml"),
23+
Path("config/griffe2md.toml"),
24+
Path("pyproject.toml"),
25+
)
26+
27+
28+
def _locate_config_file() -> Path | None:
29+
for path in CONFIG_FILE_PATHS:
30+
if path.is_file():
31+
return path
32+
return None
33+
34+
35+
def load_config() -> ConfigDict | None:
36+
"""Load the configuration if config file or config entry in pyproject.toml exists.
37+
38+
If neither config file was found or pyproject.toml file doesn't have
39+
a `[tool.griffe2md]` section, None is returned.
40+
"""
41+
if not (config_path := _locate_config_file()):
42+
return None
43+
44+
logger.debug("Loading config from %s", config_path)
45+
46+
with config_path.open("rb") as f:
47+
config = tomllib.load(f)
48+
49+
if config_path.name == "pyproject.toml":
50+
return config.get("tool", {}).get("griffe2md", None)
51+
return cast("ConfigDict", config)
52+
53+
54+
class ConfigDict(TypedDict):
55+
"""Configuration for griffe2md, griffe and mkdocstrings."""
56+
57+
allow_inspection: bool
58+
"""Allow using introspection on modules for which sources aren't available (compiled modules, etc.)."""
59+
60+
annotations_path: Literal["brief", "source", "full"]
61+
"""The verbosity for annotations path: `brief` (recommended), `source` (as written in the source), or `full`."""
62+
63+
docstring_options: dict
64+
"""mkdocstring [configuration](https://mkdocstrings.github.io/python/usage/configuration/general/)"""
65+
66+
docstring_section_style: Literal["list", "table"]
67+
"""The style used to render docstring sections."""
68+
69+
docstring_style: Literal["google", "numpy", "sphinx", "auto"] | None
70+
"""The style in which docstrings are written: `auto`, `google`, `numpy`, `sphinx`, or `None`."""
71+
72+
filters: list[str] | list[tuple[Pattern[str], bool]]
73+
"""A list of filters.
74+
75+
A filter starting with `!` will exclude matching objects instead of including them.
76+
The `members` option takes precedence over `filters` (filters will still be applied recursively
77+
to lower members in the hierarchy).
78+
"""
79+
80+
group_by_category: bool
81+
"""Group the object's children by categories: attributes, classes, functions, and modules."""
82+
83+
heading_level: int
84+
"""The initial heading level to use."""
85+
86+
inherited_members: bool | list[str]
87+
"""A boolean, or an explicit list of inherited members to render.
88+
89+
If true, select all inherited members, which can then be filtered with `members`.
90+
If false or empty list, do not select any inherited member.
91+
"""
92+
93+
line_length: int
94+
"""Maximum line length when formatting code/signatures."""
95+
96+
load_external_modules: bool
97+
"""Whether to always load external modules/packages."""
98+
99+
members: list[str] | bool | None
100+
"""A boolean, or an explicit list of members to render.
101+
102+
If true, select all members without further filtering.
103+
If false or empty list, do not render members.
104+
If none, select all members and apply further filtering with filters and docstrings.
105+
"""
106+
107+
members_order: Literal["alphabetical", "source"]
108+
"""The members ordering to use.
109+
110+
- `alphabetical`: order members alphabetically;
111+
- `source`: order members as they appear in the source file.
112+
"""
113+
114+
merge_init_into_class: bool
115+
"""Whether to merge the `__init__` method into the class' signature and docstring."""
116+
117+
preload_modules: list[str] | None
118+
"""Pre-load modules that are not specified directly in autodoc instructions (`::: identifier`).
119+
120+
It is useful when you want to render documentation for a particular member of an object,
121+
and this member is imported from another package than its parent.
122+
123+
For an imported member to be rendered, you need to add it to the `__all__` attribute
124+
of the importing module.
125+
126+
The modules must be listed as an array of strings.
127+
"""
128+
129+
separate_signature: bool
130+
"""Whether to put the whole signature in a code block below the heading.
131+
132+
If Black or Ruff are installed, the signature is also formatted using them.
133+
"""
134+
135+
show_bases: bool
136+
"""Show the base classes of a class."""
137+
138+
show_category_heading: bool
139+
"""When grouped by categories, show a heading for each category."""
140+
141+
show_docstring_attributes: bool
142+
"""Whether to display the 'Attributes' section in the object's docstring."""
143+
144+
show_docstring_classes: bool
145+
"""Whether to display the 'Classes' section in the object's docstring."""
146+
147+
show_docstring_description: bool
148+
"""Whether to display the textual block (including admonitions) in the object's docstring."""
149+
150+
show_docstring_examples: bool
151+
"""Whether to display the 'Examples' section in the object's docstring."""
152+
153+
show_docstring_functions: bool
154+
"""Whether to display the 'Functions' section in the object's docstring."""
155+
156+
show_docstring_modules: bool
157+
"""Whether to display the 'Modules' section in the object's docstring."""
158+
159+
show_docstring_other_parameters: bool
160+
"""Whether to display the 'Other Parameters' section in the object's docstring."""
161+
162+
show_docstring_parameters: bool
163+
"""Whether to display the 'Parameters' section in the object's docstring."""
164+
165+
show_docstring_raises: bool
166+
"""Whether to display the 'Raises' section in the object's docstring."""
167+
168+
show_docstring_receives: bool
169+
"""Whether to display the 'Receives' section in the object's docstring."""
170+
171+
show_docstring_returns: bool
172+
"""Whether to display the 'Returns' section in the object's docstring."""
173+
174+
show_docstring_warns: bool
175+
"""Whether to display the 'Warns' section in the object's docstring."""
176+
177+
show_docstring_yields: bool
178+
"""Whether to display the 'Yields' section in the object's docstring."""
179+
180+
show_if_no_docstring: bool
181+
"""Show the object heading even if it has no docstring or children with docstrings."""
182+
183+
show_object_full_path: bool
184+
"""Show the full Python path of every object."""
185+
186+
show_root_full_path: bool
187+
"""Show the full Python path for the root object heading."""
188+
189+
show_root_heading: bool
190+
"""Show the heading of the object at the root of the documentation tree.
191+
192+
The root object is the object referenced by the identifier after `:::`.
193+
"""
194+
195+
show_root_members_full_path: bool
196+
"""Show the full Python path of the root members."""
197+
198+
show_signature: bool
199+
"""Show methods and functions signatures."""
200+
201+
show_signature_annotations: bool
202+
"""Show the type annotations in methods and functions signatures."""
203+
204+
show_submodules: bool
205+
"""When rendering a module, show its submodules recursively."""
206+
207+
signature_crossrefs: bool
208+
"""Whether to render cross-references for type annotations in signatures."""
209+
210+
summary: bool | dict
211+
"""Whether to render summaries of modules, classes, functions (methods) and attributes."""
212+
213+
214+
default_config: ConfigDict = {
215+
"docstring_style": "google",
216+
"docstring_options": {"ignore_init_summary": True},
217+
"show_root_heading": True,
218+
"show_root_full_path": True,
219+
"show_root_members_full_path": True,
220+
"show_object_full_path": True,
221+
"show_category_heading": False,
222+
"show_if_no_docstring": True,
223+
"show_signature": True,
224+
"show_signature_annotations": False,
225+
"signature_crossrefs": False,
226+
"separate_signature": True,
227+
"line_length": 80,
228+
"merge_init_into_class": True,
229+
"show_docstring_attributes": True,
230+
"show_docstring_description": True,
231+
"show_docstring_examples": True,
232+
"show_docstring_other_parameters": True,
233+
"show_docstring_parameters": True,
234+
"show_docstring_raises": True,
235+
"show_docstring_receives": True,
236+
"show_docstring_returns": True,
237+
"show_docstring_warns": True,
238+
"show_docstring_yields": True,
239+
"show_bases": True,
240+
"show_submodules": True,
241+
"group_by_category": False,
242+
"heading_level": 2,
243+
"members_order": "alphabetical",
244+
"docstring_section_style": "list",
245+
"members": None,
246+
"inherited_members": True,
247+
"filters": ["!^_"],
248+
"annotations_path": "brief",
249+
"preload_modules": None,
250+
"load_external_modules": False,
251+
"allow_inspection": True,
252+
"summary": True,
253+
"show_docstring_classes": True,
254+
"show_docstring_functions": True,
255+
"show_docstring_modules": True,
256+
}

src/griffe2md/main.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
import re
66
import sys
77
from pathlib import Path
8-
from typing import IO, TYPE_CHECKING
8+
from typing import IO, TYPE_CHECKING, cast
99

1010
import mdformat
1111
from griffe import GriffeLoader, Parser
1212
from jinja2 import Environment, FileSystemLoader
1313

1414
from griffe2md import rendering
15+
from griffe2md.config import ConfigDict, default_config
1516

1617
if TYPE_CHECKING:
1718
from griffe import Object
@@ -27,7 +28,7 @@ def _output(text: str, to: IO | str | None = None) -> None:
2728
to.write(text)
2829

2930

30-
def prepare_context(obj: Object, config: dict | None = None) -> dict:
31+
def prepare_context(obj: Object, config: ConfigDict | None = None) -> dict:
3132
"""Prepare Jinja context.
3233
3334
Parameters:
@@ -37,13 +38,16 @@ def prepare_context(obj: Object, config: dict | None = None) -> dict:
3738
Returns:
3839
The Jinja context.
3940
"""
40-
config = dict(rendering.default_config, **(config or {}))
41+
config = cast("ConfigDict", {**default_config, **(config or {})})
4142
if config["filters"]:
42-
config["filters"] = [(re.compile(filtr.lstrip("!")), filtr.startswith("!")) for filtr in config["filters"]]
43+
config["filters"] = [
44+
(re.compile(filtr.lstrip("!")), filtr.startswith("!")) if isinstance(filtr, str) else filtr
45+
for filtr in config["filters"]
46+
]
4347

4448
heading_level = config["heading_level"]
4549
try:
46-
config["members_order"] = rendering.Order(config["members_order"])
50+
config["members_order"] = rendering.Order(config["members_order"]).value
4751
except ValueError as error:
4852
choices = "', '".join(item.value for item in rendering.Order)
4953
raise ValueError(
@@ -113,7 +117,7 @@ def prepare_env(env: Environment | None = None) -> Environment:
113117
return env
114118

115119

116-
def render_object_docs(obj: Object, config: dict | None = None) -> str:
120+
def render_object_docs(obj: Object, config: ConfigDict | None = None) -> str:
117121
"""Render docs for a given object.
118122
119123
Parameters:
@@ -129,7 +133,7 @@ def render_object_docs(obj: Object, config: dict | None = None) -> str:
129133
return mdformat.text(rendered)
130134

131135

132-
def render_package_docs(package: str, config: dict | None = None) -> str:
136+
def render_package_docs(package: str, config: ConfigDict | None = None) -> str:
133137
"""Render docs for a given package.
134138
135139
Parameters:
@@ -139,15 +143,19 @@ def render_package_docs(package: str, config: dict | None = None) -> str:
139143
Returns:
140144
Markdown.
141145
"""
142-
config = config or dict(rendering.default_config)
146+
config = cast("ConfigDict", {**default_config, **(config or {})})
143147
parser = config["docstring_style"] and Parser(config["docstring_style"])
144148
loader = GriffeLoader(docstring_parser=parser)
145149
module = loader.load(package)
146150
loader.resolve_aliases(external=True)
147151
return render_object_docs(module, config) # type: ignore[arg-type]
148152

149153

150-
def write_package_docs(package: str, config: dict | None = None, output: IO | str | None = None) -> None:
154+
def write_package_docs(
155+
package: str,
156+
config: ConfigDict | None = None,
157+
output: IO | str | None = None,
158+
) -> None:
151159
"""Write docs for a given package to a file or stdout.
152160
153161
Parameters:

0 commit comments

Comments
 (0)