Skip to content

Fix compatibility with Twisted 25 #13502

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

Merged
merged 1 commit into from
Jun 17, 2025

Conversation

nicoddemus
Copy link
Member

@nicoddemus nicoddemus commented Jun 9, 2025

The issue arises because Failure.__init__ in Twisted 25 can receive a non-None exc_value while exc_tb is None. In such cases, ExceptionInfo[BaseException].from_exc_info fails, as it expects a traceback to be present when sys.exc_info() returns a tuple. This leads to the error message 'NoneType' object is not iterable.

Implemented separate workarounds for Twisted 24 and Twisted 25, the later one being simpler and less intrusive in the long run, as it does not require monkeypatching Failure.__init__.

We can drop suppor for Twisted 24 and keep the simpler patch in a later release.

Fixes #13497

@glyph
Copy link

glyph commented Jun 9, 2025

Happy to see a fix developed so quickly and I hope this can land in a release soon.

That said, this is very fragile and likely to break again soon. turning Failure into a dataclass, making it frozen, giving it __slots__, rewriting it in Cython or Rust or something, etc, are all things that are likely to be done in service of speeding things up. I am pretty sure we avoid storing a raw exc_info like this intentionally; it's been quite a while but I seem to remember several very annoying memory leaks that would crop up with that strategy (hence Failure.cleanFailure())

@nicoddemus
Copy link
Member Author

Hmmm test_trial_exceptions_with_skips did pass for me locally, will continue to investigate.

@nicoddemus nicoddemus marked this pull request as draft June 9, 2025 23:22
@nicoddemus nicoddemus force-pushed the 13497-twisted-compatibility branch from e30e0a0 to d12b29a Compare June 9, 2025 23:54
@nicoddemus
Copy link
Member Author

nicoddemus commented Jun 10, 2025

Happy to see a fix developed so quickly and I hope this can land in a release soon.

As soon as we merge this and #13495 I will make a new release. 👍

That said, this is very fragile and likely to break again soon. turning Failure into a dataclass, making it frozen, giving it slots, rewriting it in Cython or Rust or something, etc, are all things that are likely to be done in service of speeding things up.

I definitely agree this is fragile. This workaround seems to be due to the fact that twisted calls TestCase.addError with twisted.python.Failure instead of the expected sys.exc_info, so it was decided to wrap its __init__ and store the original sys.exc_info.

I guess we can mitigate the problem a bit by storing the sys.exc_info somewhere else... 🤔

I am pretty sure we avoid storing a raw exc_info like this intentionally; it's been quite a while but I seem to remember several very annoying memory leaks that would crop up with that strategy (hence Failure.cleanFailure())

Well noted: I'm reducing the chances of a memory leak now by removing the attribute created by pytest.

But all this is definitely hacky.

PS: I'm a big fan of your blog. 😁

@nicoddemus nicoddemus force-pushed the 13497-twisted-compatibility branch 3 times, most recently from d3d4600 to 36c57ab Compare June 10, 2025 00:29
@nicoddemus nicoddemus marked this pull request as ready for review June 10, 2025 00:33
@nicoddemus nicoddemus force-pushed the 13497-twisted-compatibility branch from 36c57ab to 96f0319 Compare June 10, 2025 01:07
@glyph
Copy link

glyph commented Jun 10, 2025

Happy to see a fix developed so quickly and I hope this can land in a release soon.

As soon as we merge this and #13495 I will make a new release. 👍

Sounds great, thanks.

That said, this is very fragile and likely to break again soon. turning Failure into a dataclass, making it frozen, giving it slots, rewriting it in Cython or Rust or something, etc, are all things that are likely to be done in service of speeding things up.

I definitely agree this is fragile. This workaround seems to be due to the fact that twisted calls TestCase.addError with twisted.python.Failure instead of the expected sys.exc_info, so it was decided to wrap its __init__ and store the original sys.exc_info.

I guess we can mitigate the problem a bit by storing the sys.exc_info somewhere else... 🤔

I probably need to wait for a less hectic day to reflect on this but can Exception.__traceback__ be used to reconstruct it?

I am pretty sure we avoid storing a raw exc_info like this intentionally; it's been quite a while but I seem to remember several very annoying memory leaks that would crop up with that strategy (hence Failure.cleanFailure())

Well noted: I'm reducing the chances of a memory leak now by removing the attribute created by pytest.

Ah, awesome, glad to hear it. I didn't catch that.

It's possible that this is less common with coroutines as it was with manually-managed Deferreds, but i guess it depends if there's a hard reference to the Failure in the traceback frames…

But all this is definitely hacky.

Yeah, I look forward to figuring out some better strategy for this, and I'm happy for twisted & trial to help out here.

PS: I'm a big fan of your blog. 😁

Thanks! Remember to like and subscribe 🙃

@nicoddemus
Copy link
Member Author

nicoddemus commented Jun 10, 2025

this but can Exception.__traceback__ be used to reconstruct it?

That's a good idea, I will try it out later.

@nicoddemus
Copy link
Member Author

Meanwhile this is ready for review @pytest-dev/core

@nicoddemus nicoddemus force-pushed the 13497-twisted-compatibility branch 2 times, most recently from c48c810 to f519098 Compare June 10, 2025 22:27
@nicoddemus
Copy link
Member Author

nicoddemus commented Jun 10, 2025

@glyph

this but can Exception.traceback be used to reconstruct it?

That approach did not work, because Failure.__traceback__ at that point is always None. However, this inspired me to try a different solution: extract the sys.exc_info() tuple directly from the Failure object, similar to what was being done in the version where we monkey-patch Failure.__init__.

This change significantly simplified the code:

  1. It eliminates the need to monkey-patch Failure.__init__, making the code more future-proof.
  2. Because of (1), we no longer need to patch Failure.__init__ during every runtest protocol. This allows to move the code that registers TestCaseFunction as an IReporter to pytest_configure. Previously, this required a global to ensure it was being done only once.

The code is much simpler and more isolated. However, it has two downsides:

  1. I'm not entirely sure it is correct, although all tests are passing.
  2. It does not work for Twisted <25: in Twisted 24.11.0, at the point addError is called, sys.exc_info() returns (None, None, None), which breaks the patch.

Because of the above, I see two options (@pytest-dev/core):

  1. Release the simpler, shorter version, from the second commit. We need to advertise that it requires Twisted>=25, and might break in unexpected ways not caught by our test suite, given the patch is significantly different than the previous code.
  2. Release the more direct fix from the first commit. This works for any Twisted version (🤞), but the code is more fragile, relying on monkey patching Failure.__init__; however, not worse than before.

@Zac-HD
Copy link
Member

Zac-HD commented Jun 10, 2025

I weakly prefer option (2); breaking compat with older versions is usually a larger impact on users than picking up a fragile workaround - at least when there are active maintainers on all sides, which we have in this case.

@Pierre-Sassoulas
Copy link
Member

Agree with Zac. And longterm we could switch to the simpler implementation when twisted download are for version > 25 for ~= 90% of users if twisted according to pypistats.

@glyph
Copy link

glyph commented Jun 11, 2025

Agree with Zac. And longterm we could switch to the simpler implementation when twisted download are for version > 25 for ~= 90% of users if twisted according to pypistats.

Por que no los dos?

Specifically: add a toggle at runtime that chooses the simpler implementation when possible (i.e. when Twisted is new enough), and then by the time the old implementation gets deleted, it's just removing dead code, rather than transitioning everyone to a new, as-yet untested implementation all at once?

@nicoddemus
Copy link
Member Author

Por que no los dos?

Good call, will do that.

Copy link
Member

@RonnyPfannschmidt RonnyPfannschmidt left a comment

Choose a reason for hiding this comment

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

Is there a way to support pre 25. Else this is a breaking change?

@nicoddemus
Copy link
Member Author

Is there a way to support pre 25. Else this is a breaking change?

Indeed, but as @glyph suggested, will implement separate fixes depending on the Twisted version.

@RonnyPfannschmidt
Copy link
Member

Should we use the version metadata from importlib?

@nicoddemus
Copy link
Member Author

Should we use the version metadata from importlib?

Hopefully. 😁

@nicoddemus nicoddemus force-pushed the 13497-twisted-compatibility branch 2 times, most recently from a14c94b to 169f9ff Compare June 14, 2025 13:39
@nicoddemus
Copy link
Member Author

Both patches in place now. 👍

@nicoddemus nicoddemus added the backport 8.4.x apply to PRs at any point; backports the changes to the 8.4.x branch label Jun 14, 2025
@nicoddemus nicoddemus force-pushed the 13497-twisted-compatibility branch 3 times, most recently from b330044 to 16b7074 Compare June 16, 2025 22:30
@nicoddemus
Copy link
Member Author

Ready for review

Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

Great work !

As discussed in pytest-dev#13502, the fix for compatibility with Twisted 25+ is simpler. Therefore, it makes sense to implement both fixes (for Twisted 24 and Twisted 25) in parallel. This way, we can eventually drop support for Twisted <25 and keep only the simpler workaround.

In addition, the `unittestextras` tox environment has been replaced with dedicated test environments for `asynctest`, `Twisted 24`, and `Twisted 25`.

Fixes pytest-dev#13497
@nicoddemus nicoddemus force-pushed the 13497-twisted-compatibility branch from c8a3842 to 4e533f7 Compare June 17, 2025 20:07
@nicoddemus nicoddemus enabled auto-merge (squash) June 17, 2025 20:08
@nicoddemus
Copy link
Member Author

I'm having trouble understanding why Codecov is reporting missing coverage.

Their diff indicates that the code related to Twisted 24 isn't executing:

image

However, the log files from the py39-unittest-twisted24 job don't show those specific lines as missing:

src\_pytest\unittest.py                276     19     74     10    90%   90, 122, 144-145, 146->155, 157->160, 185-186, 187->189, 190->exit, 215, 262, 320, 423, 490-500

Indeed, lines 490-500 (in the log above) pertain to the fix for Twisted 25, which is expected behavior for the twisted24 job. Moreover, these lines appear to be covered in the screenshot provided.

Also it is clear that coverage is being uploaded.

Given that this fix is long overdue, I'll proceed with merging it.

@nicoddemus nicoddemus disabled auto-merge June 17, 2025 20:46
@nicoddemus nicoddemus merged commit 01dce85 into pytest-dev:main Jun 17, 2025
35 of 36 checks passed
Copy link

patchback bot commented Jun 17, 2025

Backport to 8.4.x: 💚 backport PR created

✅ Backport PR branch: patchback/backports/8.4.x/01dce85a89eb0b3e881303784267f14b94d9691f/pr-13502

Backported as #13531

🤖 @patchback
I'm built with octomachinery and
my source is open — https://github.com/sanitizers/patchback-github-app.

@nicoddemus nicoddemus deleted the 13497-twisted-compatibility branch June 17, 2025 20:46
patchback bot pushed a commit that referenced this pull request Jun 17, 2025
As discussed in #13502, the fix for compatibility with Twisted 25+ is simpler. Therefore, it makes sense to implement both fixes (for Twisted 24 and Twisted 25) in parallel. This way, we can eventually drop support for Twisted <25 and keep only the simpler workaround.

In addition, the `unittestextras` tox environment has been replaced with dedicated test environments for `asynctest`, `Twisted 24`, and `Twisted 25`.

Fixes #13497

(cherry picked from commit 01dce85)
nicoddemus added a commit that referenced this pull request Jun 17, 2025
As discussed in #13502, the fix for compatibility with Twisted 25+ is simpler. Therefore, it makes sense to implement both fixes (for Twisted 24 and Twisted 25) in parallel. This way, we can eventually drop support for Twisted <25 and keep only the simpler workaround.

In addition, the `unittestextras` tox environment has been replaced with dedicated test environments for `asynctest`, `Twisted 24`, and `Twisted 25`.

Fixes #13497

(cherry picked from commit 01dce85)

Co-authored-by: Bruno Oliveira <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport 8.4.x apply to PRs at any point; backports the changes to the 8.4.x branch bot:chronographer:provided (automation) changelog entry is part of PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Twisted: Incompability with pytest.skip and Twisted 25.5
5 participants