Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Add an image-basename option to the Sphinx plot directive #28187

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

Open
wants to merge 15 commits into
base: main
Choose a base branch
Loading
from

Conversation

asmeurer
Copy link

@asmeurer asmeurer commented May 8, 2024

This allows specifying the output base name of the generated image files.

This is required to use the plot directive with MyST, because the directive is broken with MyST (an issue I don't want to fix), requiring the use of eval-rst. But the way eval-rst works, the incrementing counter is not maintained across different eval-rst directives, meaning if you try to include multiple of them in the same document, the images will overwrite each other. This allows you to manually work around this with something like

```{eval-rst}
.. plot::
   :image-basename: plot-1

   ...
```

```{eval-rst}
.. plot::
   :image-basename: plot-2

   ...
```

Aside from this, it's generally useful to be able to specify the image name used for a plot, as a more informative name can be used rather than just <document-name>-1.png.

PR summary

PR checklist

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Thank you for opening your first PR into Matplotlib!

If you have not heard from us in a week or so, please leave a new comment below and that should bring it to our attention. Most of our reviewers are volunteers and sometimes things fall through the cracks.

You can also join us on gitter for real-time discussion.

For details on testing, writing docs, and our review process, please see the developer guide

We strive to be a welcoming and open project. Please follow our Code of Conduct.

format ``{counter}`` to use an incremented counter. For example,
``'plot-{counter}'`` will create files like ``plot-1.png``, ``plot-2.png``,
and so on. If the ``{counter}`` is not provided, two plots with the same
output-base-name may overwrite each other.
Copy link
Author

Choose a reason for hiding this comment

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

Perhaps the extension should actually check for this and just error.

Copy link
Author

Choose a reason for hiding this comment

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

OK, nevermind, I undid this change. This seems harder to do than I thought, because you'd need to keep track of which files are generated.

@tacaswell tacaswell added this to the v3.9.1 milestone May 8, 2024
@asmeurer
Copy link
Author

asmeurer commented May 8, 2024

For anyone interested, the error you get if you try to use the plot directive with MyST directly is ERROR: MockStateMachine has not yet implemented attribute 'insert_input'.

@asmeurer
Copy link
Author

asmeurer commented May 8, 2024

Ah, I didn't realize there actually already are unit tests for this. Let me update that.

This allows specifying the output base name of the generated image files. The
name can include '{counter}', which is automatically string formatted to an
incrementing counter. The default if it is not specified is left intact as
the current behavior, which is to use the base name of the provided script or
the RST document.

This is required to use the plot directive with MyST, because the directive is
broken with MyST (an issue I don't want to fix), requiring the use of
eval-rst. But the way eval-rst works, the incrementing counter is not
maintained across different eval-rst directives, meaning if you try to include
multiple of them in the same document, the images will overwrite each other.
This allows you to manually work around this with something like

```{eval-rst}
.. plot::
   :output-base-name: plot-1

   ...
```

```{eval-rst}
.. plot::
   :output-base-name: plot-2

   ...
```

Aside from this, it's generally useful to be able to specify the image name
used for a plot, as a more informative name can be used rather than just
'<document-name>-1.png'.
@asmeurer asmeurer force-pushed the output-base-name branch from 7fb0e35 to 615017a Compare May 8, 2024 22:20
@asmeurer
Copy link
Author

asmeurer commented May 8, 2024

For me, the Sphinx extension tests fail on main

        writing output... [ 20%] included_plot_21
E
E       stderr:
E
E       Exception occurred:
E         File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/shutil.py", line 260, in copyfile
E           with open(src, 'rb') as fsrc:
E                ^^^^^^^^^^^^^^^
E       FileNotFoundError: [Errno 2] No such file or directory: '/private/var/folders/wc/dppcpmxs1tlb36nqcw853wkm0000gn/T/pytest-of-aaronmeurer/pytest-2/test_srcset_version0/plot_directive/included_plot_21-1.2x.png'
E       The full traceback has been saved in /var/folders/wc/dppcpmxs1tlb36nqcw853wkm0000gn/T/sphinx-err-nut6ujx8.log, if you want to report the issue to the developers.
E       Please also report this if it was a user error, so that a better error message can be provided next time.
E       A bug report can be filed in the tracker at <https://github.com/sphinx-doc/sphinx/issues>. Thanks!
# Platform:         darwin; (macOS-14.4.1-arm64-arm-64bit)
# Sphinx version:   7.2.6
# Python version:   3.12.3 (CPython)
# Docutils version: 0.20.1
# Jinja2 version:   3.1.4
# Pygments version: 2.18.0

# Last messages:
#   
#   copying static files...
#   done
#   copying extra files...
#   done
#   done
#   �[2K
#   writing output... [ 20%]
#   included_plot_21
#   

# Loaded extensions:
#   sphinx.ext.mathjax (7.2.6)
#   alabaster (0.7.16)
#   sphinxcontrib.applehelp (1.0.8)
#   sphinxcontrib.devhelp (1.0.6)
#   sphinxcontrib.htmlhelp (2.0.5)
#   sphinxcontrib.serializinghtml (1.1.10)
#   sphinxcontrib.qthelp (1.0.7)
#   matplotlib.sphinxext.plot_directive (3.10.0.dev151+gce15014066)
#   matplotlib.sphinxext.figmpl_directive (3.10.0.dev151+gce15014066)

# Traceback:
Traceback (most recent call last):
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/sphinx/cmd/build.py", line 298, in build_main
    app.build(args.force_all, args.filenames)
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/sphinx/application.py", line 355, in build
    self.builder.build_update()
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 293, in build_update
    self.build(to_build,
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 363, in build
    self.write(docnames, list(updated_docnames), method)
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 571, in write
    self._write_serial(sorted(docnames))
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 581, in _write_serial
    self.write_doc(docname, doctree)
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/sphinx/builders/html/__init__.py", line 649, in write_doc
    self.docwriter.write(doctree, destination)
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/docutils/writers/__init__.py", line 80, in write
    self.translate()
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/sphinx/writers/html.py", line 36, in translate
    self.document.walkabout(visitor)
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/docutils/nodes.py", line 186, in walkabout
    if child.walkabout(visitor):
       ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/docutils/nodes.py", line 178, in walkabout
    visitor.dispatch_visit(self)
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/site-packages/sphinx/util/docutils.py", line 582, in dispatch_visit
    method(node)
  File "/Users/aaronmeurer/Documents/matplotlib/lib/matplotlib/sphinxext/figmpl_directive.py", line 174, in visit_figmpl_html
    imagedir, srcset, rel = _copy_images_figmpl(self, node)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/aaronmeurer/Documents/matplotlib/lib/matplotlib/sphinxext/figmpl_directive.py", line 163, in _copy_images_figmpl
    shutil.copyfile(abspath, imagedir / name)
  File "/Users/aaronmeurer/miniconda3/envs/ndindex-matplotlib-dev/lib/python3.12/shutil.py", line 260, in copyfile
    with open(src, 'rb') as fsrc:
         ^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/private/var/folders/wc/dppcpmxs1tlb36nqcw853wkm0000gn/T/pytest-of-aaronmeurer/pytest-2/test_srcset_version0/plot_directive/included_plot_21-1.2x.png'

@asmeurer
Copy link
Author

asmeurer commented May 9, 2024

I added a test that will hopefully work. I'm having difficulty getting some of the tests to run successfully locally right now, but I believe the asserts I added work. If CI fails I might need some help figuring out why things aren't working on my computer.

asmeurer added a commit to asmeurer/ndindex that referenced this pull request May 9, 2024
else:
source_file_name = rst_file
code = textwrap.dedent("\n".join(map(str, content)))
counter = document.attributes.get('_plot_counter', 0) + 1
document.attributes['_plot_counter'] = counter
base, ext = os.path.splitext(os.path.basename(source_file_name))
output_base = '%s-%d.py' % (base, counter)
if options['output-base-name']:
output_base = options['output-base-name'].format(counter=counter)
Copy link
Member

Choose a reason for hiding this comment

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

could we prefix in the script/rst file name here as well? If you have duplicate output names in the same rst file that is a quick search to find and fix, but if you have the collision across multiple rst files it could be much more annoying to find where they are colliding.

Copy link
Author

Choose a reason for hiding this comment

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

Well my thinking here is also that users should have full control over the filename that is produced. If we always inject something into the name, that won't be the case. For instance, if you right-click and save a plot, this will be the filename used for the image.

Really, we need to figure out how to error if the same name is used twice. My naive idea of checking if the file already exists doesn't work because it could just exist from a previous build. It's probably possible to keep a global list of already used filenames, but I'll have to figure out how to make that work with partial Sphinx rebuilds.

@tacaswell
Copy link
Member

What version of pytest are you using? I could not reproduce this issue last night.

@tacaswell tacaswell modified the milestones: v3.9.1, v3.10.0 May 9, 2024
@tacaswell
Copy link
Member

I am 👍 on this idea, one small question about the naming (I see both sides of forcing an additional prefix or not).

I re-milestoned this for 3.10 as it is a new feature which seems too big to put in via a micro release and it is too late to get this is under the wire for 3.9.

@asmeurer
Copy link
Author

asmeurer commented May 9, 2024

What version of pytest are you using? I could not reproduce this issue last night.

It's possible my local build of matplotlib is broken somehow. pytest also does a really annoying thing when I run this test on my Mac where it switches to another space like it is opening a GUI window. I also had to run

sudo mkdir -p /tmp/.X11-unix
sudo chmod 1777 /tmp/.X11-unix

or the tests would just error completely with some strange error (which fortunately ChatGPT was able to figure out).

I tried my best to follow your dev instructions, but some of the steps didn't work (e.g., the dev requirements.txt doesn't actually list any build requirements).

@tacaswell
Copy link
Member

The reason I suspect pytest is this path: '/private/var/folders/wc/dppcpmxs1tlb36nqcw853wkm0000gn/T/pytest-of-aaronmeurer/pytest-2/test_srcset_version0/plot_directive/included_plot_21-1.2x.png'

What was the strange error? I'm really not sure why you need X11 stuff for the tests of the sphinx extension to run...


We have an open PR to fix the dev install instructions. The conda environment file works, most of the regular devs who don't use conda know what to do, and we explicitly install them on CI so it fell through the cracks.

tacaswell
tacaswell previously approved these changes May 9, 2024
@asmeurer
Copy link
Author

asmeurer commented May 9, 2024

Sorry that I wasn't clear. I reproduced the Sphinx issue outside of pytest, using

cd lib/matplotlib/tests/tinypages
python -m sphinx -W -b html -d doctrees . _build/html/ -D plot_srcset=2x

The first output I showed is from pytest (which is running that command). The second is the log file from running the command directly, which shows the full traceback.

@asmeurer
Copy link
Author

asmeurer commented May 9, 2024

Ah, so looking closer at https://matplotlib.org/devdocs/devel/development_setup.html#create-a-dedicated-environment, I see there is a secret "conda" button you have to click. I really don't like that Sphinx extension that hides content behind tabs...

@tacaswell
Copy link
Member

oh, I bet our tiny pages config needs the backend set to Agg...

@asmeurer
Copy link
Author

asmeurer commented May 9, 2024

The extension supposedly does that

But perhaps my private matplotlib config is messing with things.

@asmeurer
Copy link
Author

asmeurer commented May 9, 2024

I have some work-in-progress code to do the duplicate filename checks. I'm trying to figure out why this out_of_date function is needed

def out_of_date(original, derived, includes=None):
"""
Return whether *derived* is out-of-date relative to *original* or any of
the RST files included in it using the RST include directive (*includes*).
*derived* and *original* are full paths, and *includes* is optionally a
list of full paths which may have been included in the *original*.
"""
Shouldn't Sphinx already handle that logic itself? It apparently is needed based on this issue #17860, but I'm not understanding why.

@asmeurer
Copy link
Author

asmeurer commented May 9, 2024

By the way, I saw you approved this. If you want to merge this as-is, that's fine. I can add the duplicate filename check in a separate PR. I'm still not completely sure yet if I can actually get it working.

@asmeurer
Copy link
Author

So it occurs to me that this might actually do unexpected things if a global output_base_name is set when there are partial rebuilds. I think I might just remove the ability to have a global option, as well as the counter. We can at least check that no names are the same document.

@tacaswell
Copy link
Member

This is also going to do funny looking things if you mix plots with and without set base names as the counter is always increased so I think you will get names like [doc-1.png, my_name.png, doc-3.png].

One option would be to the '{counter}' logic and move the counter incrementing into the other branch.

Another option would be to instead of thinking of this as setting the base name, change the user facing knob to be a postfix so either we append a counter OR we stick on what ever the user gave us. I think that will fix the myst problem and prevents inter-document name collisions.

@tacaswell tacaswell dismissed their stale review May 13, 2024 18:42

Too enthusiastic, still some open questions.

@asmeurer
Copy link
Author

My plan is to simplify this considerably:

  • Remove the global base-name flag, and also the {counter} thing. Just have :output-base-name: and that's it. Is it possible for a single plot directive to produce multiple files? If so, I'll still need to automatically append a counter in those cases. I think the issue you pointed out is accurate so I'll need to update the counter to work per-base name. If that's not possible, we can just ignore the counter completely when base-name is set.

  • Add a basic check for duplicate names in the same file. Checking for duplicate names across files is doable but annoying because we have to store information in the Sphinx environment so that it works across rebuilds.

@@ -174,3 +174,18 @@ Plot 21 is generated via an include directive:
Plot 22 uses a different specific function in a file with plot commands:

.. plot:: range6.py range10

Plots 23 through 25 use output-base-name.
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need 3? / What do we get more compared to one plote?

Copy link
Author

Choose a reason for hiding this comment

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

The second one probably isn't needed (it was when I had the counter thing but I removed that). The third one is testing the case when the plot comes from a file, which is a completely separate code path in the extension.

@@ -47,6 +47,12 @@

The ``.. plot::`` directive supports the following options:

``:output-base-name:`` : str
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the clarification. In that case *-base-name makes sense.

Two further thoughts:

  • would image-base-name be better (because more concrete) than output-base-name?
  • what happens / do we need to check and error if the user tries to give more than a base name, e.g. :output-base-name: ../escaped_from_image_dir/myimage or nested_dir/myimage?

@asmeurer
Copy link
Author

I'm definitely open to a better option name here. I mostly just copied what it was called in the code, but we should try to find something that's obvious to end users.

@asmeurer
Copy link
Author

what happens / do we need to check and error if the user tries to give more than a base name, e.g. :output-base-name: ../escaped_from_image_dir/myimage or nested_dir/myimage

Good question. I'll need to test it. Actually a more common error would be trying to add the extension to the base name, like myimage.png. Maybe we should disallow that?

@timhoffm
Copy link
Member

Yes, extensions should not be allowed. Possibly slightly more restrictive than need be, but for simplicity I'd go with not allowing . and / in the name. People are welcome to propose which corner case is supported by a more refined check.

@asmeurer
Copy link
Author

I made those changes. Still wondering if there are any better suggestions for the option name than output-base-name.

@timhoffm
Copy link
Member

timhoffm commented Oct 18, 2024

I'd go with image-basename. Pulling basename together follows the notation of os.path.basename and also base and name are stronger bound together than image.
Using image instead of output feels more natural. If you ask, "what does the command produce / write to disk?" The answer is more likely "an image file" rather than an "output file". "Image" is more concrete. "Output" would only be preferable if the image could also be interpreted as an input, but that cannot happen in the semantic context of the directive.

@asmeurer
Copy link
Author

OK, I've renamed output-base-name to image-basename.

del env.mpl_custom_base_names[docname]

def merge_other(self, app, env, docnames, other):
for docname in docnames:
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to loop over the docnames? AFAICS, we only want to merge other.mpl_custom_base_names into env.mpl_custom_base_names. I.e. we can directly loop over that.

Copy link
Author

Choose a reason for hiding this comment

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

OK, I simplified this function. I'm not completely sure about it, but the functionality still seems to work based on manual testing.

for docname in docnames:
if docname in other.mpl_custom_base_names:
if docname not in env.mpl_custom_base_names:
env.mpl_custom_base_names[docname] = set()
Copy link
Member

Choose a reason for hiding this comment

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

Can be left out because mpl_custom_base_names is a defaultdict.

def init_filename_registry(app):
env = app.builder.env
if not hasattr(env, 'mpl_custom_base_names'):
env.mpl_custom_base_names = defaultdict(set)
Copy link
Member

Choose a reason for hiding this comment

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

The name is a bit generic. Maybe

Suggested change
env.mpl_custom_base_names = defaultdict(set)
env.mpl_plot_image_basenames = defaultdict(set)

Or similar. This naming is library_directive_option.

@asmeurer
Copy link
Author

I think the docs build error isn't my fault this time, from what I can tell.

@asmeurer
Copy link
Author

asmeurer commented Nov 8, 2024

Any more thoughts on this?

@timhoffm
Copy link
Member

We just need a second core developer to approve.

@asmeurer
Copy link
Author

@tacaswell you looked at this previously. Do you have any thoughts?

@asmeurer asmeurer changed the title Add an output-base-name option to the Sphinx plot directive Add an image-basename option to the Sphinx plot directive Nov 11, 2024
@asmeurer
Copy link
Author

Can this be merged? It's been sitting for some time without any further review.

@timhoffm
Copy link
Member

We still need a second review from a core developer.

@jklymak
Copy link
Member

jklymak commented Feb 24, 2025

  • circleCI is failing, and the log is now gone, so it's pretty hard to approve a doc PR if the docs don't build cleanly. Our doc tests currently are working, so please rebase.
  • Has anyone tested this for performance? This has a loop through all the basenames - is that a trivial call for every image in a gallery? I guess we don't have that many plot directives, so maybe it is OK.

@asmeurer
Copy link
Author

Has anyone tested this for performance? This has a loop through all the basenames - is that a trivial call for every image in a gallery? I guess we don't have that many plot directives, so maybe it is OK.

If you're talking about _FilenameCollector, all that's doing is creating a mapping of plots to filenames, so that Sphinx can persist it between runs.

@asmeurer
Copy link
Author

asmeurer commented Mar 5, 2025

Is this CI test failure related to my changes?

@timhoffm
Copy link
Member

timhoffm commented Mar 6, 2025

No it's a flaky test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.