1
+ # Copyright (c) 2022. Analog Devices Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
1
14
"""Support for translating compact relative crossreferences in docstrings."""
2
- # author: Christopher Barber, Analog Devices | Analog Garage
3
15
4
16
from __future__ import annotations
5
17
6
18
import re
7
- from typing import List , Optional
19
+ import sys
20
+ from typing import Callable , List , Optional , Union , cast
8
21
9
22
from griffe .dataclasses import Docstring , Object
10
23
from mkdocstrings .loggers import get_logger
13
26
14
27
logger = get_logger (__name__ )
15
28
29
+ # line numbers from griffe are not reliable before python 3.8; this may eventually be fixed...
30
+ _supports_linenums = sys .version_info >= (3 , 8 )
31
+
16
32
17
33
def _re_or (* exps : str ) -> str :
18
34
"""Construct an "or" regular expression from a sequence of regular expressions.
@@ -41,7 +57,7 @@ def _re_named(name: str, exp: str, optional: bool = False) -> str:
41
57
return f"(?P<{ name } >{ exp } ){ optchar } "
42
58
43
59
44
- _RE_REL_CROSSREF = re .compile (r"\[(. +?)\]\[([\.^\(][^\]]*?|[^\]]*?\.)\]" )
60
+ _RE_REL_CROSSREF = re .compile (r"\[([^\[\]] +?)\]\[([\.^\(][^\]]*?|[^\]]*?\.)\]" )
45
61
"""Regular expression that matches relative cross-reference expressions in doc-string.
46
62
47
63
This will match a cross reference where the path expression either ends in '.'
@@ -79,6 +95,10 @@ def _re_named(name: str, exp: str, optional: bool = False) -> str:
79
95
"""Regular expression that matches a qualified python identifier."""
80
96
81
97
98
+ def _always_ok (_ref : str ) -> bool :
99
+ return True
100
+
101
+
82
102
class _RelativeCrossrefProcessor :
83
103
"""
84
104
A callable object that substitutes relative cross-reference expressions.
@@ -93,13 +113,15 @@ class _RelativeCrossrefProcessor:
93
113
_cur_offset : int
94
114
_cur_ref_parts : List [str ]
95
115
_ok : bool
116
+ _check_ref : Union [Callable [[str ], bool ], Callable [[str ], bool ]]
96
117
97
- def __init__ (self , doc : Docstring ):
118
+ def __init__ (self , doc : Docstring , checkref : Optional [ Callable [[ str ], bool ]] = None ):
98
119
self ._doc = doc
99
120
self ._cur_match = None
100
121
self ._cur_input = ""
101
122
self ._cur_offset = 0
102
123
self ._cur_ref_parts = []
124
+ self ._check_ref = checkref or _always_ok
103
125
self ._ok = True
104
126
105
127
def __call__ (self , match : re .Match ) -> str :
@@ -117,13 +139,23 @@ def __call__(self, match: re.Match) -> str:
117
139
self ._process_append_from_title (ref_match , title )
118
140
119
141
if self ._ok :
120
- result = f"[{ title } ][{ '.' .join (self ._cur_ref_parts )} ]"
142
+ new_ref = "." .join (self ._cur_ref_parts )
143
+ logger .debug (
144
+ "cross-reference substitution\n in %s:\n [%s][%s] -> [...][%s]" , # noqa: WPS323
145
+ cast (Object , self ._doc .parent ).canonical_path ,
146
+ title ,
147
+ ref ,
148
+ new_ref ,
149
+ )
150
+ if not self ._check_ref (new_ref ):
151
+ self ._error (f"Cannot load reference '{ new_ref } '" )
152
+ result = f"[{ title } ][{ new_ref } ]"
121
153
else :
122
154
result = match .group (0 )
123
155
124
156
return result
125
157
126
- def _start_match (self , match : re .Match ):
158
+ def _start_match (self , match : re .Match ) -> None :
127
159
self ._cur_match = match
128
160
self ._cur_offset = match .start (0 )
129
161
self ._cur_input = match [0 ]
@@ -143,7 +175,7 @@ def _process_append_from_title(self, ref_match: re.Match, title_text: str) -> No
143
175
return
144
176
self ._cur_ref_parts .append (id_from_title )
145
177
146
- def _process_parent_specifier (self , ref_match : re .Match ):
178
+ def _process_parent_specifier (self , ref_match : re .Match ) -> None :
147
179
if not ref_match .group ("parent" ):
148
180
return
149
181
@@ -196,11 +228,11 @@ def _process_up_specifier(self, obj: Object, ref_match: re.Match) -> Optional[Ob
196
228
level = len (ref_match .group ("up" ))
197
229
rel_obj = obj
198
230
for _ in range (level ):
199
- if rel_obj .parent is None :
231
+ if rel_obj .parent is not None :
232
+ rel_obj = rel_obj .parent
233
+ else :
200
234
self ._error (f"'{ ref_match .group ('up' )} ' has too many levels for { obj .canonical_path } " )
201
235
break
202
- else :
203
- rel_obj = rel_obj .parent
204
236
return rel_obj
205
237
206
238
def _error (self , msg : str ) -> None :
@@ -219,9 +251,10 @@ def _error(self, msg: str) -> None:
219
251
# recognize that this is a navigable location it can highlight.
220
252
prefix = f"file://{ parent .filepath } :"
221
253
line = doc .lineno
222
- if line is not None :
223
- # Add line offset to match in docstring
224
- line += doc .value .count ("\n " , 0 , self ._cur_offset )
254
+ if line is not None : # pragma: no branch
255
+ if _supports_linenums : # pragma: no branch
256
+ # Add line offset to match in docstring
257
+ line += doc .value .count ("\n " , 0 , self ._cur_offset )
225
258
prefix += f"{ line } :"
226
259
# It would be nice to add the column as well, but we cannot determine
227
260
# that without knowing how much the doc string was unindented.
@@ -232,17 +265,19 @@ def _error(self, msg: str) -> None:
232
265
self ._ok = False
233
266
234
267
235
- def substitute_relative_crossrefs (obj : Object ) :
268
+ def substitute_relative_crossrefs (obj : Object , checkref : Optional [ Callable [[ str ], bool ]] = None ) -> None :
236
269
"""Recursively expand relative cross-references in all docstrings in tree.
237
270
238
271
Arguments:
239
- obj: root object. The object's docstring will be be processed as well
240
- as all of its children recursively.
272
+ obj: a Griffe [Object][griffe.dataclasses.] whose docstrings should be modified
273
+ checkref: optional function to check whether computed cross-reference is valid.
274
+ Should return True if valid, False if not valid.
241
275
"""
242
276
doc = obj .docstring
243
- if doc :
244
- doc .value = _RE_REL_CROSSREF .sub (_RelativeCrossrefProcessor (doc ), doc .value )
277
+
278
+ if doc is not None :
279
+ doc .value = _RE_REL_CROSSREF .sub (_RelativeCrossrefProcessor (doc , checkref = checkref ), doc .value )
245
280
246
281
for member in obj .members .values ():
247
- if isinstance (member , Object ):
248
- substitute_relative_crossrefs (member )
282
+ if isinstance (member , Object ): # pragma: no branch
283
+ substitute_relative_crossrefs (member , checkref = checkref )
0 commit comments