Skip to content

gh-101859: Add caching of types.GenericAlias objects #103541

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

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions Include/internal/pycore_genericalias.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef Py_INTERNAL_GENERICALIAS_H
#define Py_INTERNAL_GENERICALIAS_H
#ifdef __cplusplus
extern "C" {
#endif

#ifndef Py_BUILD_CORE
# error "this header requires Py_BUILD_CORE define"
#endif

/* runtime lifecycle */

extern PyStatus _PyGenericAlias_Init(PyInterpreterState *interp);
extern void _PyGenericAlias_Fini(PyInterpreterState *interp);

#ifdef __cplusplus
}
#endif
#endif /* !Py_INTERNAL_GENERICALIAS_H */
5 changes: 4 additions & 1 deletion Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extern "C" {
#include "pycore_global_objects.h" // struct _Py_interp_static_objects
#include "pycore_object_state.h" // struct _py_object_state
#include "pycore_tuple.h" // struct _Py_tuple_state
#include "pycore_typeobject.h" // struct type_cache
#include "pycore_typeobject.h" // struct types_state
#include "pycore_unicodeobject.h" // struct _Py_unicode_state
#include "pycore_warnings.h" // struct _warnings_runtime_state

Expand Down Expand Up @@ -139,6 +139,9 @@ struct _is {
created and then deleted again. */
PySliceObject *slice_cache;

// Dict with existing generic alias objects:
PyObject* genericalias_cache;

struct _Py_tuple_state tuple;
struct _Py_list_state list;
struct _Py_dict_state dict_state;
Expand Down
7 changes: 7 additions & 0 deletions Lib/test/libregrtest/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ def clear_caches():
else:
fractions._hash_algorithm.cache_clear()

try:
_testcapi = sys.modules['_testcapi']
except KeyError:
pass
else:
_testcapi.genericalias_cache_clear()


def get_build_info():
# Get most important configure and build options as a list of strings.
Expand Down
53 changes: 51 additions & 2 deletions Lib/test/test_genericalias.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
import typing
from typing import Unpack

from test import support

from typing import TypeVar
T = TypeVar('T')
K = TypeVar('K')
Expand Down Expand Up @@ -94,8 +96,11 @@

class BaseTest(unittest.TestCase):
"""Test basics."""
generic_types = [type, tuple, list, dict, set, frozenset, enumerate,
defaultdict, deque,
c_generic_types = [
tuple, list, dict, set, frozenset, enumerate,
defaultdict, deque,
]
generic_types = [*c_generic_types,
SequenceMatcher,
dircmp,
FileInput,
Expand Down Expand Up @@ -173,6 +178,50 @@ def default():
else:
self.assertEqual(alias(iter((1, 2, 3))), t((1, 2, 3)))

@support.cpython_only
def test_c_genericaliases_are_cached(self):
for t in self.c_generic_types:
with self.subTest(t=t):
self.assertIs(t[int], t[int])
self.assertEqual(t[int], t[int])
self.assertIsNot(t[int], t[str])

@support.cpython_only
def test_c_genericaliases_uncachable_still_work(self):
for t in self.c_generic_types:
with self.subTest(t=t):
# Cache does not work for these args,
# but no error is present
self.assertIsNot(t[{}], t[{}])
self.assertEqual(t[{}], t[{}])

@support.cpython_only
def test_generic_alias_unpacks_are_cached(self):
self.assertIs((*tuple[int, str],)[0], (*tuple[int, str],)[0])
self.assertIsNot((*tuple[str, int],)[0], (*tuple[int, str],)[0])
self.assertIs((*tuple[T, ...],)[0], (*tuple[T, ...],)[0])
self.assertIsNot((*tuple[int, str],)[0], tuple[int, str])

@support.cpython_only
def test_generic_alias_unpacks_uncachable_still_work(self):
self.assertIsNot((*tuple[{}],)[0], (*tuple[{}],)[0])
self.assertEqual((*tuple[{}],)[0], (*tuple[{}],)[0])

@support.cpython_only
def test_genericalias_constructor_is_no_cached(self):
for t in self.generic_types:
if t is None:
continue
tname = t.__name__
with self.subTest(f"Testing {tname}"):
self.assertIsNot(GenericAlias(t, [int]), GenericAlias(t, [int]))

@support.cpython_only
def test_c_union_arg_order(self):
self.assertIsNot(int | str, str | int)
self.assertIsNot(int | str, int | None)
self.assertIsNot(int | str, int | str | None)

def test_unbound_methods(self):
t = list[int]
a = t()
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,11 @@ def __module__(self):
def test_or_type_operator_reference_cycle(self):
if not hasattr(sys, 'gettotalrefcount'):
self.skipTest('Cannot get total reference count.')
try:
import _testcapi
except ImportError:
self.skipTest('Cannot clear types.GenericAlias cache.')

gc.collect()
before = sys.gettotalrefcount()
for _ in range(30):
Expand All @@ -993,6 +998,7 @@ def test_or_type_operator_reference_cycle(self):
T.blah = U
del T
del U
_testcapi.genericalias_cache_clear()
gc.collect()
leeway = 15
self.assertLessEqual(sys.gettotalrefcount() - before, leeway,
Expand Down
1 change: 1 addition & 0 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -1688,6 +1688,7 @@ PYTHON_HEADERS= \
$(srcdir)/Include/internal/pycore_frame.h \
$(srcdir)/Include/internal/pycore_function.h \
$(srcdir)/Include/internal/pycore_genobject.h \
$(srcdir)/Include/internal/pycore_genericalias.h \
$(srcdir)/Include/internal/pycore_getopt.h \
$(srcdir)/Include/internal/pycore_gil.h \
$(srcdir)/Include/internal/pycore_global_objects.h \
Expand Down
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/unicode.c _testcapi/getargs.c _testcapi/pytime.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/pyos.c
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/unicode.c _testcapi/getargs.c _testcapi/pytime.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/pyos.c _testcapi/genericalias.c
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c

# Some testing modules MUST be built as shared libraries.
Expand Down
36 changes: 36 additions & 0 deletions Modules/_testcapi/genericalias.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include "parts.h"

#define Py_BUILD_CORE
// Needed to include both
// `Include/internal/pycore_gc.h` and
// `Include/cpython/objimpl.h`
#undef _PyGC_FINALIZED
#include "pycore_interp.h" // PyInterpreterState

static PyObject *
genericalias_cache_clear(PyObject *self, PyObject *Py_UNUSED(args))
{
PyThreadState *tstate = PyThreadState_Get();
PyInterpreterState *interp = PyThreadState_GetInterpreter(tstate);
assert(interp != NULL);
assert(interp->genericalias_cache != NULL);

PyDict_Clear(interp->genericalias_cache); // needs full PyInterpreterState

Py_RETURN_NONE;
}

static PyMethodDef test_methods[] = {
{"genericalias_cache_clear", genericalias_cache_clear, METH_NOARGS},
{NULL},
};

int
_PyTestCapi_Init_GenericAlias(PyObject *mod)
{
if (PyModule_AddFunctions(mod, test_methods) < 0) {
return -1;
}

return 0;
}
1 change: 1 addition & 0 deletions Modules/_testcapi/parts.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ int _PyTestCapi_Init_Structmember(PyObject *module);
int _PyTestCapi_Init_Exceptions(PyObject *module);
int _PyTestCapi_Init_Code(PyObject *module);
int _PyTestCapi_Init_PyOS(PyObject *module);
int _PyTestCapi_Init_GenericAlias(PyObject *mod);

#ifdef LIMITED_API_AVAILABLE
int _PyTestCapi_Init_VectorcallLimited(PyObject *module);
Expand Down
5 changes: 4 additions & 1 deletion Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3343,7 +3343,7 @@ test_gc_visit_objects_basic(PyObject *Py_UNUSED(self),
}
state.target = obj;
state.found = 0;

PyUnstable_GC_VisitObjects(gc_visit_callback_basic, &state);
Py_DECREF(obj);
if (!state.found) {
Expand Down Expand Up @@ -4189,6 +4189,9 @@ PyInit__testcapi(void)
if (_PyTestCapi_Init_PyOS(m) < 0) {
return NULL;
}
if (_PyTestCapi_Init_GenericAlias(m) < 0) {
return NULL;
}

#ifndef LIMITED_API_AVAILABLE
PyModule_AddObjectRef(m, "LIMITED_API_AVAILABLE", Py_False);
Expand Down
Loading