Skip to content

Commit

Permalink
pythongh-115490: Make the interpreter.channels and interpreter.queues…
Browse files Browse the repository at this point in the history
… Modules Handle Reloading Properly (pythongh-115493)

The problem manifested when the .py module got reloaded and the corresponding extension module didn't. The .py module registers types with the extension and the extension was not allowing that to happen more than once. The solution: let it happen more than once.
  • Loading branch information
ericsnowcurrently committed Mar 4, 2024
1 parent 207030f commit eb22e2b
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 19 deletions.
9 changes: 2 additions & 7 deletions Lib/test/libregrtest/main.py
Expand Up @@ -384,15 +384,10 @@ def run_tests_sequentially(self, runtests) -> None:

result = self.run_test(test_name, runtests, tracer)

# Unload the newly imported test modules (best effort
# finalization). To work around gh-115490, don't unload
# test.support.interpreters and its submodules even if they
# weren't loaded before.
keep = "test.support.interpreters"
# Unload the newly imported test modules (best effort finalization)
new_modules = [module for module in sys.modules
if module not in save_modules and
module.startswith(("test.", "test_"))
and not module.startswith(keep)]
module.startswith(("test.", "test_"))]
for module in new_modules:
sys.modules.pop(module, None)
# Remove the attribute of the parent module.
Expand Down
5 changes: 5 additions & 0 deletions Lib/test/test__xxinterpchannels.py
Expand Up @@ -18,6 +18,11 @@
channels = import_helper.import_module('_xxinterpchannels')


# Additional tests are found in Lib/test/test_interpreters/test_channels.py.
# New tests should be added there.
# XXX The tests here should be moved there. See the note under LowLevelTests.


##################################
# helpers

Expand Down
19 changes: 19 additions & 0 deletions Lib/test/test_interpreters/test_channels.py
@@ -1,3 +1,4 @@
import importlib
import threading
from textwrap import dedent
import unittest
Expand All @@ -11,6 +12,24 @@
from .utils import _run_output, TestBase


class LowLevelTests(TestBase):

# The behaviors in the low-level module is important in as much
# as it is exercised by the high-level module. Therefore the
# most # important testing happens in the high-level tests.
# These low-level tests cover corner cases that are not
# encountered by the high-level module, thus they
# mostly shouldn't matter as much.

# Additional tests are found in Lib/test/test__xxinterpchannels.py.
# XXX Those should be either moved to LowLevelTests or eliminated
# in favor of high-level tests in this file.

def test_highlevel_reloaded(self):
# See gh-115490 (https://github.com/python/cpython/issues/115490).
importlib.reload(channels)


class TestChannels(TestBase):

def test_create(self):
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_interpreters/test_queues.py
@@ -1,3 +1,4 @@
import importlib
import threading
from textwrap import dedent
import unittest
Expand All @@ -20,6 +21,20 @@ def tearDown(self):
pass


class LowLevelTests(TestBase):

# The behaviors in the low-level module is important in as much
# as it is exercised by the high-level module. Therefore the
# most # important testing happens in the high-level tests.
# These low-level tests cover corner cases that are not
# encountered by the high-level module, thus they
# mostly shouldn't matter as much.

def test_highlevel_reloaded(self):
# See gh-115490 (https://github.com/python/cpython/issues/115490).
importlib.reload(queues)


class QueueTests(TestBase):

def test_create(self):
Expand Down
15 changes: 10 additions & 5 deletions Modules/_xxinterpchannelsmodule.c
Expand Up @@ -2675,12 +2675,17 @@ set_channelend_types(PyObject *mod, PyTypeObject *send, PyTypeObject *recv)
return -1;
}

if (state->send_channel_type != NULL
|| state->recv_channel_type != NULL)
{
PyErr_SetString(PyExc_TypeError, "already registered");
return -1;
// Clear the old values if the .py module was reloaded.
if (state->send_channel_type != NULL) {
(void)_PyCrossInterpreterData_UnregisterClass(state->send_channel_type);
Py_CLEAR(state->send_channel_type);
}
if (state->recv_channel_type != NULL) {
(void)_PyCrossInterpreterData_UnregisterClass(state->recv_channel_type);
Py_CLEAR(state->recv_channel_type);
}

// Add and register the types.
state->send_channel_type = (PyTypeObject *)Py_NewRef(send);
state->recv_channel_type = (PyTypeObject *)Py_NewRef(recv);
if (ensure_xid_class(send, _channelend_shared) < 0) {
Expand Down
16 changes: 9 additions & 7 deletions Modules/_xxinterpqueuesmodule.c
Expand Up @@ -169,6 +169,9 @@ static int
clear_module_state(module_state *state)
{
/* external types */
if (state->queue_type != NULL) {
(void)_PyCrossInterpreterData_UnregisterClass(state->queue_type);
}
Py_CLEAR(state->queue_type);

/* QueueError */
Expand Down Expand Up @@ -1078,15 +1081,18 @@ set_external_queue_type(PyObject *module, PyTypeObject *queue_type)
{
module_state *state = get_module_state(module);

// Clear the old value if the .py module was reloaded.
if (state->queue_type != NULL) {
PyErr_SetString(PyExc_TypeError, "already registered");
return -1;
(void)_PyCrossInterpreterData_UnregisterClass(
state->queue_type);
Py_CLEAR(state->queue_type);
}
state->queue_type = (PyTypeObject *)Py_NewRef(queue_type);

// Add and register the new type.
if (ensure_xid_class(queue_type, _queueobj_shared) < 0) {
return -1;
}
state->queue_type = (PyTypeObject *)Py_NewRef(queue_type);

return 0;
}
Expand Down Expand Up @@ -1703,10 +1709,6 @@ module_clear(PyObject *mod)
{
module_state *state = get_module_state(mod);

if (state->queue_type != NULL) {
(void)_PyCrossInterpreterData_UnregisterClass(state->queue_type);
}

// Now we clear the module state.
clear_module_state(state);
return 0;
Expand Down

0 comments on commit eb22e2b

Please sign in to comment.