Skip to content
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

Validate docstring examples #364

Conversation

mathias-baumann-frequenz
Copy link
Contributor

  • Update stand-alone scripts to be import-friendly
  • Add module to validate ``` python wrapped code examples in docstrings
  • Fix docstring code example problems
  • Patch @Actor decorator to retain the origins class' doc, name and module metadata

fixes #81

@mathias-baumann-frequenz mathias-baumann-frequenz requested a review from a team as a code owner April 26, 2023 11:17
@github-actions github-actions bot added part:actor Affects an actor ot the actors utilities (decorator, etc.) part:data-pipeline Affects the data pipeline part:docs Affects the documentation part:power-management Affects the management of battery power and distribution part:tests Affects the unit, integration and performance (benchmarks) tests part:tooling Affects the development tooling (CI, deployment, dependency management, etc.) labels Apr 26, 2023
@mathias-baumann-frequenz mathias-baumann-frequenz added this to the v0.21.0 milestone Apr 26, 2023
@mathias-baumann-frequenz mathias-baumann-frequenz force-pushed the validate_docstring_examples branch 2 times, most recently from 68ff280 to aa1debf Compare April 26, 2023 14:23
Copy link
Contributor

Choose a reason for hiding this comment

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

I have a few comments, but amazing if now the examples are being tested and all really fixed!

If you really did a research and there are no tools available doing this, I'm thinking we might just create a repo for the tool, it looks like something that could be reused by other people and that the ecosystem is missing. But I'm also fine to add it here and split it in the future.

Also, please remove the "fixes #..." because we are still missing the testing of >>> blocks, right? Or split the issue in 2.

noxfile.py Outdated Show resolved Hide resolved
and method_obj.__module__ == module.__name__
):
if method_obj.__doc__ is not None:
docs.append(method_obj.__doc__)

Choose a reason for hiding this comment

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

I guess this doesn't catch class attributes or class attributes? Does it catch properties? It also doesn't catch module-level variables.

Any reason not to basically look for __doc__ in any symbol (as long as the symbol is originated in this module)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From what I learned only a few symbols have the doc property, namely modules, functions, methods and classes.

Well, that's not exactly correct, vars have the property, but it contains type documentation rather than user documentation.

E.g.

>>> x = 3
>>> dir(x)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
>>> x.__doc__
"int([x]) -> integer\nint(x, base=10) -> integer\n\nConvert a number or string to an integer, or return 0 if no arguments\nare given.  If x is a number, return x.__int__().  For floating point\nnumbers, this truncates towards zero.\n\nIf x is not a number or if base is given, then x must be a string,\nbytes, or bytearray instance representing an integer literal in the\ngiven base.  The literal can be preceded by '+' or '-' and be surrounded\nby whitespace.  The base defaults to 10.  Valid bases are 0 and 2-36.\nBase 0 means to interpret the base from the string as an integer literal.\n>>> int('0b100', base=0)\n4"

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, interesting. Well, the docs extracting tool (mkdocstrings) is considering strings following a variable a docstring, I guess it does it by parsing the Python file then. See for example:

I guess for the first version it is OK to ignore them, but we should support it in the future.

This also makes me think that testing the documentation should probably be part of building the documentation (or an option), so maybe in the future we could plug it to mkdocstring (making it a plugin or something). This will also ensure that the documentation we get to extract examples is the same as the one that will be rendered. The guy doing mkdocstrings seems very nice, he even replied to some issue in our repo when discussing about it.

Copy link
Contributor

Choose a reason for hiding this comment

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

How is it with sybil? I guess it also not considering strings after a variable declaration a docstring for the variable, right?

Choose a reason for hiding this comment

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

@Marenz ping, not sure if you could look into this. Is not super high prio, because I think it is much less likely to have examples here, but still it would be good to know. If it's not supported we can make an extra effort to try to avoid examples in these cases.

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe it is supported, as Sybil doesn't use the __doc__ property but parses the files itself

Choose a reason for hiding this comment

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

Can you confirm this? I think it will be important to decide if we allow writing examples in variable documentation or not.

tests/utils/validate_docstring_examples.py Outdated Show resolved Hide resolved
tests/utils/validate_docstring_examples.py Outdated Show resolved Hide resolved
src/frequenz/sdk/timeseries/_moving_window.py Outdated Show resolved Hide resolved
src/frequenz/sdk/timeseries/_moving_window.py Outdated Show resolved Hide resolved
src/frequenz/sdk/timeseries/_resampling.py Show resolved Hide resolved
benchmarks/timeseries/benchmark_datasourcing.py Outdated Show resolved Hide resolved
@leandro-lucarella-frequenz leandro-lucarella-frequenz changed the title validate docstring examples Validate docstring examples Apr 28, 2023
@Marenz Marenz force-pushed the validate_docstring_examples branch from dc3c3bf to cd7008a Compare May 5, 2023 10:32
@github-actions github-actions bot removed part:tests Affects the unit, integration and performance (benchmarks) tests part:docs Affects the documentation labels May 5, 2023
@Marenz Marenz force-pushed the validate_docstring_examples branch from cd7008a to e4356e2 Compare May 5, 2023 12:41
Copy link
Contributor

Choose a reason for hiding this comment

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

So, this is amazing. I love the idea of making examples run in the same import context as the file they live in!

What I don't like at all is that this is not really a pytest plugin, it is just hijacking the configuration file!

There is zero reason to run this in the context of pytest, right? You are not using any pytest facilities or features, right?

If so, just create a separate file. You don't even have to make it executable for now, we can just call it using python -m lint-code-examples or whatever.

I think this should be definitely a separate tool, so IMHO the next step would be to move it into its own repo.

src/conftest.py Show resolved Hide resolved
src/conftest.py Show resolved Hide resolved
src/conftest.py Show resolved Hide resolved
src/conftest.py Show resolved Hide resolved
src/conftest.py Show resolved Hide resolved
pyproject.toml Show resolved Hide resolved
src/frequenz/sdk/timeseries/_moving_window.py Show resolved Hide resolved
@Marenz
Copy link
Contributor

Marenz commented May 8, 2023

There is zero reason to run this in the context of pytest, right?

I liked that it re-uses the existing interface and error reporting

@leandro-lucarella-frequenz
Copy link
Contributor

I liked that it re-uses the existing interface and error reporting

Oh, really? Can you show an example output? I thought the output was just whatever pylint outputs. In any case IMHO it definitely can't be in conftest.py, maybe if it is a real pytest plugin. But still is hard for me to see an advantage over just producing one file per example and then run pylint (and other linting tools) there, that will also use the normal output of the tools. We can even reuse the option and all the noxfile paraphernalia if we just write examples in a new directory like examples/.generated/ or something like that.

@Marenz
Copy link
Contributor

Marenz commented May 8, 2023

❰marenz❙~/frequenz/frequenz-sdk-python(git:validate_docstring_examples)❱✘≻ pytest src
==================================================================== test session starts =====================================================================
platform linux -- Python 3.11.2, pytest-7.2.2, pluggy-1.0.0
rootdir: /home/marenz/frequenz/frequenz-sdk-python, configfile: pyproject.toml
plugins: anyio-3.6.2, mock-3.10.0, asyncio-0.21.0, time-machine-2.9.0
asyncio: mode=Mode.AUTO
collected 17 items

src/frequenz/sdk/actor/_decorator.py ..                                                                                                                [ 11%]
src/frequenz/sdk/actor/power_distributing/power_distributing.py .                                                                                      [ 17%]
src/frequenz/sdk/power/_distribution_algorithm.py ..........                                                                                           [ 76%]
src/frequenz/sdk/timeseries/_moving_window.py ..                                                                                                       [ 88%]
src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py .                                                                                       [ 94%]
src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py .                                                                                          [100%]

@Marenz
Copy link
Contributor

Marenz commented May 8, 2023

And here an example with a failure:

❰marenz❙~/frequenz/frequenz-sdk-python(git:validate_docstring_examples)❱✘≻ pytest src
==================================================================== test session starts =====================================================================
platform linux -- Python 3.11.2, pytest-7.2.2, pluggy-1.0.0
rootdir: /home/marenz/frequenz/frequenz-sdk-python, configfile: pyproject.toml
plugins: anyio-3.6.2, mock-3.10.0, asyncio-0.21.0, time-machine-2.9.0
asyncio: mode=Mode.AUTO
collected 17 items

src/frequenz/sdk/actor/_decorator.py .F                                                                                                                [ 11%]
src/frequenz/sdk/actor/power_distributing/power_distributing.py .                                                                                      [ 17%]
src/frequenz/sdk/power/_distribution_algorithm.py ..........                                                                                           [ 76%]
src/frequenz/sdk/timeseries/_moving_window.py ..                                                                                                       [ 88%]
src/frequenz/sdk/timeseries/_formula_engine/_formula_engine.py .                                                                                       [ 94%]
src/frequenz/sdk/timeseries/logical_meter/_logical_meter.py .                                                                                          [100%]

========================================================================== FAILURES ==========================================================================
______________________________________________________________ _decorator.py line=127 column=1 _______________________________________________________________

Example at /home/marenz/frequenz/frequenz-sdk-python/src/frequenz/sdk/actor/_decorator.py, line 127, column 1 did not evaluate as expected:
Pylint validation failed for code example:
import asyncio
import inspect
import logging
from typing import Any, Generic, Optional, Type, TypeVar
from frequenz.sdk._internal._asyncio import cancel_and_await
from frequenz.sdk.actor._decorator import *

from frequenz.channels import Receiver, Sender, Broadcast
from frequenz.channels.util import Select
@actor
class Actor1:
    def __init__(
        self,
        name: str,
        recv: Receiver[bool],
        output: Sender[bool],
    ) -> None:
        self.name = name
        self._recv = recv
        self._output = output
    async def run(self) -> None:
        async for msg in self._recv:
            await self._output.send(msg)
@actor
class Actor2:
    def __init__(
        self,
        name: str,
        recv: Receiver[bool],
        output: Sender[bool],
    ) -> None:
        self.name = name
        self._recv = recv
        self._output = output
    async def run(self) -> None:
        async for msg in self._recv:
            await self._output.send(msg)
async def main() -> None:
    input_chan: Broadcast[bool] = Broadcast("Input to A1")
    a1_chan: Broadcast[bool] = Broadcast("A1 stream")
    a2_chan: Broadcast[bool] = Broadcast("A2 stream")
    a_1 = Actor1(
        name="ActorOne",
        recv=input_chan.new_receiver(),
        output=a1_chan.new_sender(),
    )
    a_2 = Actor2(
        name="ActorTwo",
        recv=a1_chan.new_receiver(),
        output=a2_chan.new_sender(),
    )
    a2_rx = a2_chan.new_receiver()
    await input_chan.new_seder().send(True)
    msg = await a2_rx.receive()
asyncio.run(main())

Error: Command '['pylint', '--disable', 'missing-module-docstring,missing-class-docstring,missing-function-docstring,wildcard-import,reimported,unused-import,unused-variable,unused-wildcard-import,no-name-in-module,await-outside-async', '--from-stdin', '/home/marenz/frequenz/frequenz-sdk-python/src/frequenz/sdk/actor/_decorator.py']' returned non-zero exit status 2.
Output: ************* Module sdk.actor._decorator
src/frequenz/sdk/actor/_decorator.py:174:10: E1101: Instance of 'Broadcast' has no 'new_seder' member; maybe 'new_sender'? (no-member)

-------------------------------------------------------------------
Your code has been rated at 8.53/10 (previous run: 10.00/10, -1.47)


_decorator.py:127: SybilFailure
================================================================== short test summary info ===================================================================
FAILED src/frequenz/sdk/actor/_decorator.py::line:127,column:1
=============================================================== 1 failed, 16 passed in 21.83s ================================================================

@Marenz
Copy link
Contributor

Marenz commented May 8, 2023

I was kind of avoiding to generate actual files and tried to keep it all in memory, most for performance reasons.

@Marenz
Copy link
Contributor

Marenz commented May 10, 2023

Recreated under my new username at #384

@Marenz Marenz closed this May 10, 2023
llucax added a commit that referenced this pull request Jun 19, 2023
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from
3.10.0 to 3.11.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pytest-dev/pytest-mock/releases">pytest-mock's
releases</a>.</em></p>
<blockquote>
<h2>v3.11.1</h2>
<ul>
<li>
<p>Fixed introspection for failed <code>assert_has_calls</code> (<a
href="https://github.com/pytest-dev/pytest-mock/issues/365">#365</a>).</p>
</li>
<li>
<p>Updated type annotations for <code>mocker.patch</code> and
<code>mocker.spy</code> (<a
href="https://github.com/pytest-dev/pytest-mock/issues/364">#364</a>).</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst">pytest-mock's
changelog</a>.</em></p>
<blockquote>
<h2>3.11.1 (2023-06-15)</h2>
<ul>
<li>
<p>Fixed introspection for failed <code>assert_has_calls</code>
(<code>[#365](https://github.com/pytest-dev/pytest-mock/issues/365)</code>_).</p>
</li>
<li>
<p>Updated type annotations for <code>mocker.patch</code> and
<code>mocker.spy</code>
(<code>[#364](https://github.com/pytest-dev/pytest-mock/issues/364)</code>_).</p>
</li>
</ul>
<p>.. _<a
href="https://github.com/pytest-dev/pytest-mock/issues/365">#365</a>:
<a
href="https://github.com/pytest-dev/pytest-mock/pull/365">pytest-dev/pytest-mock#365</a>
.. _<a
href="https://github.com/pytest-dev/pytest-mock/issues/364">#364</a>:
<a
href="https://github.com/pytest-dev/pytest-mock/pull/364">pytest-dev/pytest-mock#364</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pytest-dev/pytest-mock/commit/d3e73f2e93f7b93eba0a36e17e43bafd969da4fe"><code>d3e73f2</code></a>
Explicitly specify the tag name during GitHub Release</li>
<li><a
href="https://github.com/pytest-dev/pytest-mock/commit/5668efe23e01673af9905febeefd9a9791b023f4"><code>5668efe</code></a>
Update CHANGELOG for 3.11.0</li>
<li><a
href="https://github.com/pytest-dev/pytest-mock/commit/c9a4d13ab4e808ef02250898a79c07b2acf76f61"><code>c9a4d13</code></a>
Improve CI</li>
<li><a
href="https://github.com/pytest-dev/pytest-mock/commit/5818160717a16b7f8867d950ed60b9dc3349ec8d"><code>5818160</code></a>
Add assert_has_calls_wrapper (<a
href="https://github.com/pytest-dev/pytest-mock/issues/365">#365</a>)</li>
<li><a
href="https://github.com/pytest-dev/pytest-mock/commit/355b5aae1a1a9b3f2c17b2a6bbfde9980967bceb"><code>355b5aa</code></a>
Fix return type annotation for patch and spy (<a
href="https://github.com/pytest-dev/pytest-mock/issues/364">#364</a>)</li>
<li><a
href="https://github.com/pytest-dev/pytest-mock/commit/8ba681201488bec69f27f6e9e2a56988a01f0d1b"><code>8ba6812</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://github.com/pytest-dev/pytest-mock/issues/362">#362</a>)</li>
<li><a
href="https://github.com/pytest-dev/pytest-mock/commit/786eaaf416fed8204a8aebb29cbf71527228a9f6"><code>786eaaf</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://github.com/pytest-dev/pytest-mock/issues/360">#360</a>)</li>
<li><a
href="https://github.com/pytest-dev/pytest-mock/commit/1cb146af21ffd0b5be24703419cf4a192c4ec0ab"><code>1cb146a</code></a>
Push tag manually</li>
<li><a
href="https://github.com/pytest-dev/pytest-mock/commit/9608e9659b1f062bb09915f3ef1c538ca7cb5c5f"><code>9608e96</code></a>
Update permissions for deploy workflow</li>
<li><a
href="https://github.com/pytest-dev/pytest-mock/commit/c778ee73bd516d465dca3512d099682ba32108fa"><code>c778ee7</code></a>
Fix deployment workflow (<a
href="https://github.com/pytest-dev/pytest-mock/issues/358">#358</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/pytest-dev/pytest-mock/compare/v3.10.0...v3.11.1">compare
view</a></li>
</ul>
</details>
<br />

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest-mock&package-manager=pip&previous-version=3.10.0&new-version=3.11.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)

</details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
part:actor Affects an actor ot the actors utilities (decorator, etc.) part:data-pipeline Affects the data pipeline part:power-management Affects the management of battery power and distribution part:tooling Affects the development tooling (CI, deployment, dependency management, etc.)
Projects
Development

Successfully merging this pull request may close these issues.

Remove outdated examples from the PowerDistributingActor Fix broken examples in docstrings
3 participants