Skip to content

Commit 22c36f5

Browse files
Auto-generate docs/functions.txt via export-functions hook from source docstrings
Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/2927dcf9-6699-49b6-8758-fd7ac587c972 Co-authored-by: nathanjmcdougall <[email protected]>
1 parent 5f0a246 commit 22c36f5

9 files changed

Lines changed: 273 additions & 62 deletions

File tree

.pre-commit-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ repos:
101101
always_run: true
102102
pass_filenames: false
103103
priority: 0
104+
- id: export-functions
105+
name: export-functions
106+
entry: uv run --frozen --offline hooks/export-functions.py
107+
args: ["--output-file=docs/functions.txt"]
108+
language: system
109+
always_run: true
110+
pass_filenames: false
111+
priority: 0
104112
- repo: local
105113
hooks:
106114
- id: deptry

AGENTS.md

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -205,44 +205,46 @@ ALWAYS check whether an existing function already covers your use case before im
205205

206206
<!-- sync:docs/functions.txt -->
207207

208-
### Dependency Management (`usethis._deps`)
209-
210-
- `get_project_deps()` — Get all project dependencies (from `project.dependencies` in `pyproject.toml`; excludes dev/group/build deps).
211-
- `get_dep_groups()` — Get all dependency groups (from the `dependency-groups` section of `pyproject.toml`) as a `dict[str, list[Dependency]]`.
212-
- `get_deps_from_group(group)` — Get the list of dependencies in a named dependency group.
213-
- `add_deps_to_group(deps, group, *, default=True)` — Add dependencies to a named group (handles backend dispatch, prints messages, registers the group as a default group).
214-
- `remove_deps_from_group(deps, group)` — Remove dependencies from a named group (handles backend dispatch, prints messages).
215-
- `is_dep_in_any_group(dep)` — Check if a dependency exists in any dependency group.
216-
- `is_dep_satisfied_in(dep, *, in_)` — Check if a dependency is satisfied by any dependency in a given list.
217-
- `get_default_groups()` — Get the list of default dependency groups (installed automatically by the package manager).
218-
- `register_default_group(group)` — Register a group in the default-groups configuration if it is not already present.
219-
220-
### Console Output (`usethis._console`)
221-
222-
- `tick_print(msg)` — Print a ✔ success/completion message (green).
223-
- `instruct_print(msg)` — Print a ☐ instruction the user must perform manually (red).
224-
- `how_print(msg)` — Print a ☐ guidance message explaining how to do something (red).
225-
- `info_print(msg)` — Print an ℹ informational message (blue).
226-
- `warn_print(msg)` — Print a ⚠ warning message (yellow; deduplicated).
227-
- `err_print(msg)` — Print a ✗ error message to stderr (red).
208+
### Dependency Management
209+
210+
- `get_project_deps()` (`usethis._deps`) — Get all project dependencies.
211+
- `get_dep_groups()` (`usethis._deps`) — Get all dependency groups from the dependency-groups section of pyproject.toml.
212+
- `get_deps_from_group()` (`usethis._deps`) — Get the list of dependencies in a named dependency group.
213+
- `register_default_group()` (`usethis._deps`) — Register a group in the default-groups configuration if it's not already there.
214+
- `get_default_groups()` (`usethis._deps`) — Get the list of default dependency groups installed automatically by the package manager.
215+
- `is_dep_satisfied_in()` (`usethis._deps`) — Check if a dependency is satisfied by any dependency in the given list.
216+
- `remove_deps_from_group()` (`usethis._deps`) — Remove dependencies from the named group if present.
217+
- `is_dep_in_any_group()` (`usethis._deps`) — Check if a dependency exists in any dependency group.
218+
- `add_deps_to_group()` (`usethis._deps`) — Add dependencies to a named group using PEP 735 dependency groups.
219+
220+
### Console Output
221+
222+
- `tick_print()` (`usethis._console`) — Print a ✔ success/completion message (green).
223+
- `instruct_print()` (`usethis._console`) — Print a ☐ instruction the user must perform manually (red).
224+
- `how_print()` (`usethis._console`) — Print a ☐ guidance message explaining how to do something (red).
225+
- `info_print()` (`usethis._console`) — Print an informational message (blue).
226+
- `err_print()` (`usethis._console`) — Print a ✗ error message to stderr (red).
227+
- `warn_print()` (`usethis._console`) — Print a ⚠ warning message (yellow; deduplicated).
228+
- `get_icon_mode()` (`usethis._console`) — Detect terminal's icon support level.
228229

229230
### Tool and Feature Detection
230231

231-
- `is_pre_commit_used()` (`usethis._detect.pre_commit`) — Check if pre-commit is active in the project (config file present or declared as a dependency).
232-
- `is_readme_used()` (`usethis._detect.readme`) — Check if a `README.md` file is present in the project.
233-
- `has_pyproject_toml_declared_build_system()` (`usethis._integrations.project.build`) — Check if a build system is declared in `pyproject.toml`.
232+
- `is_pre_commit_used()` (`usethis._detect.pre_commit`) — Check if pre-commit is being used in the project.
233+
- `is_readme_used()` (`usethis._detect.readme`) — Check if the README.md file is used.
234+
- `has_pyproject_toml_declared_build_system()` (`usethis._integrations.project.build`) — Check if a build system is declared in the project.
234235

235236
### Project Metadata
236237

237-
- `get_project_name()` (`usethis._integrations.project.name`) — Get the project name (from `pyproject.toml` if available, else heuristics).
238-
- `get_importable_packages()` (`usethis._integrations.project.packages`) — Get the names of importable packages in the source directory.
239-
- `get_source_dir_str()` (`usethis._integrations.project.layout`) — Get the source directory as a string (`"src"` or `"."`).
240-
- `get_requires_python()` (`usethis._file.pyproject_toml.requires_python`) — Get the `requires-python` constraint from `pyproject.toml`.
241-
- `get_name()` (`usethis._file.pyproject_toml.name`) — Get the project name directly from `pyproject.toml`.
238+
- `get_project_name()` (`usethis._integrations.project.name`) — The project name, from pyproject.toml if available or fallback to heuristics.
239+
- `get_importable_packages()` (`usethis._integrations.project.packages`) — Get the names of packages in the source directory that can be imported.
240+
- `get_source_dir_str()` (`usethis._integrations.project.layout`) — Get the source directory as a string ('src' or '.').
241+
- `get_requires_python()` (`usethis._file.pyproject_toml.requires_python`) — Get the requires-python constraint from pyproject.toml.
242+
- `get_required_minor_python_versions()` (`usethis._file.pyproject_toml.requires_python`) — Get Python minor versions that match the project's requires-python constraint.
243+
- `get_name()` (`usethis._file.pyproject_toml.name`) — Get the project name from pyproject.toml.
242244

243-
### Backend Dispatch (`usethis._backend.dispatch`)
245+
### Backend Dispatch
244246

245-
- `get_backend()` — Get the current package manager backend (`BackendEnum.uv`, `BackendEnum.poetry`, or `BackendEnum.none`).
247+
- `get_backend()` (`usethis._backend.dispatch`) — Get the current package manager backend.
246248

247249
<!-- /sync:docs/functions.txt -->
248250

docs/functions.txt

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,40 @@
1-
### Dependency Management (`usethis._deps`)
2-
3-
- `get_project_deps()` — Get all project dependencies (from `project.dependencies` in `pyproject.toml`; excludes dev/group/build deps).
4-
- `get_dep_groups()` — Get all dependency groups (from the `dependency-groups` section of `pyproject.toml`) as a `dict[str, list[Dependency]]`.
5-
- `get_deps_from_group(group)` — Get the list of dependencies in a named dependency group.
6-
- `add_deps_to_group(deps, group, *, default=True)` — Add dependencies to a named group (handles backend dispatch, prints messages, registers the group as a default group).
7-
- `remove_deps_from_group(deps, group)` — Remove dependencies from a named group (handles backend dispatch, prints messages).
8-
- `is_dep_in_any_group(dep)` — Check if a dependency exists in any dependency group.
9-
- `is_dep_satisfied_in(dep, *, in_)` — Check if a dependency is satisfied by any dependency in a given list.
10-
- `get_default_groups()` — Get the list of default dependency groups (installed automatically by the package manager).
11-
- `register_default_group(group)` — Register a group in the default-groups configuration if it is not already present.
12-
13-
### Console Output (`usethis._console`)
14-
15-
- `tick_print(msg)` — Print a ✔ success/completion message (green).
16-
- `instruct_print(msg)` — Print a ☐ instruction the user must perform manually (red).
17-
- `how_print(msg)` — Print a ☐ guidance message explaining how to do something (red).
18-
- `info_print(msg)` — Print an ℹ informational message (blue).
19-
- `warn_print(msg)` — Print a ⚠ warning message (yellow; deduplicated).
20-
- `err_print(msg)` — Print a ✗ error message to stderr (red).
1+
### Dependency Management
2+
3+
- `get_project_deps()` (`usethis._deps`) — Get all project dependencies.
4+
- `get_dep_groups()` (`usethis._deps`) — Get all dependency groups from the dependency-groups section of pyproject.toml.
5+
- `get_deps_from_group()` (`usethis._deps`) — Get the list of dependencies in a named dependency group.
6+
- `register_default_group()` (`usethis._deps`) — Register a group in the default-groups configuration if it's not already there.
7+
- `get_default_groups()` (`usethis._deps`) — Get the list of default dependency groups installed automatically by the package manager.
8+
- `is_dep_satisfied_in()` (`usethis._deps`) — Check if a dependency is satisfied by any dependency in the given list.
9+
- `remove_deps_from_group()` (`usethis._deps`) — Remove dependencies from the named group if present.
10+
- `is_dep_in_any_group()` (`usethis._deps`) — Check if a dependency exists in any dependency group.
11+
- `add_deps_to_group()` (`usethis._deps`) — Add dependencies to a named group using PEP 735 dependency groups.
12+
13+
### Console Output
14+
15+
- `tick_print()` (`usethis._console`) — Print a ✔ success/completion message (green).
16+
- `instruct_print()` (`usethis._console`) — Print a ☐ instruction the user must perform manually (red).
17+
- `how_print()` (`usethis._console`) — Print a ☐ guidance message explaining how to do something (red).
18+
- `info_print()` (`usethis._console`) — Print an informational message (blue).
19+
- `err_print()` (`usethis._console`) — Print a ✗ error message to stderr (red).
20+
- `warn_print()` (`usethis._console`) — Print a ⚠ warning message (yellow; deduplicated).
21+
- `get_icon_mode()` (`usethis._console`) — Detect terminal's icon support level.
2122

2223
### Tool and Feature Detection
2324

24-
- `is_pre_commit_used()` (`usethis._detect.pre_commit`) — Check if pre-commit is active in the project (config file present or declared as a dependency).
25-
- `is_readme_used()` (`usethis._detect.readme`) — Check if a `README.md` file is present in the project.
26-
- `has_pyproject_toml_declared_build_system()` (`usethis._integrations.project.build`) — Check if a build system is declared in `pyproject.toml`.
25+
- `is_pre_commit_used()` (`usethis._detect.pre_commit`) — Check if pre-commit is being used in the project.
26+
- `is_readme_used()` (`usethis._detect.readme`) — Check if the README.md file is used.
27+
- `has_pyproject_toml_declared_build_system()` (`usethis._integrations.project.build`) — Check if a build system is declared in the project.
2728

2829
### Project Metadata
2930

30-
- `get_project_name()` (`usethis._integrations.project.name`) — Get the project name (from `pyproject.toml` if available, else heuristics).
31-
- `get_importable_packages()` (`usethis._integrations.project.packages`) — Get the names of importable packages in the source directory.
32-
- `get_source_dir_str()` (`usethis._integrations.project.layout`) — Get the source directory as a string (`"src"` or `"."`).
33-
- `get_requires_python()` (`usethis._file.pyproject_toml.requires_python`) — Get the `requires-python` constraint from `pyproject.toml`.
34-
- `get_name()` (`usethis._file.pyproject_toml.name`) — Get the project name directly from `pyproject.toml`.
31+
- `get_project_name()` (`usethis._integrations.project.name`) — The project name, from pyproject.toml if available or fallback to heuristics.
32+
- `get_importable_packages()` (`usethis._integrations.project.packages`) — Get the names of packages in the source directory that can be imported.
33+
- `get_source_dir_str()` (`usethis._integrations.project.layout`) — Get the source directory as a string ('src' or '.').
34+
- `get_requires_python()` (`usethis._file.pyproject_toml.requires_python`) — Get the requires-python constraint from pyproject.toml.
35+
- `get_required_minor_python_versions()` (`usethis._file.pyproject_toml.requires_python`) — Get Python minor versions that match the project's requires-python constraint.
36+
- `get_name()` (`usethis._file.pyproject_toml.name`) — Get the project name from pyproject.toml.
3537

36-
### Backend Dispatch (`usethis._backend.dispatch`)
38+
### Backend Dispatch
3739

38-
- `get_backend()` — Get the current package manager backend (`BackendEnum.uv`, `BackendEnum.poetry`, or `BackendEnum.none`).
40+
- `get_backend()` (`usethis._backend.dispatch`) — Get the current package manager backend.

hooks/export-functions.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""Export important utility functions with docstrings to a markdown reference file.
2+
3+
Scans specified Python source files for public functions with docstrings and
4+
writes a categorized markdown reference to an output file. Only functions
5+
with a module-level docstring are included; undocumented functions are skipped.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import argparse
11+
import ast
12+
import sys
13+
from dataclasses import dataclass, field
14+
from pathlib import Path
15+
16+
17+
@dataclass
18+
class _Entry:
19+
module: str
20+
file: str
21+
22+
23+
@dataclass
24+
class _Section:
25+
heading: str
26+
entries: list[_Entry] = field(default_factory=list)
27+
28+
29+
# The categories of utility functions to include in the reference.
30+
# Each section maps a heading to a list of (module, file) entries.
31+
# Only public functions with a docstring in these files are included.
32+
SECTIONS: list[_Section] = [
33+
_Section(
34+
heading="### Dependency Management",
35+
entries=[
36+
_Entry(module="usethis._deps", file="src/usethis/_deps.py"),
37+
],
38+
),
39+
_Section(
40+
heading="### Console Output",
41+
entries=[
42+
_Entry(module="usethis._console", file="src/usethis/_console.py"),
43+
],
44+
),
45+
_Section(
46+
heading="### Tool and Feature Detection",
47+
entries=[
48+
_Entry(
49+
module="usethis._detect.pre_commit",
50+
file="src/usethis/_detect/pre_commit.py",
51+
),
52+
_Entry(
53+
module="usethis._detect.readme",
54+
file="src/usethis/_detect/readme.py",
55+
),
56+
_Entry(
57+
module="usethis._integrations.project.build",
58+
file="src/usethis/_integrations/project/build.py",
59+
),
60+
],
61+
),
62+
_Section(
63+
heading="### Project Metadata",
64+
entries=[
65+
_Entry(
66+
module="usethis._integrations.project.name",
67+
file="src/usethis/_integrations/project/name.py",
68+
),
69+
_Entry(
70+
module="usethis._integrations.project.packages",
71+
file="src/usethis/_integrations/project/packages.py",
72+
),
73+
_Entry(
74+
module="usethis._integrations.project.layout",
75+
file="src/usethis/_integrations/project/layout.py",
76+
),
77+
_Entry(
78+
module="usethis._file.pyproject_toml.requires_python",
79+
file="src/usethis/_file/pyproject_toml/requires_python.py",
80+
),
81+
_Entry(
82+
module="usethis._file.pyproject_toml.name",
83+
file="src/usethis/_file/pyproject_toml/name.py",
84+
),
85+
],
86+
),
87+
_Section(
88+
heading="### Backend Dispatch",
89+
entries=[
90+
_Entry(
91+
module="usethis._backend.dispatch",
92+
file="src/usethis/_backend/dispatch.py",
93+
),
94+
],
95+
),
96+
]
97+
98+
99+
def _get_public_functions(path: Path) -> list[tuple[str, str]]:
100+
"""Return (name, first_docstring_line) for each public function in the file.
101+
102+
Functions without a docstring are excluded.
103+
"""
104+
try:
105+
source = path.read_text(encoding="utf-8")
106+
except (OSError, UnicodeDecodeError) as exc:
107+
print(f"ERROR: Cannot read {path}: {exc}", file=sys.stderr)
108+
return []
109+
110+
try:
111+
tree = ast.parse(source)
112+
except SyntaxError as exc:
113+
print(f"ERROR: Cannot parse {path}: {exc}", file=sys.stderr)
114+
return []
115+
116+
results: list[tuple[str, str]] = []
117+
for node in ast.walk(tree):
118+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
119+
continue
120+
if node.name.startswith("_"):
121+
continue
122+
docstring = ast.get_docstring(node)
123+
if docstring is None:
124+
continue
125+
first_line = docstring.split("\n")[0].strip()
126+
if first_line:
127+
results.append((node.name, first_line))
128+
129+
return results
130+
131+
132+
def main() -> int:
133+
parser = argparse.ArgumentParser(
134+
description="Export utility function reference to a markdown file.",
135+
)
136+
parser.add_argument(
137+
"--output-file",
138+
required=True,
139+
help="Path to the output markdown file to write.",
140+
)
141+
args = parser.parse_args()
142+
143+
output_file = Path(args.output_file)
144+
145+
sections_output: list[str] = []
146+
failed = False
147+
148+
for section in SECTIONS:
149+
bullets: list[str] = []
150+
151+
for entry in section.entries:
152+
source_path = Path(entry.file)
153+
if not source_path.is_file():
154+
print(
155+
f"ERROR: Source file {source_path} not found.",
156+
file=sys.stderr,
157+
)
158+
failed = True
159+
continue
160+
161+
for func_name, first_line in _get_public_functions(source_path):
162+
bullets.append(f"- `{func_name}()` (`{entry.module}`) — {first_line}")
163+
164+
if bullets:
165+
if sections_output:
166+
sections_output.append("")
167+
sections_output.append(section.heading)
168+
sections_output.append("")
169+
sections_output.extend(bullets)
170+
171+
content = "\n".join(sections_output) + "\n"
172+
173+
output_file.parent.mkdir(parents=True, exist_ok=True)
174+
output_file.write_text(content, encoding="utf-8")
175+
176+
print(f"Function reference written to {output_file}.")
177+
178+
if failed:
179+
return 1
180+
181+
return 0
182+
183+
184+
if __name__ == "__main__":
185+
raise SystemExit(main())

src/usethis/_backend/dispatch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414

1515
def get_backend() -> Literal[BackendEnum.uv, BackendEnum.none]:
16+
"""Get the current package manager backend."""
1617
# Effectively we cache the inference, storing it in usethis_config.
1718
if usethis_config.inferred_backend is not None:
1819
return usethis_config.inferred_backend

0 commit comments

Comments
 (0)