Skip to content

Commit 00a52a1

Browse files
authored
Add colored CLI help formatting and respect NO_COLOR (#109)
* Add a colored help formatter Compared to click's default formatter, this highlights options, usuage, headings and formats option lists in a linear style (descriptions of options are on a new line). Options are also aligned so that single-dash options like "-v" stand-out more. This will also respect the NO_COLOR envirornment variable now. Also adds a short "-g" option for "--group-errors". This is loosely inspired by spin [1]. [1] https://github.com/scientific-python/spin/blob/edbd6572f7fc53fa0914fe6337b1bb2599e457fa/spin/color_format.py * Respect NO_COLOR environment variable for logged output too * Fix stubtest & typing errors * Remove unused noqa comment
1 parent 7b85beb commit 00a52a1

File tree

10 files changed

+412
-48
lines changed

10 files changed

+412
-48
lines changed

docs/command_line.md

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
The reference for docstub's command line interface.
44
It uses [Click](https://click.palletsprojects.com/en/stable/), so [shell completion](https://click.palletsprojects.com/en/stable/shell-completion/) can be enabled.
55

6+
Colored command line output can be disabled by [setting the environment variable `NO_COLOR=1`](https://no-color.org).
7+
8+
69
## `docstub`
710

811
<!--- The following block is checked by the test suite --->
@@ -14,8 +17,10 @@ Usage: docstub [OPTIONS] COMMAND [ARGS]...
1417
Generate Python stub files from docstrings.
1518
1619
Options:
17-
--version Show the version and exit.
18-
-h, --help Show this message and exit.
20+
--version
21+
Show the version and exit.
22+
-h, --help
23+
Show this message and exit.
1924
2025
Commands:
2126
clean Clean the cache.
@@ -35,35 +40,43 @@ Usage: docstub run [OPTIONS] PACKAGE_PATH
3540
3641
Generate Python stub files.
3742
38-
Given a `PACKAGE_PATH` to a Python package, generate stub files for it. Type
43+
Given a PACKAGE_PATH to a Python package, generate stub files for it. Type
3944
descriptions in docstrings will be used to fill in missing inline type
4045
annotations or to override them.
4146
4247
Options:
43-
-o, --out-dir PATH Set output directory explicitly. Stubs will be
44-
directly written into that directory while preserving
45-
the directory structure under `PACKAGE_PATH`.
46-
Otherwise, stubs are generated inplace.
47-
--config PATH Set one or more configuration file(s) explicitly.
48-
Otherwise, it will look for a `pyproject.toml` or
49-
`docstub.toml` in the current directory.
50-
--ignore GLOB Ignore files matching this glob-style pattern. Can be
51-
used multiple times.
52-
--group-errors Group identical errors together and list where they
53-
occurred. Will delay showing errors until all files
54-
have been processed. Otherwise, simply report errors
55-
as the occur.
56-
--allow-errors INT Allow this many or fewer errors. If docstub reports
57-
more, exit with error code '1'. This is useful to
58-
adopt docstub gradually. [default: 0; x>=0]
59-
-W, --fail-on-warning Return non-zero exit code when a warning is raised.
60-
Will add to '--allow-errors'.
61-
--no-cache Ignore pre-existing cache and don't create a new one.
62-
-v, --verbose Print more details. Use once to show information
63-
messages. Use -vv to print debug messages.
64-
-q, --quiet Print less details. Use once to hide warnings. Use
65-
-qq to completely silence output.
66-
-h, --help Show this message and exit.
48+
-o, --out-dir PATH
49+
Set output directory explicitly. Stubs will be directly written into
50+
that directory while preserving the directory structure under
51+
PACKAGE_PATH. Otherwise, stubs are generated inplace.
52+
--config PATH
53+
Set one or more configuration file(s) explicitly. Otherwise, it will
54+
look for a `pyproject.toml` or `docstub.toml` in the current
55+
directory.
56+
--ignore GLOB
57+
Ignore files matching this glob-style pattern. Can be used multiple
58+
times.
59+
-g, --group-errors
60+
Group identical errors together and list where they occurred. Will
61+
delay showing errors until all files have been processed. Otherwise,
62+
simply report errors as the occur.
63+
--allow-errors INT
64+
Allow this many or fewer errors. If docstub reports more, exit with
65+
error code 1. This is useful to adopt docstub gradually. [default:
66+
0; x>=0]
67+
-W, --fail-on-warning
68+
Return non-zero exit code when a warning is raised. Will add to
69+
--allow-errors.
70+
--no-cache
71+
Ignore pre-existing cache and don't create a new one.
72+
-v, --verbose
73+
Print more details. Use once to show information messages. Use -vv to
74+
print debug messages.
75+
-q, --quiet
76+
Print less details. Use once to hide warnings. Use -qq to completely
77+
silence output.
78+
-h, --help
79+
Show this message and exit.
6780
```
6881

6982
<!--- end cli-docstub-run --->
@@ -83,11 +96,14 @@ Usage: docstub clean [OPTIONS]
8396
one exists, remove it.
8497
8598
Options:
86-
-v, --verbose Print more details. Use once to show information messages.
87-
Use -vv to print debug messages.
88-
-q, --quiet Print less details. Use once to hide warnings. Use -qq to
89-
completely silence output.
90-
-h, --help Show this message and exit.
99+
-v, --verbose
100+
Print more details. Use once to show information messages. Use -vv to
101+
print debug messages.
102+
-q, --quiet
103+
Print less details. Use once to hide warnings. Use -qq to completely
104+
silence output.
105+
-h, --help
106+
Show this message and exit.
91107
```
92108

93109
<!--- end cli-docstub-clean --->

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ ignore = [
104104
"PLR2004", # Magic value used in comparison
105105
"ISC001", # Conflicts with formatter
106106
"RET504", # Assignment before `return` statement facilitates debugging
107+
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
107108
"PTH123", # Using builtin open() instead of Path.open() is fine
108109
"SIM108", # Terniary operator is always more readable
109110
"SIM103", # Don't recommend returning the condition directly

src/docstub-stubs/_cli.pyi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ from pathlib import Path
1111
from typing import Literal
1212

1313
import click
14+
from _typeshed import Incomplete
1415

1516
from ._analysis import PyImport, TypeCollector, TypeMatcher, common_known_types
1617
from ._cache import CACHE_DIR_NAME, FileCache, validate_cache
18+
from ._cli_help import HelpFormatter
1719
from ._config import Config
1820
from ._path_utils import (
1921
STUB_HEADER_COMMENT,
@@ -37,6 +39,9 @@ def _collect_type_info(
3739
) -> tuple[dict[str, PyImport], dict[str, PyImport]]: ...
3840
def _format_unknown_names(names: Iterable[str]) -> str: ...
3941
def log_execution_time() -> None: ...
42+
43+
click.Context.formatter_class = HelpFormatter
44+
4045
@click.group()
4146
def cli() -> None: ...
4247
def _add_verbosity_options(func: Callable) -> Callable: ...

src/docstub-stubs/_cli_help.pyi

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# File generated with docstub
2+
3+
import logging
4+
import os
5+
import re
6+
from collections.abc import Sequence
7+
from typing import IO, Any, ClassVar
8+
9+
import click
10+
from click.formatting import iter_rows, measure_table, wrap_text
11+
12+
logger: logging.Logger
13+
14+
try:
15+
from click._compat import should_strip_ansi as _click_should_strip_ansi
16+
17+
except Exception:
18+
19+
def _click_should_strip_ansi(
20+
stream: IO[Any] | None = ..., color: bool | None = ...
21+
) -> bool: ...
22+
23+
def should_strip_ansi(
24+
stream: IO[Any] | None = ..., color: bool | None = ...
25+
) -> bool: ...
26+
27+
class HelpFormatter(click.formatting.HelpFormatter):
28+
strip_ansi: bool
29+
30+
rule_defs: ClassVar[dict[str, tuple[str, str]]]
31+
32+
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
33+
def write_dl(
34+
self,
35+
rows: Sequence[tuple[str, str]],
36+
*args: Any,
37+
**kwargs: Any,
38+
) -> None: ...
39+
def write_heading(self, heading: str) -> None: ...
40+
def write_usage(
41+
self, prog: str, args: str = ..., prefix: str | None = ...
42+
) -> None: ...
43+
def _highlight_last(self, *, n: int, rules: list[str]) -> None: ...
44+
def _highlight(self, string: str, *, rules: list[str]) -> str: ...

src/docstub-stubs/_report.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ from typing import Any, ClassVar, Literal, Self, TextIO
88

99
import click
1010

11+
from ._cli_help import should_strip_ansi
12+
1113
logger: logging.Logger
1214

1315
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)

src/docstub/_cli.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
common_known_types,
1616
)
1717
from ._cache import CACHE_DIR_NAME, FileCache, validate_cache
18+
from ._cli_help import HelpFormatter
1819
from ._config import Config
1920
from ._path_utils import (
2021
STUB_HEADER_COMMENT,
@@ -213,7 +214,11 @@ def log_execution_time():
213214
logger.info("Finished in %s", formated_duration)
214215

215216

217+
# Overwrite click's default formatter class (stubtest balks at this)
216218
# docstub: off
219+
click.Context.formatter_class = HelpFormatter
220+
221+
217222
@click.group()
218223
# docstub: on
219224
@click.version_option(__version__)
@@ -262,7 +267,7 @@ def _add_verbosity_options(func):
262267
metavar="PATH",
263268
help="Set output directory explicitly. "
264269
"Stubs will be directly written into that directory while preserving the directory "
265-
"structure under `PACKAGE_PATH`. "
270+
"structure under PACKAGE_PATH. "
266271
"Otherwise, stubs are generated inplace.",
267272
)
268273
@click.option(
@@ -283,6 +288,7 @@ def _add_verbosity_options(func):
283288
help="Ignore files matching this glob-style pattern. Can be used multiple times.",
284289
)
285290
@click.option(
291+
"-g",
286292
"--group-errors",
287293
is_flag=True,
288294
help="Group identical errors together and list where they occurred. "
@@ -296,15 +302,15 @@ def _add_verbosity_options(func):
296302
show_default=True,
297303
metavar="INT",
298304
help="Allow this many or fewer errors. "
299-
"If docstub reports more, exit with error code '1'. "
305+
"If docstub reports more, exit with error code 1. "
300306
"This is useful to adopt docstub gradually. ",
301307
)
302308
@click.option(
303309
"-W",
304310
"--fail-on-warning",
305311
is_flag=True,
306312
help="Return non-zero exit code when a warning is raised. "
307-
"Will add to '--allow-errors'.",
313+
"Will add to --allow-errors.",
308314
)
309315
@click.option(
310316
"--no-cache",
@@ -329,7 +335,7 @@ def run(
329335
):
330336
"""Generate Python stub files.
331337
332-
Given a `PACKAGE_PATH` to a Python package, generate stub files for it.
338+
Given a PACKAGE_PATH to a Python package, generate stub files for it.
333339
Type descriptions in docstrings will be used to fill in missing inline type
334340
annotations or to override them.
335341
\f
@@ -456,7 +462,8 @@ def run(
456462
logger.warning("Syntax errors: %i", syntax_error_count)
457463
if unknown_type_names:
458464
logger.warning(
459-
"Unknown type names: %i",
465+
"Unknown type names: %i (locations: %i)",
466+
len(set(unknown_type_names)),
460467
len(unknown_type_names),
461468
extra={"details": _format_unknown_names(unknown_type_names)},
462469
)

0 commit comments

Comments
 (0)