From bdfa2fc626851698b8e4ab53afa8d90e517a118c Mon Sep 17 00:00:00 2001 From: Tianshu Huang Date: Sun, 8 Jun 2025 13:32:51 -0400 Subject: [PATCH 1/3] Change check_crossrefs to be stored as a class attribute instead of via global_config, and add a blacklist option --- .../python_xref/handler.py | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/mkdocstrings_handlers/python_xref/handler.py b/src/mkdocstrings_handlers/python_xref/handler.py index 2efeed3..053c2c8 100644 --- a/src/mkdocstrings_handlers/python_xref/handler.py +++ b/src/mkdocstrings_handlers/python_xref/handler.py @@ -17,8 +17,10 @@ from __future__ import annotations +import re import sys -from dataclasses import dataclass, fields +from dataclasses import dataclass, field, fields +from functools import partial from pathlib import Path from typing import Any, ClassVar, Mapping, MutableMapping, Optional from warnings import warn @@ -43,6 +45,7 @@ @dataclass(**_dataclass_options) class PythonRelXRefOptions(PythonOptions): check_crossrefs: bool = True + check_crossrefs_exclude: list[str] = field(default_factory=list) class PythonRelXRefHandler(PythonHandler): """Extended version of mkdocstrings Python handler @@ -62,26 +65,30 @@ def __init__(self, config: PythonConfig, base_dir: Path, **kwargs: Any) -> None: base_dir: The base directory of the project. **kwargs: Arguments passed to the parent constructor. """ - check_crossrefs = config.options.pop('check_crossrefs', None) # Remove + self.check_crossrefs = config.options.pop('check_crossrefs', None) + self.check_crossrefs_exclude = config.options.pop( + 'check_crossrefs_exclude', []) super().__init__(config, base_dir, **kwargs) - if check_crossrefs is not None: - self.global_options["check_crossrefs"] = check_crossrefs def get_options(self, local_options: Mapping[str, Any]) -> PythonRelXRefOptions: local_options = dict(local_options) - check_crossrefs = local_options.pop('check_crossrefs', None) + check_crossrefs = local_options.pop( + 'check_crossrefs', self.check_crossrefs) + check_crossrefs_exclude = local_options.pop( + 'check_crossrefs_exclude', self.check_crossrefs_exclude) _opts = super().get_options(local_options) opts = PythonRelXRefOptions( + check_crossrefs=check_crossrefs, + check_crossrefs_exclude=check_crossrefs_exclude, **{field.name: getattr(_opts, field.name) for field in fields(_opts)} ) - if check_crossrefs is not None: - opts.check_crossrefs = bool(check_crossrefs) return opts def render(self, data: CollectorItem, options: PythonOptions) -> str: if options.relative_crossrefs: - if isinstance(options, PythonRelXRefOptions): - checkref = self._check_ref if options.check_crossrefs else None + if isinstance(options, PythonRelXRefOptions) and options.check_crossrefs: + checkref = partial( + self._check_ref, exclude=options.check_crossrefs_exclude) else: checkref = None substitute_relative_crossrefs(data, checkref=checkref) @@ -98,8 +105,11 @@ def get_templates_dir(self, handler: Optional[str] = None) -> Path: handler = 'python' return super().get_templates_dir(handler) - def _check_ref(self, ref:str) -> bool: + def _check_ref(self, ref : str, exclude: list[str] = []) -> bool: """Check for existence of reference""" + for ex in exclude: + if re.match(ex, ref): + return True try: self.collect(ref, PythonOptions()) return True From f849a9818a630a9d6fd75c939ed479b4020bb303 Mon Sep 17 00:00:00 2001 From: Tianshu Huang Date: Mon, 9 Jun 2025 14:23:34 -0400 Subject: [PATCH 2/3] Cleanup for PR: docs, version bump, and pre-compile regex --- docs/config.md | 89 +++++++++++++------ docs/index.md | 4 +- src/mkdocstrings_handlers/python_xref/VERSION | 2 +- .../python_xref/handler.py | 8 +- 4 files changed, 70 insertions(+), 33 deletions(-) diff --git a/docs/config.md b/docs/config.md index ab5d5e2..f0391e7 100644 --- a/docs/config.md +++ b/docs/config.md @@ -3,38 +3,77 @@ that the handler name should be `python_xref` instead of `python`. Because this handler extends the standard [mkdocstrings-python][] handler, the same options are available. -Additional options are added by this extension. Currently, there are two: +Additional options are added by this extension. Currently, there are three: -* **relative_crossrefs** - if set to true enables use of relative path syntax in +* **relative_crossrefs**: `bool` - if set to true enables use of relative path syntax in cross-references. -* **check_crossrefs** - enables early checking of all cross-references. Note that +* **check_crossrefs**: `bool` - enables early checking of all cross-references. Note that this option only takes affect if **relative_crossrefs** is also true. This option is true by default, so this option is used to disable checking. Checking can also be disabled on a per-case basis by prefixing the reference with '?', e.g. `[something][?dontcheckme]`. -!!! Example "mkdocs.yml plugins specification using this handler" - -```yaml -plugins: -- search -- mkdocstrings: - default_handler: python_xref - handlers: - python_xref: - import: - - https://docs.python.org/3/objects.inv - options: - docstring_style: google - docstring_options: - ignore_init_summary: yes - merge_init_into_class: yes - relative_crossrefs: yes - check_crossrefs: no - separate_signature: yes - show_source: no - show_root_full_path: no -``` +* **check_crossrefs_exclude**: `list[str]` - exclude cross-references matching any of these + regex patterns from crossref checking. This option can be used disabling checking on + libraries which are very expensive to import without having to disable checking for all + cross-references. + +!!! Example "mkdocs.yml plugins specifications using this handler" + + === "Always check" + + !!! warning + + Crossrefs to libraries which are expensive to import (e.g., machine learning + frameworks) can cause very slow build times when checked! + + ```yaml + plugins: + - mkdocstrings: + default_handler: python_xref + handlers: + python_xref: + import: + - https://docs.python.org/3/objects.inv + - https://pytorch.org/docs/stable/objects.inv + options: + relative_crossrefs: yes + ``` + + === "Check all but listed exclusions" + + ```yaml + plugins: + - mkdocstrings: + default_handler: python_xref + handlers: + python_xref: + import: + - https://docs.python.org/3/objects.inv + - https://pytorch.org/docs/stable/objects.inv + options: + relative_crossrefs: yes + check_crossrefs_exclude: + - "^torch\\.(.*)" + ``` + + === "Never check" + + ```yaml + plugins: + - mkdocstrings: + default_handler: python_xref + handlers: + python_xref: + import: + - https://docs.python.org/3/objects.inv + - https://pytorch.org/docs/stable/objects.inv + options: + relative_crossrefs: yes + check_crossrefs: no + ``` + + [mkdocstrings-python]: https://mkdocstrings.github.io/python/ diff --git a/docs/index.md b/docs/index.md index 895569e..9b582d1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -105,7 +105,7 @@ but has not yet been accepted. If `relative_crossrefs` and `check_crossrefs` are both enabled (the latter is true by default), then all cross-reference expressions will be checked to ensure that they exist and failures -will be reported with the source location. Otherwise, missing cross-references will be reported +will be reported with the source location (with the exception of any crossrefs which match a pattern in `check_crossrefs_exclude`). Otherwise, missing cross-references will be reported by mkdocstrings without the source location, in which case it is often difficult to locate the source of the error. Note that the errors generated by this feature are in addition to the errors from mkdocstrings. @@ -121,5 +121,3 @@ This function returns a [Path][?pathlib.] instance. [mkdocstrings]: https://mkdocstrings.github.io/ [mkdocstrings_python]: https://mkdocstrings.github.io/python/ [relative-crossref-issue]: https://github.com/mkdocstrings/python/issues/27 - - diff --git a/src/mkdocstrings_handlers/python_xref/VERSION b/src/mkdocstrings_handlers/python_xref/VERSION index 4a02d2c..c807441 100644 --- a/src/mkdocstrings_handlers/python_xref/VERSION +++ b/src/mkdocstrings_handlers/python_xref/VERSION @@ -1 +1 @@ -1.16.2 +1.16.3 diff --git a/src/mkdocstrings_handlers/python_xref/handler.py b/src/mkdocstrings_handlers/python_xref/handler.py index 053c2c8..d13ef29 100644 --- a/src/mkdocstrings_handlers/python_xref/handler.py +++ b/src/mkdocstrings_handlers/python_xref/handler.py @@ -45,7 +45,7 @@ @dataclass(**_dataclass_options) class PythonRelXRefOptions(PythonOptions): check_crossrefs: bool = True - check_crossrefs_exclude: list[str] = field(default_factory=list) + check_crossrefs_exclude: list[str | re.Pattern] = field(default_factory=list) class PythonRelXRefHandler(PythonHandler): """Extended version of mkdocstrings Python handler @@ -66,8 +66,8 @@ def __init__(self, config: PythonConfig, base_dir: Path, **kwargs: Any) -> None: **kwargs: Arguments passed to the parent constructor. """ self.check_crossrefs = config.options.pop('check_crossrefs', None) - self.check_crossrefs_exclude = config.options.pop( - 'check_crossrefs_exclude', []) + exclude = config.options.pop('check_crossrefs_exclude', []) + self.check_crossrefs_exclude = [re.compile(p) for p in exclude] super().__init__(config, base_dir, **kwargs) def get_options(self, local_options: Mapping[str, Any]) -> PythonRelXRefOptions: @@ -105,7 +105,7 @@ def get_templates_dir(self, handler: Optional[str] = None) -> Path: handler = 'python' return super().get_templates_dir(handler) - def _check_ref(self, ref : str, exclude: list[str] = []) -> bool: + def _check_ref(self, ref : str, exclude: list[str | re.Pattern] = []) -> bool: """Check for existence of reference""" for ex in exclude: if re.match(ex, ref): From c053860ccf4e955fe1ecfc532283928999227a11 Mon Sep 17 00:00:00 2001 From: Tianshu Huang Date: Mon, 9 Jun 2025 22:22:34 -0400 Subject: [PATCH 3/3] Change default to instead of --- src/mkdocstrings_handlers/python_xref/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings_handlers/python_xref/handler.py b/src/mkdocstrings_handlers/python_xref/handler.py index d13ef29..a1a61dc 100644 --- a/src/mkdocstrings_handlers/python_xref/handler.py +++ b/src/mkdocstrings_handlers/python_xref/handler.py @@ -65,7 +65,7 @@ def __init__(self, config: PythonConfig, base_dir: Path, **kwargs: Any) -> None: base_dir: The base directory of the project. **kwargs: Arguments passed to the parent constructor. """ - self.check_crossrefs = config.options.pop('check_crossrefs', None) + self.check_crossrefs = config.options.pop('check_crossrefs', True) exclude = config.options.pop('check_crossrefs_exclude', []) self.check_crossrefs_exclude = [re.compile(p) for p in exclude] super().__init__(config, base_dir, **kwargs)