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

bpo-35753: Fix crash in doctest with unwrap-able functions #22981

Merged
merged 3 commits into from
May 5, 2021

Conversation

splbio
Copy link
Contributor

@splbio splbio commented Oct 26, 2020

Doctest needs to Ignore objects that inspect.unwrap throws ValueError due to "infinite" nesting.

This case is relatively commonly reproduced by importing unittest.mock.call into a module's namespace.

The following code produces a module that will crash doctest:

from unittest.mock import call

This is due to the call to unwrap throwing an uncaught ValueError.

For this case we can simply ignore the function and not attempt to extract docstring from this function.

This diff is intentionally done as a branch with two commits to provide failing test case and then a fix to show the problem is contained.

Future thoughts:

  1. Should catching this be optional defaulting to true, but allowing for folks to rethrow the exception to insist on truly clean code?
  2. Should we break out _find() into more sub functions so that we can more easily derive our own versions for each sub function that _find does making it easier to fix future issues?
  3. Should we include a way to denylist by id some objects so that future objects that cause problems with doctest can be passed in to ignore?

https://bugs.python.org/issue35753

@splbio
Copy link
Contributor Author

splbio commented Nov 6, 2020

@ambv : do you have a few minutes to review this?

Lib/doctest.py Outdated
# Protect against objects that cause unwrap to throw
isroutine = False
try:
isroutine = inspect.isroutine(inspect.unwrap(val))
Copy link
Contributor

Choose a reason for hiding this comment

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

Without reviewing the whole thing, a nitpick here: if unwrap is the function expected to throw here I'd leave only inspect.unwrap() call inside the try block and move the inspect.isroutine() call outside it. This may make the comment above redundant, which is a win-win.

Copy link
Member

Choose a reason for hiding this comment

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

If catching the ValueError is the correct approach, I agree with @jstasiak that only the call to unwrap should be inside the block. But I wonder whether this is not, in fact, a bug in either doctest or unwrap, but in unittest.mock.call, and should be fixed there?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stevendaprano : unfortunately several other types of things can cause unwrap to fail, here is another bug report similar where they use a similar technique: sphinx-doc/sphinx#7431

I will incorporate your and @jstasiak 's feedbacks on the code shortly and amend.

Copy link
Member

Choose a reason for hiding this comment

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

It's not clear to me from the sphinx PR that this is "several other types of things" but just the same bug in a different buggy mock object. Dunder attributes are special, and if proxies or mocks misuse them, bad things happen. The ultimate cause of the bug is in the proxy, not unwrap.

Even if we patch doctest to guard against such buggy mocks, the mocks themselves should still be fixed. We shouldn't expect every caller of unwrap to guard against bad mocks, any more than we would expect the caller to guard against code that monkey-patches the built-ins.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stevendaprano : I really disagree with the approach of moving the changes to mock.call.

I will list the reasons:

  1. If doctest is fragile against un-unwrappables then imagine someone who uses doctest, but then imports a third party library or if there are other places to fix, then doctest itself is fragile against such module. Doctest should not be fragile against un-unwrappables.
  2. Changes to mock.call are far more risky imo and effectively subtly change the behavior of mock.call in ways that could impact downstream.

As such I strongly believe that doctest itself should be made resilient to un-unwrapables, as opposed to fixing a single un-unwrappable type object.

Can we please merge this as-is to make doctest defensive against such broken modules now?

@splbio splbio force-pushed the fix-issue-35753-doctest-unwrap-master branch from ec77c87 to 33958e7 Compare November 7, 2020 16:32
@splbio
Copy link
Contributor Author

splbio commented Nov 7, 2020

Thank you @jstasiak and @stevendaprano .

I've taken the review feedback. The function _is_routine is created as a separate function now, this serves a few purposes:

  1. If unwrap fails, test against not-unwrapped version (@jstasiak).
  2. Allow override via subclass function override more easily in case someone needs a stop-gap fix to this logic.
  3. Code appears cleaner (at least to me).

Lib/test/test_doctest5.py Outdated Show resolved Hide resolved
@splbio splbio force-pushed the fix-issue-35753-doctest-unwrap-master branch 2 times, most recently from de2e327 to a878e03 Compare November 22, 2020 16:39
splbio and others added 3 commits November 22, 2020 10:32
Summary:

Ignore objects that inspect.unwrap throws due to
too many wrappers.  This is a very rare case, however
it can easily be surfaced when a module under doctest
imports unitest.mock.call into its namespace.

This commit adds a test that should fail prior to
the fix to the doctest module.
Summary:

Ignore objects that inspect.unwrap throws due to
too many wrappers.  This is a very rare case, however
it can easily be surfaced when a module under doctest
imports unitest.mock.call into its namespace.

This commit adds the fix to the module.

We simply skip any object that throws this exception.

This should handle the majority of cases.

Future thoughts:
1. Should catching this be optional defaulting to true,
but allowing for folks to rethrow the exception to insist
on truly clean code?
2. Should we break out _find() into more sub functions sp
that we can more easily derive our own versions for each
sub function that _find does making it easier to fix
future issues?
3. Should we include a way to denylist by `id` some
objects so that future objects that cause problems
with doctest can be passed in to ignore?

Test Plan:

Reviewers:
@splbio splbio force-pushed the fix-issue-35753-doctest-unwrap-master branch from a878e03 to 81e0d99 Compare November 22, 2020 18:33
@splbio
Copy link
Contributor Author

splbio commented Dec 6, 2020

@tim-one : do you have some time to review this PR please?

@splbio
Copy link
Contributor Author

splbio commented Dec 17, 2020

@rhettinger : I heard through the grapevine that tim-one is not very active, do you have time for this review or another reviewer?

@splbio
Copy link
Contributor Author

splbio commented Jan 30, 2021

@tim-one do you have time to review this?

@splbio
Copy link
Contributor Author

splbio commented Mar 15, 2021

@stevendaprano : do you have time to respond to this? I'm guessing you want me to fix python mock instead of doing it this way in this patch. I just want to confirm this before trying to write something that it may eventually be included in python as a fix.

@ambv ambv added needs backport to 3.9 only security fixes needs backport to 3.10 only security fixes labels May 5, 2021
@ambv ambv merged commit 565a318 into python:main May 5, 2021
@bedevere-bot
Copy link

@ambv: Please replace # with GH- in the commit message next time. Thanks!

@miss-islington
Copy link
Contributor

Thanks @splbio for the PR, and @ambv for merging it 🌮🎉.. I'm working now to backport this PR to: 3.10, 3.9.
🐍🍒⛏🤖

miss-islington pushed a commit to miss-islington/cpython that referenced this pull request May 5, 2021
…22981)

Ignore objects that inspect.unwrap throws due to
too many wrappers.  This is a very rare case, however
it can easily be surfaced when a module under doctest
imports unitest.mock.call into its namespace.

We simply skip any object that throws this exception.
This should handle the majority of cases.
(cherry picked from commit 565a318)

Co-authored-by: Alfred Perlstein <alfred@fb.com>
@miss-islington
Copy link
Contributor

Sorry, @splbio and @ambv, I could not cleanly backport this to 3.9 due to a conflict.
Please backport using cherry_picker on command line.
cherry_picker 565a31804c1139fe7886f38af3b3923653b0c1b3 3.9

@bedevere-bot
Copy link

GH-25926 is a backport of this pull request to the 3.10 branch.

@bedevere-bot bedevere-bot removed the needs backport to 3.10 only security fixes label May 5, 2021
@ambv ambv removed the needs backport to 3.9 only security fixes label May 5, 2021
@ambv ambv removed their assignment May 5, 2021
ambv pushed a commit that referenced this pull request May 5, 2021
…#25926)

Ignore objects that inspect.unwrap throws due to
too many wrappers.  This is a very rare case, however
it can easily be surfaced when a module under doctest
imports unitest.mock.call into its namespace.

We simply skip any object that throws this exception.
This should handle the majority of cases.
(cherry picked from commit 565a318)

Co-authored-by: Alfred Perlstein <alfred@fb.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants