Skip to content

Commit 4731365

Browse files
Implement usethis tool mkdocs (#823)
* Add scaffolding for implementation of usethis tool mkdocs * Add mkdocs to ALL_TOOL_COMMANDS * Add basic implementation of MkDocsTool class * Add `MkDocsTool` to `ALL_TOOLS` and `SupportedToolType` * Add scaffolding for YAMLFileManager * Add `use_mkdocs` to `use_tool` * Implement `get_config_spec` for `MkDocsTool` Test the build is successful with `use_mkdocs` * Add `MkDocsTool` to `OTHER_TOOLS` list for `pyproject.toml` * Update usage table for `usethis list` * Implement `use_mkdocs(..., remove=True)` * Add tests for `use_mkdocs(..., remove=True)` * Add tests for `src\usethis\_integrations\mkdocs\core.py` and `use_mkdocs(..., how=True)` * Add tests for `MkDocsTool.print_how_to_use` * Fix typo in comment in `src/usethis/_tool/impl/mkdocs.py` Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update README to document `usethis mkdocs` Tweak docstring for mkdocs command --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent c694f81 commit 4731365

15 files changed

Lines changed: 412 additions & 5 deletions

File tree

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,10 @@ declaring dependencies with `uv add`.
309309
- `usethis tool coverage.py` - Use [Coverage.py](https://github.com/nedbat/coveragepy): a code coverage measurement tool.
310310
- `usethis tool pytest` - Use the [pytest](https://github.com/pytest-dev/pytest) testing framework.
311311

312+
#### Documentation
313+
314+
- `usethis tool mkdocs` - Use [MkDocs](https://www.mkdocs.org/): project documentation sites with Markdown.
315+
312316
#### Configuration Files
313317

314318
- `usethis tool pyproject.toml` - Use a [pyproject.toml](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#writing-your-pyproject-toml) file to configure the project.
@@ -497,9 +501,8 @@ If you're using Cookiecutter, then you can update to a latest version of a templ
497501
Major features planned for later in 2025 are:
498502

499503
- Support for users who aren't using uv, e.g. poetry users,
500-
- Support for automated GitHub Actions workflows ([#57](https://github.com/usethis-python/usethis-python/issues/57)),
501-
- Support for a typechecker (likely Pyright, [#121](https://github.com/usethis-python/usethis-python/issues/121)), and
502-
- Support for documentation pages (likely using mkdocs, [#188](https://github.com/usethis-python/usethis-python/issues/188)).
504+
- Support for automated GitHub Actions workflows ([#57](https://github.com/usethis-python/usethis-python/issues/57)), and
505+
- Support for a typechecker (likely Pyright, [#121](https://github.com/usethis-python/usethis-python/issues/121)).
503506

504507
Other features are tracked in the [GitHub Issues](https://github.com/usethis-python/usethis-python/issues) page.
505508

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ layers = [
239239
"codespell | deptry | import_linter | pyproject_fmt | requirements_txt",
240240
"ruff",
241241
"pytest : coverage_py",
242-
"pre_commit",
242+
"pre_commit | mkdocs",
243243
]
244244
containers = [ "usethis._tool.impl" ]
245245
exhaustive = true
@@ -249,7 +249,7 @@ name = "usethis._integrations"
249249
type = "layers"
250250
layers = [
251251
"ci | pre_commit",
252-
"uv | pytest | pydantic | sonarqube",
252+
"uv | mkdocs | pytest | pydantic | sonarqube",
253253
"project | python",
254254
"file",
255255
]

src/usethis/_config_file.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager
99
from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager
1010
from usethis._integrations.file.toml.io_ import TOMLFileManager
11+
from usethis._integrations.file.yaml.io_ import YAMLFileManager
1112
from usethis._integrations.uv.toml import UVTOMLManager
1213

1314
if TYPE_CHECKING:
@@ -24,6 +25,7 @@ def files_manager() -> Iterator[None]:
2425
DotRuffTOMLManager(),
2526
DotPytestINIManager(),
2627
DotImportLinterManager(),
28+
MkDocsYMLManager(),
2729
PytestINIManager(),
2830
RuffTOMLManager(),
2931
ToxINIManager(),
@@ -72,6 +74,14 @@ def relative_path(self) -> Path:
7274
return Path(".ruff.toml")
7375

7476

77+
class MkDocsYMLManager(YAMLFileManager):
78+
"""Class to manage the mkdocs.yml file."""
79+
80+
@property
81+
def relative_path(self) -> Path:
82+
return Path("mkdocs.yml")
83+
84+
7585
class PytestINIManager(INIFileManager):
7686
"""Class to manage the pytest.ini file."""
7787

src/usethis/_core/tool.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from usethis._console import box_print, tick_print
1111
from usethis._integrations.ci.bitbucket.used import is_bitbucket_used
1212
from usethis._integrations.file.pyproject_toml.valid import ensure_pyproject_validity
13+
from usethis._integrations.mkdocs.core import add_docs_dir
1314
from usethis._integrations.pre_commit.core import (
1415
install_pre_commit_hooks,
1516
remove_pre_commit_config,
@@ -28,6 +29,7 @@
2829
from usethis._tool.impl.coverage_py import CoveragePyTool
2930
from usethis._tool.impl.deptry import DeptryTool
3031
from usethis._tool.impl.import_linter import ImportLinterTool
32+
from usethis._tool.impl.mkdocs import MkDocsTool
3133
from usethis._tool.impl.pre_commit import PreCommitTool
3234
from usethis._tool.impl.pyproject_fmt import PyprojectFmtTool
3335
from usethis._tool.impl.pyproject_toml import PyprojectTOMLTool
@@ -156,6 +158,30 @@ def use_import_linter(*, remove: bool = False, how: bool = False) -> None:
156158
tool.remove_managed_files()
157159

158160

161+
def use_mkdocs(*, remove: bool = False, how: bool = False) -> None:
162+
tool = MkDocsTool()
163+
164+
if how:
165+
tool.print_how_to_use()
166+
return
167+
168+
if not remove:
169+
ensure_pyproject_toml()
170+
(usethis_config.cpd() / "mkdocs.yml").touch()
171+
172+
add_docs_dir()
173+
174+
tool.add_doc_deps()
175+
tool.add_configs()
176+
177+
tool.print_how_to_use()
178+
else:
179+
# N.B. no need to remove configs because they all lie in managed files.
180+
181+
tool.remove_doc_deps()
182+
tool.remove_managed_files()
183+
184+
159185
def use_pre_commit(*, remove: bool = False, how: bool = False) -> None:
160186
tool = PreCommitTool()
161187

@@ -498,6 +524,8 @@ def use_tool(
498524
use_deptry(remove=remove, how=how)
499525
elif isinstance(tool, ImportLinterTool):
500526
use_import_linter(remove=remove, how=how)
527+
elif isinstance(tool, MkDocsTool):
528+
use_mkdocs(remove=remove, how=how)
501529
elif isinstance(tool, PreCommitTool):
502530
use_pre_commit(remove=remove, how=how)
503531
elif isinstance(tool, PyprojectFmtTool):

src/usethis/_integrations/mkdocs/__init__.py

Whitespace-only changes.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from usethis._config import usethis_config
2+
from usethis._console import tick_print
3+
from usethis._integrations.project.name import get_project_name
4+
5+
6+
def add_docs_dir() -> None:
7+
"""Create the `docs` directory and an `docs/index.md` file if they do not exist."""
8+
docs_dir = usethis_config.cpd() / "docs"
9+
if not docs_dir.exists():
10+
tick_print("Creating '/docs'.")
11+
docs_dir.mkdir()
12+
write_index = True
13+
elif not (docs_dir / "index.md").exists():
14+
tick_print("Writing '/docs/index.md'.")
15+
write_index = True
16+
else:
17+
write_index = False
18+
if write_index:
19+
(docs_dir / "index.md").write_text(
20+
f"""\
21+
# {get_project_name()}
22+
23+
Welcome to the documentation for {get_project_name()}.
24+
"""
25+
)

src/usethis/_interface/tool.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,29 @@ def import_linter(
113113
_run_tool(use_import_linter, remove=remove, how=how)
114114

115115

116+
@app.command(
117+
name="mkdocs",
118+
help="Use MkDocs: project documentation sites with Markdown.",
119+
rich_help_panel="Documentation",
120+
)
121+
def mkdocs(
122+
remove: bool = remove_opt,
123+
how: bool = how_opt,
124+
offline: bool = offline_opt,
125+
quiet: bool = quiet_opt,
126+
frozen: bool = frozen_opt,
127+
) -> None:
128+
from usethis._config import usethis_config
129+
from usethis._config_file import files_manager
130+
from usethis._core.tool import use_mkdocs
131+
132+
with (
133+
usethis_config.set(offline=offline, quiet=quiet, frozen=frozen),
134+
files_manager(),
135+
):
136+
_run_tool(use_mkdocs, remove=remove, how=how)
137+
138+
116139
@app.command(
117140
name="pre-commit",
118141
help="Use the pre-commit framework to manage and maintain pre-commit hooks.",
@@ -275,6 +298,7 @@ def _run_tool(caller: UseToolFunc, *, remove: bool, how: bool, **kwargs: Any):
275298
"coverage.py",
276299
"deptry",
277300
"import-linter",
301+
"mkdocs",
278302
"pre-commit",
279303
"pyproject.toml",
280304
"pyproject-fmt",

src/usethis/_tool/all_.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from usethis._tool.impl.coverage_py import CoveragePyTool
77
from usethis._tool.impl.deptry import DeptryTool
88
from usethis._tool.impl.import_linter import ImportLinterTool
9+
from usethis._tool.impl.mkdocs import MkDocsTool
910
from usethis._tool.impl.pre_commit import PreCommitTool
1011
from usethis._tool.impl.pyproject_fmt import PyprojectFmtTool
1112
from usethis._tool.impl.pyproject_toml import PyprojectTOMLTool
@@ -18,6 +19,7 @@
1819
| CoveragePyTool
1920
| DeptryTool
2021
| ImportLinterTool
22+
| MkDocsTool
2123
| PreCommitTool
2224
| PyprojectFmtTool
2325
| PyprojectTOMLTool
@@ -31,6 +33,7 @@
3133
CoveragePyTool(),
3234
DeptryTool(),
3335
ImportLinterTool(),
36+
MkDocsTool(),
3437
PreCommitTool(),
3538
PyprojectFmtTool(),
3639
PyprojectTOMLTool(),

src/usethis/_tool/base.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ def get_test_deps(self, *, unconditional: bool = False) -> list[Dependency]:
8484
"""
8585
return []
8686

87+
def get_doc_deps(self, *, unconditional: bool = False) -> list[Dependency]:
88+
"""The tool's documentation dependencies.
89+
90+
These should all be considered characteristic of this particular tool.
91+
92+
Args:
93+
unconditional: Whether to return all possible dependencies regardless of
94+
whether they are relevant to the current project.
95+
"""
96+
return []
97+
8798
def get_config_spec(self) -> ConfigSpec:
8899
"""Get the configuration specification for this tool.
89100
@@ -165,6 +176,12 @@ def is_declared_as_dep(self) -> bool:
165176
for dep in self.get_test_deps(unconditional=True)
166177
)
167178

179+
if not _is_declared:
180+
_is_declared = any(
181+
is_dep_in_any_group(dep)
182+
for dep in self.get_doc_deps(unconditional=True)
183+
)
184+
168185
return _is_declared
169186

170187
def add_dev_deps(self) -> None:
@@ -179,6 +196,12 @@ def add_test_deps(self) -> None:
179196
def remove_test_deps(self) -> None:
180197
remove_deps_from_group(self.get_test_deps(unconditional=True), "test")
181198

199+
def add_doc_deps(self) -> None:
200+
add_deps_to_group(self.get_doc_deps(), "doc")
201+
202+
def remove_doc_deps(self) -> None:
203+
remove_deps_from_group(self.get_doc_deps(unconditional=True), "doc")
204+
182205
def get_pre_commit_repos(self) -> list[LocalRepo | UriRepo]:
183206
"""Get the pre-commit repository definitions for the tool."""
184207
return [c.repo for c in self.get_pre_commit_config().repo_configs]

src/usethis/_tool/impl/mkdocs.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
from typing import TYPE_CHECKING
5+
6+
from usethis._config_file import MkDocsYMLManager
7+
from usethis._console import box_print
8+
from usethis._integrations.project.name import get_project_name
9+
from usethis._integrations.uv.deps import Dependency
10+
from usethis._integrations.uv.used import is_uv_used
11+
from usethis._tool.base import Tool
12+
from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec
13+
14+
if TYPE_CHECKING:
15+
from usethis._io import KeyValueFileManager
16+
17+
18+
class MkDocsTool(Tool):
19+
# https://www.mkdocs.org/
20+
21+
@property
22+
def name(self) -> str:
23+
return "MkDocs"
24+
25+
def print_how_to_use(self) -> None:
26+
if is_uv_used():
27+
box_print("Run 'uv run mkdocs build' to build the documentation.")
28+
box_print("Run 'uv run mkdocs serve' to serve the documentation locally.")
29+
else:
30+
box_print("Run 'mkdocs build' to build the documentation.")
31+
box_print("Run 'mkdocs serve' to serve the documentation locally.")
32+
33+
def get_doc_deps(self, *, unconditional: bool = False) -> list[Dependency]:
34+
deps = [Dependency(name="mkdocs")]
35+
36+
if unconditional:
37+
deps.append(Dependency(name="mkdocs-material"))
38+
39+
return deps
40+
41+
def get_config_spec(self) -> ConfigSpec:
42+
"""Get the configuration specification for this tool.
43+
44+
This includes the file managers and resolution methodology.
45+
"""
46+
return ConfigSpec.from_flat(
47+
file_managers=[
48+
MkDocsYMLManager(),
49+
],
50+
resolution="first_content",
51+
config_items=[
52+
ConfigItem(
53+
description="Site Name",
54+
root={
55+
Path("mkdocs.yml"): ConfigEntry(
56+
keys=["site_name"],
57+
get_value=lambda: get_project_name(),
58+
),
59+
},
60+
),
61+
ConfigItem(
62+
description="Navigation",
63+
root={
64+
Path("mkdocs.yml"): ConfigEntry(
65+
keys=["nav"],
66+
get_value=lambda: [{"Home": "index.md"}],
67+
),
68+
},
69+
),
70+
],
71+
)
72+
73+
def get_managed_files(self) -> list[Path]:
74+
"""Get (relative) paths to files managed by (solely) this tool."""
75+
return [Path("mkdocs.yml")]
76+
77+
def preferred_file_manager(self) -> KeyValueFileManager:
78+
"""If there is no currently active config file, this is the preferred one."""
79+
# Should set the mkdocs.yml file manager as the preferred one
80+
return MkDocsYMLManager()

0 commit comments

Comments
 (0)