diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..68c0313 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,17 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version, and other tools you might need +build: + os: ubuntu-24.04 + tools: + python: "3.13" + commands: + # https://docs.readthedocs.com/platform/stable/build-customization.html#install-dependencies-with-uv + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + - uv run --group docs sphinx-build -T -b html -d docs/_build/doctrees -D language=en docs $READTHEDOCS_OUTPUT/html diff --git a/README.md b/README.md index d2b736e..7675f94 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,15 @@ > [!NOTE] > **In early development!** -> Expect bugs, missing features, and incomplete documentation. -> Docstub is still evaluating which features it needs to support as the community gives feedback. -> Several features are experimental and included to make adoption of docstub easier. -> Long-term, some of these might be discouraged or removed as docstub matures. +> Docstub is not feature-complete or thoroughly tested yet. +> Its behavior, configuration or command line interface may change significantly between releases. -docstub is a command-line tool to generate [Python stub files](https://typing.python.org/en/latest/guides/writing_stubs.html) (i.e., PYI files) from type descriptions found in [numpydoc](https://numpydoc.readthedocs.io)-style docstrings. +docstub is a command-line tool to generate [Python stub files](https://typing.python.org/en/latest/guides/writing_stubs.html). +It extracts necessary type information from [NumPyDoc style](https://numpydoc.readthedocs.io) docstrings. Many packages in the scientific Python ecosystem already describe expected parameter and return types in their docstrings. Docstub aims to take advantage of these and help with the adoption of type annotations. -It does so by supporting widely used readable conventions such as `array of dtype` or `iterable of int(s)` which it translates into valid type annotations. +It does so by supporting widely used readable conventions such as `array of dtype` or `iterable of int(s)` which are translated into valid type annotations. ## Installation & getting started diff --git a/docs/command_line.md b/docs/command_line.md index 16089a9..a782969 100644 --- a/docs/command_line.md +++ b/docs/command_line.md @@ -1,11 +1,14 @@ -# Command line reference +# Command line -## Command `docstub` +The reference for docstub's command line interface. +It uses [Click](https://click.palletsprojects.com/en/stable/), so [shell completion](https://click.palletsprojects.com/en/stable/shell-completion/) can be enabled. + +## `docstub` -```plain +```none Usage: docstub [OPTIONS] COMMAND [ARGS]... Generate Python stub files from docstrings. @@ -22,12 +25,12 @@ Commands: -## Command `docstub run` +## `docstub run` -```plain +```none Usage: docstub run [OPTIONS] PACKAGE_PATH Generate Python stub files. @@ -66,12 +69,12 @@ Options: -## Command `docstub clean` +## `docstub clean` -```plain +```none Usage: docstub clean [OPTIONS] Clean the cache. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..25d7b40 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,73 @@ +# Configuration file for the Sphinx documentation builder. +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +from datetime import date + +# -- Project information ------------------------------------------------------ + +project = "docstub" +copyright = f"{date.today().year}, docstub contributors" + +templates_path = ["templates"] + + +# -- Extension configuration -------------------------------------------------- + +extensions = [ + "sphinx.ext.intersphinx", + "sphinx_copybutton", + # https://numpydoc.readthedocs.io/ + "myst_parser", +] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "typing": ("https://typing.python.org/en/latest/", None), +} + +myst_enable_extensions = [ + # Enable fieldlist to allow for Field Lists like in rST (e.g., :orphan:) + "fieldlist", + # Enable fencing directives with `:::` + "colon_fence", +] + +myst_heading_anchors = 3 + + +# -- HTML output -------------------------------------------------------------- + +html_theme = "furo" + +html_static_path = ["static"] + +html_css_files = ["furo_overrides.css"] + +html_title = "docstub docs" + +html_theme_options = { + "light_css_variables": { + # Make font less harsh on light theme + "color-foreground-primary": "#363636", + "color-announcement-background": "var(--color-admonition-title-background--important)", + "color-announcement-text": "var(--color-content-foreground)", + "admonition-font-size": "var(--font-size--normal)", + }, + "dark_css_variables": { + "color-announcement-background": "var(--color-admonition-title-background--important)", + }, + "announcement": "🧪 In early development! API and behavior may break between releases.", +} + +html_sidebars = { + "**": [ + "sidebar/brand.html", + "sidebar/search.html", + "sidebar/scroll-start.html", + "sidebar/navigation.html", + "external-links.html", + "sidebar/ethical-ads.html", + "sidebar/scroll-end.html", + "sidebar/variant-selector.html", + ] +} diff --git a/docs/configuration.md b/docs/configuration.md index a0a9024..d5ac9fb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,15 +1,17 @@ -# Configuration reference +# Configuration files -Docstub will automatically look for configuration in files named +Docstub will automatically look for configuration files in the current directory. +These files must be named -- `pyproject.toml`, and +- `pyproject.toml` - `docstub.toml` -in the current working directory. -If config files are explicitly passed to the command line interface via the `--config` option, docstub won't look implicitly look for files in the current directory. -Multiple configuration files can be used, whose content will be merged. +Alternatively, config files can also be passed in the command line with the `--config` option. +In this case, docstub will not look for configuration files in the current directory. +When multiple configuration files are passed explicitly, their content is merged. -Out of the box, docstub makes use of an internal configuration file [`numpy_config.toml`](../src/docstub/numpy_config.toml) which provides defaults to use NumPy types. +Out of the box, docstub makes use of an internal configuration files are always loaded. +One such file is [`numpy_config.toml`](../src/docstub/numpy_config.toml) which provides defaults for NumPy types. ## Configuration fields in `[tool.docstub]` @@ -19,7 +21,7 @@ All configuration must be declared inside a `[tool.docstub]` table. ### `ignore_files` -- [TOML type](https://toml.io/en/latest): array of string(s) +[TOML type](https://toml.io/en/latest): array of string(s) Ignore files and directories matching these [glob-style patterns](https://docs.python.org/3/library/glob.html#glob.translate). Patterns that don't start with "/" are interpreted as relative to the @@ -39,7 +41,7 @@ ignore_files = [ ### `types` -- [TOML type](https://toml.io/en/latest): table, mapping string to string +[TOML type](https://toml.io/en/latest): table, mapping string to string Types and their external modules to use in docstrings. Docstub can't yet automatically discover where to import types from other packages from. @@ -60,7 +62,7 @@ NDArray = "numpy.typing" ### `type_prefixes` -- [TOML type](https://toml.io/en/latest): table, mapping string to string +[TOML type](https://toml.io/en/latest): table, mapping string to string Prefixes for external modules to match types in docstrings. Docstub can't yet automatically discover where to import types from other packages from. @@ -81,7 +83,7 @@ plt = "matplotlib.pyplot ### `type_nicknames` -- [TOML type](https://toml.io/en/latest): table, mapping string to string +[TOML type](https://toml.io/en/latest): table, mapping string to string Nicknames for types that can be used in docstrings to describe valid Python types or annotations. @@ -90,6 +92,8 @@ Example: ```toml [tool.docstub.type_nicknames] func = "Callable" +buffer = "collections.abc.Buffer" ``` -- Will map `func` to the `Callable` type from the `typing` module. +- Will map `func` to the `Callable`. +- Will map `buffer` to `collections.abc.Buffer`. diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 0000000..02178aa --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,24 @@ +# Glossary + +This section defines central terms used in this documentation. + +:::{glossary} +:sorted: + +doctype + A type description of a type in a docstring, such as of a parameter, return value, or attribute. + Any {term}`annotation expression` is valid as a doctype, but doctypes support an [extended syntax](typing_syntax.md) with natural language variants. + +type name + The name of a single (atomic) type. + A type name can include a {term}`type prefix`. + An {term}`annotation expression` can contain multiple typen names. + For example, the annotation expression `collections.abc.Iterable[int or float]` consists of the three names `collections.abc.Iterable`, `int` and `float`. + +type prefix + A dot-delimited prefix that is part of a {term}`type name`. + The prefix can describe the full path of a type or consist of an alias. + For example, `collections.abc.Iterable` has the type prefix `collections.abc`. + `np.int` has the prefix `np` which may be an alias for `numpy`. + [Type prefixes can be defined in the configuration](configuration.md#type_prefixes) or are inferred by docstub from import statements it can see. +::: \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..bf54a8f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,35 @@ +# docstub documentation + +:::{admonition} In early development! +:class: important + +Docstub is not feature-complete or thoroughly tested yet. +Its behavior, configuration or command line interface may change significantly between releases. +::: + +docstub is a command-line tool to generate [Python stub files](https://typing.python.org/en/latest/spec/distributing.html#stub-files). +It extracts necessary type information from [NumPyDoc style](https://numpydoc.readthedocs.io) docstrings. + +Many packages in the scientific Python ecosystem already describe expected parameter and return types in their docstrings. +Docstub aims to take advantage of these and help with the adoption of type annotations. +It does so by supporting widely used readable conventions such as `array of dtype` or `iterable of int(s)` which are translated into valid type annotations. + +--- + +:::{toctree} +:caption: User guides +:maxdepth: 1 + +introduction +::: + +:::{toctree} +:caption: Reference +:maxdepth: 1 + +command_line +configuration +typing_syntax +glossary +release_notes/index +::: diff --git a/docs/user_guide.md b/docs/introduction.md similarity index 79% rename from docs/user_guide.md rename to docs/introduction.md index e6d72e7..21b5bdb 100644 --- a/docs/user_guide.md +++ b/docs/introduction.md @@ -1,12 +1,4 @@ -# User guide - -> [!NOTE] -> **In early development!** -> Expect bugs, missing features, and incomplete documentation. -> Docstub is still evaluating which features it needs to support as the community gives feedback. -> Several features are experimental and included to make adoption of docstub easier. -> Long-term, some of these might be discouraged or removed as docstub matures. - +# Introduction ## Installation @@ -19,22 +11,20 @@ pip install docstub In case you want to check out an unreleased version you can install directly from the repository with: ```shell -pip install docstub @ git+https://github.com/scientific-python/docstub' +pip install 'docstub @ git+https://github.com/scientific-python/docstub' ``` To pin to a specific commit you can append `@COMMIT_SHA` to the repository URL above. -## Getting started +## First example -Consider a simple example with the following documented function +Consider a simple file `example.py` with the following documented function -```python -# example.py - +```{code-block} python def example_metric(image, *, mask=None, sigma=1.0, method='standard'): """Pretend to calculate a local metric between two images. @@ -65,12 +55,12 @@ Feeding this input to docstub with docstub run example.py ``` -will create `example.pyi` in the same directory +will create the [stub file](https://typing.python.org/en/latest/spec/distributing.html#stub-files) `example.pyi` in the same directory -```python +```{code-block} python # File generated with docstub from collections.abc import Iterable @@ -93,15 +83,15 @@ def example_metric( There are several interesting things to note here: - Many existing conventions that the scientific Python ecosystem uses, will work out of the box. - In this case, docstub knew how to translate `array-like`, `array of dtype uint8` into a valid Python type for the stub file. - In a similar manner, `or` can be used as a "natural language" alternative to `|` to form unions. - You can find more details in [Typing syntax in docstrings](typing_syntax.md). + In this case, docstub knew how to translate `array-like`, `array of dtype uint8` into a valid {term}`annotation expression` for the stub file. + In a similar manner, `or` was used as a "natural language" alternative to `|` to form unions. + This alternative extended syntax is described in [](typing_syntax.md). - Optional arguments that default to `None` are recognized and a `| None` is appended automatically. The `optional` or `default = ...` part don't influence the annotation. -- Referencing the `float` and `Iterable` types worked out of the box. - All builtin types as well as types from the standard libraries `typing` and `collections.abc` module can be used like this. +- Referencing the `float` and `Iterable` worked out of the box. + All [built-in types](https://docs.python.org/3/library/stdtypes.html#built-in-types) as well as types from the standard library's `typing` and `collections.abc` module can be used like this. Necessary imports will be added automatically to the stub file. @@ -135,16 +125,17 @@ ski = "skimage" which will enable any type that is prefixed with `ski.` or `sklearn.tree.`, e.g. `ski.transform.AffineTransform` or `sklearn.tree.DecisionTreeClassifier`. -> [!IMPORTANT] -> Docstub doesn't check that types actually exist or if a symbol is a valid type. -> We always recommend validating the generated stubs with a full type checker! - -> [!TIP] -> Docstub currently collects types statically. -> So it won't see compiled modules and won't be able to generate stubs for them. -> For now, you can add stubs for compiled modules yourself and docstub will include these in the generated output. -> Support for dynamic type collection is on the roadmap. +:::{important} +Docstub doesn't check that types actually exist or if a symbol is a valid type. +We always recommend validating the generated stubs with a full type checker! +::: +:::{tip} +Docstub currently collects types statically. +So it won't see compiled modules and won't be able to generate stubs for them. +For now, you can add stubs for compiled modules yourself and docstub will include these in the generated output. +Support for dynamic type collection is on the roadmap. +::: The codebase docstub is running on may already use existing conventions to refer to common types (or you may want to do so). Docstub refers to these alternatives as "type nicknames". @@ -167,9 +158,10 @@ Two command line options can help addressing these errors gradually: This way you can adjust the upper bound of allowed errors as they are addressed. Useful, if you are running in docstub in continuous integration. -> [!TIP] -> If you are trying out docstub and have feedback or problems, we'd love to hear from you! -> Feel welcome to [open an issue](https://github.com/scientific-python/docstub/issues/new/choose). 🚀 +:::{tip} +If you are trying out docstub and have feedback or problems, we'd love to hear from you! +Feel welcome to [open an issue](https://github.com/scientific-python/docstub/issues/new/choose). 🚀 +::: ## Dealing with typing problems diff --git a/docs/release_notes/index.md b/docs/release_notes/index.md new file mode 100644 index 0000000..16997f0 --- /dev/null +++ b/docs/release_notes/index.md @@ -0,0 +1,9 @@ +# Release notes + +:::{toctree} +:maxdepth: 1 + +v0.4.0 +v0.3.0 +v0.2.0 +::: diff --git a/docs/release_notes/v0.2.0.md b/docs/release_notes/v0.2.0.md index 43f0859..8a43202 100644 --- a/docs/release_notes/v0.2.0.md +++ b/docs/release_notes/v0.2.0.md @@ -1,4 +1,4 @@ -## docstub 0.2.0 +# docstub 0.2.0 A first prototype of the tool with the following features: diff --git a/docs/static/furo_overrides.css b/docs/static/furo_overrides.css new file mode 100644 index 0000000..0efb107 --- /dev/null +++ b/docs/static/furo_overrides.css @@ -0,0 +1,9 @@ +html { + scroll-behavior: auto; +} + +/* Left-align tables instead of centering them */ +article .align-default { + margin-left: unset; + margin-right: unset; +} diff --git a/docs/templates/external-links.html b/docs/templates/external-links.html new file mode 100644 index 0000000..54dd0ea --- /dev/null +++ b/docs/templates/external-links.html @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/docs/typing_syntax.md b/docs/typing_syntax.md index 4849ccb..2a54612 100644 --- a/docs/typing_syntax.md +++ b/docs/typing_syntax.md @@ -1,19 +1,12 @@ # Typing syntax in docstrings -> [!NOTE] -> **In early development!** -> Expect bugs, missing features, and incomplete documentation. -> Docstub is still evaluating which features it needs to support as the community gives feedback. -> Several features are experimental and included to make adoption of docstub easier. -> Long-term, some of these might be discouraged or removed as docstub matures. - Docstub defines its own [grammar](../src/docstub/doctype.lark) to parse and transform type information in docstrings (doctypes) into valid Python type expressions. This grammar fully supports [Python's conventional typing syntax](https://typing.python.org/en/latest/index.html). So any type expression that is valid in Python, can be used in a docstrings as is. In addition, docstub extends this syntax with several "natural language" expressions that are commonly used in the scientific Python ecosystem. Docstrings should follow a form that is inspired by the NumPyDoc style: -``` +```none Section name ------------ name : doctype, optional_info @@ -21,10 +14,10 @@ name : doctype, optional_info ``` - `name` might be the name of a parameter, attribute or similar. -- `doctype` the actual type information that will be transformed into a Python type. -- `optional_info` is optional and captures anything after the first comma (that is not inside a type expression). +- `doctype` contains the actual type information that will be transformed into a Python type (see also {term}`doctype`). +- `optional_info` is optional and captures anything after the first (top-level) comma. It is useful to provide additional information for readers. - Its presence and content doesn't currently affect the resulting type annotation. + Its presence and content doesn't currently affect the generated {term}`annotation expression`. ## Unions @@ -65,10 +58,11 @@ and **mappings** exist. | `dict of {str: int}` | `dict[str, int]` | -> [!TIP] -> While it is possible to nest these variants repeatedly, it is discouraged to do so to keep type descriptions readable. -> For complex annotations with nested containers, consider using Python's conventional syntax. -> In the future, docstub may warn against or disallow nesting these natural language variants. +:::{tip} +While it is possible to nest these variants repeatedly, it decreases the readability. +For complex nested annotations with nested containers, consider using Python's conventional syntax. +In the future, docstub may warn against or disallow nesting these natural language variants. +::: ## Shape and dtype syntax for arrays @@ -84,10 +78,11 @@ This expression allows adding shape and datatype information for data structures | `array-like of DTYPE` | `ArrayLike[DTYPE]` | | `array_like of dtype DTYPE` | `ArrayLike[DTYPE]` | -> [!NOTE] -> Noting the **shape** of an array in the docstring is supported. -> However, Python's typing system is not yet able to express this information. -> It is therefore not included in the resulting type annotation. +:::{note} +Noting the **shape** of an array in the docstring is supported. +However, Python's typing system is not yet able to express this information. +It is therefore not included in the resulting type annotation. +::: | Docstring type | Python type annotation | |--------------------------|------------------------| @@ -107,16 +102,17 @@ Instead of using [`typing.Literal`](https://docs.python.org/3/library/typing.htm | `{-1, 0, 3, True, False}` | `Literal[-1, 0, 3, True, False]` | | `{"red", "blue", None}` | `Literal["red", "blue", None]` | -> [!TIP] -> Enclosing a single value `{X}` is currently allowed but discouraged. -> Instead consider the more explicit `Literal[X]`. - -> [!WARNING] -> Python's `typing.Literal` only supports a restricted set of parameters. -> E.g., `float` literals are not yet supported by the type system but are allowed by docstub. -> Addressing this use case is on the roadmap. -> See [issue 47](https://github.com/scientific-python/docstub/issues/47) for more details. - +:::{tip} +Enclosing a single value `{X}` is allowed. +However, `Literal[X]` is more explicit. +::: + +:::{warning} +Python's `typing.Literal` only supports a restricted set of parameters. +E.g., `float` literals are not yet supported by the type system but are allowed by docstub. +Addressing this use case is on the roadmap. +See [issue 47](https://github.com/scientific-python/docstub/issues/47) for more details. +::: ## reStructuredText role diff --git a/pyproject.toml b/pyproject.toml index 699b40b..428fcbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,16 +49,25 @@ docstub = "docstub.__main__:cli" [dependency-groups] -dev = [ - "pre-commit >=4.3.0", - "ipython", -] test = [ "pytest >=8.4.1", "pytest-cov >= 5.0.0", "mypy >=1.17.0", "basedpyright >=1.31", ] +docs = [ + "sphinx", + "furo", + "numpydoc", + "myst-parser", + "sphinx-copybutton", +] +dev = [ + {include-group = "test"}, + {include-group = "docs"}, + "pre-commit >=4.3.0", + "ipython", +] [tool.setuptools_scm] diff --git a/src/docstub/__init__.py b/src/docstub/__init__.py index 17da713..85f19b2 100644 --- a/src/docstub/__init__.py +++ b/src/docstub/__init__.py @@ -1,8 +1,6 @@ -""" -Copyright (c) 2024 Lars Grüter. All rights reserved. +# Copyright (c) 2024 Lars Grüter. All rights reserved. -docstub: Generate Python stub files (PYI) from docstrings -""" +"""Generate Python stub files (PYI) from docstrings.""" from ._version import __version__ diff --git a/src/docstub/_report.py b/src/docstub/_report.py index e1e4dbe..1622f8d 100644 --- a/src/docstub/_report.py +++ b/src/docstub/_report.py @@ -304,6 +304,7 @@ def emit_grouped(self): will be grouped together. """ # Group by report + # TODO use itertools.groupby here? groups = {} for record in self._records: group_id = record.getMessage(), getattr(record, "details", "") diff --git a/tests/test_analysis.py b/tests/test_analysis.py index 5f930a2..99a0305 100644 --- a/tests/test_analysis.py +++ b/tests/test_analysis.py @@ -320,6 +320,19 @@ def test_scoped_type_prefix(self, module_factory): assert type_name == "cal.January" assert py_import == PyImport(implicit="sub.module:cal") + def test_nicknames(self, caplog): + types = { + "Buffer": PyImport(from_="collections.abc", import_="Buffer"), + } + type_nicknames = { + "buffer": "collections.abc.Buffer", + } + matcher = TypeMatcher(types=types, type_nicknames=type_nicknames) + + type_name, py_import = matcher.match("buffer") + assert type_name == "Buffer" + assert py_import == PyImport(from_="collections.abc", import_="Buffer") + def test_nested_nicknames(self, caplog): types = { "Foo": PyImport(implicit="Foo"), diff --git a/tests/test_docs.py b/tests/test_docs.py index 4b65884..1c6054e 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -12,16 +12,16 @@ PROJECT_ROOT = Path(__file__).parent.parent -def test_user_guide_example(tmp_path): - # Load user guide - md_file = PROJECT_ROOT / "docs/user_guide.md" +def test_introduction_example(tmp_path): + # Load introduction + md_file = PROJECT_ROOT / "docs/introduction.md" with md_file.open("r") as io: md_content = io.read() # Extract code block for example.py regex_py = ( r"" - r"\n+```python(.*)```\n+" + r"\n+```{code-block} python(.*)```\n+" r"" ) matches_py = re.findall(regex_py, md_content, flags=re.DOTALL) @@ -35,7 +35,7 @@ def test_user_guide_example(tmp_path): runner = CliRunner() run_result = runner.invoke(_cli.run, [str(py_file)]) # noqa: F841 - # Load created PYI file, this is what we expect to find in the user guide's + # Load created PYI file, this is what we expect to find in the introduction's # code block for example.pyi pyi_file = py_file.with_suffix(".pyi") assert pyi_file.is_file() @@ -45,7 +45,7 @@ def test_user_guide_example(tmp_path): # Extract code block for example.pyi from guide regex_pyi = ( r"" - r"\n+```python(.*)```\n+" + r"\n+```{code-block} python(.*)```\n+" r"" ) matches_pyi = re.findall(regex_pyi, md_content, flags=re.DOTALL) @@ -62,7 +62,7 @@ def test_user_guide_example(tmp_path): def test_command_line_reference(command, name): ctx = click.Context(command, info_name=name) expected_help = f""" -```plain +```none {command.get_help(ctx)} ``` """.strip()