Skip to content

Fine-grained Configuration of check_crossrefs #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 64 additions & 25 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
4 changes: 1 addition & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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


2 changes: 1 addition & 1 deletion src/mkdocstrings_handlers/python_xref/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.16.2
1.16.3
30 changes: 20 additions & 10 deletions src/mkdocstrings_handlers/python_xref/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,6 +45,7 @@
@dataclass(**_dataclass_options)
class PythonRelXRefOptions(PythonOptions):
check_crossrefs: bool = True
check_crossrefs_exclude: list[str | re.Pattern] = field(default_factory=list)

class PythonRelXRefHandler(PythonHandler):
"""Extended version of mkdocstrings Python handler
Expand All @@ -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', 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)
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)
Expand All @@ -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 | re.Pattern] = []) -> bool:
"""Check for existence of reference"""
for ex in exclude:
if re.match(ex, ref):
return True
try:
self.collect(ref, PythonOptions())
return True
Expand Down