Skip to content

Fix plot directive with func calls #21661

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 21, 2022
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
12 changes: 12 additions & 0 deletions doc/api/next_api_changes/removals/21661-TAC.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plot directive removals
~~~~~~~~~~~~~~~~~~~~~~~

The public methods:

- ``matplotlib.sphinxext.split_code_at_show``
- ``matplotlib.sphinxext.unescape_doctest``
- ``matplotlib.sphinxext.run_code``

have been removed.

The deprecated *encoding* option to the plot directive has been removed.
98 changes: 30 additions & 68 deletions lib/matplotlib/sphinxext/plot_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,6 @@
changed using the ``plot_html_show_source_link`` variable in
:file:`conf.py` (which itself defaults to True).

``:encoding:`` : str
If this source file is in a non-UTF8 or non-ASCII encoding, the
encoding must be specified using the ``:encoding:`` option. The
encoding will not be inferred using the ``-*- coding -*-`` metacomment.

``:context:`` : bool or str
If provided, the code will be run in the context of all previous plot
directives for which the ``:context:`` option was specified. This only
Expand Down Expand Up @@ -166,7 +161,7 @@
import matplotlib
from matplotlib.backend_bases import FigureManagerBase
import matplotlib.pyplot as plt
from matplotlib import _api, _pylab_helpers, cbook
from matplotlib import _pylab_helpers, cbook

matplotlib.use("agg")

Expand Down Expand Up @@ -200,11 +195,6 @@ def _option_format(arg):
return directives.choice(arg, ('python', 'doctest'))


def _deprecated_option_encoding(arg):
_api.warn_deprecated("3.5", name="encoding", obj_type="option")
return directives.encoding(arg)


def mark_plot_labels(app, document):
"""
To make plots referenceable, we need to move the reference from the
Expand Down Expand Up @@ -254,7 +244,6 @@ class PlotDirective(Directive):
'format': _option_format,
'context': _option_context,
'nofigs': directives.flag,
'encoding': _deprecated_option_encoding,
'caption': directives.unchanged,
}

Expand Down Expand Up @@ -316,47 +305,25 @@ def contains_doctest(text):
return bool(m)


@_api.deprecated("3.5", alternative="doctest.script_from_examples")
def unescape_doctest(text):
"""
Extract code from a piece of text, which contains either Python code
or doctests.
"""
if not contains_doctest(text):
return text
code = ""
for line in text.split("\n"):
m = re.match(r'^\s*(>>>|\.\.\.) (.*)$', line)
if m:
code += m.group(2) + "\n"
elif line.strip():
code += "# " + line.strip() + "\n"
else:
code += "\n"
return code


@_api.deprecated("3.5")
def split_code_at_show(text):
def _split_code_at_show(text, function_name):
"""Split code at plt.show()."""
return _split_code_at_show(text)[1]


def _split_code_at_show(text):
"""Split code at plt.show()."""
parts = []
is_doctest = contains_doctest(text)
part = []
for line in text.split("\n"):
if (not is_doctest and line.strip() == 'plt.show()') or \
(is_doctest and line.strip() == '>>> plt.show()'):
part.append(line)
if function_name is None:
parts = []
part = []
for line in text.split("\n"):
if ((not is_doctest and line.startswith('plt.show(')) or
(is_doctest and line.strip() == '>>> plt.show()')):
part.append(line)
parts.append("\n".join(part))
part = []
else:
part.append(line)
if "\n".join(part).strip():
parts.append("\n".join(part))
part = []
else:
part.append(line)
if "\n".join(part).strip():
parts.append("\n".join(part))
else:
parts = [text]
return is_doctest, parts


Expand Down Expand Up @@ -469,15 +436,6 @@ class PlotError(RuntimeError):
pass


@_api.deprecated("3.5")
def run_code(code, code_path, ns=None, function_name=None):
"""
Import a Python module from a path, and run the function given by
name, if function_name is not None.
"""
_run_code(unescape_doctest(code), code_path, ns, function_name)


def _run_code(code, code_path, ns=None, function_name=None):
"""
Import a Python module from a path, and run the function given by
Expand Down Expand Up @@ -566,28 +524,30 @@ def render_figures(code, code_path, output_dir, output_base, context,
Save the images under *output_dir* with file names derived from
*output_base*
"""
if function_name is not None:
output_base = f'{output_base}_{function_name}'
formats = get_plot_formats(config)

# Try to determine if all images already exist

is_doctest, code_pieces = _split_code_at_show(code)
is_doctest, code_pieces = _split_code_at_show(code, function_name)

# Look for single-figure output files first
all_exists = True
img = ImageFile(output_base, output_dir)
for format, dpi in formats:
if context or out_of_date(code_path, img.filename(format),
includes=code_includes):
all_exists = False
break
img.formats.append(format)
else:
all_exists = True

if all_exists:
return [(code, [img])]

# Then look for multi-figure output files
results = []
all_exists = True
for i, code_piece in enumerate(code_pieces):
images = []
for j in itertools.count():
Expand All @@ -611,6 +571,8 @@ def render_figures(code, code_path, output_dir, output_base, context,
if not all_exists:
break
results.append((code_piece, images))
else:
all_exists = True

if all_exists:
return results
Expand Down Expand Up @@ -793,13 +755,13 @@ def run(arguments, content, options, state_machine, state, lineno):

# make figures
try:
results = render_figures(code,
source_file_name,
build_dir,
output_base,
keep_context,
function_name,
config,
results = render_figures(code=code,
code_path=source_file_name,
output_dir=build_dir,
output_base=output_base,
context=keep_context,
function_name=function_name,
config=config,
context_reset=context_opt == 'reset',
close_figs=context_opt == 'close-figs',
code_includes=source_file_includes)
Expand Down
38 changes: 20 additions & 18 deletions lib/matplotlib/tests/test_sphinxext.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@
minversion=None if sys.version_info < (3, 10) else '4.1.3')


def build_sphinx_html(source_dir, doctree_dir, html_dir, extra_args=None):
# Build the pages with warnings turned into errors
extra_args = [] if extra_args is None else extra_args
cmd = [sys.executable, '-msphinx', '-W', '-b', 'html',
'-d', str(doctree_dir), str(source_dir), str(html_dir), *extra_args]
proc = Popen(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True,
env={**os.environ, "MPLBACKEND": ""})
out, err = proc.communicate()

assert proc.returncode == 0, \
f"sphinx build failed with stdout:\n{out}\nstderr:\n{err}\n"
if err:
pytest.fail(f"sphinx build emitted the following warnings:\n{err}")

assert html_dir.is_dir()


def test_tinypages(tmp_path):
shutil.copytree(Path(__file__).parent / 'tinypages', tmp_path,
dirs_exist_ok=True)
Expand Down Expand Up @@ -60,7 +77,7 @@ def plot_directive_file(num):
assert b'# Only a comment' in html_contents
# check plot defined in external file.
assert filecmp.cmp(range_4, img_dir / 'range4.png')
assert filecmp.cmp(range_6, img_dir / 'range6.png')
assert filecmp.cmp(range_6, img_dir / 'range6_range6.png')
# check if figure caption made it into html file
assert b'This is the caption for plot 15.' in html_contents
# check if figure caption using :caption: made it into html file
Expand All @@ -74,6 +91,8 @@ def plot_directive_file(num):
# Plot 21 is range(6) plot via an include directive. But because some of
# the previous plots are repeated, the argument to plot_file() is only 17.
assert filecmp.cmp(range_6, plot_file(17))
# plot 22 is from the range6.py file again, but a different function
assert filecmp.cmp(range_10, img_dir / 'range6_range10.png')

# Modify the included plot
contents = (tmp_path / 'included_plot_21.rst').read_bytes()
Expand Down Expand Up @@ -159,20 +178,3 @@ def test_show_source_link_false(tmp_path, plot_html_show_source_link):
build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=[
'-D', f'plot_html_show_source_link={plot_html_show_source_link}'])
assert len(list(html_dir.glob("**/index-1.py"))) == 0


def build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=None):
# Build the pages with warnings turned into errors
extra_args = [] if extra_args is None else extra_args
cmd = [sys.executable, '-msphinx', '-W', '-b', 'html',
'-d', str(doctree_dir), str(tmp_path), str(html_dir), *extra_args]
proc = Popen(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True,
env={**os.environ, "MPLBACKEND": ""})
out, err = proc.communicate()

assert proc.returncode == 0, \
f"sphinx build failed with stdout:\n{out}\nstderr:\n{err}\n"
if err:
pytest.fail(f"sphinx build emitted the following warnings:\n{err}")

assert html_dir.is_dir()
7 changes: 7 additions & 0 deletions lib/matplotlib/tests/tinypages/range6.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ def range6():
plt.figure()
plt.plot(range(6))
plt.show()


def range10():
"""The function that should be executed."""
plt.figure()
plt.plot(range(10))
plt.show()
9 changes: 5 additions & 4 deletions lib/matplotlib/tests/tinypages/some_plots.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,9 @@ Plot 14 uses ``include-source``:

# Only a comment

Plot 15 uses an external file with the plot commands and a caption (the
encoding is ignored and just verifies the deprecation is not broken):
Plot 15 uses an external file with the plot commands and a caption:

.. plot:: range4.py
:encoding: utf-8

This is the caption for plot 15.

Expand Down Expand Up @@ -168,8 +166,11 @@ scenario:

plt.figure()
plt.plot(range(4))

Plot 21 is generated via an include directive:

.. include:: included_plot_21.rst

Plot 22 uses a different specific function in a file with plot commands:

.. plot:: range6.py range10