Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ containers =
usethis._tool.impl.base
layers =
pyproject_toml
codespell | deptry | import_linter | mkdocs | pyproject_fmt | requirements_txt | tach | ty
codespell | deptry | import_linter | mkdocs | pyproject_fmt | requirements_txt | tach | ty | zensical
ruff
pytest : coverage_py
pre_commit
Expand All @@ -99,7 +99,7 @@ containers =
layers =
all_
pyproject_toml
codespell | deptry | import_linter | mkdocs | pyproject_fmt | requirements_txt | tach | ty
codespell | deptry | import_linter | mkdocs | pyproject_fmt | requirements_txt | tach | ty | zensical
ruff
pytest : coverage_py
pre_commit
Expand Down
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ usethis # usethis: Automatically manage Python tooling
│ │ ├── requirements_txt # requirements.txt tool implementation.
│ │ ├── ruff # Ruff tool implementation.
│ │ ├── tach # Tach tool implementation.
│ │ └── ty # ty tool implementation.
│ │ ├── ty # ty tool implementation.
│ │ └── zensical # Zensical tool implementation.
│ └── spec # Tool specification implementations.
│ ├── all_ # Registry of all available tool specifications.
│ ├── codespell # Codespell tool specification.
Expand All @@ -156,7 +157,8 @@ usethis # usethis: Automatically manage Python tooling
│ ├── requirements_txt # requirements.txt tool specification.
│ ├── ruff # Ruff tool specification.
│ ├── tach # Tach tool specification.
│ └── ty # ty tool specification.
│ ├── ty # ty tool specification.
│ └── zensical # Zensical tool specification.
├── _toolset # Predefined groups of related tools.
│ ├── arch # Architecture enforcement toolset.
│ ├── doc # Documentation toolset.
Expand Down Expand Up @@ -279,6 +281,7 @@ ALWAYS check whether an existing function already covers your use case before im
- `use_ruff()` (`usethis._core.tool`) — Add Ruff to the project.
- `use_tach()` (`usethis._core.tool`) — Add and configure the Tach architecture enforcement tool.
- `use_ty()` (`usethis._core.tool`) — Add and configure the ty type checker tool.
- `use_zensical()` (`usethis._core.tool`) — Add and configure the Zensical documentation site generator tool.
- `get_project_deps()` (`usethis._deps`) — Get all project dependencies.
- `get_dep_groups()` (`usethis._deps`) — Get all dependency groups from pyproject.toml.
- `get_deps_from_group()` (`usethis._deps`) — Get the list of dependencies in a named dependency group.
Expand Down Expand Up @@ -400,6 +403,7 @@ ALWAYS check whether an existing function already covers your use case before im
- `ruff()` (`usethis._ui.interface.tool`) — Use Ruff: an extremely fast Python linter and code formatter.
- `tach()` (`usethis._ui.interface.tool`) — Use Tach: enforce self-imposed dependency and interface rules.
- `ty()` (`usethis._ui.interface.tool`) — Use the ty type checker: an extremely fast Python type checker.
- `zensical()` (`usethis._ui.interface.tool`) — Use Zensical: a modern static site generator for project documentation.
- `typecheck()` (`usethis._ui.interface.typecheck`) — Add a recommended type checker to the project.
- `version()` (`usethis._ui.interface.version`) — Print the installed version of usethis.

Expand Down
6 changes: 1 addition & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ uv run pyinstrument -m pytest

With any `pytest` options you wish to include, e.g. `-k` to run specific tests, or `--collect-only` to only profile test collection time. This will generate a CLI-friendly report of where time is being spent. For an interactive HTML report, you can run `pyinstrument` with the `-r=html` option before the `-m pytest` part.

A common pattern in the test suite is to use a [pytest fixture](https://docs.pytest.org/en/7.1.x/how-to/fixtures.html) to get a temporary directory, and then use the `usethis._test.change_cwd` context manager in the test to simulate running usethis from within a project. If you're writing a new test and noticing unexpected creations or modifications of files, it's a sign that the working directory has not been properly configured for the test.
A common pattern in the test suite is to use a [pytest fixture](https://docs.pytest.org/en/7.1.x/how-to/fixtures.html) to get a temporary directory, and then use the `_test.change_cwd` context manager in the test (or `from _test import change_cwd`) to simulate running usethis from within a project. If you're writing a new test and noticing unexpected creations or modifications of files, it's a sign that the working directory has not been properly configured for the test.

## Documentation

Expand Down Expand Up @@ -172,10 +172,6 @@ Tool implementations are defined in classes in the `usethis._tool.impl` module.

- You should write tests in `tests/usethis/_core/test_core_tool` for the `use_*` function, following the pattern of the other tests in that module for other tools.

#### Register the tool as a peer in `PyprojectTOMLTool`

- Add your `Tool` subclass instance to the `OTHER_TOOLS` list in `usethis._tool.impl.base.pyproject_toml`. This list tracks all tools other than `pyproject.toml` itself, and is used by `PyprojectTOMLTool` to detect active configuration. A corresponding test `test_in_sync_with_all_tools` will fail if this step is missed.

#### Update tests

- Some tests may need updating as a result of new tool registration. In particular:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Additionally, the command line reference documentation can be viewed with `useth
### Manage Tooling

- [`usethis arch`](https://usethis.readthedocs.io/en/stable/cli/reference#usethis-arch) — Add/Configure recommended architecture analysis tools (namely, [Import Linter](https://import-linter.readthedocs.io/en/stable/)).
- [`usethis doc`](https://usethis.readthedocs.io/en/stable/cli/reference#usethis-doc) — Add/Configure recommended documentation tools (namely, [MkDocs](https://www.mkdocs.org/)).
- [`usethis doc`](https://usethis.readthedocs.io/en/stable/cli/reference#usethis-doc) — Add/Configure recommended documentation tools (namely, [Zensical](https://zensical.org/)).
- [`usethis format`](https://usethis.readthedocs.io/en/stable/cli/reference#usethis-format) — Add/Configure recommended formatters (namely, [Ruff](https://docs.astral.sh/ruff/formatter/) and [pyproject-fmt](https://pyproject-fmt.readthedocs.io/en/latest/)).
- [`usethis hook`](https://usethis.readthedocs.io/en/stable/cli/reference#usethis-hook) — Add/Configure a recommended git hook framework (namely, [pre-commit](https://github.com/pre-commit/pre-commit)).
- [`usethis lint`](https://usethis.readthedocs.io/en/stable/cli/reference#usethis-lint) — Add/Configure recommended linters (namely, [Ruff](https://docs.astral.sh/ruff/linter) and [deptry](https://github.com/fpgmaas/deptry)).
Expand All @@ -90,6 +90,7 @@ Additionally, the command line reference documentation can be viewed with `useth
- [`usethis tool coverage.py`](https://usethis.readthedocs.io/en/stable/cli/reference#testing) - Use [Coverage.py](https://github.com/nedbat/coveragepy): a code coverage measurement tool.
- [`usethis tool pytest`](https://usethis.readthedocs.io/en/stable/cli/reference#testing) - Use the [pytest](https://github.com/pytest-dev/pytest) testing framework.
- [`usethis tool mkdocs`](https://usethis.readthedocs.io/en/stable/cli/reference#documentation) - Use [MkDocs](https://www.mkdocs.org/): Generate project documentation sites with Markdown.
- [`usethis tool zensical`](https://usethis.readthedocs.io/en/stable/cli/reference#documentation) - Use [Zensical](https://zensical.org/): a modern static site generator for project documentation.
- [`usethis tool pyproject.toml`](https://usethis.readthedocs.io/en/stable/cli/reference#testing) - Use a [pyproject.toml](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#writing-your-pyproject-toml) file to configure the project.
- [`usethis tool requirements.txt`](https://usethis.readthedocs.io/en/stable/cli/reference#testing) - Use a [requirements.txt](https://pip.pypa.io/en/stable/reference/requirements-file-format/) file exported from the uv lockfile.

Expand Down
3 changes: 2 additions & 1 deletion docs/cli/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## Manage Tooling

- [`usethis arch`](reference.md#usethis-arch) — Add/Configure recommended architecture analysis tools (namely, [Import Linter](https://import-linter.readthedocs.io/en/stable/)).
- [`usethis doc`](reference.md#usethis-doc) — Add/Configure recommended documentation tools (namely, [MkDocs](https://www.mkdocs.org/)).
- [`usethis doc`](reference.md#usethis-doc) — Add/Configure recommended documentation tools (namely, [Zensical](https://zensical.org/)).
- [`usethis format`](reference.md#usethis-format) — Add/Configure recommended formatters (namely, [Ruff](https://docs.astral.sh/ruff/formatter/) and [pyproject-fmt](https://pyproject-fmt.readthedocs.io/en/latest/)).
- [`usethis hook`](reference.md#usethis-hook) — Add/Configure a recommended git hook framework (namely, [pre-commit](https://github.com/pre-commit/pre-commit)).
- [`usethis lint`](reference.md#usethis-lint) — Add/Configure recommended linters (namely, [Ruff](https://docs.astral.sh/ruff/linter) and [deptry](https://github.com/fpgmaas/deptry)).
Expand All @@ -26,6 +26,7 @@
- [`usethis tool coverage.py`](reference.md#testing) - Use [Coverage.py](https://github.com/nedbat/coveragepy): a code coverage measurement tool.
- [`usethis tool pytest`](reference.md#testing) - Use the [pytest](https://github.com/pytest-dev/pytest) testing framework.
- [`usethis tool mkdocs`](reference.md#documentation) - Use [MkDocs](https://www.mkdocs.org/): Generate project documentation sites with Markdown.
- [`usethis tool zensical`](reference.md#documentation) - Use [Zensical](https://zensical.org/): a modern static site generator for project documentation.
- [`usethis tool pyproject.toml`](reference.md#testing) - Use a [pyproject.toml](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#writing-your-pyproject-toml) file to configure the project.
- [`usethis tool requirements.txt`](reference.md#testing) - Use a [requirements.txt](https://pip.pypa.io/en/stable/reference/requirements-file-format/) file exported from the uv lockfile.

Expand Down
2 changes: 2 additions & 0 deletions docs/functions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
- `use_ruff()` (`usethis._core.tool`) — Add Ruff to the project.
- `use_tach()` (`usethis._core.tool`) — Add and configure the Tach architecture enforcement tool.
- `use_ty()` (`usethis._core.tool`) — Add and configure the ty type checker tool.
- `use_zensical()` (`usethis._core.tool`) — Add and configure the Zensical documentation site generator tool.
- `get_project_deps()` (`usethis._deps`) — Get all project dependencies.
- `get_dep_groups()` (`usethis._deps`) — Get all dependency groups from pyproject.toml.
- `get_deps_from_group()` (`usethis._deps`) — Get the list of dependencies in a named dependency group.
Expand Down Expand Up @@ -192,5 +193,6 @@
- `ruff()` (`usethis._ui.interface.tool`) — Use Ruff: an extremely fast Python linter and code formatter.
- `tach()` (`usethis._ui.interface.tool`) — Use Tach: enforce self-imposed dependency and interface rules.
- `ty()` (`usethis._ui.interface.tool`) — Use the ty type checker: an extremely fast Python type checker.
- `zensical()` (`usethis._ui.interface.tool`) — Use Zensical: a modern static site generator for project documentation.
- `typecheck()` (`usethis._ui.interface.typecheck`) — Add a recommended type checker to the project.
- `version()` (`usethis._ui.interface.version`) — Print the installed version of usethis.
6 changes: 4 additions & 2 deletions docs/module-tree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ usethis # usethis: Automatically manage Python tooling
│ │ ├── requirements_txt # requirements.txt tool implementation.
│ │ ├── ruff # Ruff tool implementation.
│ │ ├── tach # Tach tool implementation.
│ │ └── ty # ty tool implementation.
│ │ ├── ty # ty tool implementation.
│ │ └── zensical # Zensical tool implementation.
│ └── spec # Tool specification implementations.
│ ├── all_ # Registry of all available tool specifications.
│ ├── codespell # Codespell tool specification.
Expand All @@ -145,7 +146,8 @@ usethis # usethis: Automatically manage Python tooling
│ ├── requirements_txt # requirements.txt tool specification.
│ ├── ruff # Ruff tool specification.
│ ├── tach # Tach tool specification.
│ └── ty # ty tool specification.
│ ├── ty # ty tool specification.
│ └── zensical # Zensical tool specification.
├── _toolset # Predefined groups of related tools.
│ ├── arch # Architecture enforcement toolset.
│ ├── doc # Documentation toolset.
Expand Down
2 changes: 1 addition & 1 deletion ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ lint.flake8-bugbear.extend-immutable-calls = [ "typer.Argument", "typer.Option"
lint.flake8-builtins.strict-checking = true
lint.flake8-tidy-imports.banned-api."functools.singledispatch".msg = "Use if-branch isinstance logic instead of singledispatch."
lint.flake8-tidy-imports.banned-api."functools.singledispatchmethod".msg = "Use if-branch isinstance logic instead of singledispatchmethod."
lint.flake8-tidy-imports.banned-api."typer.testing.CliRunner".msg = "Use `usethis._test.CliRunner` instead of `typer.CliRunner`."
lint.flake8-tidy-imports.banned-api."typer.testing.CliRunner".msg = "Use `_test.CliRunner` instead of `typer.CliRunner`."
lint.flake8-type-checking.quote-annotations = true
lint.flake8-type-checking.runtime-evaluated-base-classes = [ "pydantic.BaseModel" ]
lint.flake8-type-checking.runtime-evaluated-decorators = [ "typer.Typer.command" ]
Expand Down
4 changes: 4 additions & 0 deletions src/_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use_ruff,
use_tach,
use_ty,
use_zensical,
)
from usethis._fallback import FALLBACK_PRE_COMMIT_VERSION
from usethis._file.yaml.errors import YAMLDecodeError
Expand All @@ -50,6 +51,7 @@
from usethis._tool.impl.base.ruff import RuffTool
from usethis._tool.impl.base.tach import TachTool
from usethis._tool.impl.base.ty import TyTool
from usethis._tool.impl.base.zensical import ZensicalTool
from usethis.errors import UsethisError

if TYPE_CHECKING:
Expand Down Expand Up @@ -284,6 +286,8 @@ def use_tool( # noqa: PLR0912
use_tach(remove=remove, how=how)
elif isinstance(tool, TyTool):
use_ty(remove=remove, how=how)
elif isinstance(tool, ZensicalTool):
use_zensical(remove=remove, how=how)
else:
# Having the assert_never here is effectively a way of testing cases are
# exhaustively handled, which ensures it is kept up to date with ALL_TOOLS,
Expand Down
10 changes: 10 additions & 0 deletions src/usethis/_config_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def files_manager() -> Iterator[None]:
ToxINIManager(),
TyTOMLManager(),
UVTOMLManager(),
ZensicalTOMLManager(),
):
yield

Expand Down Expand Up @@ -177,3 +178,12 @@ class TyTOMLManager(TOMLFileManager):
@override
def relative_path(self) -> Path:
return Path("ty.toml")


class ZensicalTOMLManager(TOMLFileManager):
"""Class to manage the zensical.toml file."""

@property
@override
def relative_path(self) -> Path:
return Path("zensical.toml")
25 changes: 25 additions & 0 deletions src/usethis/_core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from usethis._tool.impl.base.ruff import RuffTool
from usethis._tool.impl.base.tach import TachTool
from usethis._tool.impl.base.ty import TyTool
from usethis._tool.impl.base.zensical import ZensicalTool
from usethis._tool.rule import RuleConfig
from usethis._types.backend import BackendEnum
from usethis._types.deps import Dependency
Expand Down Expand Up @@ -570,3 +571,27 @@ def use_ty(*, remove: bool = False, how: bool = False) -> None:
tool.remove_pre_commit_repo_configs()
tool.remove_dev_deps()
tool.remove_managed_files()


def use_zensical(*, remove: bool = False, how: bool = False) -> None:
"""Add and configure the Zensical documentation site generator tool."""
tool = ZensicalTool()

if how:
tool.print_how_to_use()
return

if not remove:
ensure_dep_declaration_file()

add_docs_dir()

tool.add_doc_deps()
tool.add_configs()

tool.print_how_to_use()
else:
# N.B. no need to remove configs because they all lie in managed files.

Comment on lines +594 to +595

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use_zensical(remove=True) assumes configs only live in managed files, but ZensicalToolSpec.config_spec() can select mkdocs.yml as the active config file. In that case, removing Zensical currently leaves behind managed config entries in mkdocs.yml and can cause the tool to still be detected as used. Consider adjusting the removal path (e.g., call tool.remove_configs() with care) and/or changing which config items are considered managed so removal and usage detection behave consistently.

Suggested change
# N.B. no need to remove configs because they all lie in managed files.
# Remove configs explicitly because they may live in active project config
# files such as mkdocs.yml, not only in managed files.
tool.remove_configs()

Copilot uses AI. Check for mistakes.
tool.remove_doc_deps()
tool.remove_managed_files()
3 changes: 3 additions & 0 deletions src/usethis/_tool/all_.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from usethis._tool.impl.base.ruff import RuffTool
from usethis._tool.impl.base.tach import TachTool
from usethis._tool.impl.base.ty import TyTool
from usethis._tool.impl.base.zensical import ZensicalTool

SupportedToolType: TypeAlias = (
CodespellTool
Expand All @@ -32,6 +33,7 @@
| RuffTool
| TachTool
| TyTool
| ZensicalTool
)

ALL_TOOLS: list[SupportedToolType] = [
Expand All @@ -49,4 +51,5 @@
RuffTool(),
TachTool(),
TyTool(),
ZensicalTool(),
]
35 changes: 35 additions & 0 deletions src/usethis/_tool/impl/base/zensical.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Zensical tool implementation."""

from __future__ import annotations

from typing import final

from typing_extensions import assert_never, override

from usethis._backend.dispatch import get_backend
from usethis._backend.poetry.detect import is_poetry_used
from usethis._backend.uv.detect import is_uv_used
from usethis._console import how_print
from usethis._tool.base import Tool
from usethis._tool.impl.spec.zensical import ZensicalToolSpec
from usethis._types.backend import BackendEnum


@final
class ZensicalTool(ZensicalToolSpec, Tool):
@override
def print_how_to_use(self) -> None:
backend = get_backend()
if backend is BackendEnum.uv and is_uv_used():
how_print("Run 'uv run zensical build' to build the documentation.")
how_print("Run 'uv run zensical serve' to serve the documentation locally.")
elif backend is BackendEnum.poetry and is_poetry_used():
how_print("Run 'poetry run zensical build' to build the documentation.")
how_print(
"Run 'poetry run zensical serve' to serve the documentation locally."
)
elif backend in (BackendEnum.none, BackendEnum.uv, BackendEnum.poetry):
how_print("Run 'zensical build' to build the documentation.")
how_print("Run 'zensical serve' to serve the documentation locally.")
else:
assert_never(backend)
3 changes: 3 additions & 0 deletions src/usethis/_tool/impl/spec/all_.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from usethis._tool.impl.spec.ruff import RuffToolSpec
from usethis._tool.impl.spec.tach import TachToolSpec
from usethis._tool.impl.spec.ty import TyToolSpec
from usethis._tool.impl.spec.zensical import ZensicalToolSpec

ALL_TOOL_SPECS: list[
CodespellToolSpec
Expand All @@ -30,6 +31,7 @@
| RuffToolSpec
| TachToolSpec
| TyToolSpec
| ZensicalToolSpec
] = [
# Alphabetical order, matching ALL_TOOLS in usethis._tool.all_
CodespellToolSpec(),
Expand All @@ -45,4 +47,5 @@
RuffToolSpec(),
TachToolSpec(),
TyToolSpec(),
ZensicalToolSpec(),
]
Loading
Loading