-
-
Notifications
You must be signed in to change notification settings - Fork 4
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
xdoctest #59
Comments
Hi, I'm the author of xdoctest. I'd be interested in knowing if there is something about xdoctest that does not suit your needs? If so perhaps we can integrate features there? I think many of the directives xdoctest has can handle the use-cases scipy-doctest was written for. Xdoctest has skip blocks via |
Hey @Erotemic !
What would you list as major improvements of xdoctest over the vanilla doctest? |
I'd be interested in adding a flexible floating point checker to the xdoctest checker. Currently there is quite a bit of normalization that happens by default and anything that isn't normalized can typically be handled with use of ellipsis, which is enabled by default.
Not quite sure that his means. You don't test doctests in private scopes by default? I think that would be easy to integrate into xdoctest because each one knows where it came from, so an option to disable doctests in locations with a leading "_" wouldn't be hard.
xdoctest is configurable, but for the most part I've been the only person with eyes on the core code. I'd be interested in at least seeing if there are internal architecture improvements that could be made. I know for a fact my parser could be faster, but it's not that slow to begin with so there's been little need to hammer on it.
I find directives useful when used sparingly. I think its important that they are readily identifiable as directives though. Personally, I think However, in xdoctest, I am fairly flexible about what can be counted as a directive, as my work on this started fairly organically ~2015 in the IBEIS project. There are some hard coded things like In any case I'm certainly not opposed to expanding the existing directives.
In the README see: the Enhancements section, as well as the enhancements demo More info: AST ParserThe biggest improvement is that it doesn't use regex to "parse" Python's grammar, and instead uses the more appropriate AST. This means instead of being forced to write:
And manually identify which lines are continuations of a previous statement, the parser does it for you, so you can use a more simple rule to create a doctest: just prefix everything with ">>> "
(Although, if you look at the xdoctest README you will see it is even more flexible than that). New DirectivesI mentioned some of the new directives in the previous post. There are also multi-line directives, which means they apply to all lines underneath them. The old doctest module can only put directives inline. old: >>> x = 1 # xdoctest: +SKIP
>>> x = 2 # xdoctest: +SKIP
>>> x = 3 # xdoctest: +SKIP
>>> x = 4 # xdoctest: +SKIP new: >>> # xdoctest: +SKIP
>>> x = 1
>>> x = 2
>>> x = 3
>>> x = 4 You can "undo" a directive by using the "-" instead of "+", e.g. >>> # xdoctest: +SKIP
>>> x = 1
>>> x = 2
>>> # xdoctest: -SKIP
>>> x = 3
>>> x = 4 It's not very pretty, so I rarely use this, but it does come in handy. Better RunnerYou can run every doctest in a package with the Better OutputWhen a doctest runs in verbose mode it will print the code it is about to execute. For failures it will print the failed line relative to the doctest (original behavior) as well as the absolute line relative to the file it lives in (new feature). If there are failures, it will also output a CLI invocation of xdoctest to re-test just the specific tests that failed, which makes debugging easier. Forgiving DefaultsUsers should be encouraged to write examples, and that means preventing annoying failures. In the old doctest, if you didn't specify a "want" statement, for something that produce stdout or a value, it would fail. In xdoctest it will just assume you don't want to test that line of output. It also lets you compare all stdout at the end of the doctest instead of always one-by-one. The following test fails with old doctests, but works with xdoctest: >>> print('one')
>>> print('two')
>>> print('three')
one
two
three MiscThere are a lot more miscellaneous things that I might not remember off the top of my head. There are multiple "finders" in the sense that the "style" can be to parse google-style docstrings or simple freeform docstrings (which works well for numpy style). In pytorch it helped a lot to be able to force every doctest to have an implicit "import torch" at the top. That is available by the There is an experimental feature to export all doctests as standalone unit-tests. I also have a PyCon 2020 Presentation where I talk about some of the improvements. Corresponding slides are here. |
I've been going through the scipy_doctest code to understand it better and I have a few thoughts / observations.
Using a stopword list could make my doctests more concise.
Thinking more about flexible checkers, it might even be nice if there was a way to inject custom checkers. The idea is the user provides the doctest configuration with a set of candidate normalization functions that it can use. When the doctest runner gets a got/want test, it sequentially tries all normalizations to see if any pass, and if none do, then fail. I think that's what both xdoctest and scipy_doctest are roughly doing, so an extension to allow user supplied normalizations would probably not be difficult.
Also for fun, I ran xdoctest on a develop install of scipy via |
Thanks @Erotemic for starting the discussion! I'm going to have to respond piecemeal over the next few days.
Glad that you liked it :-). To me, this was a hack to work around the fact that in scipy namedtuples are not a part of the API, so one cannot
are useful when a doctest wants to access a local file, yes. It's not always possible or convenient to construct one on the fly. Here's one example, where the user guide talks about reading Matlab/Octave files: https://docs.scipy.org/doc/scipy/tutorial/io.html
yes, "random". Out of the box, it's
That's basically a placeholder for user library-specific context. Two immediate usages:
Two examples: https://github.com/scipy/scipy/blob/main/scipy/conftest.py#L295 and https://github.com/numpy/numpy/blob/main/numpy/conftest.py#L173
Yeah. Pretty much all of scipy-doctest has grown up from accounting for various failures observed in the scipy docs.
scipy-doctest uses a context manager to switch to a display-less MPL backend: |
Now for a bigger picture.
That it looks like a comment is by design! Regardless, undoing directives is a very cool idea, actually! One other move in the design space where it seems xdoctest and scipy-doctest differ is that I tried to minimize deviations from the stdlib doctest module. My reasoning was (and still is) that the doctest syntax is quirky enough, and adding more flexibility adds more cognitive load. W.r.t. extending stdlib doctest vs reimplementation --- I was and still am lazy, so the more I can reuse and the less I need to reimplement, the better! (and I wouldn't touch the regex parsing in stdlib doctest with anything other than a very long stick). Your extensions to the doctest syntax are nice though, so the benefit from reimplementing is clear. I'm not entirely sure though why you need to account for different docstring styles (numpydoc vs google etc)? As long as there's
Where does this logic live, is it the runner or a parser?
Having a tool dictating how to write doctest examples is something scipy-doctest definitely tries to avoid! And a sizeable fraction of hoops scipy-doctest DTChecker jumps through are exactly about this. I definitely love the idea of porting DTChecker to xdoctest. One thing though: the DTChecker is very reliant on NumPy, and I've not a clear picture of how to make it work for e.g. pytorch. Do you? Maybe array API is the answer, or a subclass to override I'll definitely need to study the xdoctest code some more. I'll also take a look if I can plug the xdoctest's Parser, along the lines of https://github.com/scipy/scipy_doctest/blob/main/scipy_doctest/tests/test_runner.py#L94 |
Going through things I've missed in your comments:
Yes, basically. There's a worked example in https://github.com/scipy/scipy_doctest?tab=readme-ov-file#rough-edges-and-sharp-bits (under doctest collection strategies)
Where does the configuration live, is there a central-ish place to collect various bits of configuration?
Yeah, we had an implicit
Not entirely sure what is the difference between dynamic and static here?
Mind expanding on what do you mean by normalizations here? |
I'm of the opinion that the stdlib doctest module is fundamentally flawed (e.g. due to the underpowered parsing, insistence on checking for output on every single line) and that the extra developer cognitive load is justified if it results in a net decrease in the complexity that needs to be taught to a new user. But I acknowledge there is a trade-off here.
Because you might want different blocks of doc-strings to represent fundamentally different tests that are not impacted by the previous state and can pass or fail independently. However, by default xdoctest uses the "freeform" parser, which effectively puts all doctest parts into the same test object.
The runner. Additionally, something I really value is the ability to "accumulate" output and then match it all at the end. This specifically happens here. If a statement produces output, and there is no requested "want" statement, the runner will remember it. Then if a latter part does get a "want" string there are two ways for it to pass:
This means you can write a test as I previously described in the "forgiving defaults" section:
An opinion I've formed while working on this is that the doctest runner should work really hard to find a way in which the "want" output does match the "got" output. A "got/want" test should only fail if there is something very obviously wrong (and even then the frequency with which you see you see
It might make sense to have numpy be an optional package, and if it exists, it enriches xdoctest with additional checks that it can perform on "got/want" tests. For torch I just made heavy use of ellipses, IGNORE_WANT, and regular in-doctest assert statements to avoid the "got/want" pattern.
The config lives in the xdoctest.doctest_example.DoctestConfig class. You can see it used to help populate the basic CLI here and the pytest CLI here. The default directive state is defined here, and it can be overwritten by the above config class. Admittedly, the config system has a bit of baggage and could be simplified, but at its core it is just a dictionary that gets assigned to each
This is one of the major difference between xdoctest and stdlib doctest. By default we do not import the user code when we find the doctests. Instead we make use of the AST (in a different way that it is used in the context of actually parsing the docstrings) to statically (i.e. no executed user-code) enumerate all of the top-level functions and classes with docstrings. Using this module-parsing strategy, user code is only executed at doctest runtime, which can help prevent side effects and improve response time of the gathering step. Now, this is great if you code all of your docstrings explicitly, but it does break down if you programatically generate an API. Thus xdoctest offers a "dynamic" parsing strategy which basically works the same way regular doctest does: import the module, enumerate its members, look at the
A normalization is the way to flexibly match got/want strings. E.g. use regex to remove trailing whitespace, remove newlines, removing ansi escape sequences, replacing quotes. It's a bit simpler than going into the want string and trying to exact a Python object out of it and then compare them on a programmatic level. Normalize the strings: check if they are equal, if so pass, otherwise fallback to more heavyweight pattern matching. Thinking about it it might not be possible to frame float comparison as a normalization, so perhaps this point is moot. |
Prior art: Take a look at https://github.com/Erotemic/xdoctest
is used by pytorch (?)
The text was updated successfully, but these errors were encountered: