10000 gh-76785: Add PyInterpreterConfig Helpers by ericsnowcurrently · Pull Request #117170 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-76785: Add PyInterpreterConfig Helpers #117170

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
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bdaef6b
Fix a comment.
ericsnowcurrently Mar 7, 2024
6342635
Add PyInterpreterConfig helpers.
ericsnowcurrently Mar 21, 2024
c48dd00
Add config helpers to _testinternalcapi.
ericsnowcurrently Mar 21, 2024
0796fe9
Use the new helpers in run_in_subinterp_with_config().
ericsnowcurrently Mar 22, 2024
8993b41
Move the PyInterpreterConfig utils to their own file.
ericsnowcurrently Mar 22, 2024
a113017
_PyInterpreterState_ResolveConfig() -> _PyInterpreterConfig_InitFromS…
ericsnowcurrently Mar 22, 2024
fce72b8
_testinternalcapi.new_interpreter_config() -> _xxsubinterpreters.new_…
ericsnowcurrently Mar 22, 2024
c52e484
_testinternalcapi.get_interpreter_config() -> _xxsubinterpreters.get_…
ericsnowcurrently Mar 22, 2024
a2983ce
Call _PyInterpreterState_RequireIDRef() in _interpreters._incref().
ericsnowcurrently Mar 22, 2024
05a081e
_testinternalcapi.interpreter_incref() -> _interpreters._incref()
ericsnowcurrently Mar 23, 2024
8a39bbc
Supporting passing a config to _xxsubinterpreters.create().
ericsnowcurrently Mar 22, 2024
1173cd1
Factor out new_interpreter().
ericsnowcurrently Mar 22, 2024
92c11d3
Fix test_import.
ericsnowcurrently Mar 25, 2024
edda48d
Fix an outdent.
ericsnowcurrently Mar 25, 2024
5f617ed
Call _PyInterpreterState_RequireIDRef() in the right places.
ericsnowcurrently Apr 1, 2024
10000 c504c79
Drop an unnecessary _PyInterpreterState_IDInitref() call.
ericsnowcurrently Apr 1, 2024
8a75c90
Reduce to just the new internal C-API.
ericsnowcurrently Apr 2, 2024
a38cda7
Adjust test_get_config.
ericsnowcurrently Apr 2, 2024
cae0482
Remove trailing whitespace.
ericsnowcurrently Apr 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Reduce to just the new internal C-API.
  • Loading branch information
ericsnowcurrently committed Apr 2, 2024
commit 8a75c9002b0a1f52b64bb7ded652a4a6e48f4e58
2 changes: 1 addition & 1 deletion Lib/test/support/interpreters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def __str__(self):

def create():
"""Return a new (idle) Python interpreter."""
id = _interpreters.create(reqrefs=True)
id = _interpreters.create(isolated=True)
return Interpreter(id)


Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test__xxsubinterpreters.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ def f():
def test_create_daemon_thread(self):
with self.subTest('isolated'):
expected = 'spam spam spam spam spam'
subinterp = interpreters.create('isolated')
subinterp = interpreters.create(isolated=True)
script, file = _captured_script(f"""
import threading
def f():
Expand All @@ -604,7 +604,7 @@ def f():
self.assertEqual(out, expected)

with self.subTest('not isolated'):
subinterp = interpreters.create('legacy')
subinterp = interpreters.create(isolated=False)
script, file = _captured_script("""
import threading
def f():
Expand Down
280 changes: 265 additions & 15 deletions Lib/test/test_capi/test_misc.py
341A
Original file line number Diff line number Diff line change
Expand Up @@ -2204,6 +2204,256 @@ def test_module_state_shared_in_global(self):
self.assertEqual(main_attr_id, subinterp_attr_id)


class InterpreterConfigTests(unittest.TestCase):

supported = {
'isolated': types.SimpleNamespace(
use_main_obmalloc=False,
allow_fork=False,
allow_exec=False,
allow_threads=True,
allow_daemon_threads=False,
check_multi_interp_extensions=True,
gil='own',
),
'legacy': types.SimpleNamespace(
use_main_obmalloc=True,
allow_fork=True,
allow_exec=True,
allow_threads=True,
allow_daemon_threads=True,
check_multi_interp_extensions=False,
gil='shared',
),
'empty': types.SimpleNamespace(
use_main_obmalloc=False,
allow_fork=False,
allow_exec=False,
allow_threads=False,
allow_daemon_threads=False,
check_multi_interp_extensions=False,
gil='default',
),
}
gil_supported = ['default', 'shared', 'own']

def iter_all_configs(self):
for use_main_obmalloc in (True, False):
for allow_fork in (True, False):
for allow_exec in (True, False):
for allow_threads in (True, False):
for allow_daemon in (True, False):
for checkext in (True, False):
for gil in ('shared', 'own', 'default'):
yield types.SimpleNamespace(
use_main_obmalloc=use_main_obmalloc,
allow_fork=allow_fork,
allow_exec=allow_exec,
allow_threads=allow_threads,
allow_daemon_threads=allow_daemon,
check_multi_interp_extensions=checkext,
gil=gil,
)

def assert_ns_equal(self, ns1, ns2, msg=None):
# This is mostly copied from TestCase.assertDictEqual.
self.assertEqual(type(ns1), type(ns2))
if ns1 == ns2:
return

import difflib
import pprint
from unittest.util import _common_shorten_repr
standardMsg = '%s != %s' % _common_shorten_repr(ns1, ns2)
diff = ('\n' + '\n'.join(difflib.ndiff(
pprint.pformat(vars(ns1)).splitlines(),
pprint.pformat(vars(ns2)).splitlines())))
diff = f'namespace({diff})'
standardMsg = self._truncateMessage(standardMsg, diff)
self.fail(self._formatMessage(msg, standardMsg))

def test_predefined_config(self):
def check(name, expected):
expected = self.supported[expected]
args = (name,) if name else ()

config1 = _testinternalcapi.new_interp_config(*args)
self.assert_ns_equal(config1, expected)
self.assertIsNot(config1, expected)

config2 = _testinternalcapi.new_interp_config(*args)
self.assert_ns_equal(config2, expected)
self.assertIsNot(config2, expected)
self.assertIsNot(config2, config1)

with self.subTest('default'):
check(None, 'isolated')

for name in self.supported:
with self.subTest(name):
check(name, name)

def test_update_from_dict(self):
for name, vanilla in self.supported.items():
with self.subTest(f'noop ({name})'):
expected = vanilla
overrides = vars(vanilla)
config = _testinternalcapi.new_interp_config(name, **overrides)
self.assert_ns_equal(config, expected)

with self.subTest(f'change all ({name})'):
overrides = {k: not v for k, v in vars(vanilla).items()}
for gil in self.gil_supported:
if vanilla.gil == gil:
continue
overrides['gil'] = gil
expected = types.SimpleNamespace(**overrides)
config = _testinternalcapi.new_interp_config(
name, **overrides)
self.assert_ns_equal(config, expected)

# Override individual fields.
for field, old in vars(vanilla).items():
if field == 'gil':
values = [v for v in self.gil_supported if v != old]
else:
values = [not old]
for val in values:
with self.subTest(f'{name}.{field} ({old!r} -> {val!r})'):
overrides = {field: val}
expected = types.SimpleNamespace(
**dict(vars(vanilla), **overrides),
)
config = _testinternalcapi.new_interp_config(
name, **overrides)
self.assert_ns_equal(config, expected)

with self.subTest('unsupported field'):
for name in self.supported:
with self.assertRaises(ValueError):
_testinternalcapi.new_interp_config(name, spam=True)

# Bad values for bool fields.
for field, value in vars(self.supported['empty']).items():
if field == 'gil':
continue
assert isinstance(value, bool)
for value in [1, '', 'spam', 1.0, None, object()]:
with self.subTest(f'unsupported value ({field}={value!r})'):
with self.assertRaises(TypeError):
_testinternalcapi.new_interp_config(**{field: value})

# Bad values for .gil.
for value in [True, 1, 1.0, None, object()]:
with self.subTest(f'unsupported value(gil={value!r})'):
with self.assertRaises(TypeError):
_testinternalcapi.new_interp_config(gil=value)
for value in ['', 'spam']:
with self.subTest(f'unsupported value (gil={value!r})'):
with self.assertRaises(ValueError):
_testinternalcapi.new_interp_config(gil=value)

@requires_subinterpreters
def test_interp_init(self):
questionable = [
# strange
dict(
allow_fork=True,
allow_exec=False,
),
dict(
gil='shared',
use_main_obmalloc=False,
),
# risky
dict(
allow_fork=True,
allow_threads=True,
),
# ought to be invalid?
dict(
allow_threads=False,
allow_daemon_threads=True,
),
dict(
gil='own',
use_main_obmalloc=True,
),
]
invalid = [
dict(
use_main_obmalloc=False,
check_multi_interp_extensions=False
),
]
def match(config, override_cases):
ns = vars(config)
for overrides in override_cases:
if dict(ns, **overrides) == ns:
return True
return False

def check(config):
script = 'pass'
rc = _testinternalcapi.run_in_subinterp_with_config(script, config)
self.assertEqual(rc, 0)

for config in self.iter_all_configs():
if config.gil == 'default':
continue
if match(config, invalid):
with self.subTest(f'invalid: {config}'):
with self.assertRaises(RuntimeError):
check(config)
elif match(config, questionable):
with self.subTest(f'questionable: {config}'):
check(config)
else:
with self.subTest(f'valid: {config}'):
check(config)

F438 @requires_subinterpreters
def test_get_config(self):
def new_interp(config):
interpid = _testinternalcapi.new_interpreter(config)
def ensure_destroyed():
try:
_interpreters.destroy(interpid)
except _interpreters.InterpreterNotFoundError:
pass
self.addCleanup(ensure_destroyed)
return interpid

with self.subTest('main'):
expected = _testinternalcapi.new_interp_config('legacy')
expected.gil = 'own'
interpid = _interpreters.get_main()
config = _testinternalcapi.get_interp_config(interpid)
self.assert_ns_equal(config, expected)

with self.subTest('isolated'):
expected = _testinternalcapi.new_interp_config('isolated')
interpid = new_interp('isolated')
config = _testinternalcapi.get_interp_config(interpid)
self.assert_ns_equal(config, expected)

with self.subTest('legacy'):
expected = _testinternalcapi.new_interp_config('legacy')
interpid = new_interp('legacy')
config = _testinternalcapi.get_interp_config(interpid)
self.assert_ns_equal(config, expected)

with self.subTest('custom'):
orig = _testinternalcapi.new_interp_config(
'empty',
use_main_obmalloc=True,
gil='shared',
)
interpid = new_interp(orig)
config = _testinternalcapi.get_interp_config(interpid)
self.assert_ns_equal(config, orig)


@requires_subinterpreters
class InterpreterIDTests(unittest.TestCase):

Expand Down Expand Up @@ -2284,8 +2534,8 @@ def test_linked_lifecycle_does_not_exist(self):
link = _testinternalcapi.link_interpreter_refcount
unlink = _testinternalcapi.unlink_interpreter_refcount
get_refcount = _testinternalcapi.get_interpreter_refcount
incref = (lambda id: _interpreters._incref(id, implieslink=False))
decref = _interpreters._decref
incref = _testinternalcapi.interpreter_incref
decref = _testinternalcapi.interpreter_decref

with self.subTest('never existed'):
interpid = _testinternalcapi.unused_interpreter_id()
Expand Down Expand Up @@ -2327,7 +2577,7 @@ def test_linked_lifecycle_initial(self):
get_refcount = _testinternalcapi.get_interpreter_refcount

# A new interpreter will start out not linked, with a refcount of 0.
interpid = self.new_interpreter()
interpid = _testinternalcapi.new_interpreter()
self.add_interp_cleanup(interpid)
linked = is_linked(interpid)
refcount = get_refcount(interpid)
Expand All @@ -2339,10 +2589,10 @@ def test_linked_lifecycle_never_linked(self):
exists = _testinternalcapi.interpreter_exists
is_linked = _testinternalcapi.interpreter_refcount_linked
get_refcount = _testinternalcapi.get_interpreter_refcount
incref = (lambda id: _interpreters._incref(id, implieslink=False))
decref = _interpreters._decref
incref = _testinternalcapi.interpreter_incref
decref = _testinternalcapi.interpreter_decref

interpid = self.new_interpreter()
interpid = _testinternalcapi.new_interpreter()
self.add_interp_cleanup(interpid)

# Incref will not automatically link it.
Expand All @@ -2367,7 +2617,7 @@ def test_linked_lifecycle_link_unlink(self):
link = _testinternalcapi.link_interpreter_refcount
unlink = _testinternalcapi.unlink_interpreter_refcount

interpid = self.new_interpreter()
interpid = _testinternalcapi.new_interpreter()
self.add_interp_cleanup(interpid)

# Linkin 10000 g at refcount 0 does not destroy the interpreter.
Expand All @@ -2389,10 +2639,10 @@ def test_linked_lifecycle_link_incref_decref(self):
is_linked = _testinternalcapi.interpreter_refcount_linked
link = _testinternalcapi.link_interpreter_refcount
get_refcount = _testinternalcapi.get_interpreter_refcount
incref = (lambda id: _interpreters._incref(id, implieslink=False))
decref = _interpreters._decref
incref = _testinternalcapi.interpreter_incref
decref = _testinternalcapi.interpreter_decref

interpid = self.new_interpreter()
interpid = _testinternalcapi.new_interpreter()
self.add_interp_cleanup(interpid)

# Linking it will not change the refcount.
Expand All @@ -2416,9 +2666,9 @@ def test_linked_lifecycle_incref_link(self):
is_linked = _testinternalcapi.interpreter_refcount_linked
link = _testinternalcapi.link_interpreter_refcount
get_refcount = _testinternalcapi.get_interpreter_refcount
incref = (lambda id: _interpreters._incref(id, implieslink=False))
incref = _testinternalcapi.interpreter_incref

interpid = self.new_interpreter()
interpid = _testinternalcapi.new_interpreter()
self.add_interp_cleanup(interpid)

incref(interpid)
Expand All @@ -2438,10 +2688,10 @@ def test_linked_lifecycle_link_incref_unlink_decref(self):
link = _testinternalcapi.link_interpreter_refcount
unlink = _testinternalcapi.unlink_interpreter_refcount
get_refcount = _testinternalcapi.get_interpreter_refcount
incref = (lambda id: _interpreters._incref(id, implieslink=False))
decref = _interpreters._decref
incref = _testinternalcapi.interpreter_incref
decref = _testinternalcapi.interpreter_decref

interpid = self.new_interpreter()
interpid = _testinternalcapi.new_interpreter()
self.add_interp_cleanup(interpid)

link(interpid)
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2163,7 +2163,7 @@ def re_load(self, name, mod):
# subinterpreters

def add_subinterpreter(self):
interpid = _interpreters.create('legacy')
interpid = _interpreters.create(isolated=False)
def ensure_destroyed():
try:
_interpreters.destroy(interpid)
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_importlib/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ def test_magic_number(self):
class IncompatibleExtensionModuleRestrictionsTests(unittest.TestCase):

def run_with_own_gil(self, script):
interpid = _interpreters.create('isolated')
interpid = _interpreters.create(isolated=True)
def ensure_destroyed():
try:
_interpreters.destroy(interpid)
Expand All @@ -669,7 +669,7 @@ def ensure_destroyed():
raise ImportError(excsnap.msg)

def run_with_shared_gil(self, script):
interpid = _interpreters.create('legacy')
interpid = _interpreters.create(isolated=False)
def ensure_destroyed():
try:
_interpreters.destroy(interpid)
Expand Down
Loading
0