Skip to content
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
9 changes: 9 additions & 0 deletions doc/_static/theme_override.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@ div[class^="highlight"] a:hover {
.rst-versions.shift-up {
overflow-y: visible;
}

a[class^="sphx-glr-backref-module-"] {
text-decoration: none;
background-color: rgba(0, 0, 0, 0) !important;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why !important here? It makes it hard to override by package developers, I think (I'm no CSS expert so please bear with me :-))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for building the SG docs, it shouldn't affect actual use of SG by downstream libraries. The one CSS change that will is in gallery.css

}
a.sphx-glr-backref-module-sphinx_gallery {
text-decoration: underline;
background-color: #E6E6E6;
}
117 changes: 85 additions & 32 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -406,36 +406,89 @@ configuration option setup for Sphinx-Gallery.
:emphasize-lines: 12-22, 32-42
:linenos:

.. note::
By default, Sphinx-gallery will inspect global variables (and code objects)
at the end of each code block to try to find classes of variables and
method calls. It also tries to find methods called on classes.
For example, this code::

lst = [1, 2]
fig, ax = plt.subplots()
ax.plot(lst)

should end up with the following links (assuming intersphinx is set up
properly):

- :class:`lst <python:list>`
- :func:`plt.subplots <matplotlib.pyplot.subplots>`
- :class:`fig <matplotlib.figure.Figure>`
- :class:`ax <matplotlib.axes.Axes>`
- :meth:`ax.plot <matplotlib.axes.Axes.plot>`

However, this feature is might not work properly in all instances.
Moreover, if variable names get reused in the same script to refer to
different classes, it will break.

To disable this global variable introspection, you can use the configuration
key::

sphinx_gallery_conf = {
...
'inspect_global_variables' : False,
}
Toggling global variable inspection
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

By default, Sphinx-gallery will inspect global variables (and code objects)
at the end of each code block to try to find classes of variables and
method calls. It also tries to find methods called on classes.
For example, this code::

lst = [1, 2]
fig, ax = plt.subplots()
ax.plot(lst)

should end up with the following links (assuming intersphinx is set up
properly):

- :class:`lst <python:list>`
- :func:`plt.subplots <matplotlib.pyplot.subplots>`
- :class:`fig <matplotlib.figure.Figure>`
- :class:`ax <matplotlib.axes.Axes>`
- :meth:`ax.plot <matplotlib.axes.Axes.plot>`

However, this feature might not work properly in all instances.
Moreover, if variable names get reused in the same script to refer to
different classes, it will break.

To disable this global variable introspection, you can use the configuration
key::

sphinx_gallery_conf = {
...
'inspect_global_variables' : False,
}

Stylizing code links using CSS
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Each link in the code blocks will be decorated with two or three CSS classes.

1. ``sphx-glr-backref-module-*``
The module where the object is documented.
For example, ``sphx-glr-backref-module-matplotlib-figure``.
2. ``sphx-glr-backref-type-*``
The type of the object. This is a sanitized intersphinx type, for
example a ``py:class`` will have the CSS class
``sphx-glr-backref-type-py-class``.
3. ``sphx-glr-backref-instance``
A class that is added if the object is an instance of a class
(rather than, e.g., a class itself, method, or function).
By default, Sphinx-Gallery adds the following CSS in ``gallery.css``:

.. code-block:: css

a.sphx-glr-backref-instance {
text-decoration: none;
}

This is done to reduce the visual impact of instance linking
in example code. This means that for the following code::

x = Figure()

here ``x`` is an instance of a class, so it will have the
``sphx-glr-backref-instance`` CSS class, and it will not be decorated;
and ``Figure`` is a class, so it will not have the
``sphx-glr-backref-instance`` CSS class, and will thus be decorated the
standard way for links in the given parent styles.

These three CSS classes are meant to give fine-grained control over how
different links are decorated. For example, using CSS selectors you could
choose to avoid highlighting any ``sphx-glr-backref-*`` links except for ones
that you whitelist (e.g., those from your own module). For example:

.. code-block:: css

a[class^="sphx-glr-backref-module-"] {
text-decoration: none;
}
a[class^="sphx-glr-backref-module-matplotlib"] {
text-decoration: underline;
}

There are likely elements other than ``text-decoration`` that might be worth
setting, as well.

.. _custom_default_thumb:

Expand Down Expand Up @@ -984,7 +1037,7 @@ tuple, in order of preference. The representation methods currently supported
are:

* ``__repr__`` - returns the official string representation of an object. This
is what is returned when your Python shell evaluates an expression.
is what is returned when your Python shell evaluates an expression.
* ``__str__`` - returns a string containing a nicely printable representation
of an object. This is what is used when you ``print()`` an object or pass it
to ``format()``.
Expand Down Expand Up @@ -1045,7 +1098,7 @@ method which would thus be captured. You can prevent this by:

* add ``plt.show()`` (which does not return anything) to the end of your
code block. For example::

import matplotlib.pyplot as plt

plt.plot([1, 2, 3, 4], [1, 4, 9, 16])
Expand Down
4 changes: 4 additions & 0 deletions sphinx_gallery/_static/gallery.css
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,7 @@ p.sphx-glr-signature a.reference.external {
.sphx-glr-clear{
clear: both;
}

a.sphx-glr-backref-instance {
text-decoration: none;
}
20 changes: 15 additions & 5 deletions sphinx_gallery/backreferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import codecs
import collections
from html import escape
import inspect
import os
import re
import warnings
Expand Down Expand Up @@ -67,9 +68,18 @@ def get_mapping(self):
if local_name in self.imported_names:
# Join import path to relative path
full_name = self.imported_names[local_name] + remainder
yield name, full_name, class_attr
if local_name in self.global_variables:
obj = self.global_variables[local_name]
if remainder:
for level in remainder[1:].split('.'):
obj = getattr(obj, level)
Comment on lines +74 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be missing something but why is a for loop needed here? Won't obj always be the attribute of the last split level, when you get out of the for loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some case doing c = a.b.c did not work but doing c = a.b; c = c.c did. Can't remember offhand if it had to do with module nesting or class attributes or what...

...but fortunately I was so annoyed by it that I wrote a test to capture it, and if I just do obj = getattr(obj, remainder[1:]) (what I think you're suggesting) I get:

AttributeError: module 'numpy' has no attribute 'random.RandomState'

Which is probably because of this somewhat unexpected behavior:

>>> import numpy
>>> getattr(numpy, 'random.RandomState')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/larsoner/python/numpy/numpy/__init__.py", line 219, in __getattr__
    raise AttributeError("module {!r} has no attribute "
AttributeError: module 'numpy' has no attribute 'random.RandomState'
>>> numpy.random.RandomState
<class 'numpy.random.mtrand.RandomState'>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I am suggesting obj = remainder[1:].split('.')[-1] - the last loop of the for loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then obj is a str rather than the actual object

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops

level = remainder[1:].split('.')[-1]
obj = getattr(obj, level)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I get it now. Sorry and thanks for explaining.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, hopefully it's not too bad to add your changes from #584 on top once this is in -- then it will be my turn to be confused during review :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently you are allowed to have . in attribute names!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope never to encounter such code :)

is_class = inspect.isclass(obj)
else:
is_class = False
yield name, full_name, class_attr, is_class
elif local_name in self.global_variables:
obj = self.global_variables[local_name]
is_class = inspect.isclass(obj)
if remainder and remainder[0] == '.': # maybe meth or attr
method = [remainder[1:]]
class_attr = True
Expand All @@ -91,7 +101,7 @@ def get_mapping(self):
for depth in range(len(module), 0, -1):
full_name = '.'.join(
module[:depth] + [class_name] + method)
yield name, full_name, class_attr
yield name, full_name, class_attr, is_class


def _from_import(a, b):
Expand Down Expand Up @@ -157,10 +167,10 @@ def identify_names(script_blocks, global_variables=None, node=''):
names = list(finder.get_mapping())
# Get matches from docstring inspection
text = '\n'.join(txt for kind, txt, _ in script_blocks if kind == 'text')
names.extend((x, x, False) for x in re.findall(_regex, text))
names.extend((x, x, False, False) for x in re.findall(_regex, text))
example_code_obj = collections.OrderedDict() # order is important
fill_guess = dict()
for name, full_name, class_like in names:
for name, full_name, class_like, is_class in names:
if name in example_code_obj:
continue # if someone puts it in the docstring and code
# name is as written in file (e.g. np.asarray)
Expand All @@ -178,7 +188,7 @@ def identify_names(script_blocks, global_variables=None, node=''):
# get shortened module name
module_short = _get_short_module_name(module, attribute)
cobj = {'name': attribute, 'module': module,
'module_short': module_short}
'module_short': module_short, 'is_class': is_class}
if module_short is not None:
example_code_obj[name] = cobj
elif name not in fill_guess:
Expand Down
30 changes: 25 additions & 5 deletions sphinx_gallery/docs_resolv.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ def _handle_http_url_error(e, msg='fetching'):
type(e).__name__, error_msg))


def _sanitize_css_class(s):
for x in '~!@$%^&*()+=,./\';:"?><[]\\{}|`#':
s = s.replace(x, '-')
return s


def _embed_code_links(app, gallery_conf, gallery_dir):
# Add resolvers for the packages for which we want to show links
doc_resolvers = {}
Expand All @@ -284,7 +290,8 @@ def _embed_code_links(app, gallery_conf, gallery_dir):
gallery_dir))

# patterns for replacement
link_pattern = ('<a href="%s" title="View documentation for %s">%s</a>')
link_pattern = (
'<a href="{link}" title="{title}" class="{css_class}">{text}</a>')
orig_pattern = '<span class="n">%s</span>'
period = '<span class="o">.</span>'

Expand Down Expand Up @@ -318,7 +325,8 @@ def _embed_code_links(app, gallery_conf, gallery_dir):
cname = cobj['name']

# Try doc resolvers first
link = None
link = type_ = None
is_instance = False
if this_module in doc_resolvers:
try:
link = doc_resolvers[this_module].resolve(
Expand All @@ -327,7 +335,7 @@ def _embed_code_links(app, gallery_conf, gallery_dir):
_handle_http_url_error(
e, msg='resolving %s.%s' % (modname, cname))

# next try intersphinx
# next try intersphinx (which gives us the type_ as well)
if this_module == modname == 'builtins':
this_module = 'python'
elif modname in builtin_modules:
Expand All @@ -342,15 +350,27 @@ def _embed_code_links(app, gallery_conf, gallery_dir):
# only python domain
if key.startswith('py') and want in value:
link = value[want][2]
type_ = key
# differentiate classes from instances
is_instance = ('py:class' in type_ and
not cobj['is_class'])
break

if link is not None:
parts = name.split('.')
name_html = period.join(orig_pattern % part
for part in parts)
full_function_name = '%s.%s' % (modname, cname)
str_repl[name_html] = link_pattern % (
link, full_function_name, name_html)
css_class = ("sphx-glr-backref-module-" +
_sanitize_css_class(modname))
if type_ is not None:
css_class += (" sphx-glr-backref-type-" +
_sanitize_css_class(type_))
if is_instance:
css_class += " sphx-glr-backref-instance"
str_repl[name_html] = link_pattern.format(
link=link, title=full_function_name,
css_class=css_class, text=name_html)
break # loop over possible module names

# do the replacement in the html file
Expand Down
45 changes: 34 additions & 11 deletions sphinx_gallery/tests/test_backreferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,26 @@ def test_identify_names(unicode_sample):
"""Test name identification."""
expected = {
'os.path.join':
{'name': 'join', 'module': 'os.path', 'module_short': 'os.path'},
{
'name': 'join',
'module': 'os.path',
'module_short': 'os.path',
'is_class': False,
},
'br.identify_names':
{'name': 'identify_names',
'module': 'sphinx_gallery.back_references',
'module_short': 'sphinx_gallery.back_references'},
{
'name': 'identify_names',
'module': 'sphinx_gallery.back_references',
'module_short': 'sphinx_gallery.back_references',
'is_class': False,
},
'identify_names':
{'name': 'identify_names',
'module': 'sphinx_gallery.back_references',
'module_short': 'sphinx_gallery.back_references'}
{
'name': 'identify_names',
'module': 'sphinx_gallery.back_references',
'module_short': 'sphinx_gallery.back_references',
'is_class': False,
},
}
_, script_blocks = split_code_and_text_blocks(unicode_sample)
res = sg.identify_names(script_blocks)
Expand All @@ -98,9 +109,20 @@ def test_identify_names2(tmpdir):
print(c)
e.HelloWorld().f.g
"""
expected = {'c': {'name': 'c', 'module': 'a.b', 'module_short': 'a.b'},
'e.HelloWorld': {'name': 'HelloWorld', 'module': 'd',
'module_short': 'd'}}
expected = {
'c': {
'name': 'c',
'module': 'a.b',
'module_short': 'a.b',
'is_class': False,
},
'e.HelloWorld': {
'name': 'HelloWorld',
'module': 'd',
'module_short': 'd',
'is_class': False,
}
}

fname = tmpdir.join("indentify_names.py")
fname.write(code_str, 'wb')
Expand All @@ -118,7 +140,8 @@ def test_identify_names2(tmpdir):
This example uses :func:`h.i`.
'''
""" + code_str.split(b"'''")[-1]
expected['h.i'] = {u'module': u'h', u'module_short': u'h', u'name': u'i'}
expected['h.i'] = {u'module': u'h', u'module_short': u'h', u'name': u'i',
'is_class': False}

fname = tmpdir.join("indentify_names.py")
fname.write(code_str, 'wb')
Expand Down
12 changes: 9 additions & 3 deletions sphinx_gallery/tests/test_full.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,10 @@ def test_embed_links_and_styles(sphinx_app):
# ensure we've linked properly
assert '#module-matplotlib.colors' in lines
assert 'matplotlib.colors.is_color_like' in lines
assert 'class="sphx-glr-backref-module-matplotlib-colors sphx-glr-backref-type-py-function">' in lines # noqa
assert '#module-numpy' in lines
assert 'numpy.arange.html' in lines
assert 'class="sphx-glr-backref-module-numpy sphx-glr-backref-type-py-function">' in lines # noqa
assert '#module-matplotlib.pyplot' in lines
assert 'pyplot.html' in lines
assert 'matplotlib.figure.Figure.html#matplotlib.figure.Figure.tight_layout' in lines # noqa
Expand All @@ -193,6 +195,10 @@ def test_embed_links_and_styles(sphinx_app):
assert 'stdtypes.html#list' in lines
assert 'warnings.html#warnings.warn' in lines
assert 'itertools.html#itertools.compress' in lines
assert 'numpy.ndarray.html' in lines
# instances have an extra CSS class
assert 'class="sphx-glr-backref-module-matplotlib-figure sphx-glr-backref-type-py-class sphx-glr-backref-instance"><span class="n">x</span></a>' in lines # noqa
assert 'class="sphx-glr-backref-module-matplotlib-figure sphx-glr-backref-type-py-class"><span class="n">Figure</span></a>' in lines # noqa

try:
import memory_profiler # noqa, analysis:ignore
Expand All @@ -213,9 +219,9 @@ def test_embed_links_and_styles(sphinx_app):
assert '.. code-block:: python3\n' in rst

# warnings
want_warn = ('plot_numpy_matplotlib.py:35: RuntimeWarning: This'
' warning should show up in the output')
assert want_warn in lines
want_warn = (r'.*plot_numpy_matplotlib\.py:[0-9][0-9]: RuntimeWarning: This'
r' warning should show up in the output.*')
assert re.match(want_warn, lines, re.DOTALL) is not None
sys.stdout.write(lines)


Expand Down
Loading