sphinx-doc / sphinx

The Sphinx documentation generator
https://www.sphinx-doc.org/
Other
6.47k stars 2.1k forks source link

Add and standardize a control sequence for non-rendering lines in code blocks #10555

Open tgross35 opened 2 years ago

tgross35 commented 2 years ago

Is your feature request related to a problem? Please describe. There are handful of tools that help with the formatting and checking of .. code-block:: python, but sometimes context is needed to make those work. My idea is to add a simple character sequence to indicate that specific lines of code should not be rendered.

The sphinx doctest module allows for some things like this, but requires code that is quite verbose and only supports pycon style. The proposal here would be simpler to use, easier to read, and more terse without breaking anything.

Describe the solution you'd like

I would propose using a control character like ##!. The only thing necessary from sphinx would be to hide comment lines starting with those characters. Essentially just ensure that this:

.. code-block:: python

    ##! from typing import Optional, TYPE_CHECKING
    ##! if TYPE_CHECKING:
    ##! from mymodule.something import CoolClass
    def  fn(x: Optional[int]) -> CoolClass:
        return CoolClass(x)
    ##! assert fn(5) == CoolClass(5)

Displays identical to

.. code-block:: python

    def  fn(x: Optional[int]) -> CoolClass:
        return CoolClass(x)

Some example use cases and benefits:

Describe alternatives you've considered

Pycon-style doctests, but that is not a good solution for many cases as mentioned above. https://www.sphinx-doc.org/en/master/usage/extensions/doctest.html

Additional context

The way this works is inspired by rustdoc https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html#hiding-portions-of-the-example

For control characters - it has to start with # to be valid python, ## is pretty commonly used in comments so that's no good, #! is used by shebang, so ##! it is. Happy medium that perfectly hints it's an executable comment.

Thanks! I'd be happy to submit a PR if this gets positive feedback.

AA-Turner commented 2 years ago

Hi Trevor,

I don't really understand the motivating examples. How would having comments in the code-block allow you to run the asserts?

A

tgross35 commented 2 years ago

Hi Adam,

it wouldn’t run exactly as written - the goal is simply so that any tool could strip those characters and then be left with 100% valid Python, and a working example.

The ##! would just be a directive to sphinx to not show those lines - that’s all that sphinx would need to do. Then a very thin wrapper (plug-in or external tool) could delete those characters (not lines) and use the remaining code as valid Python.

This is similar to the current way doctest work. But instead of using testsetup/testcleanup, code that would usually go into testsetup/testcleanup just goes into the same block as the displayed code, but gets marked with ##!

I’ll write an example script that illustrates what I mean

tgross35 commented 2 years ago

Ok, say you have the following somewhere in your .rst:

.. code-block:: python 

    ##! from __future__ import annotations
    ##! from typing import Optional
    class ColorRed:
        color = 'RED'

        def colorize(self, x: Optional[str]) -> ColorRed:
            if x.lower() == 'red':
                return ColorRed()

    ##! assert ColorRed().colorize('RED').color == 'RED'

And the RST parser correctly pulls the code block into python:

code_block_raw= """##! from __future__ import annotations
##! from typing import Optional

class ColorRed:
    color = 'RED'

    def colorize(self, x: Optional[str]) -> ColorRed:
        if x.lower() == 'red':
            return ColorRed()

##! assert ColorRed().colorize('RED').color == 'RED'
"""

Sphinx part

Instead of sending that to rendering, sphinx just needs to run a quick modifier to remove those commented lines:

code_block_displayable = "\n".join(
    line for line in code_block_raw.splitlines() if not line.startswith('##!')
)

Result, to be rendered:

class ColorRed:
    color = 'RED'

    def colorize(self, x: Optional[str]) -> ColorRed:
        if x.lower() == 'red':
            return ColorRed()

Tool part

Now if you're a tool or plugin that wants access to fully runnable code, you can easily just remove the control sequence.

lines = []
for line in code_block_raw.splitlines():
    if line.startswith('##!'):
        lines.append(line.strip('##! '))
    else:
        lines.append(line)

code_block_runnable= "\n".join(lines)

and get the following result:

from __future__ import annotations
from typing import Optional
class ColorRed:
    color = 'RED'

    def colorize(self, x: Optional[str]) -> ColorRed:
        if x.lower() == 'red':
            return ColorRed()

assert ColorRed().colorize('RED').color == 'RED'

Then do whatever is applicable with it

# These functions are simplifications of the potential API
flake8(code_block_runnable) # verify code block lints
mypy(code_block_runnable) # validate types
exec(code_block_runnable) # verify the code executes without error
# Allow pytest runner to validate this code
pytest_doctest.register_test(f"def file_123_test():\n    {code_block_runnable}")
# Maybe a plugin that adds a play button
sphinx_runnable_code_plugin.register_popup(code_block_runnable)
AA-Turner commented 2 years ago

Ahh, I see. This would need to be implemented in external tools to be useful.

Firstly (minor) on spelling: tilde (~) has precedenct in Sphinx for "supress", so perhaps ##~ over ##!?

I am though wary of an xkcd 927 situation if we specify this magic prefix and then no-one uses it, or other tools have their own similar construct. If there is precedent in other lightweight markup formats (asciidoc, POD, JavaDoc, etc) for a specific style, it would be better to try and follow that rather than inventing something ourselves.

@chrisjsewell -- does markdown (commonmark?) have such a constuct that you know of?

A

tgross35 commented 2 years ago

Completely understood about the support and all! One of the reasons I’m asking is because I recently became the maintainer of the (currently not working) flake8-rst package, and would kind of like to grow that into a useable standard.

I also does a bit of rust work, these easy nonprinting lines are something I miss the most from their (well thought out imo) docs standard - figured I might as well start at the top and see if there’s interest.

AA-Turner commented 2 years ago

flake8-rst

Have you seen sphinx-lint? I wonder if there's any crossover.

I think it's a useful idea, and we might even be able to use the same magic string for doctests, but I am hesitant to add something we'll have to support basically forever if it turns out that everyone else uses some different formulation for the same problem.

A

tgross35 commented 2 years ago

I was not familiar with sphinx-lint, but it seems like maybe that’s meant to validate the entire rst file? Rather than checking for validity of the code blocks within it

tk0miya commented 2 years ago

You can modify the content of code blocks via your own extension. So it's better to implement such a filter as an extension. I think it's difficult to make a standard syntax and keep it maintained. I suppose we'll need to invent new standards for other languages if we introduce this feature to Sphinx.

AA-Turner commented 2 years ago

So it's better to implement such a filter as an extension.

I think Trevor's question is if Sphinx will as the core application bless such a syntax. The risk of leaving it to extensions is that two extensions use two different conflicting magic strings, and you are then out of options in your documentation. I share your concerns on doing it right, but if it is done at all I think it should be in the core.

A

tgross35 commented 2 years ago

Hey Takeshi,

You are right that this is doable with an extension - I did consider that route. But Adam expressed my concern exactly: there is no guarantee that all tools agree on the same thing unless there's some sort of standard.

With that in mind, I opened an issue on the python discussion forum to see if a standard from even higher up might be of interest: https://discuss.python.org/t/a-standard-for-non-displaying-executable-lines-of-code-in-documentation/16570. I don't expect it to get much traction there, but I suppose I will wait and see.

I don't think there's any reason to worry about other languages at this point since python is (to my knowledge) the main use case for sphinx, and there are already plenty of extensions and tools that interact with python code in sphinx-style rst that don't exist for other languages (though I may just not know about them). This would just be something that makes what they do easier.

Assuming nothing happens with my python discussion and you'd prefer something in an extension, I'd be happy if sphinx could just put in their documentation something like "If you would like to hide lines of code from a python code block, Sphinx convention is to begin the lines with ???. However, you will need an extension such as ABC or DEF to render it". My thought was just that as it would literally be just a few lines of code, it may have a better home in sphinx core than an extension.

cjw296 commented 1 year ago

@tgross35 - Have you had a look at Sybil? I think moving the stuff you're looking to hide to invisible code blocks would likely do what you're after:


.. invisible-code-block: python

    from typing import Optional, TYPE_CHECKING
    if TYPE_CHECKING:
        from mymodule.something import CoolClass

.. code-block:: python

    def  fn(x: Optional[int]) -> CoolClass:
        return CoolClass(x)

.. invisible-code-block: python

    assert fn(5) == CoolClass(5)