diff --git a/config/pytest.ini b/config/pytest.ini index 25f9c92e..4f43c18e 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -10,6 +10,7 @@ testpaths = # action:message_regex:warning_class:module_regex:line filterwarnings = error - # TODO: remove once pytest-xdist 4 is released + # TODO: Remove once pytest-xdist 4 is released. ignore:.*rsyncdir:DeprecationWarning:xdist + # TODO: Remove once mkdocstrings stops setting fallback function. ignore:.*fallback anchor function:DeprecationWarning:mkdocstrings diff --git a/docs/schema.json b/docs/schema.json deleted file mode 100644 index e1863d26..00000000 --- a/docs/schema.json +++ /dev/null @@ -1,316 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft-07/schema", - "title": "Python handler for mkdocstrings.", - "type": "object", - "properties": { - "python": { - "markdownDescription": "https://mkdocstrings.github.io/python/", - "type": "object", - "properties": { - "import": { - "title": "Inventories to import.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/#global-only-options", - "type": "array", - "items": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "object", - "properties": { - "url": { - "title": "URL of the inventory file.", - "type": "string" - }, - "base_url": { - "title": "Base URL used to build references URLs.", - "type": "string" - }, - "domains": { - "title": "Domains to import from the inventory.", - "description": "If not defined it will only import 'py' domain.", - "type": "array", - "items": { - "type": "string" - } - } - } - } - ] - } - }, - "paths": { - "title": "Local absolute/relative paths (relative to mkdocs.yml) to search packages into.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/#paths", - "type": "array", - "items": { - "type": "string", - "format": "path" - } - }, - "load_external_modules": { - "title": "Load external modules to resolve aliases.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/#load_external_modules", - "type": "boolean", - "default": false - }, - "options": { - "title": "Options for collecting and rendering objects.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", - "type": "object", - "properties": { - "docstring_style": { - "title": "The docstring style to use when parsing docstrings.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#docstring_style", - "enum": [ - "google", - "numpy", - "sphinx" - ], - "default": "google" - }, - "docstring_options": { - "title": "The options for the docstring parser.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#docstring_options", - "default": null, - "items": { - "$ref": "https://raw.githubusercontent.com/mkdocstrings/griffe/master/docs/schema-docstrings-options.json" - } - }, - "show_root_heading": { - "title": "Show the heading of the object at the root of the documentation tree.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/headings/#show_root_heading", - "type": "boolean", - "default": false - }, - "show_root_toc_entry": { - "title": "If the root heading is not shown, at least add a ToC entry for it.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/headings/#show_root_toc_entry", - "type": "boolean", - "default": true - }, - "show_root_full_path": { - "title": "Show the full Python path for the root object heading.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/headings/#show_root_full_path", - "type": "boolean", - "default": true - }, - "show_root_members_full_path": { - "title": "Show the full Python path of the root members.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/headings/#show_root_members_full_path", - "type": "boolean", - "default": false - }, - "show_object_full_path": { - "title": "Show the full Python path of every object.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/headings/#show_object_full_path", - "type": "boolean", - "default": false - }, - "show_symbol_type_heading": { - "title": "Show the symbol type in headings (e.g. mod, class, func and attr).", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/headings/#show_symbol_type_heading", - "type": "boolean", - "default": false - }, - "show_symbol_type_toc": { - "title": "Show the symbol type in the Table of Contents (e.g. mod, class, func and attr).", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/headings/#show_symbol_type_toc", - "type": "boolean", - "default": false - }, - "show_category_heading": { - "title": "When grouped by categories, show a heading for each category.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/headings/#show_category_heading", - "type": "boolean", - "default": false - }, - "show_if_no_docstring": { - "title": "Show the object heading even if it has no docstring or children with docstrings.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_if_no_docstring", - "type": "boolean", - "default": false - }, - "show_signature": { - "title": "Show methods and functions signatures.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/signatures/#show_signature", - "type": "boolean", - "default": true - }, - "show_signature_annotations": { - "title": "Show the type annotations in methods and functions signatures.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/signatures/#show_signature_annotations", - "type": "boolean", - "default": false - }, - "separate_signature": { - "title": "Whether to put the whole signature in a code block below the heading. If a formatter (Black or Ruff) is installed, the signature is also formatted using it.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/signatures/#separate_signature", - "type": "boolean", - "default": false - }, - "line_length": { - "title": "Maximum line length when formatting code/signatures.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/signatures/#line_length", - "type": "integer", - "default": 60 - }, - "merge_init_into_class": { - "title": "Whether to merge the `__init__` method into the class' signature and docstring.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#merge_init_into_class", - "type": "boolean", - "default": false - }, - "show_docstring_attributes": { - "title": "Whether to display the \"Attributes\" section in the object's docstring.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_attributes", - "type": "boolean", - "default": true - }, - "show_docstring_description": { - "title": "Whether to display the textual block (including admonitions) in the object's docstring.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_description", - "type": "boolean", - "default": true - }, - "show_docstring_examples": { - "title": "Whether to display the \"Examples\" section in the object's docstring.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_examples", - "type": "boolean", - "default": true - }, - "show_docstring_other_parameters": { - "title": "Whether to display the \"Other Parameters\" section in the object's docstring.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_other_parameters", - "type": "boolean", - "default": true - }, - "show_docstring_parameters": { - "title": "Whether to display the \"Parameters\" section in the object's docstring.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_parameters", - "type": "boolean", - "default": true - }, - "show_docstring_raises": { - "title": "Whether to display the \"Raises\" section in the object's docstring.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_raises", - "type": "boolean", - "default": true - }, - "show_docstring_receives": { - "title": "Whether to display the \"Receives\" section in the object's docstring.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_receives", - "type": "boolean", - "default": true - }, - "show_docstring_returns": { - "title": "Whether to display the \"Returns\" section in the object's docstring.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_returns", - "type": "boolean", - "default": true - }, - "show_docstring_warns": { - "title": "Whether to display the \"Warns\" section in the object's docstring.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_warns", - "type": "boolean", - "default": true - }, - "show_docstring_yields": { - "title": "Whether to display the \"Yields\" section in the object's docstring.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#show_docstring_yields", - "type": "boolean", - "default": true - }, - "show_source": { - "title": "Show the source code of this object.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/general/#show_source", - "type": "boolean", - "default": true - }, - "show_bases": { - "title": "Show the base classes of a class.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/general/#show_bases", - "type": "boolean", - "default": true - }, - "show_submodules": { - "title": "When rendering a module, show its submodules recursively.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/members/#show_submodules", - "type": "boolean", - "default": false - }, - "group_by_category": { - "title": "Group the object's children by categories: attributes, classes, functions, and modules.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/members/#group_by_category", - "type": "boolean", - "default": true - }, - "heading_level": { - "title": "The initial heading level to use.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/headings/#heading_level", - "type": "integer", - "default": 2 - }, - "members_order": { - "title": "The members ordering to use.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/members/#members_order", - "enum": [ - "alphabetical", - "source" - ], - "default": "alphabetical" - }, - "docstring_section_style": { - "title": "The style used to render docstring sections.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/docstrings/#docstring_section_style", - "enum": [ - "list", - "spacy", - "table" - ], - "default": "table" - }, - "members": { - "title": "An explicit list of members to render.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/members/#members", - "type": [ - "boolean", - "array" - ], - "default": null - }, - "filters": { - "title": "A list of filters applied to filter objects based on their name. A filter starting with `!` will exclude matching objects instead of including them. The `members` option takes precedence over `filters` (filters will still be applied recursively to lower members in the hierarchy).", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/members/#filters", - "type": "array", - "default": [ - "!^_[^_]" - ] - }, - "annotations_path": { - "title": "The verbosity for annotations path.", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/signatures/#annotations_path", - "enum": [ - "brief", - "source" - ], - "default": "brief" - }, - "preload_modules": { - "title": "Pre-load modules. It permits to resolve aliases pointing to these modules (packages), and therefore render members of an object that are external to the given object (originating from another package).", - "markdownDescription": "https://mkdocstrings.github.io/python/usage/configuration/general/#preload_modules", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false -} \ No newline at end of file diff --git a/docs/usage/configuration/docstrings.md b/docs/usage/configuration/docstrings.md index 0cf2dac1..ae925f23 100644 --- a/docs/usage/configuration/docstrings.md +++ b/docs/usage/configuration/docstrings.md @@ -1,5 +1,6 @@ # Docstrings options +[](){#option-docstring_style} ## `docstring_style` - **:octicons-package-24: Type [`str`][] :material-equal: `"google"`{ title="default value" }** @@ -29,6 +30,9 @@ plugins: docstring_style: numpy ``` +WARNING: **The style is applied to the specified object only, not its members.** Local `docstring_style` options (in `:::` instructions) will only be applied to the specified object, and not its members. Instead of changing the style when rendering, we strongly recommend to *set the right style as early as possible*, for example by using the [auto-style](https://mkdocstrings.github.io/griffe/reference/docstrings/#auto-style) (sponsors only), or with a custom Griffe extension + + /// admonition | Preview type: preview @@ -81,6 +85,7 @@ def greet(name: str) -> str: //// /// +[](){#option-docstring_options} ## `docstring_options` - **:octicons-package-24: Type [`dict`][] :material-equal: `{}`{ title="default value" }** @@ -155,6 +160,7 @@ ok //// /// +[](){#option-docstring_section_style} ## `docstring_section_style` - **:octicons-package-24: Type [`str`][] :material-equal: `"table"`{ title="default value" }** @@ -246,6 +252,7 @@ by reserving more horizontal space on the second column. //// /// +[](){#option-merge_init_into_class} ## `merge_init_into_class` - **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** @@ -317,6 +324,7 @@ class Thing: //// /// +[](){#option-relative_crossrefs} ## `relative_crossrefs` [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — @@ -429,6 +437,7 @@ class Class: INFO: **There is an alternative, third-party Python handler that handles relative references: [mkdocstrings-python-xref](https://github.com/analog-garage/mkdocstrings-python-xref).** +[](){#option-scoped_crossrefs} ## `scoped_crossrefs` [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — @@ -449,7 +458,7 @@ The following order is applied when resolving a name in a given scope: In practice, it means that the name is first looked up in members, then it is compared against the parent name (only if it's a class), then it is looked up in siblings. It continues climbing up the object tree until there's no parent, in which case it raises a name resolution error. -Cross-referencing an imported object will directly link to this object if the objects inventory of the project it comes from was [loaded][import]. You won't be able to cross-reference it within your own documentation with scoped references, if you happen to be rendering this external object too. In that case, you can use an absolute reference or a [relative][relative_crossrefs] one instead. +Cross-referencing an imported object will directly link to this object if the objects inventory of the project it comes from was [loaded][inventories]. You won't be able to cross-reference it within your own documentation with scoped references, if you happen to be rendering this external object too. In that case, you can use an absolute reference or a [relative][relative_crossrefs] one instead. Another limitation is that you won't be able to reference an external package if its name can be resolved in the current object's scope. @@ -538,6 +547,7 @@ class Class: /// +[](){#option-show_if_no_docstring} ## `show_if_no_docstring` - **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** @@ -607,6 +617,7 @@ class ClassWithoutDocstring: //// /// +[](){#option-show_docstring_attributes} ## `show_docstring_attributes` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -659,6 +670,7 @@ class Class: //// /// +[](){#option-show_docstring_functions} ## `show_docstring_functions` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -733,7 +745,7 @@ class Class: //// /// - +[](){#option-show_docstring_classes} ## `show_docstring_classes` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -792,6 +804,7 @@ class Class: //// /// +[](){#option-show_docstring_modules} ## `show_docstring_modules` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -848,6 +861,7 @@ Modules: //// /// +[](){#option-show_docstring_description} ## `show_docstring_description` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -911,6 +925,7 @@ class Class: //// /// +[](){#option-show_docstring_examples} ## `show_docstring_examples` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -964,6 +979,7 @@ hello //// /// +[](){#option-show_docstring_other_parameters} ## `show_docstring_other_parameters` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -1014,6 +1030,7 @@ def do_something(**kwargs): //// /// +[](){#option-show_docstring_parameters} ## `show_docstring_parameters` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -1064,6 +1081,7 @@ def do_something(whatever: int = 0): //// /// +[](){#option-show_docstring_raises} ## `show_docstring_raises` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -1115,6 +1133,7 @@ def raise_runtime_error(): //// /// +[](){#option-show_docstring_receives} ## `show_docstring_receives` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -1174,6 +1193,7 @@ def iter_skip( //// /// +[](){#option-show_docstring_returns} ## `show_docstring_returns` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -1225,6 +1245,7 @@ def rand() -> int: //// /// +[](){#option-show_docstring_warns} ## `show_docstring_warns` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -1276,6 +1297,7 @@ def warn(): //// /// +[](){#option-show_docstring_yields} ## `show_docstring_yields` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** diff --git a/docs/usage/configuration/general.md b/docs/usage/configuration/general.md index dd621735..3983a616 100644 --- a/docs/usage/configuration/general.md +++ b/docs/usage/configuration/general.md @@ -1,5 +1,6 @@ # General options +[](){#option-allow_inspection} ## `allow_inspection` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -18,6 +19,10 @@ and sometimes the collected data is inaccurate (depending on the tool that was used to compile the module) or too low-level/technical for API documentation. +See also [`force_inspection`](#force_inspection). + +WARNING: **Packages are loaded only once.** When mkdocstrings-python collects data from a Python package (thanks to [Griffe](https://mkdocstrings.github.io/griffe/)), it collects *the entire package* and *caches it*. Next time an object from the same package is rendered, the package is retrieved from the cache and not collected again. The `allow_inspection` option will therefore only have an effect the first time a package is collected, and will do nothing for objects rendered afterwards. + ```yaml title="in mkdocs.yml (global configuration)" plugins: - mkdocstrings: @@ -55,6 +60,145 @@ plugins: //// /// +[](){#option-extensions} +## `extensions` + +- **:octicons-package-24: Type <code><autoref identifier="list" optional>list</autoref>[<autoref identifier="str" optional>str</autoref> | <autoref identifier="dict" optional>dict</autoref>[<autoref identifier="str" optional>str</autoref>, <autoref identifier="dict" optional>dict</autoref>[<autoref identifier="str" optional>str</autoref>, <autoref identifier="typing.Any" optional>Any</autoref>]]]</code> :material-equal: `[]`{ title="default value" }** +<!-- - **:octicons-project-template-24: Template :material-null:** (contained in [`class.html`][class template]) --> + +The `extensions` option lets you enable [Griffe extensions](https://mkdocstrings.github.io/griffe/extensions/), which enhance or modify the data collected from Python sources (or compiled modules). + +Elements in the list can be strings or dictionaries. + +Strings denote the path to an extension module, like `griffe_typingdoc`, or to an extension class directly, like `griffe_typingdoc.TypingDocExtension`. When using a module path, all extensions within that module will be loaded and enabled. Strings can also be the path to a Python module, and a class name separated with `:`, like `scripts/griffe_extensions.py` or `scripts/griffe_extensions.py:MyExtension`. + +Dictionaries have a single key, which is the module/class path (as a dot-separated qualifier or file path and colon-separated class name, like above), and its value is another dictionary specifying options that will be passed when to class constructors when instantiating extensions. + +WARNING: **Packages are loaded only once.** When mkdocstrings-python collects data from a Python package (thanks to [Griffe](https://mkdocstrings.github.io/griffe/)), it collects *the entire package* and *caches it*. Next time an object from the same package is rendered, the package is retrieved from the cache and not collected again. Only the extensions specified the first time the package is loaded will be used. You cannot use a different set of extensions for specific objects rendered afterwards, and you cannot deactivate extensions for objects rendered afterwards either. + +```yaml title="in mkdocs.yml (global configuration)" +plugins: +- mkdocstrings: + handlers: + python: + options: + extensions: + - griffe_sphinx + - griffe_pydantic: {schema: true} + - scripts/exts.py:DynamicDocstrings: + paths: [mypkg.mymod.myobj] +``` + +```md title="or in docs/some_page.md (local configuration)" +::: your_package.your_module.your_func + options: + extensions: + - griffe_typingdoc +``` + +[](){#option-extra} +## `extra` + +- **:octicons-package-24: Type [`dict`][] :material-equal: `{}`{ title="default value" }** +<!-- - **:octicons-project-template-24: Template :material-null:** (contained in [`class.html`][class template]) --> + +The `extra` option lets you inject additional variables into the Jinja context used when rendering templates. You can then use this extra context in your [overridden templates][templates]. + +Local `extra` options will be merged into the global `extra` option: + +```yaml title="in mkdocs.yml (global configuration)" +plugins: +- mkdocstrings: + handlers: + python: + options: + extra: + hello: world +``` + +```md title="in docs/some_page.md (local configuration)" +::: your_package.your_module.your_func + options: + extra: + foo: bar +``` + +...will inject both `hello` and `foo` into the Jinja context when rendering `your_package.your_module.your_func`. + +> WARNING: Previously, extra options were supported directly under the `options` key. +> +> ```yaml +> plugins: +> - mkdocstrings: +> handlers: +> python: +> options: +> hello: world +> ``` +> +> Now that we introduced optional validation of options and automatic JSON schema generation thanks to Pydantic, we require extra options to be put under `options.extra`. Extra options directly under `options` are still supported, but deprecated, and will emit deprecation warnings. Support will be removed in a future version of mkdocstrings-python. + +[](){#option-find_stubs_package} +## `find_stubs_package` + +- **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** +<!-- - **:octicons-project-template-24: Template :material-null:** (contained in [`class.html`][class template]) --> + +When looking for documentation specified in [autodoc instructions][autodoc syntax] (`::: identifier`), also look for +the stubs package as defined in [PEP 561](https://peps.python.org/pep-0561/) if it exists. This is useful when +most of your documentation is separately provided by such a package and not inline in your main package. + +WARNING: **Packages are loaded only once.** When mkdocstrings-python collects data from a Python package (thanks to [Griffe](https://mkdocstrings.github.io/griffe/)), it collects *the entire package* and *caches it*. Next time an object from the same package is rendered, the package is retrieved from the cache and not collected again. The `find_stubs_package` option will therefore only have an effect the first time a package is collected, and will do nothing for objects rendered afterwards. + +```yaml title="in mkdocs.yml (global configuration)" +plugins: +- mkdocstrings: + handlers: + python: + options: + find_stubs_package: true +``` + +```md title="or in docs/some_page.md (local configuration)" +::: your_package.your_module.your_func + options: + find_stubs_package: true +``` + +```python title="your_package/your_module.py" + +def your_func(a, b): + # Function code + ... + +# rest of your code +``` + +```python title="your_package-stubs/your_module.pyi" + +def your_func(a: int, b: str): + """ + <Function docstring> + """ + ... + +# rest of your code +``` + +/// admonition | Preview + type: preview + +//// tab | With find_stubs_package +<h2><code>your_func</code></h2> +<p>Function docstring</p> +//// + +//// tab | Without find_stubs_package +<h2><code>your_func</code></h2> +//// +/// + +[](){#option-force_inspection} ## `force_inspection` - **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** @@ -64,6 +208,8 @@ Whether to force inspecting modules (importing them) even if their source code i This option is useful when you know that dynamic analysis (inspection) yields better results than static analysis. Do not use this blindly: the recommended approach is to write a Griffe extension that will improve extracted API data. See [How to selectively inspect objects](https://mkdocstrings.github.io/griffe/guide/users/how-to/selectively-inspect/). +See also [`allow_inspection`](#allow_inspection). + ```yaml title="in mkdocs.yml (global configuration)" plugins: - mkdocstrings: @@ -79,6 +225,65 @@ plugins: force_inspection: true ``` +WARNING: **Packages are loaded only once.** When mkdocstrings-python collects data from a Python package (thanks to [Griffe](https://mkdocstrings.github.io/griffe/)), it collects *the entire package* and *caches it*. Next time an object from the same package is rendered, the package is retrieved from the cache and not collected again. The `force_inspection` option will therefore only have an effect the first time a package is collected, and will do nothing for objects rendered afterwards. + +[](){#option-preload_modules} +## `preload_modules` + +- **:octicons-package-24: Type <code><autoref identifier="list" optional>list</autoref>[<autoref identifier="str" optional>str</autoref>] | None</code> :material-equal: `None`{ title="default value" }** +<!-- - **:octicons-project-template-24: Template :material-null:** (N/A) --> + +Pre-load modules that are not specified directly in [autodoc instructions][autodoc syntax] (`::: identifier`). +It is useful when you want to render documentation for a particular member of an object, +and this member is imported from another package than its parent. + +For an imported member to be rendered, +you need to add it to the [`__all__`][__all__] attribute of the importing module. +The package from which the imported object originates must be accessible to the handler +(see [Finding modules](../index.md#finding-modules)). + +```yaml title="in mkdocs.yml (global configuration)" +plugins: +- mkdocstrings: + handlers: + python: + options: + preload_modules: + - their_package +``` + +```md title="or in docs/some_page.md (local configuration)" +::: your_package.your_module + options: + preload_modules: + - their_package +``` + +```python title="your_package/your_module.py" +from their_package.their_module import their_object + +__all__ = ["their_object"] + +# rest of your code +``` + +/// admonition | Preview + type: preview + +//// tab | With preloaded modules +<h2><code>your_module</code></h2> +<p>Docstring of your module.</p> +<h3><code>their_object</code></h3> +<p>Docstring of their object.</p> +//// + +//// tab | Without preloaded modules +<h2><code>your_module</code></h2> +<p>Docstring of your module.</p> +//// +/// + +[](){#option-show_bases} ## `show_bases` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -116,6 +321,7 @@ plugins: //// /// +[](){#option-show_inheritance_diagram} ## `show_inheritance_diagram` [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — @@ -201,6 +407,7 @@ Mixin2A --> Mixin2B because these classes do not exist in our documentation.* /// +[](){#option-show_source} ## `show_source` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -245,115 +452,3 @@ def some_function(): <p>Docstring of the function.</p> //// /// - -## `preload_modules` - -- **:octicons-package-24: Type <code><autoref identifier="list" optional>list</autoref>[<autoref identifier="str" optional>str</autoref>] | None</code> :material-equal: `None`{ title="default value" }** -<!-- - **:octicons-project-template-24: Template :material-null:** (N/A) --> - -Pre-load modules that are not specified directly in [autodoc instructions][autodoc syntax] (`::: identifier`). -It is useful when you want to render documentation for a particular member of an object, -and this member is imported from another package than its parent. - -For an imported member to be rendered, -you need to add it to the [`__all__`][__all__] attribute of the importing module. -The package from which the imported object originates must be accessible to the handler -(see [Finding modules](../index.md#finding-modules)). - -```yaml title="in mkdocs.yml (global configuration)" -plugins: -- mkdocstrings: - handlers: - python: - options: - preload_modules: - - their_package -``` - -```md title="or in docs/some_page.md (local configuration)" -::: your_package.your_module - options: - preload_modules: - - their_package -``` - -```python title="your_package/your_module.py" -from their_package.their_module import their_object - -__all__ = ["their_object"] - -# rest of your code -``` - -/// admonition | Preview - type: preview - -//// tab | With preloaded modules -<h2><code>your_module</code></h2> -<p>Docstring of your module.</p> -<h3><code>their_object</code></h3> -<p>Docstring of their object.</p> -//// - -//// tab | Without preloaded modules -<h2><code>your_module</code></h2> -<p>Docstring of your module.</p> -//// -/// - -## `find_stubs_package` - -- **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** -<!-- - **:octicons-project-template-24: Template :material-null:** (contained in [`class.html`][class template]) --> - -When looking for documentation specified in [autodoc instructions][autodoc syntax] (`::: identifier`), also look for -the stubs package as defined in [PEP 561](https://peps.python.org/pep-0561/) if it exists. This is useful when -most of your documentation is separately provided by such a package and not inline in your main package. - -```yaml title="in mkdocs.yml (global configuration)" -plugins: -- mkdocstrings: - handlers: - python: - options: - find_stubs_package: true -``` - -```md title="or in docs/some_page.md (local configuration)" -::: your_package.your_module.your_func - options: - find_stubs_package: true -``` - -```python title="your_package/your_module.py" - -def your_func(a, b): - # Function code - ... - -# rest of your code -``` - -```python title="your_package-stubs/your_module.pyi" - -def your_func(a: int, b: str): - """ - <Function docstring> - """ - ... - -# rest of your code -``` - -/// admonition | Preview - type: preview - -//// tab | With find_stubs_package -<h2><code>your_func</code></h2> -<p>Function docstring</p> -//// - -//// tab | Without find_stubs_package -<h2><code>your_func</code></h2> -//// -/// diff --git a/docs/usage/configuration/headings.md b/docs/usage/configuration/headings.md index 33062baf..b4314b77 100644 --- a/docs/usage/configuration/headings.md +++ b/docs/usage/configuration/headings.md @@ -1,5 +1,22 @@ # Headings options +[](){#option-heading} +## `heading` + +- **:octicons-package-24: Type [`str`][] :material-equal: `""`{ title="default value" }** +<!-- - **:octicons-project-template-24: Template :material-null:** (N/A) --> + +A custom string to use as the heading of the root object (i.e. the object specified directly after the identifier `:::`). This will override the default heading generated by the plugin. See also the [`toc_label` option][option-toc_label]. + +WARNING: **Not advised to be used as a global configuration option.** This option is not advised to be used as a global configuration option, as it will override the default heading for all objects. It is recommended to use it only in specific cases where you want to override the heading for a specific object. + +```md title="in docs/some_page.md (local configuration)" +::: path.to.module + options: + heading: "My fancy module" +``` + +[](){#option-heading_level} ## `heading_level` - **:octicons-package-24: Type [`int`][] :material-equal: `2`{ title="default value" }** @@ -57,39 +74,7 @@ plugins: //// /// -## `heading` - -- **:octicons-package-24: Type [`str`][] :material-equal: `""`{ title="default value" }** -<!-- - **:octicons-project-template-24: Template :material-null:** (N/A) --> - -A custom string to use as the heading of the root object (i.e. the object specified directly after the identifier `:::`). This will override the default heading generated by the plugin. - -WARNING: **Not advised to be used as a global configuration option.** This option is not advised to be used as a global configuration option, as it will override the default heading for all objects. It is recommended to use it only in specific cases where you want to override the heading for a specific object. - -```md title="in docs/some_page.md (local configuration)" -::: path.to.module - options: - heading: "My fancy module" -``` - -## `toc_label` - -- **:octicons-package-24: Type [`str`][] :material-equal: `""`{ title="default value" }** -<!-- - **:octicons-project-template-24: Template :material-null:** (N/A) --> - -A custom string to use as the label in the Table of Contents for the root object (i.e. the one specified directly after the identifier `:::`). This will override the default label generated by the plugin. - -WARNING: **Not advised to be used as a global configuration option.** This option is not advised to be used as a global configuration option, as it will override the default label for all objects. It is recommended to use it only in specific cases where you want to override the label for a specific object. - -NOTE: **Use with/without `heading`.** If you use this option without specifying a custom `heading`, the default heading will be used in the page, but the label in the Table of Contents will be the one you specified. By providing both an option for `heading` and `toc_label`, we leave the customization entirely up to you. - -```md title="in docs/some_page.md (local configuration)" -::: path.to.module - options: - heading: "My fancy module" - toc_label: "My fancy module" -``` - +[](){#option-parameter_headings} ## `parameter_headings` [:octicons-tag-24: Insiders 1.6.0](../../insiders/changelog.md#1.6.0) @@ -212,6 +197,7 @@ To customize symbols, see [Customizing symbol types](../customization.md/#symbol /// +[](){#option-show_root_heading} ## `show_root_heading` - **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** @@ -275,6 +261,7 @@ plugins: //// /// +[](){#option-show_root_toc_entry} ## `show_root_toc_entry` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -332,6 +319,7 @@ More text. //// /// +[](){#option-show_root_full_path} ## `show_root_full_path` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -377,6 +365,7 @@ plugins: //// /// +[](){#option-show_root_members_full_path} ## `show_root_members_full_path` - **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** @@ -425,6 +414,7 @@ plugins: //// /// +[](){#option-show_object_full_path} ## `show_object_full_path` - **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** @@ -478,6 +468,7 @@ plugins: //// /// +[](){#option-show_category_heading} ## `show_category_heading` - **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** @@ -543,6 +534,7 @@ plugins: //// /// +[](){#option-show_symbol_type_heading} ## `show_symbol_type_heading` [:octicons-tag-24: Insiders 1.1.0](../../insiders/changelog.md#1.1.0) @@ -607,6 +599,7 @@ plugins: //// /// +[](){#option-show_symbol_type_toc} ## `show_symbol_type_toc` [:octicons-tag-24: Insiders 1.1.0](../../insiders/changelog.md#1.1.0) @@ -670,3 +663,22 @@ plugins: </ul> //// /// + +[](){#option-toc_label} +## `toc_label` + +- **:octicons-package-24: Type [`str`][] :material-equal: `""`{ title="default value" }** +<!-- - **:octicons-project-template-24: Template :material-null:** (N/A) --> + +A custom string to use as the label in the Table of Contents for the root object (i.e. the one specified directly after the identifier `:::`). This will override the default label generated by the plugin. See also the [`heading` option][option-heading]. + +WARNING: **Not advised to be used as a global configuration option.** This option is not advised to be used as a global configuration option, as it will override the default label for all objects. It is recommended to use it only in specific cases where you want to override the label for a specific object. + +NOTE: **Use with/without `heading`.** If you use this option without specifying a custom `heading`, the default heading will be used in the page, but the label in the Table of Contents will be the one you specified. By providing both an option for `heading` and `toc_label`, we leave the customization entirely up to you. + +```md title="in docs/some_page.md (local configuration)" +::: path.to.module + options: + heading: "My fancy module" + toc_label: "My fancy module" +``` diff --git a/docs/usage/configuration/members.md b/docs/usage/configuration/members.md index 220a26fe..363f7e0a 100644 --- a/docs/usage/configuration/members.md +++ b/docs/usage/configuration/members.md @@ -1,5 +1,6 @@ # Members options +[](){#option-members} ## `members` - **:octicons-package-24: Type <code><autoref identifier="list" optional>list</autoref>[<autoref identifier="str" optional>str</autoref>] | @@ -95,6 +96,7 @@ this_attribute = 0 INFO: **The default behavior (with unspecified `members` or `members: null`) is to use [`filters`][].** +[](){#option-inherited_members} ## `inherited_members` - **:octicons-package-24: Type <code><autoref identifier="list" optional>list</autoref>[<autoref identifier="str" optional>str</autoref>] | @@ -259,6 +261,7 @@ class Main(Base): /// +[](){#option-members_order} ## `members_order` - **:octicons-package-24: Type [`str`][] :material-equal: `"alphabetical"`{ title="default value" }** @@ -329,6 +332,7 @@ def function_c(): //// /// +[](){#option-filters} ## `filters` - **:octicons-package-24: Type <code><autoref identifier="list" optional>list</autoref>[<autoref identifier="str" optional>str</autoref>] | None</code> :material-equal: `["!^_[^_]"]`{ title="default value" }** @@ -427,6 +431,7 @@ Here are some common filters that you might to want to use. - `["!^_[^_]"]`: exclude all private/protected objects, keep special ones (default filters) /// +[](){#option-group_by_category} ## `group_by_category` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -496,6 +501,7 @@ def function_d(): //// /// +[](){#option-show_submodules} ## `show_submodules` - **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** @@ -550,6 +556,7 @@ package //// /// +[](){#option-summary} ## `summary` [:octicons-tag-24: Insiders 1.2.0](../../insiders/changelog.md#1.2.0) @@ -643,6 +650,7 @@ plugins: //// /// +[](){#option-show_labels} ## `show_labels` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** diff --git a/docs/usage/configuration/signatures.md b/docs/usage/configuration/signatures.md index 345e1536..c97cb5a6 100644 --- a/docs/usage/configuration/signatures.md +++ b/docs/usage/configuration/signatures.md @@ -1,5 +1,6 @@ # Signatures options +[](){#option-annotations_path} ## `annotations_path` - **:octicons-package-24: Type [`str`][] :material-equal: `"brief"`{ title="default value" }** @@ -146,6 +147,7 @@ def convert(text: str, md: Markdown) -> Markup: //// /// +[](){#option-line_length} ## `line_length` - **:octicons-package-24: Type [`int`][] :material-equal: `60`{ title="default value" }** @@ -198,6 +200,7 @@ plugins: //// /// +[](){#option-modernize_annotations} ## `modernize_annotations` [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — @@ -283,8 +286,7 @@ plugins: /// - - +[](){#option-show_signature} ## `show_signature` - **:octicons-package-24: Type [`bool`][] :material-equal: `True`{ title="default value" }** @@ -323,6 +325,7 @@ plugins: //// /// +[](){#option-show_signature_annotations} ## `show_signature_annotations` - **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** @@ -377,6 +380,7 @@ function(param1, param2=None) //// /// +[](){#option-separate_signature} ## `separate_signature` - **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** @@ -429,6 +433,7 @@ function(param1, param2=None) //// /// +[](){#option-signature_crossrefs} ## `signature_crossrefs` [:octicons-tag-24: Insiders 1.0.0](../../insiders/changelog.md#1.0.0) @@ -476,6 +481,7 @@ plugins: //// /// +[](){#option-unwrap_annotated} ## `unwrap_annotated` - **:octicons-package-24: Type [`bool`][] :material-equal: `False`{ title="default value" }** diff --git a/docs/usage/index.md b/docs/usage/index.md index 87f0a13f..84110936 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -75,10 +75,11 @@ plugins: Some options are **global only**, and go directly under the handler's name. -#### `import` +[](){#setting-inventories} +#### `inventories` -This option is used to import Sphinx-compatible objects inventories from other -documentation sites. For example, you can import the standard library +This option is used to load Sphinx-compatible objects inventories from other +documentation sites. For example, you can load the standard library objects inventory like this: ```yaml title="mkdocs.yml" @@ -90,9 +91,9 @@ plugins: - https://docs.python-requests.org/en/master/objects.inv ``` -When importing an inventory, you enable automatic cross-references +When loading an inventory, you enable automatic cross-references to other documentation sites like the standard library docs -or any third-party package docs. Typically, you want to import +or any third-party package docs. Typically, you want to load the inventories of your project's dependencies, at least those that are used in the public API. @@ -101,8 +102,8 @@ for more details. [inventories]: https://mkdocstrings.github.io/usage/#cross-references-to-other-projects-inventories -Additionally, the Python handler accepts a `domains` option in the import items, -which allows to select the inventory domains to select. +Additionally, the Python handler accepts a `domains` option in the inventory options, +which allows to select the inventory domains to load. By default the Python handler only selects the `py` domain (for Python objects). You might find useful to also enable the [`std` domain][std domain]: @@ -113,29 +114,12 @@ plugins: - mkdocstrings: handlers: python: - import: + inventories: - url: https://docs.python-requests.org/en/master/objects.inv domains: [std, py] ``` -NOTE: The `import` option is common to *all* handlers, however -they might implement it differently, or not even implement it. - -#### `paths` - -This option is used to provide filesystem paths in which to search for Python modules. -Non-absolute paths are computed as relative to MkDocs configuration file. Example: - -```yaml title="mkdocs.yml" -plugins: -- mkdocstrings: - handlers: - python: - paths: [src] # search packages in the src folder -``` - -More details at [Finding modules](#finding-modules). - +[](){#setting-load_external_modules} #### `load_external_modules` This option allows resolving aliases (imports) to any external module. @@ -165,6 +149,28 @@ plugins: [__all__]: https://docs.python.org/3/tutorial/modules.html#importing-from-a-package +[](){#setting-locale} +#### `locale` + +The locale to use when translating template strings. The translation system is not fully ready yet, so we don't recommend setting the option for now. + +[](){#setting-paths} +#### `paths` + +This option is used to provide filesystem paths in which to search for Python modules. +Non-absolute paths are computed as relative to MkDocs configuration file. Example: + +```yaml title="mkdocs.yml" +plugins: +- mkdocstrings: + handlers: + python: + paths: [src] # search packages in the src folder +``` + +More details at [Finding modules](#finding-modules). + +[](){#setting-options} ### Global/local options The other options can be used both globally *and* locally, under the `options` key. @@ -199,13 +205,6 @@ in the following pages: - [Docstrings options](configuration/docstrings.md): options related to docstrings (parsing and rendering) - [Signature options](configuration/signatures.md): options related to signatures and type annotations -#### Options summary - -::: mkdocstrings_handlers.python.handler.PythonHandler.default_config - options: - show_root_heading: false - show_root_toc_entry: false - ## Finding modules There are multiple ways to tell the handler where to find your packages/modules. diff --git a/duties.py b/duties.py index bd051334..9e516ce5 100644 --- a/duties.py +++ b/duties.py @@ -88,6 +88,8 @@ def check_types(ctx: Context) -> None: ctx.run( tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), + # TODO: Update when Pydantic supports 3.14. + nofail=sys.version_info >= (3, 14), ) diff --git a/mkdocs.yml b/mkdocs.yml index 2d546126..396f738f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,6 +13,9 @@ validation: absolute_links: warn unrecognized_links: warn +hooks: +- scripts/mkdocs_hooks.py + nav: - Home: - Overview: index.md @@ -160,6 +163,8 @@ plugins: docstring_options: ignore_init_summary: true docstring_section_style: list + extensions: + - scripts/griffe_extensions.py filters: ["!^_"] heading_level: 1 inherited_members: true diff --git a/pyproject.toml b/pyproject.toml index c6f3cc50..b4a453e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,9 +30,10 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "mkdocstrings>=0.26", + "mkdocstrings>=0.28", "mkdocs-autorefs>=1.2", "griffe>=0.49", + "typing-extensions>=4.0; python_version < '3.11'", ] [project.urls] @@ -106,6 +107,7 @@ dev = [ "mkdocs-git-revision-date-localized-plugin>=1.2", "mkdocs-literate-nav>=0.6", "mkdocs-material>=9.5", + "pydantic>=2.10", "mkdocs-minify-plugin>=0.8", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", @@ -113,3 +115,4 @@ dev = [ [tool.inline-snapshot] storage-dir = "tests/snapshots" +format-command = "ruff format --config config/ruff.toml --stdin-filename {filename}" diff --git a/scripts/griffe_extensions.py b/scripts/griffe_extensions.py new file mode 100644 index 00000000..4ff0c8cc --- /dev/null +++ b/scripts/griffe_extensions.py @@ -0,0 +1,46 @@ +"""Custom extensions for Griffe.""" + +from __future__ import annotations + +import ast +from typing import Any + +import griffe + +logger = griffe.get_logger("griffe_extensions") + + +class CustomFields(griffe.Extension): + """Support our custom dataclass fields.""" + + def on_attribute_instance( + self, + *, + attr: griffe.Attribute, + agent: griffe.Visitor | griffe.Inspector, + **kwargs: Any, # noqa: ARG002 + ) -> None: + """Fetch descriptions from `Field` annotations.""" + if attr.docstring: + return + try: + field: griffe.ExprCall = attr.annotation.slice.elements[1] # type: ignore[union-attr] + except AttributeError: + return + + if field.canonical_path == "mkdocstrings_handler.python.config.Field": + description = next( + attr.value + for attr in field.arguments + if isinstance(attr, griffe.ExprKeyword) and attr.name == "description" + ) + if not isinstance(description, str): + logger.warning(f"Field description of {attr.path} is not a static string") + description = str(description) + + attr.docstring = griffe.Docstring( + ast.literal_eval(description), + parent=attr, + parser=agent.docstring_parser, + parser_options=agent.docstring_options, + ) diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py new file mode 100644 index 00000000..bfa74e5c --- /dev/null +++ b/scripts/mkdocs_hooks.py @@ -0,0 +1,41 @@ +"""Generate a JSON schema of the Python handler configuration.""" + +import json +from dataclasses import fields +from os.path import join +from typing import Any + +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.plugins import get_plugin_logger + +from mkdocstrings_handlers.python.config import PythonInputConfig, PythonInputOptions + +# TODO: Update when Pydantic supports Python 3.14 (sources and duties as well). +try: + from pydantic import TypeAdapter +except ImportError: + TypeAdapter = None # type: ignore[assignment,misc] + + +logger = get_plugin_logger(__name__) + + +def on_post_build(config: MkDocsConfig, **kwargs: Any) -> None: # noqa: ARG001 + """Write `schema.json` to the site directory.""" + if TypeAdapter is None: + logger.info("Pydantic is not installed, skipping JSON schema generation") + return + adapter = TypeAdapter(PythonInputConfig) + schema = adapter.json_schema() + schema["$schema"] = "https://json-schema.org/draft-07/schema" + with open(join(config.site_dir, "schema.json"), "w") as file: + json.dump(schema, file, indent=2) + logger.debug("Generated JSON schema") + + autorefs = config["plugins"]["autorefs"] + for field in fields(PythonInputConfig): + if f"setting-{field.name}" not in autorefs._primary_url_map: + logger.warning(f"Handler setting `{field.name}` is not documented") + for field in fields(PythonInputOptions): + if f"option-{field.name}" not in autorefs._primary_url_map: + logger.warning(f"Configuration option `{field.name}` is not documented") diff --git a/src/mkdocstrings_handlers/python/config.py b/src/mkdocstrings_handlers/python/config.py new file mode 100644 index 00000000..07f34397 --- /dev/null +++ b/src/mkdocstrings_handlers/python/config.py @@ -0,0 +1,994 @@ +"""Configuration and options dataclasses.""" + +from __future__ import annotations + +import re +import sys +from dataclasses import field, fields +from typing import TYPE_CHECKING, Annotated, Any, Literal + +# YORE: EOL 3.10: Replace block with line 2. +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +try: + # When Pydantic is available, use it to validate options (done automatically). + # Users can therefore opt into validation by installing Pydantic in development/CI. + # When building the docs to deploy them, Pydantic is not required anymore. + + # When building our own docs, Pydantic is always installed (see `docs` group in `pyproject.toml`) + # to allow automatic generation of a JSON Schema. The JSON Schema is then referenced by mkdocstrings, + # which is itself referenced by mkdocs-material's schema system. For example in VSCode: + # + # "yaml.schemas": { + # "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" + # } + from inspect import cleandoc + + from pydantic import Field as BaseField + from pydantic.dataclasses import dataclass + + _base_url = "https://mkdocstrings.github.io/python/usage" + + def Field( # noqa: N802, D103 + *args: Any, + description: str, + group: Literal["general", "headings", "members", "docstrings", "signatures"] | None = None, + parent: str | None = None, + **kwargs: Any, + ) -> None: + def _add_markdown_description(schema: dict[str, Any]) -> None: + url = f"{_base_url}/{f'configuration/{group}/' if group else ''}#{parent or schema['title']}" + schema["markdownDescription"] = f"[DOCUMENTATION]({url})\n\n{schema['description']}" + + return BaseField( + *args, + description=cleandoc(description), + field_title_generator=lambda name, _: name, + json_schema_extra=_add_markdown_description, + **kwargs, + ) +except ImportError: + from dataclasses import dataclass # type: ignore[no-redef] + + def Field(*args: Any, **kwargs: Any) -> None: # type: ignore[misc] # noqa: D103, N802 + pass + + +if TYPE_CHECKING: + from collections.abc import MutableMapping + + +# YORE: EOL 3.9: Remove block. +_dataclass_options = {"frozen": True} +if sys.version_info >= (3, 10): + _dataclass_options["kw_only"] = True + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class GoogleStyleOptions: + """Google style docstring options.""" + + ignore_init_summary: Annotated[ + bool, + Field( + group="docstrings", + parent="docstring_options", + description="Whether to ignore the summary in `__init__` methods' docstrings.", + ), + ] = False + + returns_multiple_items: Annotated[ + bool, + Field( + group="docstrings", + parent="docstring_options", + description="""Whether to parse multiple items in `Yields` and `Returns` sections. + + When true, each item's continuation lines must be indented. + When false (single item), no further indentation is required. + """, + ), + ] = True + + returns_named_value: Annotated[ + bool, + Field( + group="docstrings", + parent="docstring_options", + description="""Whether to parse `Yields` and `Returns` section items as name and description, rather than type and description. + + When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`. + When false, parentheses are optional but the items cannot be named: `int: Description`. + """, + ), + ] = True + + returns_type_in_property_summary: Annotated[ + bool, + Field( + group="docstrings", + parent="docstring_options", + description="Whether to parse the return type of properties at the beginning of their summary: `str: Summary of the property`.", + ), + ] = False + + receives_multiple_items: Annotated[ + bool, + Field( + group="docstrings", + parent="docstring_options", + description="""Whether to parse multiple items in `Receives` sections. + + When true, each item's continuation lines must be indented. + When false (single item), no further indentation is required. + """, + ), + ] = True + + receives_named_value: Annotated[ + bool, + Field( + group="docstrings", + parent="docstring_options", + description="""Whether to parse `Receives` section items as name and description, rather than type and description. + + When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`. + When false, parentheses are optional but the items cannot be named: `int: Description`. + """, + ), + ] = True + + trim_doctest_flags: Annotated[ + bool, + Field( + group="docstrings", + parent="docstring_options", + description="Whether to remove doctest flags from Python example blocks.", + ), + ] = True + + warn_unknown_params: Annotated[ + bool, + Field( + group="docstrings", + parent="docstring_options", + description="Warn about documented parameters not appearing in the signature.", + ), + ] = True + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class NumpyStyleOptions: + """Numpy style docstring options.""" + + ignore_init_summary: Annotated[ + bool, + Field( + group="docstrings", + parent="docstring_options", + description="Whether to ignore the summary in `__init__` methods' docstrings.", + ), + ] = False + + trim_doctest_flags: Annotated[ + bool, + Field( + group="docstrings", + parent="docstring_options", + description="Whether to remove doctest flags from Python example blocks.", + ), + ] = True + + warn_unknown_params: Annotated[ + bool, + Field( + group="docstrings", + parent="docstring_options", + description="Warn about documented parameters not appearing in the signature.", + ), + ] = True + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class SphinxStyleOptions: + """Sphinx style docstring options.""" + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class PerStyleOptions: + """Per style options.""" + + google: Annotated[ + GoogleStyleOptions, + Field( + group="docstrings", + parent="docstring_options", + description="Google-style options.", + ), + ] = field(default_factory=GoogleStyleOptions) + + numpy: Annotated[ + NumpyStyleOptions, + Field( + group="docstrings", + parent="docstring_options", + description="Numpydoc-style options.", + ), + ] = field(default_factory=NumpyStyleOptions) + + sphinx: Annotated[ + SphinxStyleOptions, + Field( + group="docstrings", + parent="docstring_options", + description="Sphinx-style options.", + ), + ] = field(default_factory=SphinxStyleOptions) + + @classmethod + def from_data(cls, **data: Any) -> Self: + """Create an instance from a dictionary.""" + if "google" in data: + data["google"] = GoogleStyleOptions(**data["google"]) + if "numpy" in data: + data["numpy"] = NumpyStyleOptions(**data["numpy"]) + if "sphinx" in data: + data["sphinx"] = SphinxStyleOptions(**data["sphinx"]) + return cls(**data) + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class AutoStyleOptions: + """Auto style docstring options.""" + + method: Annotated[ + Literal["heuristics", "max_sections"], + Field( + group="docstrings", + parent="docstring_options", + description="The method to use to determine the docstring style.", + ), + ] = "heuristics" + + style_order: Annotated[ + list[str], + Field( + group="docstrings", + parent="docstring_options", + description="The order of the docstring styles to try.", + ), + ] = field(default_factory=lambda: ["sphinx", "google", "numpy"]) + + default: Annotated[ + str | None, + Field( + group="docstrings", + parent="docstring_options", + description="The default docstring style to use if no other style is detected.", + ), + ] = None + + per_style_options: Annotated[ + PerStyleOptions, + Field( + group="docstrings", + parent="docstring_options", + description="Per-style options.", + ), + ] = field(default_factory=PerStyleOptions) + + @classmethod + def from_data(cls, **data: Any) -> Self: + """Create an instance from a dictionary.""" + if "per_style_options" in data: + data["per_style_options"] = PerStyleOptions.from_data(**data["per_style_options"]) + return cls(**data) + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class SummaryOption: + """Summary option.""" + + attributes: Annotated[ + bool, + Field( + group="members", + parent="summary", + description="Whether to render summaries of attributes.", + ), + ] = False + + functions: Annotated[ + bool, + Field( + group="members", + parent="summary", + description="Whether to render summaries of functions (methods).", + ), + ] = False + + classes: Annotated[ + bool, + Field( + group="members", + parent="summary", + description="Whether to render summaries of classes.", + ), + ] = False + + modules: Annotated[ + bool, + Field( + group="members", + parent="summary", + description="Whether to render summaries of modules.", + ), + ] = False + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class PythonInputOptions: + """Accepted input options.""" + + allow_inspection: Annotated[ + bool, + Field( + group="general", + description="Whether to allow inspecting modules when visiting them is not possible.", + ), + ] = True + + force_inspection: Annotated[ + bool, + Field( + group="general", + description="Whether to force using dynamic analysis when loading data.", + ), + ] = False + + annotations_path: Annotated[ + Literal["brief", "source", "full"], + Field( + group="signatures", + description="The verbosity for annotations path: `brief` (recommended), `source` (as written in the source), or `full`.", + ), + ] = "brief" + + docstring_options: Annotated[ + GoogleStyleOptions | NumpyStyleOptions | SphinxStyleOptions | AutoStyleOptions | None, + Field( + group="docstrings", + description="""The options for the docstring parser. + + See [docstring parsers](https://mkdocstrings.github.io/griffe/reference/docstrings/) and their options in Griffe docs. + """, + ), + ] = None + + docstring_section_style: Annotated[ + Literal["table", "list", "spacy"], + Field( + group="docstrings", + description="The style used to render docstring sections.", + ), + ] = "table" + + docstring_style: Annotated[ + Literal["auto", "google", "numpy", "sphinx"] | None, + Field( + group="docstrings", + description="The docstring style to use: `auto`, `google`, `numpy`, `sphinx`, or `None`.", + ), + ] = "google" + + extensions: Annotated[ + list[str | dict[str, Any]], + Field( + group="general", + description="A list of Griffe extensions to load.", + ), + ] = field(default_factory=list) + + filters: Annotated[ + list[str], + Field( + group="members", + description="""A list of filters applied to filter objects based on their name. + + A filter starting with `!` will exclude matching objects instead of including them. + The `members` option takes precedence over `filters` (filters will still be applied recursively + to lower members in the hierarchy). + """, + ), + ] = field(default_factory=lambda: ["!^_[^_]"]) + + find_stubs_package: Annotated[ + bool, + Field( + group="general", + description="Whether to load stubs package (package-stubs) when extracting docstrings.", + ), + ] = False + + group_by_category: Annotated[ + bool, + Field( + group="members", + description="Group the object's children by categories: attributes, classes, functions, and modules.", + ), + ] = True + + heading: Annotated[ + str, + Field( + group="headings", + description="A custom string to override the autogenerated heading of the root object.", + ), + ] = "" + + heading_level: Annotated[ + int, + Field( + group="headings", + description="The initial heading level to use.", + ), + ] = 2 + + inherited_members: Annotated[ + bool | list[str], + Field( + group="members", + description="""A boolean, or an explicit list of inherited members to render. + + If true, select all inherited members, which can then be filtered with `members`. + If false or empty list, do not select any inherited member. + """, + ), + ] = False + + line_length: Annotated[ + int, + Field( + group="signatures", + description="Maximum line length when formatting code/signatures.", + ), + ] = 60 + + members: Annotated[ + list[str] | bool | None, + Field( + group="members", + description="""A boolean, or an explicit list of members to render. + + If true, select all members without further filtering. + If false or empty list, do not render members. + If none, select all members and apply further filtering with filters and docstrings. + """, + ), + ] = None + + members_order: Annotated[ + Literal["alphabetical", "source"], + Field( + group="members", + description="""The members ordering to use. + + - `alphabetical`: order by the members names, + - `source`: order members as they appear in the source file. + """, + ), + ] = "alphabetical" + + merge_init_into_class: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to merge the `__init__` method into the class' signature and docstring.", + ), + ] = False + + modernize_annotations: Annotated[ + bool, + Field( + group="signatures", + description="Whether to modernize annotations, for example `Optional[str]` into `str | None`.", + ), + ] = False + + parameter_headings: Annotated[ + bool, + Field( + group="headings", + description="Whether to render headings for parameters (therefore showing parameters in the ToC).", + ), + ] = False + + preload_modules: Annotated[ + list[str], + Field( + group="general", + description="""Pre-load modules that are not specified directly in autodoc instructions (`::: identifier`). + + It is useful when you want to render documentation for a particular member of an object, + and this member is imported from another package than its parent. + + For an imported member to be rendered, you need to add it to the `__all__` attribute + of the importing module. + + The modules must be listed as an array of strings. + """, + ), + ] = field(default_factory=list) + + relative_crossrefs: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to enable the relative crossref syntax.", + ), + ] = False + + scoped_crossrefs: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to enable the scoped crossref ability.", + ), + ] = False + + separate_signature: Annotated[ + bool, + Field( + group="signatures", + description="""Whether to put the whole signature in a code block below the heading. + + If Black or Ruff are installed, the signature is also formatted using them. + """, + ), + ] = False + + show_bases: Annotated[ + bool, + Field( + group="general", + description="Show the base classes of a class.", + ), + ] = True + + show_category_heading: Annotated[ + bool, + Field( + group="headings", + description="When grouped by categories, show a heading for each category.", + ), + ] = False + + show_docstring_attributes: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Attributes' section in the object's docstring.", + ), + ] = True + + show_docstring_classes: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Classes' section in the object's docstring.", + ), + ] = True + + show_docstring_description: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the textual block (including admonitions) in the object's docstring.", + ), + ] = True + + show_docstring_examples: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Examples' section in the object's docstring.", + ), + ] = True + + show_docstring_functions: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Functions' or 'Methods' sections in the object's docstring.", + ), + ] = True + + show_docstring_modules: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Modules' section in the object's docstring.", + ), + ] = True + + show_docstring_other_parameters: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Other Parameters' section in the object's docstring.", + ), + ] = True + + show_docstring_parameters: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Parameters' section in the object's docstring.", + ), + ] = True + + show_docstring_raises: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Raises' section in the object's docstring.", + ), + ] = True + + show_docstring_receives: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Receives' section in the object's docstring.", + ), + ] = True + + show_docstring_returns: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Returns' section in the object's docstring.", + ), + ] = True + + show_docstring_warns: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Warns' section in the object's docstring.", + ), + ] = True + + show_docstring_yields: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to display the 'Yields' section in the object's docstring.", + ), + ] = True + + show_if_no_docstring: Annotated[ + bool, + Field( + group="docstrings", + description="Show the object heading even if it has no docstring or children with docstrings.", + ), + ] = False + + show_inheritance_diagram: Annotated[ + bool, + Field( + group="docstrings", + description="Show the inheritance diagram of a class using Mermaid.", + ), + ] = False + + show_labels: Annotated[ + bool, + Field( + group="docstrings", + description="Whether to show labels of the members.", + ), + ] = True + + show_object_full_path: Annotated[ + bool, + Field( + group="docstrings", + description="Show the full Python path of every object.", + ), + ] = False + + show_root_full_path: Annotated[ + bool, + Field( + group="docstrings", + description="Show the full Python path for the root object heading.", + ), + ] = True + + show_root_heading: Annotated[ + bool, + Field( + group="headings", + description="""Show the heading of the object at the root of the documentation tree. + + The root object is the object referenced by the identifier after `:::`. + """, + ), + ] = False + + show_root_members_full_path: Annotated[ + bool, + Field( + group="headings", + description="Show the full Python path of the root members.", + ), + ] = False + + show_root_toc_entry: Annotated[ + bool, + Field( + group="headings", + description="If the root heading is not shown, at least add a ToC entry for it.", + ), + ] = True + + show_signature_annotations: Annotated[ + bool, + Field( + group="signatures", + description="Show the type annotations in methods and functions signatures.", + ), + ] = False + + show_signature: Annotated[ + bool, + Field( + group="signatures", + description="Show methods and functions signatures.", + ), + ] = True + + show_source: Annotated[ + bool, + Field( + group="general", + description="Show the source code of this object.", + ), + ] = True + + show_submodules: Annotated[ + bool, + Field( + group="members", + description="When rendering a module, show its submodules recursively.", + ), + ] = False + + show_symbol_type_heading: Annotated[ + bool, + Field( + group="headings", + description="Show the symbol type in headings (e.g. mod, class, meth, func and attr).", + ), + ] = False + + show_symbol_type_toc: Annotated[ + bool, + Field( + group="headings", + description="Show the symbol type in the Table of Contents (e.g. mod, class, methd, func and attr).", + ), + ] = False + + signature_crossrefs: Annotated[ + bool, + Field( + group="signatures", + description="Whether to render cross-references for type annotations in signatures.", + ), + ] = False + + summary: Annotated[ + bool | SummaryOption, + Field( + group="members", + description="Whether to render summaries of modules, classes, functions (methods) and attributes.", + ), + ] = field(default_factory=SummaryOption) + + toc_label: Annotated[ + str, + Field( + group="headings", + description="A custom string to override the autogenerated toc label of the root object.", + ), + ] = "" + + unwrap_annotated: Annotated[ + bool, + Field( + group="signatures", + description="Whether to unwrap `Annotated` types to show only the type without the annotations.", + ), + ] = False + + extra: Annotated[ + dict[str, Any], + Field( + group="general", + description="Extra options.", + ), + ] = field(default_factory=dict) + + @classmethod + def _extract_extra(cls, data: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: + field_names = {field.name for field in fields(cls)} + copy = data.copy() + return {name: copy.pop(name) for name in data if name not in field_names}, copy + + # YORE: Bump 2: Remove block. + def __init__(self, **kwargs: Any) -> None: + """Initialize the instance.""" + extra_fields = self._extract_extra(kwargs) + for name, value in kwargs.items(): + object.__setattr__(self, name, value) + if extra_fields: + object.__setattr__(self, "_extra", extra_fields) + + @classmethod + def coerce(cls, **data: Any) -> MutableMapping[str, Any]: + """Coerce data.""" + if "docstring_options" in data: + docstring_style = data.get("docstring_style", "google") + docstring_options = data["docstring_options"] + if docstring_options is not None: + if docstring_style == "auto": + docstring_options = AutoStyleOptions.from_data(**docstring_options) + elif docstring_style == "google": + docstring_options = GoogleStyleOptions(**docstring_options) + elif docstring_style == "numpy": + docstring_options = NumpyStyleOptions(**docstring_options) + elif docstring_style == "sphinx": + docstring_options = SphinxStyleOptions(**docstring_options) + data["docstring_options"] = docstring_options + if "summary" in data: + summary = data["summary"] + if summary is True: + summary = SummaryOption(attributes=True, functions=True, classes=True, modules=True) + elif summary is False: + summary = SummaryOption(attributes=False, functions=False, classes=False, modules=False) + else: + summary = SummaryOption(**summary) + data["summary"] = summary + return data + + @classmethod + def from_data(cls, **data: Any) -> Self: + """Create an instance from a dictionary.""" + return cls(**cls.coerce(**data)) + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class PythonOptions(PythonInputOptions): # type: ignore[override,unused-ignore] + """Final options passed as template context.""" + + filters: list[tuple[re.Pattern, bool]] = field(default_factory=list) # type: ignore[assignment] + """A list of filters applied to filter objects based on their name.""" + + summary: SummaryOption = field(default_factory=SummaryOption) + """Whether to render summaries of modules, classes, functions (methods) and attributes.""" + + @classmethod + def coerce(cls, **data: Any) -> MutableMapping[str, Any]: + """Create an instance from a dictionary.""" + if "filters" in data: + data["filters"] = [ + (re.compile(filtr.lstrip("!")), filtr.startswith("!")) for filtr in data["filters"] or () + ] + return super().coerce(**data) + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class Inventory: + """An inventory.""" + + url: Annotated[ + str, + Field( + parent="inventories", + description="The URL of the inventory.", + ), + ] + + base: Annotated[ + str | None, + Field( + parent="inventories", + description="The base URL of the inventory.", + ), + ] = None + + domains: Annotated[ + list[str], + Field( + parent="inventories", + description="The domains to load from the inventory.", + ), + ] = field(default_factory=lambda: ["py"]) + + @property + def _config(self) -> dict[str, Any]: + return {"base": self.base, "domains": self.domains} + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class PythonInputConfig: + """Python handler configuration.""" + + inventories: Annotated[ + list[str | Inventory], + Field(description="The inventories to load."), + ] = field(default_factory=list) + + paths: Annotated[ + list[str], + Field(description="The paths in which to search for Python packages."), + ] = field(default_factory=lambda: ["."]) + + load_external_modules: Annotated[ + bool | None, + Field(description="Whether to always load external modules/packages."), + ] = None + + options: Annotated[ + PythonInputOptions, + Field(description="Configuration options for collecting and rendering objects."), + ] = field(default_factory=PythonInputOptions) + + locale: Annotated[ + str | None, + Field(description="The locale to use when translating template strings."), + ] = None + + @classmethod + def coerce(cls, **data: Any) -> MutableMapping[str, Any]: + """Coerce data.""" + return data + + @classmethod + def from_data(cls, **data: Any) -> Self: + """Create an instance from a dictionary.""" + return cls(**cls.coerce(**data)) + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class PythonConfig(PythonInputConfig): # type: ignore[override,unused-ignore] + """Python handler configuration.""" + + inventories: list[Inventory] = field(default_factory=list) # type: ignore[assignment] + options: dict[str, Any] = field(default_factory=dict) # type: ignore[assignment] + + @classmethod + def coerce(cls, **data: Any) -> MutableMapping[str, Any]: + """Coerce data.""" + if "inventories" in data: + data["inventories"] = [ + Inventory(url=inv) if isinstance(inv, str) else Inventory(**inv) for inv in data["inventories"] + ] + return data diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 5725d566..bf22876d 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -5,12 +5,12 @@ import glob import os import posixpath -import re import sys -from collections import ChainMap from contextlib import suppress +from dataclasses import asdict from pathlib import Path from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar +from warnings import warn from griffe import ( AliasResolutionError, @@ -21,17 +21,18 @@ load_extensions, patch_loggers, ) -from mkdocstrings.extension import PluginError -from mkdocstrings.handlers.base import BaseHandler, CollectionError, CollectorItem +from mkdocs.exceptions import PluginError +from mkdocstrings.handlers.base import BaseHandler, CollectionError, CollectorItem, HandlerOptions from mkdocstrings.inventory import Inventory from mkdocstrings.loggers import get_logger from mkdocstrings_handlers.python import rendering +from mkdocstrings_handlers.python.config import PythonConfig, PythonOptions if TYPE_CHECKING: - from collections.abc import Iterator, Mapping, Sequence + from collections.abc import Iterator, Mapping, MutableMapping, Sequence - from markdown import Markdown + from mkdocs.config.defaults import MkDocsConfig if sys.version_info >= (3, 11): @@ -55,215 +56,78 @@ def chdir(path: str) -> Iterator[None]: # noqa: D103 patch_loggers(get_logger) +def _warn_extra_options(names: Sequence[str]) -> None: + warn( + "Passing extra options directly under `options` is deprecated. " + "Instead, pass them under `options.extra`, and update your templates. " + f"Current extra (unrecognized) options: {', '.join(sorted(names))}", + DeprecationWarning, + stacklevel=3, + ) + + class PythonHandler(BaseHandler): """The Python handler class.""" - name: str = "python" + name: ClassVar[str] = "python" """The handler's name.""" - domain: str = "py" # to match Sphinx's default domain + + domain: ClassVar[str] = "py" """The cross-documentation domain/language for this handler.""" - enable_inventory: bool = True + + enable_inventory: ClassVar[bool] = True """Whether this handler is interested in enabling the creation of the `objects.inv` Sphinx inventory file.""" - fallback_theme = "material" + + fallback_theme: ClassVar[str] = "material" """The fallback theme.""" - fallback_config: ClassVar[dict] = {"fallback": True} - """The configuration used to collect item during autorefs fallback.""" - default_config: ClassVar[dict] = { - "find_stubs_package": False, - "docstring_style": "google", - "docstring_options": {}, - "show_symbol_type_heading": False, - "show_symbol_type_toc": False, - "show_root_heading": False, - "show_root_toc_entry": True, - "show_root_full_path": True, - "show_root_members_full_path": False, - "show_object_full_path": False, - "show_category_heading": False, - "show_if_no_docstring": False, - "show_signature": True, - "show_signature_annotations": False, - "signature_crossrefs": False, - "separate_signature": False, - "line_length": 60, - "merge_init_into_class": False, - "relative_crossrefs": False, - "scoped_crossrefs": False, - "show_docstring_attributes": True, - "show_docstring_functions": True, - "show_docstring_classes": True, - "show_docstring_modules": True, - "show_docstring_description": True, - "show_docstring_examples": True, - "show_docstring_other_parameters": True, - "show_docstring_parameters": True, - "show_docstring_raises": True, - "show_docstring_receives": True, - "show_docstring_returns": True, - "show_docstring_warns": True, - "show_docstring_yields": True, - "show_source": True, - "show_bases": True, - "show_inheritance_diagram": False, - "show_submodules": False, - "group_by_category": True, - "heading": "", - "toc_label": "", - "heading_level": 2, - "members_order": rendering.Order.alphabetical.value, - "docstring_section_style": "table", - "members": None, - "inherited_members": False, - "filters": ["!^_[^_]"], - "annotations_path": "brief", - "preload_modules": None, - "allow_inspection": True, - "force_inspection": False, - "summary": False, - "show_labels": True, - "unwrap_annotated": False, - "parameter_headings": False, - "modernize_annotations": False, - } - """Default handler configuration. - - Attributes: General options: - find_stubs_package (bool): Whether to load stubs package (package-stubs) when extracting docstrings. Default `False`. - allow_inspection (bool): Whether to allow inspecting modules when visiting them is not possible. Default: `True`. - force_inspection (bool): Whether to force using dynamic analysis when loading data. Default: `False`. - show_bases (bool): Show the base classes of a class. Default: `True`. - show_inheritance_diagram (bool): Show the inheritance diagram of a class using Mermaid. Default: `False`. - show_source (bool): Show the source code of this object. Default: `True`. - preload_modules (list[str] | None): Pre-load modules that are - not specified directly in autodoc instructions (`::: identifier`). - It is useful when you want to render documentation for a particular member of an object, - and this member is imported from another package than its parent. - - For an imported member to be rendered, you need to add it to the `__all__` attribute - of the importing module. - - The modules must be listed as an array of strings. Default: `None`. - - Attributes: Headings options: - heading (str): A custom string to override the autogenerated heading of the root object. - toc_label (str): A custom string to override the autogenerated toc label of the root object. - heading_level (int): The initial heading level to use. Default: `2`. - parameter_headings (bool): Whether to render headings for parameters (therefore showing parameters in the ToC). Default: `False`. - show_root_heading (bool): Show the heading of the object at the root of the documentation tree - (i.e. the object referenced by the identifier after `:::`). Default: `False`. - show_root_toc_entry (bool): If the root heading is not shown, at least add a ToC entry for it. Default: `True`. - show_root_full_path (bool): Show the full Python path for the root object heading. Default: `True`. - show_root_members_full_path (bool): Show the full Python path of the root members. Default: `False`. - show_object_full_path (bool): Show the full Python path of every object. Default: `False`. - show_category_heading (bool): When grouped by categories, show a heading for each category. Default: `False`. - show_symbol_type_heading (bool): Show the symbol type in headings (e.g. mod, class, meth, func and attr). Default: `False`. - show_symbol_type_toc (bool): Show the symbol type in the Table of Contents (e.g. mod, class, methd, func and attr). Default: `False`. - - Attributes: Members options: - inherited_members (list[str] | bool | None): A boolean, or an explicit list of inherited members to render. - If true, select all inherited members, which can then be filtered with `members`. - If false or empty list, do not select any inherited member. Default: `False`. - members (list[str] | bool | None): A boolean, or an explicit list of members to render. - If true, select all members without further filtering. - If false or empty list, do not render members. - If none, select all members and apply further filtering with filters and docstrings. Default: `None`. - members_order (str): The members ordering to use. Options: `alphabetical` - order by the members names, - `source` - order members as they appear in the source file. Default: `"alphabetical"`. - filters (list[str] | None): A list of filters applied to filter objects based on their name. - A filter starting with `!` will exclude matching objects instead of including them. - The `members` option takes precedence over `filters` (filters will still be applied recursively - to lower members in the hierarchy). Default: `["!^_[^_]"]`. - group_by_category (bool): Group the object's children by categories: attributes, classes, functions, and modules. Default: `True`. - show_submodules (bool): When rendering a module, show its submodules recursively. Default: `False`. - summary (bool | dict[str, bool]): Whether to render summaries of modules, classes, functions (methods) and attributes. - show_labels (bool): Whether to show labels of the members. Default: `True`. - - Attributes: Docstrings options: - docstring_style (str): The docstring style to use: `google`, `numpy`, `sphinx`, or `None`. Default: `"google"`. - docstring_options (dict): The options for the docstring parser. See [docstring parsers](https://mkdocstrings.github.io/griffe/reference/docstrings/) and their options in Griffe docs. - docstring_section_style (str): The style used to render docstring sections. Options: `table`, `list`, `spacy`. Default: `"table"`. - merge_init_into_class (bool): Whether to merge the `__init__` method into the class' signature and docstring. Default: `False`. - relative_crossrefs (bool): Whether to enable the relative crossref syntax. Default: `False`. - scoped_crossrefs (bool): Whether to enable the scoped crossref ability. Default: `False`. - show_if_no_docstring (bool): Show the object heading even if it has no docstring or children with docstrings. Default: `False`. - show_docstring_attributes (bool): Whether to display the "Attributes" section in the object's docstring. Default: `True`. - show_docstring_functions (bool): Whether to display the "Functions" or "Methods" sections in the object's docstring. Default: `True`. - show_docstring_classes (bool): Whether to display the "Classes" section in the object's docstring. Default: `True`. - show_docstring_modules (bool): Whether to display the "Modules" section in the object's docstring. Default: `True`. - show_docstring_description (bool): Whether to display the textual block (including admonitions) in the object's docstring. Default: `True`. - show_docstring_examples (bool): Whether to display the "Examples" section in the object's docstring. Default: `True`. - show_docstring_other_parameters (bool): Whether to display the "Other Parameters" section in the object's docstring. Default: `True`. - show_docstring_parameters (bool): Whether to display the "Parameters" section in the object's docstring. Default: `True`. - show_docstring_raises (bool): Whether to display the "Raises" section in the object's docstring. Default: `True`. - show_docstring_receives (bool): Whether to display the "Receives" section in the object's docstring. Default: `True`. - show_docstring_returns (bool): Whether to display the "Returns" section in the object's docstring. Default: `True`. - show_docstring_warns (bool): Whether to display the "Warns" section in the object's docstring. Default: `True`. - show_docstring_yields (bool): Whether to display the "Yields" section in the object's docstring. Default: `True`. - - Attributes: Signatures/annotations options: - annotations_path (str): The verbosity for annotations path: `brief` (recommended), or `source` (as written in the source). Default: `"brief"`. - line_length (int): Maximum line length when formatting code/signatures. Default: `60`. - show_signature (bool): Show methods and functions signatures. Default: `True`. - show_signature_annotations (bool): Show the type annotations in methods and functions signatures. Default: `False`. - signature_crossrefs (bool): Whether to render cross-references for type annotations in signatures. Default: `False`. - separate_signature (bool): Whether to put the whole signature in a code block below the heading. - If a formatter (Black or Ruff) is installed, the signature is also formatted using it. Default: `False`. - unwrap_annotated (bool): Whether to unwrap `Annotated` types to show only the type without the annotations. Default: `False`. - modernize_annotations (bool): Whether to modernize annotations, for example `Optional[str]` into `str | None`. Default: `False`. - """ - def __init__( - self, - *args: Any, - config_file_path: str | None = None, - paths: list[str] | None = None, - locale: str = "en", - load_external_modules: bool | None = None, - **kwargs: Any, - ) -> None: + def __init__(self, config: PythonConfig, base_dir: Path, **kwargs: Any) -> None: """Initialize the handler. Parameters: - *args: Handler name, theme and custom templates. - config_file_path: The MkDocs configuration file path. - paths: A list of paths to use as Griffe search paths. - locale: The locale to use when rendering content. - load_external_modules: Load external modules when resolving aliases. - **kwargs: Same thing, but with keyword arguments. + config: The handler configuration. + base_dir: The base directory of the project. + **kwargs: Arguments passed to the parent constructor. """ - super().__init__(*args, **kwargs) + super().__init__(**kwargs) + + self.config = config + self.base_dir = base_dir + + # YORE: Bump 2: Replace block with `self.global_options = config.options`. + global_extra, global_options = PythonOptions._extract_extra(config.options) + if global_extra: + _warn_extra_options(global_extra.keys()) # type: ignore[arg-type] + self._global_extra = global_extra + self.global_options = global_options # Warn if user overrides base templates. - if custom_templates := kwargs.get("custom_templates", ()): - config_dir = Path(config_file_path or "./mkdocs.yml").parent - for theme_dir in config_dir.joinpath(custom_templates, "python").iterdir(): + if self.custom_templates: + for theme_dir in base_dir.joinpath(self.custom_templates, "python").iterdir(): if theme_dir.joinpath("_base").is_dir(): logger.warning( f"Overriding base template '{theme_dir.name}/_base/<template>.html.jinja' is not supported, " f"override '{theme_dir.name}/<template>.html.jinja' instead", ) - self._config_file_path = config_file_path - self._load_external_modules = load_external_modules - paths = paths or [] + paths = config.paths or [] # Expand paths with glob patterns. - glob_base_dir = os.path.dirname(os.path.abspath(config_file_path)) if config_file_path else "." - with chdir(glob_base_dir): + with chdir(str(base_dir)): resolved_globs = [glob.glob(path) for path in paths] paths = [path for glob_list in resolved_globs for path in glob_list] - # By default, add the directory of the config file to the search paths. - if not paths and config_file_path: - paths.append(os.path.dirname(config_file_path)) + # By default, add the base directory to the search paths. + if not paths: + paths.append(str(base_dir)) # Initialize search paths from `sys.path`, eliminating empty paths. search_paths = [path for path in sys.path if path] for path in reversed(paths): # If it's not absolute, make path relative to the config file path, then make it absolute. - if not os.path.isabs(path) and config_file_path: - path = os.path.abspath(os.path.join(os.path.dirname(config_file_path), path)) # noqa: PLW2901 + if not os.path.isabs(path): + path = os.path.abspath(base_dir / path) # noqa: PLW2901 # Don't add duplicates. if path not in search_paths: search_paths.insert(0, path) @@ -271,16 +135,18 @@ def __init__( self._paths = search_paths self._modules_collection: ModulesCollection = ModulesCollection() self._lines_collection: LinesCollection = LinesCollection() - self._locale = locale - @classmethod + def get_inventory_urls(self) -> list[tuple[str, dict[str, Any]]]: + """Return the URLs of the inventory files to download.""" + return [(inv.url, inv._config) for inv in self.config.inventories] + + @staticmethod def load_inventory( - cls, in_file: BinaryIO, url: str, base_url: str | None = None, domains: list[str] | None = None, - **kwargs: Any, # noqa: ARG003 + **kwargs: Any, # noqa: ARG004 ) -> Iterator[tuple[str, str]]: """Yield items and their URLs from an inventory file streamed from `in_file`. @@ -303,47 +169,69 @@ def load_inventory( for item in Inventory.parse_sphinx(in_file, domain_filter=domains).values(): yield item.name, posixpath.join(base_url, item.uri) - def collect(self, identifier: str, config: Mapping[str, Any]) -> CollectorItem: # noqa: D102 + def get_options(self, local_options: Mapping[str, Any]) -> HandlerOptions: + """Get combined default, global and local options. + + Arguments: + local_options: The local options. + + Returns: + The combined options. + """ + # YORE: Bump 2: Remove block. + local_extra, local_options = PythonOptions._extract_extra(local_options) # type: ignore[arg-type] + if local_extra: + _warn_extra_options(local_extra.keys()) # type: ignore[arg-type] + unknown_extra = self._global_extra | local_extra + + extra = {**self.global_options.get("extra", {}), **local_options.get("extra", {})} + options = {**self.global_options, **local_options, "extra": extra} + # YORE: Bump 2: Replace `, **unknown_extra` with `` within line. + try: + return PythonOptions.from_data(**options, **unknown_extra) + except Exception as error: + raise PluginError(f"Invalid options: {error}") from error + + def collect(self, identifier: str, options: PythonOptions) -> CollectorItem: # noqa: D102 module_name = identifier.split(".", 1)[0] unknown_module = module_name not in self._modules_collection - if config.get("fallback", False) and unknown_module: + if options == {} and unknown_module: raise CollectionError("Not loading additional modules during fallback") - final_config = ChainMap(config, self.default_config) # type: ignore[arg-type] - parser_name = final_config["docstring_style"] - parser_options = final_config["docstring_options"] + parser_name = options.docstring_style parser = parser_name and Parser(parser_name) + parser_options = options.docstring_options and asdict(options.docstring_options) if unknown_module: - extensions = self.normalize_extension_paths(final_config.get("extensions", [])) + extensions = self.normalize_extension_paths(options.extensions) loader = GriffeLoader( extensions=load_extensions(*extensions), search_paths=self._paths, docstring_parser=parser, - docstring_options=parser_options, + docstring_options=parser_options, # type: ignore[arg-type] modules_collection=self._modules_collection, lines_collection=self._lines_collection, - allow_inspection=final_config["allow_inspection"], - force_inspection=final_config["force_inspection"], + allow_inspection=options.allow_inspection, + force_inspection=options.force_inspection, ) try: - for pre_loaded_module in final_config.get("preload_modules") or []: + for pre_loaded_module in options.preload_modules: if pre_loaded_module not in self._modules_collection: loader.load( pre_loaded_module, try_relative_path=False, - find_stubs_package=final_config["find_stubs_package"], + find_stubs_package=options.find_stubs_package, ) loader.load( module_name, try_relative_path=False, - find_stubs_package=final_config["find_stubs_package"], + find_stubs_package=options.find_stubs_package, ) except ImportError as error: raise CollectionError(str(error)) from error unresolved, iterations = loader.resolve_aliases( implicit=False, - external=self._load_external_modules, + external=self.config.load_external_modules, ) if unresolved: logger.debug(f"{len(unresolved)} aliases were still unresolved after {iterations} iterations") @@ -364,70 +252,29 @@ def collect(self, identifier: str, config: Mapping[str, Any]) -> CollectorItem: return doc_object - def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: # noqa: D102 (ignore missing docstring) - final_config = ChainMap(config, self.default_config) # type: ignore[arg-type] - + def render(self, data: CollectorItem, options: PythonOptions) -> str: # noqa: D102 (ignore missing docstring) template_name = rendering.do_get_template(self.env, data) template = self.env.get_template(template_name) - # Heading level is a "state" variable, that will change at each step - # of the rendering recursion. Therefore, it's easier to use it as a plain value - # than as an item in a dictionary. - heading_level = final_config["heading_level"] - try: - final_config["members_order"] = rendering.Order(final_config["members_order"]) - except ValueError as error: - choices = "', '".join(item.value for item in rendering.Order) - raise PluginError( - f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'.", - ) from error - - if final_config["filters"]: - final_config["filters"] = [ - (re.compile(filtr.lstrip("!")), filtr.startswith("!")) for filtr in final_config["filters"] - ] - - summary = final_config["summary"] - if summary is True: - final_config["summary"] = { - "attributes": True, - "functions": True, - "classes": True, - "modules": True, - } - elif summary is False: - final_config["summary"] = { - "attributes": False, - "functions": False, - "classes": False, - "modules": False, - } - else: - final_config["summary"] = { - "attributes": summary.get("attributes", False), - "functions": summary.get("functions", False), - "classes": summary.get("classes", False), - "modules": summary.get("modules", False), - } - return template.render( **{ - "config": final_config, + "config": options, data.kind.value: data, - "heading_level": heading_level, + # Heading level is a "state" variable, that will change at each step + # of the rendering recursion. Therefore, it's easier to use it as a plain value + # than as an item in a dictionary. + "heading_level": options.heading_level, "root": True, - "locale": self._locale, + "locale": self.config.locale, }, ) - def update_env(self, md: Markdown, config: dict) -> None: + def update_env(self, config: Any) -> None: # noqa: ARG002 """Update the Jinja environment with custom filters and tests. Parameters: - md: The Markdown instance. - config: The configuration dictionary. + config: The SSG configuration. """ - super().update_env(md, config) self.env.trim_blocks = True self.env.lstrip_blocks = True self.env.keep_trailing_newline = False @@ -448,24 +295,22 @@ def update_env(self, md: Markdown, config: dict) -> None: self.env.globals["AutorefsHook"] = rendering.AutorefsHook self.env.tests["existing_template"] = lambda template_name: template_name in self.env.list_templates() - def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: # noqa: D102 (ignore missing docstring) - anchors = [data.path] + def get_aliases(self, identifier: str) -> tuple[str, ...]: # noqa: D102 (ignore missing docstring) try: - if data.canonical_path != data.path: - anchors.append(data.canonical_path) - for anchor in data.aliases: - if anchor not in anchors: - anchors.append(anchor) + data = self._modules_collection[identifier] + except KeyError: + return () + aliases = [data.path] + try: + for alias in [data.canonical_path, *data.aliases]: + if alias not in aliases: + aliases.append(alias) except AliasResolutionError: - return tuple(anchors) - return tuple(anchors) + return tuple(aliases) + return tuple(aliases) def normalize_extension_paths(self, extensions: Sequence) -> Sequence: """Resolve extension paths relative to config file.""" - if self._config_file_path is None: - return extensions - - base_path = os.path.dirname(self._config_file_path) normalized = [] for ext in extensions: @@ -478,7 +323,7 @@ def normalize_extension_paths(self, extensions: Sequence) -> Sequence: if pth.endswith(".py") or ".py:" in pth or "/" in pth or "\\" in pth: # This is a system path. Normalize it, make it absolute relative to config file path. - pth = os.path.abspath(os.path.join(base_path, pth)) + pth = os.path.abspath(self.base_dir / pth) if options is not None: normalized.append({pth: options}) @@ -489,35 +334,25 @@ def normalize_extension_paths(self, extensions: Sequence) -> Sequence: def get_handler( - *, - theme: str, - custom_templates: str | None = None, - config_file_path: str | None = None, - paths: list[str] | None = None, - locale: str = "en", - load_external_modules: bool | None = None, - **config: Any, # noqa: ARG001 + handler_config: MutableMapping[str, Any], + tool_config: MkDocsConfig, + **kwargs: Any, ) -> PythonHandler: """Simply return an instance of `PythonHandler`. Arguments: - theme: The theme to use when rendering contents. - custom_templates: Directory containing custom templates. - config_file_path: The MkDocs configuration file path. - paths: A list of paths to use as Griffe search paths. - locale: The locale to use when rendering content. - load_external_modules: Load external modules when resolving aliases. - **config: Configuration passed to the handler. + handler_config: The handler configuration. + tool_config: The tool (SSG) configuration. Returns: An instance of `PythonHandler`. """ + base_dir = Path(tool_config.config_file_path or "./mkdocs.yml").parent + if "inventories" not in handler_config and "import" in handler_config: + warn("The 'import' key is renamed 'inventories' for the Python handler", FutureWarning, stacklevel=1) + handler_config["inventories"] = handler_config.pop("import", []) return PythonHandler( - handler="python", - theme=theme, - custom_templates=custom_templates, - config_file_path=config_file_path, - paths=paths, - locale=locale, - load_external_modules=load_external_modules, + config=PythonConfig.from_data(**handler_config), + base_dir=base_dir, + **kwargs, ) diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py index 085f0c34..836e4164 100644 --- a/src/mkdocstrings_handlers/python/rendering.py +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -2,17 +2,17 @@ from __future__ import annotations -import enum import random import re import string import subprocess import sys import warnings +from dataclasses import replace from functools import lru_cache from pathlib import Path from re import Match, Pattern -from typing import TYPE_CHECKING, Any, Callable, ClassVar +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal from griffe import ( Alias, @@ -42,15 +42,6 @@ logger = get_logger(__name__) -class Order(enum.Enum): - """Enumeration for the possible members ordering.""" - - alphabetical = "alphabetical" - """Alphabetical order.""" - source = "source" - """Source code order.""" - - def _sort_key_alphabetical(item: CollectorItem) -> Any: # chr(sys.maxunicode) is a string that contains the final unicode # character, so if 'name' isn't found on the object, the item will go to @@ -65,9 +56,10 @@ def _sort_key_source(item: CollectorItem) -> Any: return item.lineno if item.lineno is not None else -1 +Order = Literal["alphabetical", "source"] order_map = { - Order.alphabetical: _sort_key_alphabetical, - Order.source: _sort_key_source, + "alphabetical": _sort_key_alphabetical, + "source": _sort_key_source, } @@ -159,8 +151,7 @@ def do_format_signature( new_context = context.parent else: new_context = dict(context.parent) - new_context["config"] = dict(new_context["config"]) - new_context["config"]["show_signature_annotations"] = annotations + new_context["config"] = replace(new_context["config"], show_signature_annotations=annotations) signature = template.render(new_context, function=function, signature=True) signature = _format_signature(callable_path, signature, line_length) @@ -215,7 +206,7 @@ def do_format_attribute( env = context.environment # TODO: Stop using `do_get_template` when `*.html` templates are removed. template = env.get_template(do_get_template(env, "expression")) - annotations = context.parent["config"]["show_signature_annotations"] + annotations = context.parent["config"].show_signature_annotations signature = str(attribute_path).strip() if annotations and attribute.annotation: @@ -578,7 +569,7 @@ def do_as_functions_section( Returns: A functions docstring section. """ - keep_init_method = not context.parent["config"]["merge_init_into_class"] + keep_init_method = not context.parent["config"].merge_init_into_class return DocstringSectionFunctions( [ DocstringFunction( diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/children.html.jinja b/src/mkdocstrings_handlers/python/templates/material/_base/children.html.jinja index d8d7b87b..40c011ed 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/children.html.jinja +++ b/src/mkdocstrings_handlers/python/templates/material/_base/children.html.jinja @@ -111,7 +111,7 @@ Context: {% filter heading(heading_level, id=html_id ~ "-modules") %}Modules{% endfilter %} {% endif %} {% with heading_level = heading_level + extra_level %} - {% for module in modules|order_members(config.members_order.alphabetical, members_list) %} + {% for module in modules|order_members("alphabetical", members_list) %} {% if members_list is not none or (not module.is_alias or module.is_public) %} {% include module|get_template with context %} {% endif %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/summary/modules.html.jinja b/src/mkdocstrings_handlers/python/templates/material/_base/summary/modules.html.jinja index 15e78088..606711c5 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/summary/modules.html.jinja +++ b/src/mkdocstrings_handlers/python/templates/material/_base/summary/modules.html.jinja @@ -14,7 +14,7 @@ inherited_members=config.inherited_members, keep_no_docstrings=config.show_if_no_docstring, ) - |order_members(config.members_order.alphabetical, members_list) + |order_members("alphabetical", members_list) |as_modules_section(check_public=not members_list) %} {% if section %}{% include "docstring/modules"|get_template with context %}{% endif %} diff --git a/tests/helpers.py b/tests/helpers.py index 91ea4eee..37c127e4 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -30,15 +30,18 @@ def mkdocs_conf(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[MkDo Yields: MkDocs config. """ - conf = MkDocsConfig() while hasattr(request, "_parent_request") and hasattr(request._parent_request, "_parent_request"): request = request._parent_request + params = getattr(request, "param", {}) + plugins = params.pop("plugins", [{"mkdocstrings": {}}]) + + conf = MkDocsConfig() conf_dict = { "site_name": "foo", "site_url": "https://example.org/", "site_dir": str(tmp_path), - "plugins": [{"mkdocstrings": {"default_handler": "python"}}], + "plugins": plugins, **getattr(request, "param", {}), } # Re-create it manually as a workaround for https://github.com/mkdocs/mkdocs/issues/2289 @@ -90,5 +93,5 @@ def handler(plugin: MkdocstringsPlugin, ext_markdown: Markdown) -> PythonHandler A handler instance. """ handler = plugin.handlers.get_handler("python") - handler._update_env(ext_markdown, plugin.handlers._config) + handler._update_env(ext_markdown) return handler # type: ignore[return-value] diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index 05dcfeb3..a1cf518d 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -39,9 +39,11 @@ def _render(handler: PythonHandler, package: TmpPackage, final_options: dict[str handler_options.setdefault("show_root_heading", True) handler_options.setdefault("show_source", False) + options = handler.get_options(handler_options) + handler._paths = [str(package.tmpdir)] try: - data = handler.collect(package.name, handler_options) + data = handler.collect(package.name, options) finally: # We're using a session handler, so we need to reset its state after each call. # This is not thread-safe, but pytest-xdist uses subprocesses, so it's fine. @@ -49,7 +51,7 @@ def _render(handler: PythonHandler, package: TmpPackage, final_options: dict[str handler._lines_collection = LinesCollection() handler._paths = [] - html = handler.render(data, handler_options) + html = handler.render(data, options) return _normalize_html(html) diff --git a/tests/test_handler.py b/tests/test_handler.py index 6c2381db..365b5f23 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -10,36 +10,35 @@ import pytest from griffe import DocstringSectionExamples, DocstringSectionKind, temporary_visited_module -from mkdocstrings_handlers.python.handler import CollectionError, PythonHandler, get_handler +from mkdocstrings_handlers.python.config import PythonOptions +from mkdocstrings_handlers.python.handler import CollectionError, PythonHandler if TYPE_CHECKING: from pathlib import Path + from mkdocstrings.plugin import MkdocstringsPlugin -def test_collect_missing_module() -> None: + +def test_collect_missing_module(handler: PythonHandler) -> None: """Assert error is raised for missing modules.""" - handler = get_handler(theme="material") with pytest.raises(CollectionError): - handler.collect("aaaaaaaa", {}) + handler.collect("aaaaaaaa", PythonOptions()) -def test_collect_missing_module_item() -> None: +def test_collect_missing_module_item(handler: PythonHandler) -> None: """Assert error is raised for missing items within existing modules.""" - handler = get_handler(theme="material") with pytest.raises(CollectionError): - handler.collect("mkdocstrings.aaaaaaaa", {}) + handler.collect("mkdocstrings.aaaaaaaa", PythonOptions()) -def test_collect_module() -> None: +def test_collect_module(handler: PythonHandler) -> None: """Assert existing module can be collected.""" - handler = get_handler(theme="material") - assert handler.collect("mkdocstrings", {}) + assert handler.collect("mkdocstrings", PythonOptions()) -def test_collect_with_null_parser() -> None: +def test_collect_with_null_parser(handler: PythonHandler) -> None: """Assert we can pass `None` as parser when collecting.""" - handler = get_handler(theme="material") - assert handler.collect("mkdocstrings", {"docstring_style": None}) + assert handler.collect("mkdocstrings", PythonOptions(docstring_style=None)) @pytest.mark.parametrize( @@ -71,7 +70,7 @@ def test_render_docstring_examples_section(handler: PythonHandler) -> None: assert "Hello" in rendered -def test_expand_globs(tmp_path: Path) -> None: +def test_expand_globs(tmp_path: Path, plugin: MkdocstringsPlugin) -> None: """Assert globs are correctly expanded. Parameters: @@ -86,24 +85,16 @@ def test_expand_globs(tmp_path: Path) -> None: globbed_paths = [tmp_path.joinpath(globbed_name) for globbed_name in globbed_names] for path in globbed_paths: path.touch() - handler = PythonHandler( - handler="python", - theme="material", - config_file_path=str(tmp_path.joinpath("mkdocs.yml")), - paths=["*exp*"], - ) + plugin.handlers._tool_config.config_file_path = str(tmp_path.joinpath("mkdocs.yml")) + handler: PythonHandler = plugin.handlers.get_handler("python", {"paths": ["*exp*"]}) # type: ignore[assignment] for path in globbed_paths: assert str(path) in handler._paths -def test_expand_globs_without_changing_directory() -> None: +def test_expand_globs_without_changing_directory(plugin: MkdocstringsPlugin) -> None: """Assert globs are correctly expanded when we are already in the right directory.""" - handler = PythonHandler( - handler="python", - theme="material", - config_file_path="mkdocs.yml", - paths=["*.md"], - ) + plugin.handlers._tool_config.config_file_path = "mkdocs.yml" + handler: PythonHandler = plugin.handlers.get_handler("python", {"paths": ["*.md"]}) # type: ignore[assignment] for path in list(glob(os.path.abspath(".") + "/*.md")): assert path in handler._paths @@ -130,12 +121,15 @@ def test_expand_globs_without_changing_directory() -> None: (False, {"dot.notation.path.to.pyextension": {"option": "value"}}), ], ) -def test_extension_paths(tmp_path: Path, expect_change: bool, extension: str | dict) -> None: +def test_extension_paths( + tmp_path: Path, + expect_change: bool, + extension: str | dict, + plugin: MkdocstringsPlugin, +) -> None: """Assert extension paths are resolved relative to config file.""" - handler = get_handler( - theme="material", - config_file_path=str(tmp_path.joinpath("mkdocs.yml")), - ) + plugin.handlers._tool_config.config_file_path = str(tmp_path.joinpath("mkdocs.yml")) + handler: PythonHandler = plugin.handlers.get_handler("python") # type: ignore[assignment] normalized = handler.normalize_extension_paths([extension])[0] if expect_change: if isinstance(normalized, str) and isinstance(extension, str): @@ -172,4 +166,4 @@ def function(self): module["Class"].lineno = None module["Class.function"].lineno = None module["attribute"].lineno = None - assert handler.render(module, {"show_source": True}) + assert handler.render(module, PythonOptions(show_source=True)) diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 081702f4..98da5d9c 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -143,14 +143,14 @@ def main(self): ... @pytest.mark.parametrize( ("order", "members_list", "expected_names"), [ - (rendering.Order.alphabetical, None, ["a", "b", "c"]), - (rendering.Order.source, None, ["c", "b", "a"]), - (rendering.Order.alphabetical, ["c", "b"], ["c", "b"]), - (rendering.Order.source, ["a", "c"], ["a", "c"]), - (rendering.Order.alphabetical, [], ["a", "b", "c"]), - (rendering.Order.source, [], ["c", "b", "a"]), - (rendering.Order.alphabetical, True, ["a", "b", "c"]), - (rendering.Order.source, False, ["c", "b", "a"]), + ("alphabetical", None, ["a", "b", "c"]), + ("source", None, ["c", "b", "a"]), + ("alphabetical", ["c", "b"], ["c", "b"]), + ("source", ["a", "c"], ["a", "c"]), + ("alphabetical", [], ["a", "b", "c"]), + ("source", [], ["c", "b", "a"]), + ("alphabetical", True, ["a", "b", "c"]), + ("source", False, ["c", "b", "a"]), ], ) def test_ordering_members(order: rendering.Order, members_list: list[str | None], expected_names: list[str]) -> None: diff --git a/tests/test_themes.py b/tests/test_themes.py index 035f453e..a7b44795 100644 --- a/tests/test_themes.py +++ b/tests/test_themes.py @@ -38,5 +38,6 @@ def test_render_themes_templates_python(identifier: str, handler: PythonHandler) identifier: Parametrized identifier. handler: Python handler (fixture). """ - data = handler.collect(identifier, {}) - handler.render(data, {}) + options = handler.get_options({}) + data = handler.collect(identifier, options) + handler.render(data, options)