epythet.config

Single source of truth for a project’s documentation configuration.

Everything epythet needs to build a site comes from two places, read in this order of precedence:

  1. pyproject.toml: the [project] table for name, version and authors, and the [tool.epythet] table for documentation choices.

  2. setup.cfg: the [metadata] section (legacy projects).

When both files exist, pyproject.toml wins as soon as it has a [project] table. (epythet 0.1.x silently preferred setup.cfg, which was wrong for the projects that had migrated to pyproject.toml but kept a stale setup.cfg.)

The result is a DocsConfig, an immutable dataclass. The legacy 5-tuple accessor epythet.config_parser.parse_config() is derived from it and keeps its signature, so old docsrc/conf.py copies keep working.

The [tool.epythet] keys, all optional:

[tool.epythet]
display_name = "Dol"          # site title; default: the project name
copyright = "2024, Jane Doe"  # footer line; default: none rendered
theme = "auto"                # "auto" | "furo" | "shibuya" | ... | any installed theme
accent = "#3661ac"            # default: derived from the package name (OKLCH)
mode = "auto"                 # "auto" | "light" | "dark"
ignore = ["tests/", "scrap/", "examples/"]  # path substrings to skip
api_generator = "auto"        # "auto" | "autosummary" (imports the package) | "autoapi" (static)
agent_outputs = true          # llms.txt + .md twins of every page
aggregates = ["md"]           # flat single-document twins at the site root
ai_artifacts = true           # "For AI agents" page when skills/agents/CLAUDE.md exist
ai_artifacts_template = ""    # project-relative file overriding that page's template
provenance = true             # build footer, about-this-build page, build_info.json; "minimal": no page
provenance_template = ""      # project-relative file overriding the about-this-build page template
package_dir = "src/dol"       # default: found by convention
docs_dir = "docsrc"           # where the Sphinx sources live

[tool.epythet.theme_options]  # verbatim passthrough into html_theme_options
[tool.epythet.readme]         # project override of the user-level README policy (see epythet.userconfig)
announcement = "v2 is in beta"
>>> import tempfile, pathlib
>>> d = pathlib.Path(tempfile.mkdtemp())
>>> _ = (d / "pyproject.toml").write_text('''
... [project]
... name = "my-pkg"
... version = "1.2.3"
... authors = [{name = "Jane Doe"}]
... [tool.epythet]
... theme = "furo"
... ''')
>>> _ = (d / "my_pkg").mkdir()
>>> _ = (d / "my_pkg" / "__init__.py").write_text("")
>>> cfg = load_config(d)
>>> cfg.name, cfg.version, cfg.author, cfg.theme, cfg.package_dir.name
('my-pkg', '1.2.3', 'Jane Doe', 'furo', 'my_pkg')

Module Attributes

DEFAULT_IGNORE

Path substrings skipped by default when discovering modules to document.

ALWAYS_IGNORE

a __main__ is a command line, not an API.

DEFAULT_DOCS_DIR

Directory under the project root holding the Sphinx sources.

PACKAGE_DIR_CANDIDATES

Directory candidates (relative to the project root) that may hold the package.

NON_PACKAGE_DIRS

Top-level directories never taken for the package when guessing by convention.

IMPORT_PROBE_TIMEOUT

Seconds allowed for the import probe behind api_generator = "auto".

Functions

find_package_dir(project_dir, name)

Locate the package directory for name under project_dir by convention.

load_config(project_dir, **overrides)

Read a project's documentation configuration.

resolve_api_generator(config)

The generator to run: autosummary when the package imports, else autoapi.

split_ignore(ignore)

Normalise ignore patterns: each item may itself be a comma-separated list.

Classes

DocsConfig(project_dir, name[, version, ...])

Everything needed to generate a project's documentation.

Exceptions

ConfigError

A project's documentation configuration is missing or invalid.

epythet.config.ALWAYS_IGNORE: tuple[str, ...] = ('__main__',)

a __main__ is a command line, not an API. (autosummary still imports it once while discovering the package, as it does every submodule; a __main__ that runs argparse at import survives that because Sphinx turns its SystemExit into a skipped import.)

Type:

Modules that never get a page, whatever ignore says

exception epythet.config.ConfigError[source]

Bases: ValueError

A project’s documentation configuration is missing or invalid.

epythet.config.DEFAULT_DOCS_DIR = 'docsrc'

Directory under the project root holding the Sphinx sources.

epythet.config.DEFAULT_IGNORE: tuple[str, ...] = ('tests/', 'scrap/', 'examples/')

Path substrings skipped by default when discovering modules to document.

class epythet.config.DocsConfig(project_dir, name, version='', author='', description='', display_name='', copyright='', repo_url='', theme='auto', accent='', mode='auto', theme_options=<factory>, readme=<factory>, ignore=('tests/', 'scrap/', 'examples/'), api_generator='auto', agent_outputs=True, aggregates=('md', ), ai_artifacts=True, ai_artifacts_template='', provenance=True, provenance_template='', package_dir=None, docs_dir='docsrc')[source]

Bases: object

Everything needed to generate a project’s documentation.

Attributes mirror the [tool.epythet] keys; see the module docstring. project_dir and package_dir are absolute paths.

property api_ignore: tuple[str, ...]

what the API generators skip.

>>> DocsConfig(project_dir="/tmp/x", name="x", ignore=["tests/"]).api_ignore
('tests/', '__main__')
Type:

ignore plus ALWAYS_IGNORE

property docsrc_dir: Path

Absolute path of the Sphinx source directory.

property legacy_tuple: tuple[str, str, str, str, str]

The 5-tuple that epythet.config_parser.parse_config() returns.

property package_name: str

The importable package name (my-pkg becomes my_pkg).

property resolved_api_generator: str

see resolve_api_generator().

Type:

api_generator with "auto" resolved

with_overrides(**changes)[source]

A copy with some fields replaced (None values are ignored).

Return type:

DocsConfig

epythet.config.IMPORT_PROBE_TIMEOUT = 120

Seconds allowed for the import probe behind api_generator = "auto".

epythet.config.NON_PACKAGE_DIRS = frozenset({'docs', 'docsrc', 'examples', 'misc', 'scrap', 'test', 'tests'})

Top-level directories never taken for the package when guessing by convention.

epythet.config.PACKAGE_DIR_CANDIDATES: tuple[str, ...] = ('{name}', 'src/{name}')

Directory candidates (relative to the project root) that may hold the package.

epythet.config.find_package_dir(project_dir, name)[source]

Locate the package directory for name under project_dir by convention.

Tries <name>/ then src/<name>/ (with - mapped to _), returning the first that contains an __init__.py; None when nothing matches.

Return type:

Path | None

>>> find_package_dir("/nonexistent", "nothing") is None
True
epythet.config.load_config(project_dir, **overrides)[source]

Read a project’s documentation configuration.

Parameters:
  • project_dir (str | Path) – the project root (holding pyproject.toml or setup.cfg), or a path to one of those files.

  • overrides – field values that win over the files (None is ignored), e.g. ignore=["tests/"] from a command line flag.

Raises:

ConfigError – when neither configuration file is found.

Return type:

DocsConfig

epythet.config.resolve_api_generator(config)[source]

The generator to run: autosummary when the package imports, else autoapi.

autosummary imports the package and documents what it finds (aliases, partials, re-exports); when the import fails, in CI typically because an optional dependency is missing, it produces an empty API section and a successful build. auto probes the import once, in a subprocess with the project root on sys.path (as the build has it), and falls back to the static autoapi generator, printing why. An explicit value is returned as is, and so is "auto" when the package directory is unknown.

Return type:

str

epythet.config.split_ignore(ignore)[source]

Normalise ignore patterns: each item may itself be a comma-separated list.

The publish action passes its ignore input verbatim as one argument, --ignore tests/,scrap/,examples/, and setup.cfg values are strings; both must mean three patterns, not one that never matches.

Return type:

tuple[str, ...]

>>> split_ignore(["tests/,scrap/", " examples/ ", "", "tests/"])
('tests/', 'scrap/', 'examples/')