Skip to content

Commit 516fd29

Browse files
authored
Add auto-linking of fully qualified property and method names (#201)
* Minor auto-linking tweaks. - Do not auto-link a class or constructor name if followed by .<alphanumeric> (E.g. a fully-qualified property or method) - Do not auto-link a property name in a property docstring if preceded or followed by <non-breaking space>. * Add auto-linking of fully qualified property and method names. - Requires matlab_auto_link = "all"
1 parent b7aacf6 commit 516fd29

File tree

5 files changed

+56
-14
lines changed

5 files changed

+56
-14
lines changed

README.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Additional Configuration
8282

8383
``matlab_auto_link``
8484
Automatically convert the names of known entities (e.g. classes, functions,
85-
properties, methods) to links Valid values are ``"basic"``
85+
properties, methods) to links. Valid values are ``"basic"``
8686
and ``"all"``.
8787

8888
* ``"basic"`` - Auto-links (1) known classes, functions, properties, or
@@ -94,6 +94,7 @@ Additional Configuration
9494

9595
* ``"all"`` - Auto-links everything included with ``"basic"``, plus all known
9696
classes and functions everywhere else they appear in any docstring, any
97+
fully qualified (including class name) property or method names, any
9798
names ending with "()" within class, property, or method docstrings that
9899
match a method of the corresponding class, and any property or method names
99100
in their own docstrings. Note that a non-breaking space before or after

sphinxcontrib/mat_documenters.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -285,12 +285,21 @@ def auto_link_all(self, docstrings):
285285
role = o.ref_role()
286286
if role in ["class", "func"]:
287287
nn = n.replace("+", "") # remove + from name
288-
pat = (
289-
r"(?<!(`|\.|\+|<|@| ))\b" # negative look-behind for ` . + < @ <non-breaking space>
290-
+ nn.replace(".", r"\.") # escape .
291-
+ r"\b(?!(`| |\sProperties|\sMethods):)" # negative look-ahead for ` or " Properties:" or " Methods:"
292-
)
288+
# negative look-behind for ` . + < @ <non-breaking space>
289+
look_behind = r"(?<!(`|\.|\+|<|@| ))\b"
290+
# negative look-ahead for ` or <non-breaking space> or
291+
# " Properties:" or " Methods:" or .<alphanum>
292+
look_ahead = r"\b(?!(`| |\sProperties:|\sMethods:|\.\w))"
293+
look_ahead2 = r"\b(?!(`| |\sProperties:|\sMethods:))"
294+
# entity_name is NOT followed by .<property_or_method>
295+
pat = look_behind + nn.replace(".", r"\.") + look_ahead
293296
p = re.compile(pat)
297+
if role == "class":
298+
# entity_name IS followed by .<property_or_method>
299+
pat2 = (
300+
look_behind + nn.replace(".", r"\.") + r"\.(\w+)" + look_ahead2
301+
)
302+
p2 = re.compile(pat2)
294303
no_link_state = 0 # normal mode (no literal block detected)
295304
for i in range(len(docstrings)):
296305
for j in range(len(docstrings[i])):
@@ -301,6 +310,24 @@ def auto_link_all(self, docstrings):
301310
docstrings[i][j] = p.sub(
302311
f":{role}:`{nn}`", docstrings[i][j]
303312
)
313+
if role == "class":
314+
if match := p2.search(docstrings[i][j]):
315+
# if match.group(1) is a property
316+
# -> :attr:`{nn}.{match.group(1)}`
317+
for nnn, ooo in o.properties.items():
318+
if match.group(2) == nnn:
319+
docstrings[i][j] = p2.sub(
320+
f":attr:`{nn}.{nnn}`", docstrings[i][j]
321+
)
322+
break
323+
# if match.group(1) is a method
324+
# -> :meth:`{nn}.{match.group(1)}`
325+
for nnn, ooo in o.methods.items():
326+
if match.group(2) == nnn:
327+
docstrings[i][j] = p2.sub(
328+
f":meth:`{nn}.{nnn}`", docstrings[i][j]
329+
)
330+
break
304331

305332
return docstrings
306333

@@ -1301,8 +1328,9 @@ def class_object(self):
13011328

13021329
def auto_link_self(self, docstrings):
13031330
name = self.object.name
1304-
# negative look-behind for ` . < @ <non-breaking space>
1305-
p = re.compile(r"(?<!(`|\.|<|@| ))\b" + name + r"\b(?! )")
1331+
# negative look-behind for ` or . or < or @ or <non-breaking space>
1332+
# and negative look-ahead for <non-breaking space> or .<alphanum>
1333+
p = re.compile(r"(?<!(`|\.|<|@| ))\b" + name + r"\b(?! |\.\w)")
13061334
no_link_state = 0 # normal mode (no literal block detected)
13071335
for i in range(len(docstrings)):
13081336
for j in range(len(docstrings[i])):
@@ -1398,8 +1426,9 @@ def class_object(self):
13981426

13991427
def auto_link_self(self, docstrings):
14001428
name = self.object.name
1401-
# negative look-behind for ` or . or <
1402-
p = re.compile(r"(?<!(`|\.|<))\b" + name + r"\b")
1429+
# negative look-behind for ` or . or < or <non-breaking space>
1430+
# and negative look-ahead for <non-breaking space>
1431+
p = re.compile(r"(?<!(`|\.|<| ))\b" + name + r"\b(?! )")
14031432
no_link_state = 0 # normal mode (no literal block detected)
14041433
for i in range(len(docstrings)):
14051434
for j in range(len(docstrings[i])):

tests/roots/test_autodoc/target/ClassExample.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
end
2121
methods
2222
function mc = ClassExample(a)
23+
% Links to fully qualified names package.ClassBar.foos,
24+
% package.ClassBar.doBar, and ClassExample.mymethod.
2325
mc.a = a;
2426
end
2527

tests/test_autodoc.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_target(make_app, rootdir):
3636
assert len(content) == 1
3737
assert (
3838
content[0].astext()
39-
== "target\n\n\n\nclass target.ClassExample\n\nBases: handle\n\nExample class\n\nClassExample Properties:\n\na - first property of ClassExample\nb - second property of ClassExample\nc - third property of ClassExample\n\nClassExample Methods:\n\nClassExample - the constructor and a reference to mymethod()\nmymethod - a method in ClassExample\n\nSee also BaseClass, baseFunction, b, unknownEntity, mymethod.\n\nProperty Summary\n\n\n\n\n\na\n\na property\n\n\n\nb\n\na property with default value\n\n\n\nc\n\na property with multiline default value\n\nMethod Summary\n\n\n\n\n\nmymethod(b)\n\nA method in ClassExample\n\nParameters\n\nb – an input to mymethod()"
39+
== "target\n\n\n\nclass target.ClassExample\n\nBases: handle\n\nExample class\n\nClassExample Properties:\n\na - first property of ClassExample\nb - second property of ClassExample\nc - third property of ClassExample\n\nClassExample Methods:\n\nClassExample - the constructor and a reference to mymethod()\nmymethod - a method in ClassExample\n\nSee also BaseClass, baseFunction, b, unknownEntity, mymethod.\n\nConstructor Summary\n\n\n\n\n\nClassExample(a)\n\nLinks to fully qualified names package.ClassBar.foos,\npackage.ClassBar.doBar, and ClassExample.mymethod.\n\nProperty Summary\n\n\n\n\n\na\n\na property\n\n\n\nb\n\na property with default value\n\n\n\nc\n\na property with multiline default value\n\nMethod Summary\n\n\n\n\n\nmymethod(b)\n\nA method in ClassExample\n\nParameters\n\nb – an input to mymethod()"
4040
)
4141
assert (
4242
property_section.rawsource
@@ -61,7 +61,7 @@ def test_target_show_default_value(make_app, rootdir):
6161
assert len(content) == 1
6262
assert (
6363
content[0].astext()
64-
== "target\n\n\n\nclass target.ClassExample\n\nBases: handle\n\nExample class\n\nClassExample Properties:\n\na - first property of ClassExample\nb - second property of ClassExample\nc - third property of ClassExample\n\nClassExample Methods:\n\nClassExample - the constructor and a reference to mymethod()\nmymethod - a method in ClassExample\n\nSee also BaseClass, baseFunction, b, unknownEntity, mymethod.\n\nProperty Summary\n\n\n\n\n\na\n\na property\n\n\n\nb = 10\n\na property with default value\n\n\n\nc = [10; ... 30]\n\na property with multiline default value\n\nMethod Summary\n\n\n\n\n\nmymethod(b)\n\nA method in ClassExample\n\nParameters\n\nb – an input to mymethod()"
64+
== "target\n\n\n\nclass target.ClassExample\n\nBases: handle\n\nExample class\n\nClassExample Properties:\n\na - first property of ClassExample\nb - second property of ClassExample\nc - third property of ClassExample\n\nClassExample Methods:\n\nClassExample - the constructor and a reference to mymethod()\nmymethod - a method in ClassExample\n\nSee also BaseClass, baseFunction, b, unknownEntity, mymethod.\n\nConstructor Summary\n\n\n\n\n\nClassExample(a)\n\nLinks to fully qualified names package.ClassBar.foos,\npackage.ClassBar.doBar, and ClassExample.mymethod.\n\nProperty Summary\n\n\n\n\n\na\n\na property\n\n\n\nb = 10\n\na property with default value\n\n\n\nc = [10; ... 30]\n\na property with multiline default value\n\nMethod Summary\n\n\n\n\n\nmymethod(b)\n\nA method in ClassExample\n\nParameters\n\nb – an input to mymethod()"
6565
)
6666
assert (
6767
property_section.rawsource
@@ -110,6 +110,7 @@ def test_target_auto_link_all(make_app, rootdir):
110110
property_section = content[0][2][1][2][0] # a bit fragile, I know
111111
method_section = content[0][2][1][2][1] # a bit fragile, I know
112112
see_also_line = content[0][2][1][3] # a bit fragile, I know
113+
constructor_desc = content[0][2][1][4][0][0][1][2][1][0] # a bit fragile, I know
113114
assert len(content) == 1
114115
assert (
115116
property_section.rawsource
@@ -123,6 +124,10 @@ def test_target_auto_link_all(make_app, rootdir):
123124
see_also_line.rawsource
124125
== "See also :class:`BaseClass`, :func:`baseFunction`, :attr:`b <target.ClassExample.b>`, ``unknownEntity``, :meth:`mymethod() <target.ClassExample.mymethod>`."
125126
)
127+
assert (
128+
constructor_desc.rawsource
129+
== "Links to fully qualified names :attr:`package.ClassBar.foos`,\n:meth:`package.ClassBar.doBar`, and :meth:`ClassExample.mymethod`."
130+
)
126131

127132

128133
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher")

tests/test_autodoc_short_links.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_target(make_app, rootdir):
3737
assert len(content) == 1
3838
assert (
3939
content[0].astext()
40-
== "target\n\n\n\nclass ClassExample\n\nBases: handle\n\nExample class\n\nClassExample Properties:\n\na - first property of ClassExample\nb - second property of ClassExample\nc - third property of ClassExample\n\nClassExample Methods:\n\nClassExample - the constructor and a reference to mymethod()\nmymethod - a method in ClassExample\n\nSee also BaseClass, baseFunction, b, unknownEntity, mymethod.\n\nProperty Summary\n\n\n\n\n\na\n\na property\n\n\n\nb\n\na property with default value\n\n\n\nc\n\na property with multiline default value\n\nMethod Summary\n\n\n\n\n\nmymethod(b)\n\nA method in ClassExample\n\nParameters\n\nb – an input to mymethod()"
40+
== "target\n\n\n\nclass ClassExample\n\nBases: handle\n\nExample class\n\nClassExample Properties:\n\na - first property of ClassExample\nb - second property of ClassExample\nc - third property of ClassExample\n\nClassExample Methods:\n\nClassExample - the constructor and a reference to mymethod()\nmymethod - a method in ClassExample\n\nSee also BaseClass, baseFunction, b, unknownEntity, mymethod.\n\nConstructor Summary\n\n\n\n\n\nClassExample(a)\n\nLinks to fully qualified names package.ClassBar.foos,\npackage.ClassBar.doBar, and ClassExample.mymethod.\n\nProperty Summary\n\n\n\n\n\na\n\na property\n\n\n\nb\n\na property with default value\n\n\n\nc\n\na property with multiline default value\n\nMethod Summary\n\n\n\n\n\nmymethod(b)\n\nA method in ClassExample\n\nParameters\n\nb – an input to mymethod()"
4141
)
4242
assert (
4343
property_section.rawsource
@@ -62,7 +62,7 @@ def test_target_show_default_value(make_app, rootdir):
6262
assert len(content) == 1
6363
assert (
6464
content[0].astext()
65-
== "target\n\n\n\nclass ClassExample\n\nBases: handle\n\nExample class\n\nClassExample Properties:\n\na - first property of ClassExample\nb - second property of ClassExample\nc - third property of ClassExample\n\nClassExample Methods:\n\nClassExample - the constructor and a reference to mymethod()\nmymethod - a method in ClassExample\n\nSee also BaseClass, baseFunction, b, unknownEntity, mymethod.\n\nProperty Summary\n\n\n\n\n\na\n\na property\n\n\n\nb = 10\n\na property with default value\n\n\n\nc = [10; ... 30]\n\na property with multiline default value\n\nMethod Summary\n\n\n\n\n\nmymethod(b)\n\nA method in ClassExample\n\nParameters\n\nb – an input to mymethod()"
65+
== "target\n\n\n\nclass ClassExample\n\nBases: handle\n\nExample class\n\nClassExample Properties:\n\na - first property of ClassExample\nb - second property of ClassExample\nc - third property of ClassExample\n\nClassExample Methods:\n\nClassExample - the constructor and a reference to mymethod()\nmymethod - a method in ClassExample\n\nSee also BaseClass, baseFunction, b, unknownEntity, mymethod.\n\nConstructor Summary\n\n\n\n\n\nClassExample(a)\n\nLinks to fully qualified names package.ClassBar.foos,\npackage.ClassBar.doBar, and ClassExample.mymethod.\n\nProperty Summary\n\n\n\n\n\na\n\na property\n\n\n\nb = 10\n\na property with default value\n\n\n\nc = [10; ... 30]\n\na property with multiline default value\n\nMethod Summary\n\n\n\n\n\nmymethod(b)\n\nA method in ClassExample\n\nParameters\n\nb – an input to mymethod()"
6666
)
6767
assert (
6868
property_section.rawsource
@@ -111,6 +111,7 @@ def test_target_auto_link_all(make_app, rootdir):
111111
property_section = content[0][2][1][2][0] # a bit fragile, I know
112112
method_section = content[0][2][1][2][1] # a bit fragile, I know
113113
see_also_line = content[0][2][1][3] # a bit fragile, I know
114+
constructor_desc = content[0][2][1][4][0][0][1][2][1][0] # a bit fragile, I know
114115
assert len(content) == 1
115116
assert (
116117
property_section.rawsource
@@ -124,6 +125,10 @@ def test_target_auto_link_all(make_app, rootdir):
124125
see_also_line.rawsource
125126
== "See also :class:`BaseClass`, :func:`baseFunction`, :attr:`b <ClassExample.b>`, ``unknownEntity``, :meth:`mymethod() <ClassExample.mymethod>`."
126127
)
128+
assert (
129+
constructor_desc.rawsource
130+
== "Links to fully qualified names :attr:`package.ClassBar.foos`,\n:meth:`package.ClassBar.doBar`, and :meth:`ClassExample.mymethod`."
131+
)
127132

128133

129134
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher")

0 commit comments

Comments
 (0)