1717
1818import re
1919import sys
20- from typing import Callable , List , Optional , Union , cast
20+ from typing import Callable , List , Optional , cast
2121
2222from griffe .dataclasses import Docstring , Object
2323from mkdocstrings .loggers import get_logger
@@ -57,8 +57,10 @@ def _re_named(name: str, exp: str, optional: bool = False) -> str:
5757 optchar = "?" if optional else ""
5858 return f"(?P<{ name } >{ exp } ){ optchar } "
5959
60+ _RE_CROSSREF = re .compile (r"\[([^\[\]]+?)\]\[([^\[\]]*?)\]" )
61+ """Regular expression that matches general cross-references."""
6062
61- _RE_REL_CROSSREF = re .compile (r"\[([^\[\]]+?)\]\[([\.^\(][^\]]*?|[^\]]*?\.)\]" )
63+ _RE_REL_CROSSREF = re .compile (r"\[([^\[\]]+?)\]\[(\??(?: [\.^\(][^\]]*?|[^\]]*?\.) )\]" )
6264"""Regular expression that matches relative cross-reference expressions in doc-string.
6365
6466This will match a cross reference where the path expression either ends in '.'
@@ -98,13 +100,13 @@ def _re_named(name: str, exp: str, optional: bool = False) -> str:
98100"""Regular expression that matches a qualified python identifier."""
99101
100102
101- def _always_ok (_ref :str ) -> bool :
103+ def _always_ok (_ref : str ) -> bool :
102104 return True
103105
104106
105107class _RelativeCrossrefProcessor :
106108 """
107- A callable object that substitutes relative cross-reference expressions.
109+ A callable object that can substitute relative cross-reference expressions.
108110
109111 This is intended to be used as a substitution function by `re.sub`
110112 to process relative cross-references in a doc-string.
@@ -116,7 +118,7 @@ class _RelativeCrossrefProcessor:
116118 _cur_offset : int
117119 _cur_ref_parts : List [str ]
118120 _ok : bool
119- _check_ref : Union [ Callable [[str ],bool ], Callable [[ str ], bool ] ]
121+ _check_ref : Callable [[str ], bool ]
120122
121123 def __init__ (self , doc : Docstring , checkref : Optional [Callable [[str ], bool ]] = None ):
122124 self ._doc = doc
@@ -128,27 +130,52 @@ def __init__(self, doc: Docstring, checkref: Optional[Callable[[str], bool]] = N
128130 self ._ok = True
129131
130132 def __call__ (self , match : re .Match ) -> str :
133+ """
134+ Process a cross-reference expression.
135+
136+ This should be called with a match from the _RE_CROSSREF expression
137+ which matches expression of the form [<title>][<ref>].
138+ Group 1 matches the <title> and 2 the <ref>.
139+ """
131140 self ._start_match (match )
132141
133142 title = match [1 ]
134143 ref = match [2 ]
135144
136- ref_match = _RE_REL .fullmatch (ref )
137- if ref_match is None :
138- self ._error (f"Bad syntax in relative cross reference: '{ ref } '" )
145+ checkref = self ._check_ref
146+ if ref .startswith ("?" ):
147+ # Turn off cross-ref check
148+ ref = ref [1 :]
149+ checkref = _always_ok
150+
151+ new_ref = ""
152+
153+ # TODO support special syntax to turn off checking
154+
155+ if not _RE_REL_CROSSREF .fullmatch (match .group (0 )):
156+ # Just a regular cross reference
157+ new_ref = ref if ref else title
139158 else :
140- self ._process_parent_specifier (ref_match )
141- self ._process_relname (ref_match )
142- self ._process_append_from_title (ref_match , title )
143-
144- if self ._ok :
145- new_ref = '.' .join (self ._cur_ref_parts )
146- logger .debug (
147- "cross-reference substitution\n in %s:\n [%s][%s] -> [...][%s]" ,
148- cast (Object ,self ._doc .parent ).canonical_path , title , ref , new_ref
149- )
150- if not self ._check_ref (new_ref ):
151- self ._error (f"Cannot load reference '{ new_ref } '" )
159+ ref_match = _RE_REL .fullmatch (ref )
160+ if ref_match is None :
161+ self ._error (f"Bad syntax in relative cross reference: '{ ref } '" )
162+ else :
163+ self ._process_parent_specifier (ref_match )
164+ self ._process_relname (ref_match )
165+ self ._process_append_from_title (ref_match , title )
166+
167+ if self ._ok :
168+ new_ref = '.' .join (self ._cur_ref_parts )
169+ logger .debug (
170+ "cross-reference substitution\n in %s:\n [%s][%s] -> [...][%s]" ,
171+ cast (Object , self ._doc .parent ).canonical_path , title , ref , new_ref
172+ )
173+
174+ # builtin names get handled specially somehow, so don't check here
175+ if new_ref not in __builtins__ and not checkref (new_ref ): # type: ignore[operator]
176+ self ._error (f"Cannot load reference '{ new_ref } '" )
177+
178+ if new_ref :
152179 result = f"[{ title } ][{ new_ref } ]"
153180 else :
154181 result = match .group (0 )
@@ -265,7 +292,7 @@ def _process_up_specifier(self, obj: Object, ref_match: re.Match) -> Optional[Ob
265292 break
266293 return rel_obj
267294
268- def _error (self , msg : str , just_warn :bool = False ) -> None :
295+ def _error (self , msg : str , just_warn : bool = False ) -> None :
269296 """Logs a warning for a specific crossref in a docstring.
270297
271298 This will include the filepath and line number if available.
@@ -281,8 +308,8 @@ def _error(self, msg: str, just_warn:bool = False) -> None:
281308 # recognize that this is a navigable location it can highlight.
282309 prefix = f"file://{ parent .filepath } :"
283310 line = doc .lineno
284- if line is not None : # pragma: no branch
285- if _supports_linenums : # pragma: no branch
311+ if line is not None : # pragma: no branch
312+ if _supports_linenums : # pragma: no branch
286313 # Add line offset to match in docstring. This can still be
287314 # short if the doc string has leading newlines.
288315 line += doc .value .count ("\n " , 0 , self ._cur_offset )
@@ -296,7 +323,7 @@ def _error(self, msg: str, just_warn:bool = False) -> None:
296323 self ._ok = just_warn
297324
298325
299- def substitute_relative_crossrefs (obj : Object , checkref : Optional [Callable [[str ],bool ]] = None ) -> None :
326+ def substitute_relative_crossrefs (obj : Object , checkref : Optional [Callable [[str ], bool ]] = None ) -> None :
300327 """Recursively expand relative cross-references in all docstrings in tree.
301328
302329 Arguments:
@@ -307,9 +334,8 @@ def substitute_relative_crossrefs(obj: Object, checkref: Optional[Callable[[str]
307334 doc = obj .docstring
308335
309336 if doc is not None :
310- doc .value = _RE_REL_CROSSREF .sub (_RelativeCrossrefProcessor (doc , checkref = checkref ), doc .value )
337+ doc .value = _RE_CROSSREF .sub (_RelativeCrossrefProcessor (doc , checkref = checkref ), doc .value )
311338
312339 for member in obj .members .values ():
313- if isinstance (member , Object ): # pragma: no branch
314- substitute_relative_crossrefs (member , checkref = checkref )
315-
340+ if isinstance (member , Object ): # pragma: no branch
341+ substitute_relative_crossrefs (member , checkref = checkref )
0 commit comments