Skip to content

Commit 6594ff5

Browse files
authored
ENH: Create backreferences for default roles (#1122)
1 parent ec3d796 commit 6594ff5

File tree

7 files changed

+87
-44
lines changed

7 files changed

+87
-44
lines changed

doc/configuration.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,9 @@ enables you to link to any examples that either:
446446
code.
447447
2. Refer to that function/method/attribute/object/class using sphinx markup
448448
``:func:``/``:meth:``/``:attr:``/``:obj:``/``:class:`` in a text
449-
block.
449+
block. You can omit this role markup if you have set the `default_role
450+
<https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-default_role>`_
451+
in your ``conf.py`` to any of these roles.
450452

451453
The former is useful for auto-documenting functions that are used and classes
452454
that are explicitly instantiated. The generated links are called implicit

examples/plot_6_function_identifier.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import os.path as op # noqa, analysis:ignore
3030
import matplotlib.pyplot as plt
3131
import sphinx_gallery
32-
from sphinx_gallery.backreferences import identify_names
32+
from sphinx_gallery.backreferences import identify_names, _make_ref_regex
3333
from sphinx_gallery.py_source_parser import split_code_and_text_blocks
3434

3535
filename = 'plot_6_function_identifier.py'
@@ -39,7 +39,7 @@
3939

4040
_, script_blocks = split_code_and_text_blocks(filename)
4141

42-
names = identify_names(script_blocks)
42+
names = identify_names(script_blocks, _make_ref_regex())
4343

4444
# %%
4545
# In the code block above, we use the internal function ``identify_names`` to

sphinx_gallery/backreferences.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,22 @@ def _get_short_module_name(module_name, obj_name):
204204
return short_name
205205

206206

207-
# keep in synch w/ configuration.rst "Add mini-galleries for API documentation"
208-
_regex = re.compile(r':(?:'
209-
r'func|'
210-
r'meth|'
211-
r'attr|'
212-
r'obj|'
213-
r'class):`~?(\S*)`'
214-
)
215-
216-
217-
def identify_names(script_blocks, global_variables=None, node=''):
207+
def _make_ref_regex(config=None):
208+
"""Make regex to find reference to python objects."""
209+
# keep roles variable in sync values shown in configuration.rst
210+
# "Add mini-galleries for API documentation"
211+
roles = 'func|meth|attr|obj|class'
212+
default_role = config['default_role'] or '' if config else ''
213+
def_role_regex = (
214+
'|[^:]?' if re.fullmatch(f'(?:py:)?({roles})', default_role) else ''
215+
)
216+
# reference can have a separate title `title <reference>`,
217+
# don't match ``literal`` or `!disabled.references`
218+
return (rf'(?::(?:{roles}):{def_role_regex})' # role prefix
219+
r'(?!``)`~?[^!]*?<?([^!~\s<>`]+)>?(?!``)`') # reference
220+
221+
222+
def identify_names(script_blocks, ref_regex, global_variables=None, node=''):
218223
"""Build a codeobj summary by identifying and resolving used names."""
219224
if node == '': # mostly convenience for testing functions
220225
c = '\n'.join(txt for kind, txt, _ in script_blocks if kind == 'code')
@@ -226,7 +231,9 @@ def identify_names(script_blocks, global_variables=None, node=''):
226231
names = list(finder.get_mapping())
227232
# Get matches from docstring inspection (explicit matches)
228233
text = '\n'.join(txt for kind, txt, _ in script_blocks if kind == 'text')
229-
names.extend((x, x, False, False, True) for x in re.findall(_regex, text))
234+
names.extend(
235+
(x, x, False, False, True) for x in re.findall(ref_regex, text)
236+
)
230237
example_code_obj = collections.OrderedDict() # order is important
231238
# Make a list of all guesses, in `_embed_code_links` we will break
232239
# when we find a match

sphinx_gallery/gen_rst.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
optipng)
4141
from . import glr_path_static
4242
from .backreferences import (_write_backreferences, _thumbnail_div,
43-
identify_names, THUMBNAIL_PARENT_DIV,
44-
THUMBNAIL_PARENT_DIV_CLOSE)
43+
identify_names, _make_ref_regex,
44+
THUMBNAIL_PARENT_DIV, THUMBNAIL_PARENT_DIV_CLOSE)
4545
from .downloads import CODE_DOWNLOAD
4646
from .py_source_parser import (split_code_and_text_blocks,
4747
remove_config_comments,
@@ -1134,7 +1134,9 @@ def generate_file_rst(fname, target_dir, src_dir, gallery_conf,
11341134
global_variables = script_vars['example_globals']
11351135
else:
11361136
global_variables = None
1137-
example_code_obj = identify_names(script_blocks, global_variables, node)
1137+
ref_regex = _make_ref_regex(gallery_conf['app'].config)
1138+
example_code_obj = identify_names(script_blocks, ref_regex,
1139+
global_variables, node)
11381140
if example_code_obj:
11391141
codeobj_fname = target_file[:-3] + '_codeobj.pickle.new'
11401142
with open(codeobj_fname, 'wb') as fid:

sphinx_gallery/tests/conftest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ def pytest_report_header(config, startdir):
2727
@pytest.fixture
2828
def gallery_conf(tmpdir):
2929
"""Set up a test sphinx-gallery configuration."""
30-
app = Mock(spec=Sphinx, config=dict(source_suffix={'.rst': None}),
31-
extensions=[])
30+
app = Mock(
31+
spec=Sphinx,
32+
config=dict(source_suffix={".rst": None}, default_role=None),
33+
extensions=[],
34+
)
3235
gallery_conf = gen_gallery._fill_gallery_conf_defaults(
3336
{}, app=app)
3437
gen_gallery._update_gallery_conf_builder_inited(

sphinx_gallery/tests/test_backreferences.py

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def test_thumbnail_div(content, tooltip, is_backref):
6666
assert html_div == reference
6767

6868

69-
def test_identify_names(unicode_sample):
69+
def test_identify_names(unicode_sample, gallery_conf):
7070
"""Test name identification."""
7171
expected = {
7272
'os.path.join':
@@ -95,12 +95,13 @@ def test_identify_names(unicode_sample):
9595
}],
9696
}
9797
_, script_blocks = split_code_and_text_blocks(unicode_sample)
98-
res = sg.identify_names(script_blocks)
98+
ref_regex = sg._make_ref_regex(gallery_conf['app'].config)
99+
res = sg.identify_names(script_blocks, ref_regex)
99100
assert expected == res
100101

101102

102-
def test_identify_names2(tmpdir):
103-
"""Test more name identification."""
103+
def test_identify_names_implicit(tmpdir, gallery_conf):
104+
"""Test implicit name identification."""
104105
code_str = b"""
105106
'''
106107
Title
@@ -148,26 +149,52 @@ def test_identify_names2(tmpdir):
148149
fname.write(code_str, 'wb')
149150

150151
_, script_blocks = split_code_and_text_blocks(fname.strpath)
151-
res = sg.identify_names(script_blocks)
152+
ref_regex = sg._make_ref_regex(gallery_conf['app'].config)
153+
res = sg.identify_names(script_blocks, ref_regex)
152154

153155
assert expected == res
154156

155-
code_str = b"""
156-
'''
157-
Title
158-
-----
159-
160-
This example uses :func:`k.l` and :meth:`~m.n`.
161-
'''
162-
""" + code_str.split(b"'''")[-1]
163-
expected['k.l'] = [{'module': 'k', 'module_short': 'k', 'name': 'l',
164-
'is_class': False, 'is_explicit': True}]
165-
expected['m.n'] = [{'module': 'm', 'module_short': 'm', 'name': 'n',
166-
'is_class': False, 'is_explicit': True}]
167157

168-
fname = tmpdir.join("identify_names.py")
169-
fname.write(code_str, 'wb')
170-
_, script_blocks = split_code_and_text_blocks(fname.strpath)
171-
res = sg.identify_names(script_blocks)
172-
173-
assert expected == res
158+
cobj = dict(
159+
module="m", module_short="m", name="n", is_class=False, is_explicit=True
160+
)
161+
162+
163+
@pytest.mark.parametrize(
164+
'text, default_role, ref, cobj',
165+
[
166+
(':func:`m.n`', None, 'm.n', cobj),
167+
(':func:`~m.n`', 'obj', 'm.n', cobj),
168+
(':func:`Title <m.n>`', None, 'm.n', cobj),
169+
(':func:`!m.n` or `!t <~m.n>`', None, None, None),
170+
('`m.n`', 'obj', 'm.n', cobj),
171+
('`m.n`', None, None, None), # see comment below
172+
(':ref:`m.n`', None, None, None),
173+
('`m.n`', 'ref', None, None),
174+
('``literal``', 'obj', None, None),
175+
],
176+
ids=[
177+
'regular',
178+
'show only last component',
179+
'with title',
180+
'no link for !',
181+
'default_role obj',
182+
'no default_role', # see comment below
183+
'non-python role',
184+
'non-python default_role',
185+
'literal',
186+
],
187+
)
188+
# the sphinx default value for default_role is None = no change, the docutils
189+
# default is title-reference (set by the default-role directive), see
190+
# www.sphinx-doc.org/en/master/usage/configuration.html#confval-default_role
191+
# and docutils.sourceforge.io/docs/ref/rst/roles.html
192+
def test_identify_names_explicit(text, default_role, ref, cobj, gallery_conf):
193+
"""Test explicit name identification."""
194+
if default_role:
195+
gallery_conf['app'].config['default_role'] = default_role
196+
script_blocks = [('text', text, 1)]
197+
expected = {ref: [cobj]} if ref else {}
198+
ref_regex = sg._make_ref_regex(gallery_conf['app'].config)
199+
actual = sg.identify_names(script_blocks, ref_regex)
200+
assert expected == actual

sphinx_gallery/tests/tinybuild/examples/plot_numpy_matplotlib.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
# nested resolution resolves to numpy.random.mtrand.RandomState:
4040
rng = np.random.RandomState(0)
4141
# test Issue 583
42-
sphinx_gallery.backreferences.identify_names([('text', 'Text block', 1)])
42+
sphinx_gallery.backreferences.identify_names(
43+
[('text', 'Text block', 1)],
44+
sphinx_gallery.backreferences._make_ref_regex({'default_role': None}))
4345
# 583: methods don't link properly
4446
dc = sphinx_gallery.backreferences.DummyClass()
4547
dc.run()

0 commit comments

Comments
 (0)