Skip to content

gh-152548: Add the test.support.isolation.runInSubprocess() decorator - #152551

Merged
serhiy-storchaka merged 8 commits into
python:mainfrom
serhiy-storchaka:test-run_in_subprocess
Jul 27, 2026
Merged

gh-152548: Add the test.support.isolation.runInSubprocess() decorator#152551
serhiy-storchaka merged 8 commits into
python:mainfrom
serhiy-storchaka:test-run_in_subprocess

Conversation

@serhiy-storchaka

@serhiy-storchaka serhiy-storchaka commented Jun 29, 2026

Copy link
Copy Markdown
Member

Add test.support.isolation.runInSubprocess(), a decorator to run a test in a fresh interpreter subprocess, so that it does not share global or interpreter state with the rest of the test run.

  • Decorates a test method (only that method runs in a subprocess) or a TestCase subclass (the whole class runs in a single subprocess, with its setUpClass()/setUp()/tearDown()/tearDownClass() running there rather than in the parent).
  • Failures, errors and skips — including those of individual subTest()s — are reported for the corresponding test; a failure or an error shows the original subprocess traceback. A setUpClass()/setUpModule() failure or skip is reported for the whole class.
  • The subprocess inherits the parent's resource (-u), memory-limit (-M), verbosity (-v) and failfast (-f) configuration, so requires_resource(), requires(), bigmemtest() and similar behave the same in both processes.
  • A decorated test is skipped where subprocesses are unavailable, since it must spawn one.
  • test.support.isolation.runningInSubprocess is true while running in the subprocess, so fixtures can choose what to run there.

Demonstrated by three stacked PRs converting existing tests: #152565 (test_audit), #152570 (test_eintr) and #152639 (test_signal).

🤖 Generated with Claude Code

@serhiy-storchaka serhiy-storchaka added needs backport to 3.13 bugs and security fixes needs backport to 3.14 bugs and security fixes needs backport to 3.15 pre-release feature fixes, bugs and security fixes labels Jun 29, 2026
Run a test in a fresh interpreter subprocess, so that it does not share global
or interpreter state with the rest of the test run.  It can decorate a test
method (only that method runs in a subprocess) or a TestCase subclass (the
whole class runs in one subprocess, with its setUpClass()/setUp()/tearDown()/
tearDownClass() running once there).

Failures, errors and skips, including those of individual subtests, are
reported for the test and show the original subprocess traceback.  The
subprocess inherits the parent's resource, memory and verbosity configuration,
so that requires_resource(), bigmemtest() and similar behave the same in both
processes.

The test.support.running_isolated flag is true in the subprocess, so that
fixtures can choose what to run there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@serhiy-storchaka
serhiy-storchaka force-pushed the test-run_in_subprocess branch from 560e882 to 2d5b380 Compare June 29, 2026 07:27
Fixes the ruff F401 "imported but unused" lint failure for the re-exports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@read-the-docs-community

read-the-docs-community Bot commented Jun 29, 2026

Copy link
Copy Markdown

@isolated() always runs the test in a subprocess, so skip it in the
parent process on platforms that lack subprocess support, the same way
the tests it replaces were guarded by requires_subprocess().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@encukou encukou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Adding some thoughts as I read this.

I'm not convinced that this feature is worth the complexity.
(I'm also not convinced that it's complete -- but for an internal testing helper, that's OK.)

Comment thread Lib/test/support/__init__.py Outdated
Comment on lines +1094 to +1097
# Run a test method or class in an isolated subprocess. Implemented in a
# dedicated module so that its frames carry the __unittest marker and are
# stripped from reported tracebacks.
from test.support._isolation import isolated, running_isolated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this need to be support.isolated? Regrtest has been moving toward more granular helpers; this could be e.g. support.isolation.isolated.

If it needs to stay in this namespace, why are the imports not at the start of the file?

Comment thread Lib/test/support/_isolation.py Outdated
def _remote(detail):
# Wrap the subprocess traceback the way concurrent.futures does, so it is
# clearly delimited when shown as the cause.
return _RemoteTraceback('\n"""\n%s"""' % detail)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe use newer syntax here?

Comment thread Lib/test/support/_isolation.py Outdated
if kind == 'skipped':
test.skipTest(detail) # the detail is the skip reason, not a traceback
elif kind == 'failure':
raise test.failureException('test failed in the subprocess') \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Per PEP 8, prefer parentheses over the backslash.

Comment on lines +84 to +89
def _check_subprocess_support():
# isolated() always runs the test in a subprocess, so skip (in the parent)
# on platforms that do not support spawning one.
import test.support as support
if not support.has_subprocess_support:
raise unittest.SkipTest('requires subprocess support')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Skipping tests on platforms without subprocsess support looks dangerous to me: it's not obvious at all that @isolated will do that, so I worry people will apply the decorator to tests that don't strictly need it.
Perhaps name it @run_in_subprocess, or require @skipIf(not has_subprocess_support in call sites?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

run_in_subprocess was the initial name. I changed it to reflect the purpose rather than mechanism. It also better fits unittest which already have IsolatedAsyncTestCase and in general do not use snake_case.

The final destination for this feature is unittest. I placed it in test.support first for two reasons. 1) to test and dogfeed it, 2) to simplify backports. But in future it will be moved to unittest.

I can move it to the test.support submodule for now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Making it failing instead of skipping if subprocess is not available will just require every use to be decorated with require_resources('subprocess'). This will just add boilerplate, which this feature purposes to eliminate.

Tests which run code in subprocesses already skipped if subprocess is not supported. This decorator will replace asser_python_ok with hand-written scripts in which you cannot even use assertEqual.

@encukou encukou Jun 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

run_in_subprocess was the initial name. I changed it to reflect the purpose rather than mechanism.

But, “hiding” the mechanism obscures the fact that the tests are (now silently) skipped if subprocesses aren't available. I don't think that's acceptable.

It could be @isolation.run_in_subprocess(), or @isolated(subprocess=True), or fail rather than skip (so a decorator is necessary).

Tests which run code in subprocesses already skipped if subprocess is not supported. This decorator will replace assert_python_ok

But, assert_python_ok usage makes it clear that a subprocess is involved. @isolated hides that in docs.

Comment on lines +180 to +181
orig_setUpClass = cls.setUpClass.__func__
orig_tearDownClass = cls.tearDownClass.__func__

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What's the __func__ for? Could this use the bound classmethod?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is for subclasses. A bound classmethod is bound to the decoration-time class. At run time, we need to run a method bound to a run-time class.

Added an explanation comment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A test would be good too, especially if you want to put this in unittest.

Comment thread Lib/test/support/_isolation.py Outdated
The test is skipped on platforms without subprocess support.
"""
def decorator(obj):
if isinstance(obj, type):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this be unittest.TestCase, as per the docstring?

Comment on lines +51 to +54
outcomes = [_outcome('failure', t, tb) for t, tb in result.failures]
outcomes += [_outcome('error', t, tb) for t, tb in result.errors]
outcomes += [_outcome('skipped', t, reason) for t, reason in result.skipped]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will ignore any other outcomes, like expectedFailures.
Also, collectedDurations isn't preserved.

Comment thread Doc/library/test.rst Outdated
Comment on lines +991 to +992
Fixtures can test :data:`running_isolated` to decide what to run in each
process.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The added docs repeat this 3 times.

Comment thread Doc/library/test.rst Outdated
Comment on lines +977 to +978
:meth:`~unittest.TestCase.setUp` and :meth:`~unittest.TestCase.tearDown`
run both in the parent process (as usual) and in the subprocess around the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same for all the other fixtures, right?

Move the helper into a public test.support.isolation submodule (used as
"from test.support import isolation"), drop the test.support re-export,
and document running_isolated and isolated() under that module.

Replay expected failures and forward subprocess durations to the parent,
so an @expectedfailure isolated test is no longer misreported as an
unexpected success and reported timings reflect the subprocess run.

Add test.test_support.TestIsolated covering the outcomes, subtest
reporting, traceback-as-cause, duration forwarding and the no-subprocess
skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
serhiy-storchaka and others added 2 commits June 30, 2026 14:49
…orator

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eview

Rename the decorator to runInSubprocess() and the flag to runningInSubprocess
so the subprocess mechanism (and the skip when it is unavailable) is explicit.
Suppress Windows CRT assertion dialogs in the isolated child so a debug build
does not hang on a modal dialog, and add a subclass fixture-binding test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@serhiy-storchaka

Copy link
Copy Markdown
Member Author

I need this feature for the curses tests on NetBSD and Windows (they only work with one terminal per process).

@johnslavik
johnslavik self-requested a review July 27, 2026 08:15

@encukou encukou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks!
Let's start using this internally.

@serhiy-storchaka

Copy link
Copy Markdown
Member Author

Thank you for your review.

serhiy-storchaka and others added 2 commits July 27, 2026 21:07
… child output

Hook the unittest _call*() indirections instead of replacing the test methods
and fixtures found at decoration time, so that what a subclass of an isolated
class adds or overrides also runs in the subprocess, and report an error when
an overriding setUpClass() bypasses the subprocess altogether.

Pass the configuration to the child on the command line instead of the
environment: the child is marked as the subprocess in-process, so it can never
spawn one in turn, and the marker is no longer inherited by processes that the
test itself spawns.

Decode the output of the child explicitly, so that an undecodable byte cannot
hide the failure it is part of.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fore the test

Every module imported before the test runs is state that the test would not
see in a normal test run.  Serialize with marshal instead of json, since it is
built in, and resolve the qualname in the imported module instead of letting
loadTestsFromName() find it by trying imports that fail, which pulls in
importlib.resources.

The child now imports nothing that a normal test run does not, and starts in
63 ms instead of 111 ms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@serhiy-storchaka
serhiy-storchaka enabled auto-merge (squash) July 27, 2026 18:40
@serhiy-storchaka serhiy-storchaka changed the title gh-152548: Add a test.support.isolated() decorator gh-152548: Add the test.support.isolation.runInSubprocess() decorator Jul 27, 2026
@serhiy-storchaka
serhiy-storchaka merged commit bde526d into python:main Jul 27, 2026
50 checks passed
@miss-islington-app

Copy link
Copy Markdown

Thanks @serhiy-storchaka for the PR 🌮🎉.. I'm working now to backport this PR to: 3.13, 3.14, 3.15.
🐍🍒⛏🤖

@miss-islington-app

Copy link
Copy Markdown

Sorry, @serhiy-storchaka, I could not cleanly backport this to 3.14 due to a conflict.
Please backport using cherry_picker on command line.

cherry_picker bde526d7be55014ec49fd44b831c948664ce508b 3.14

@bedevere-app

bedevere-app Bot commented Jul 27, 2026

Copy link
Copy Markdown

GH-154780 is a backport of this pull request to the 3.15 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.15 pre-release feature fixes, bugs and security fixes label Jul 27, 2026
@miss-islington-app

Copy link
Copy Markdown

Sorry, @serhiy-storchaka, I could not cleanly backport this to 3.13 due to a conflict.
Please backport using cherry_picker on command line.

cherry_picker bde526d7be55014ec49fd44b831c948664ce508b 3.13

@bedevere-app

bedevere-app Bot commented Jul 27, 2026

Copy link
Copy Markdown

GH-154783 is a backport of this pull request to the 3.14 branch.

@bedevere-app

bedevere-app Bot commented Jul 27, 2026

Copy link
Copy Markdown

GH-154784 is a backport of this pull request to the 3.13 branch.

@bedevere-app bedevere-app Bot removed needs backport to 3.14 bugs and security fixes needs backport to 3.13 bugs and security fixes labels Jul 27, 2026
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.

2 participants