keewis / blackdoc

run black on documentation code snippets
https://blackdoc.readthedocs.io
MIT License
47 stars 4 forks source link

`doctest` command continuation and chained methods #173

Open Swandog opened 1 year ago

Swandog commented 1 year ago

I'm having a problem with a doctest in one of my repos. We have some doctests where methods are chained, and they are written across multiple lines using the "command continuation" format. But I seem to have trouble getting both doctest and blackdoc to work simultaneously.

This example is trivial, but I think shows the problem:

def do_things():
    """
    do things

    Examples
    --------
    do this

    >>> g=[8,2,3]
    >>> sorted(g) \
          .pop()
    8
    """
    pass

def do_things_differently():
    """
    do things

    Examples
    --------
    do this

    >>> g=[8,2,3]
    >>> sorted(g) \\
    ...   .pop()
    8
    """
    pass

def do_other_things():
    """
    do things

    Examples
    --------
    do this

    >>> g=[8,2,3]
    >>> sorted(g) \
    ...   .pop()
    8
    """
    pass

def do_that():
    """
    do things

    Examples
    --------
    do this

    >>> g = [8, 2, 3]
    >>> sorted(g)
          .pop()
    8
    """
    pass

if __name__ == "__main__":
    import doctest

    doctest.testmod()

do_things works as a doctest, but it fails to parse in blackdoc: error: cannot format /private/tmp/testing/test.py: Cannot parse: 12:0: EOF in multi-line statement do_things_differently also works as a doctest, but fails to parse in blackdoc: error: cannot format /private/tmp/testing/test.py: Cannot parse: 28:9: sorted(g) \\ do_other_things parses in blackdoc (and it suggests collapsing lines 44-45 into 1 line), but does not parse as a doctest:

**********************************************************************
File "/private/tmp/testing/test.py", line 44, in __main__.do_other_things
Failed example:
    sorted(g)     ...   .pop()
Exception raised:
    Traceback (most recent call last):
      File "/usr/local/Cellar/python@3.10/3.10.10_1/Frameworks/Python.framework/Versions/3.10/lib/python3.10/doctest.py", line 1350, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest __main__.do_other_things[1]>", line 1
        sorted(g)     ...   .pop()
                      ^^^
    SyntaxError: invalid syntax
**********************************************************************

do_that parses correctly in blackdoc, but gives the wrong answer in doctest:

**********************************************************************
File "/private/tmp/testing/test.py", line 60, in __main__.do_that
Failed example:
    sorted(g)
Expected:
          .pop()
    8
Got:
    [2, 3, 8]
**********************************************************************
1 items had failures:
   1 of   2 in __main__.do_that
***Test Failed*** 1 failures.

Obviously in this case we could just put everything on one line, but in general in our repo we'd to have it chained, on multiple lines. Is there a way to format this that I'm not seeing?

OS: MacOS 13.2.1 Python: 3.10.10 Blackdoc: 0.3.8

Thanks for your help.

keewis commented 1 year ago

The reason do_things and do_things_differently work with doctest but not with blackdoc is that doctest executes the file, and python will concatenate any lines with trailing \ with the following line. That's also why do_other_things fail with doctest: the concatenated lines contain the prompt.

blackdoc does not know about \ (because I didn't teach it to), so any example that makes use of that will inevitably fail. I'm open to adding support for that since that's one of the things black will reformat, but it seems a bit tricky to implement and I'm also unlikely to have time for that anytime soon.

For new code I'd use parens: they don't have the issues \ has, and it's also what black will suggest using:

-    >>> g=[8,2,3]
-    >>> (sorted(g)
-    ...   .long_method_name1()
-    ...   .long_method_name2()
-    ...   .long_method_name3()
-    ...   .long_method_name4()
-    ...   .long_method_name5())
+    >>> g = [8, 2, 3]
+    >>> (
+    ...     sorted(g)
+    ...     .long_method_name1()
+    ...     .long_method_name2()
+    ...     .long_method_name3()
+    ...     .long_method_name4()
+    ...     .long_method_name5()
+    ... )
     8
    """