-
-
Notifications
You must be signed in to change notification settings - Fork 17
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
Add an example directive to mark in-text examples for the example gallery #22
Conversation
This package will gather all Sphinx extensions related to the example gallery. There will be two main parts: a directive that marks examples, and extensions that index and render those examples. This boilerplate includes the standard Sphinx setup function for the extension.
This includes the sphinx_astropy.ext.example Sphinx extension by default in Sphinx builds.
This directive marks the scope of example content in the original documentation, and lets authors add a title and tags. Currently a pass-through directive. It parses the content of the directive and adds it back to the document Examples are persisted in the build environment for later post-processing (to build the example gallery). Examples are keyed by their unique ID (a slugified version of the title). The dict items contains metadata and the content of the example (to later generate standalone example pages). The directive also collects title and tags as metadata.
This target node lets us backlink to the example in the main documentation. In html, the link is an id on the first element of the example content. example IDs are unique since they're the keys of the env.sphinx_astropy_examples dictionary in the environment.
sphinx.testing.fixtures let us build Sphinx sites from pytest and then inspect the built site. There can be multiple test sites, each test site is a directory in the sphinx_astropy/test/roots/ directory. This is the same pattern that Sphinx uses for its test, so this is likely the easiest way to test our Sphinx extensions. http://www.sphinx-doc.org/en/master/devguide.html#unit-testing Note I had to add the pytest_plugins line, to load the Sphinx pytest plugin, from a new conftest.py file at the root of the project, not from sphinx_astropy/tests/conftest.py The reason for this is outlined in https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files The rest of the pytest configuration is done in sphinx_astropy/tests/conftest.py, which is where you'd expect most configuration to go. This configuration is largely based on Sphinx's: https://github.com/sphinx-doc/sphinx/blob/master/tests/conftest.py
The example-marker.rst file contains several instances of the example directive, testing different conditions (having tags, or not, and having different types of content in the example).
This test generates a site in the XML format since then it's easy to search for nodes and their attributes. Unfortunately this test strategy doesn't work for Sphinx <1.7, because the pytest fixtures aren't available. Thus I have pytest skip these tests for Sphinx <1.7. I think this is still the best way to test sphinx extensions and will continue to be so in the future because this is how Sphinx tests itself.
What does "doesn't work" mean? It does nothing, or there are errors? |
Also, would this work in a docstring, too? If yes please add a test for it. |
Sorry I was superficial about that. This is what I see (circle ci link):
Sphinx 1.7 is the oldest version in the test matrix that's working. Now, it's entirely possible it's just a small API mismatch and we could conditionally patch it. I haven't spent any real time looking into it yet. Do you have a big user base on Sphinx 1.6 still? Or have most projects moved on to Sphinx 1.7/1.8/2.x.
Good point. I hadn't thought of that! I'll check and test that. |
Sphinx 1.7 was released in Feb 2018, so it sounds OK to me to bump the version, at least for the directive. @astrofrog - what do you think? |
This looks great to me - I don't have any comments! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me, though I have a few comments below. I've also re-activated CircleCI so hopefully the tests will run now. Dropping Sphinx 1.6 seems reasonable.
sphinx_astropy/conf/v1.py
Outdated
@@ -133,7 +133,8 @@ def check_sphinx_version(expected_version): | |||
'sphinx_astropy.ext.doctest', | |||
'sphinx_astropy.ext.changelog_links', | |||
'sphinx_astropy.ext.missing_static', | |||
'sphinx.ext.mathjax'] | |||
'sphinx.ext.mathjax', | |||
'sphinx_astropy.ext.example'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swap order so that the astropy ones are together
def rootdir(): | ||
"""Directory containing Sphinx projects for testing. | ||
""" | ||
return path(__file__).parent.abspath() / 'roots' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why 'roots'? in sphinx-automodapi we use 'cases', so that might be clearer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that cases
is the better word here. roots
comes from how Sphinx does their testing (see Sphinx's tests/roots directory).
I can rename the roots
directory to cases
, but there'll still be the word root
in the name of this fixture and in the testroot
parameter to @pytest.mark.sphinx
.
The rootdir()
fixture is picked up by the pytest.mark.sphinx
's testroot
parameter to build the absolute path to a given Sphinx test case:
@pytest.mark.sphinx('xml', testroot='example-gallery')
def test_example_directive_targets(app, status, warning):
# ...
@astrofrog, so are you fine with the directory being cases
but using keeping the roots
terminology from the Sphinx?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that sounds good! 👍
sphinx_astropy/ext/example/marker.py
Outdated
if self.example_id in env.sphinx_astropy_examples: | ||
raise SphinxError( | ||
'There is already an example titled "{self.title}" ' | ||
'({self.docname:self.lineno})'.format(self=self.title)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a test that exercises this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops I really messed up that untested error handling! I've fixed this code and added a test case.
There is a new test case (test-example-gallery-duplicates) because otherwise the SphinxError would always be raised for regular testing of the example directive.
As the docstring comment says, I found a weird case that while enabling the numpydoc extension in the test environment, I would get false alarms about duplicate examples already in the build environment. These duplicate examples came from other test functions. Somehow the environment is being preserved across builds now that numpydoc is activated. To make the ExampleMarkerDirective robust against this case, it's now making sure that the duplicate instance is from a different document and line number before raising a SphinxError.
If the tests are failing, it's useful to see the debug-level logging. Note: the '2' verbosity enables DEBUG-level logging. I can't find a cleaner alias to this. Also, it would be nicer to make this the default while using the sphinx pytest mark, but I can't find an easy way to do that.
Now the test-example-gallery root is using an autodoc+numpydoc processing pipeline in its build configuration. This confirms that the directive does work in a docstring as expected.
@bsipocz I've added a test case with an autodoc+numpydoc pipeline and I've confirmed that the directive is processed as usual. That said, while talking to @kelle we might want to discourage While testing for compatibility with |
We already have quite a lot of numpydoc |
Started a new issue (astropy/astropy-tutorials#378) for discussing whether or not we want to put examples from docstrings into the Example Gallery. I'm totally fine with the functionality existing, I'm just not convinced we should use it. |
I'm convinced that we shouldn't use it for all the cases. But some are nice enough (but then maybe they should be moved out of the docstrings, I don't know). |
I just realized I need to add some event handlers because the directive is storing state in the environment:
I'll take care of these shortly. |
Can we add authors as optional metadata? |
@eblur we could add an authors field, or any other relevant metadata fields. Is authorship something that astropy wants to start displaying? I see that the tutorials in learn.astropy.org have authors, but the "main" docs.astropy.org docs don't. |
How do you define authors? The first one who adds an example or everyone who touches those examples? |
Right now we have been adding authors to tutorials any time there is an update. I don't think it needs to be displayed prominently in the example gallery, but I could imagine instances where people might want a receipt for their level of participation. It also provides a receipt for their level of expertise in Astropy. Does simply relying on the github history provide a an accurate description of those items? I'm not sure. I suggest it as an optional piece of meta-data because obviously a lot of the examples are already written and we may not have a record of who actually wrote them. |
Yes, but it goes against what we have in the core package, including its documentation. There are no authors, its a community product where people are listed in the credits page alphabetically. |
I don't care about this as much as you do, so let's forget about it. Sounds like this is a much deeper question than I intended it to be. |
The purge_doc callback is required to remove examples from a cached environment if a document is removed in a subsequent build. Otherwise the examples in the cached environment from previous builds would continue to exist in subsequent builds. The tests simulate a env-purge-doc event and separately ensure that purge_examples got registered as a env-purge-doc callback.
This refactoring allows _check_for_existing_example to be used outside the ExampleMarkerDirective, like in a env-merge-info event callback.
This env-merge-info callback handles merging sphinx_astropy_examples from parallel build environments when Sphinx is run in parallel read mode. The tests run a full-scale integration-type build with Sphinx running in parallel (-j 4).
I've dealt with handling parallel builds and environment cleanup. I think the last thing to deal with is getting confirmation from @astrofrog on the "cases" versus "roots" terminology discussed in this thread. I'm moving on to focusing on the other part of the example extension that actually generates the example gallery pages. I'm still working out how this will come together, but it may follow a pattern similar to automodapi in order to add additional pages to the build. In that case, some of the internal functionality in the |
By the way, we now require Sphinx 1.7 and above in sphinx-automodapi so feel free to make that change here too and mention it in the changelog. One question I forgot to ask before is how this interacts with the pytest plugin for rst docs - do the examples here get picked up as tests that should be run? (if not, we'll need to make sure the pytest-doctestplus plugin is updated so that this works). At this point, I'm inclined to say that I think we should wait for the complete solution before merging since some of the implementation here might still change (I don't think we'd want to do a release of sphinx-astropy with only the partial implementation so people would need to install the developer version anyway to try it out, and at that point they could also just install it from your fork). |
@jonathansick - just to check, what is the status of this? |
@astrofrog Sorry I've forgotten to close this PR after we talked about delaying the merge until the merge is ready. I've got a new PR in the works with the full extension (end-to-end extracting examples and publishing the example gallery). The draft PR in my fork is jonathansick#2 What I have left to do is:
|
@jonathansick - thanks! I just left a quick comment on that PR regarding parallel processing, but I won't review the rest yet. |
This PR is the first part of the example gallery work described in astropy/astropy#7242. It adds an
example
directive that marks examples in documentation text. For example:The example directive does not change the visual appearance of the example content in the documentation text, but it does record the example in the build environment so that it can be republished in the example gallery (work to be done).
Example metadata
Internally, the example directive stores examples in the build environment under the
sphinx_astropy_examples
attribute. Examples are keyed byexample-src-*slugified-title*
and metadata for each example consists of:title
tags
(set of tag strings)contents
(unparsed content of the directive)docname
lineno
Target node
The example directive also adds a
target
node to the start of the example content in the original content source. This will make it possible to backlink to the original source of the example.Testing
For this work I'm using the same testing strategy that Sphinx is using for its extensions. The idea is that we add mock Sphinx projects to
sphinx_astropy/tests/roots
. Each subdirectory is an isolated example site. These sites are associated with a test module. Test functions use apytest.mark.sphinx
directive to do isolated Sphinx builds where we can test the output in various formats (html, latex, xml, or "dummy" to just see docutils nodes):The downside of this test strategy is that it doesn't work (as far as I can tell) with Sphinx 1.6, so I'm having pytest ignore these tests in the Sphinx 1.6 build. I suggest this trade-off is ok because it makes it convenient to realistically test Sphinx extensions in all newer versions of Sphinx.