Skip to content
Merged
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
1 change: 1 addition & 0 deletions python/cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"your_name": "",
"your_email": "",
"add_docs": [false, true],
"add_cli": [false, true],
"add_fastapi": [false, true]
}
8 changes: 7 additions & 1 deletion python/hooks/post_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@
shutil.rmtree("docs")
Path(".readthedocs.yaml").unlink()

if not {{ cookiecutter.add_cli }}:
Path("./src/{{ cookiecutter.project_slug }}/cli.py").unlink()
if {{ cookiecutter.add_docs }}:
Path("./docs/source/cli_reference.rst").unlink()

if not {{ cookiecutter.add_fastapi }}:
Path("tests/test_api.py").unlink()
Path("src/{{ cookiecutter.project_slug }}/api.py").unlink()
Path("src/{{ cookiecutter.project_slug }}/models.py").unlink()
Path("src/{{ cookiecutter.project_slug }}/config.py").unlink()
Path("src/{{ cookiecutter.project_slug }}/logging.py").unlink()

if (not {{ cookiecutter.add_fastapi }}) and (not {{ cookiecutter.add_cli }}):
Path("./src/{{ cookiecutter.project_slug }}/logging.py").unlink()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. _cli-reference:

Command-line interface
----------------------

.. click:: {{ cookiecutter.project_slug }}.cli:cli
:prog: {{ cookiecutter.project_slug | replace("_", "-") }}
:nested: full
103 changes: 103 additions & 0 deletions python/{{cookiecutter.project_slug}}/docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,106 @@ def linkcode_resolve(domain, info):
# -- code block style --------------------------------------------------------
pygments_style = "default"
pygements_dark_style = "monokai"


# -- sphinx-click ------------------------------------------------------------
# These functions let us write descriptions/docstrings in a way that doesn't look
# weird in the Click CLI, but get additional formatting in the sphinx-click autodocs for
# better readability.
import re

from click.core import Context
from sphinx.application import Sphinx
from sphinx_click.ext import _get_usage, _indent


CMD_PATTERN = r"--[^ ]+"
STR_PATTERN = r"\"[^ ]+\""
SNAKE_PATTERN = r"[A-Z]+_[A-Z_]*[A-Z][., ]"


def _add_formatting_to_string(line: str) -> str:
"""Add fixed-width code formatting to span sections in lines:

* shell options, eg `--update_all`
* double-quoted strings, eg `"HGNC"`
* all caps SNAKE_CASE env vars, eg `GENE_NORM_REMOTE_DB_URL`
"""
for pattern in (CMD_PATTERN, STR_PATTERN, SNAKE_PATTERN):
line = re.sub(pattern, lambda x: f"``{x.group()}``", line)
return line


def process_description(app: Sphinx, ctx: Context, lines: list[str]):
"""Add custom formatting to sphinx-click autodoc descriptions.

* remove :param: :return: etc
* add fixed-width (code) font to certain words
* add code block formatting to example shell commands
* move primary usage example to the top of the description

Because we have to modify the lines list in place, we have to make multiple passes
through it to format everything correctly.
"""
if not lines:
return

# chop off params
param_boundary = None
for i, line in enumerate(lines):
if ":param" in line:
param_boundary = i
break
if param_boundary is not None:
del lines[param_boundary:]
lines[-1] = ""

# add code formatting to strings, commands, and env vars
lines_to_fmt = []
for i, line in enumerate(lines):
if line.startswith((" ", ">>> ", "|")):
continue # skip example code blocks
if any(
[
re.findall(CMD_PATTERN, line),
re.findall(STR_PATTERN, line),
re.findall(SNAKE_PATTERN, line),
]
):
lines_to_fmt.append(i)
for line_num in lines_to_fmt:
lines[line_num] = _add_formatting_to_string(lines[line_num])

# add code block formatting to example console commands
for i in range(len(lines) - 1, -1, -1):
if lines[i].startswith((" ", "| ")):
if lines[i].startswith("| "):
lines[i] = lines[i][3:]
if (i == 0 or lines[i - 1] == "\b" or lines[i - 1] == ""):
lines.insert(i, "")
lines.insert(i, ".. code-block:: console")

# put usage at the top of the description
lines.insert(0, "")
for usage_line in _get_usage(ctx).splitlines()[::-1]:
lines.insert(0, _indent(usage_line))
lines.insert(0, "")
lines.insert(0, ".. code-block:: shell")


def process_option(app: Sphinx, ctx: Context, lines: list[str]):
"""Add fixed-width formatting to strings in sphinx-click autodoc options."""
for i, line in enumerate(lines):
if re.findall(STR_PATTERN, line):
lines[i] = re.sub(STR_PATTERN, lambda x: f"``{x.group()}``", line)


def setup(app):
"""Used to hook format customization into sphinx-click build.

In particular, since we move usage to the top of the command description, we need
an extra hook here to silence the built-in usage section.
"""
app.connect("sphinx-click-process-description", process_description)
app.connect("sphinx-click-process-options", process_option)
app.connect("sphinx-click-process-usage", lambda app, ctx, lines: lines.clear())
3 changes: 3 additions & 0 deletions python/{{cookiecutter.project_slug}}/docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
Installation<install>
Usage<usage>
API Reference<reference/index>
{%- if cookiecutter.add_cli %}
CLI Reference<cli>
{%- endif %}
Changelog<changelog>
Contributing<contributing>
License<license>
23 changes: 16 additions & 7 deletions python/{{cookiecutter.project_slug}}/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ classifiers = [
requires-python = ">=3.11"
description = "{{ cookiecutter.description }}"
license = {file = "LICENSE"}
{%- if cookiecutter.add_fastapi %}
dependencies = [
{%- if cookiecutter.add_cli %}
"click",
{%- endif %}
{%- if cookiecutter.add_fastapi %}
"fastapi",
"pydantic~=2.1",
{%- endif %}
]
{% else %}
dependencies = []
{% endif -%}
dynamic = ["version"]

[project.optional-dependencies]
Expand All @@ -48,9 +49,12 @@ docs = [
"sphinx-copybutton==0.5.2",
"sphinxext-opengraph==0.8.2",
"furo==2023.3.27",
"sphinx-github-changelog==1.2.1"
"sphinx-github-changelog==1.2.1",
{%- if cookiecutter.add_cli %}
"sphinx-click==5.0.1",
{%- endif %}
]
{% endif %}
{%- endif %}

[project.urls]
Homepage = "https://github.com/{{ cookiecutter.org }}/{{ cookiecutter.repo }}"
Expand All @@ -60,6 +64,9 @@ Source = "https://github.com/{{ cookiecutter.org }}/{{ cookiecutter.repo }}"
"Bug Tracker" = "https://github.com/{{ cookiecutter.org }}/{{ cookiecutter.repo }}/issues"

[project.scripts]
{%- if cookiecutter.add_cli %}
{{ cookiecutter.project_slug | replace("_", "-") }} = "{{ cookiecutter.project_slug }}.cli:cli"
{%- endif %}

[build-system]
requires = ["setuptools>=64", "setuptools_scm>=8"]
Expand All @@ -79,7 +86,9 @@ branch = true

[tool.ruff]
src = ["src"]
{% if cookiecutter.add_docs %}exclude = ["docs/source/conf.py"]{% endif %}
{%- if cookiecutter.add_docs %}
exclude = ["docs/source/conf.py"]
{%- endif %}

[tool.ruff.lint]
select = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Provide CLI for application."""

import click

from {{ cookiecutter.project_slug }} import __version__
from {{ cookiecutter.project_slug }}.logging import initialize_logs


@click.group()
@click.version_option(__version__)
def cli() -> None:
"""Short description of CLI.

\b
$ echo "provide a multiline description with a leading \\b"
$ echo "more commands here"
$ echo "otherwise, a single indent will pick up proper formatting"

Conclude by summarizing additional commands
""" # noqa: D301
initialize_logs()
Loading