Skip to content

Commit f2373f4

Browse files
authored
Merge branch 'main' into fix-132413
2 parents 539cfed + 27128e4 commit f2373f4

File tree

8 files changed

+358
-37
lines changed

8 files changed

+358
-37
lines changed

Include/internal/pycore_code.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,47 @@ PyAPI_FUNC(int) _PyCode_SetUnboundVarCounts(
614614
PyObject *globalsns,
615615
PyObject *builtinsns);
616616

617+
618+
/* "Stateless" code is a function or code object which does not rely on
619+
* external state or internal state. It may rely on arguments and
620+
* builtins, but not globals or a closure. Thus it does not rely
621+
* on __globals__ or __closure__, and a stateless function
622+
* is equivalent to its code object.
623+
*
624+
* Stateless code also does not keep any persistent state
625+
* of its own, so it can't have any executors, monitoring,
626+
* instrumentation, or "extras" (i.e. co_extra).
627+
*
628+
* Stateless code may create nested functions, including closures.
629+
* However, nested functions must themselves be stateless, except they
630+
* *can* close on the enclosing locals.
631+
*
632+
* Stateless code may return any value, including nested functions and closures.
633+
*
634+
* Stateless code that takes no arguments and doesn't return anything
635+
* may be treated like a script.
636+
*
637+
* We consider stateless code to be "portable" if it does not return
638+
* any object that holds a reference to any of the code's locals. Thus
639+
* generators and coroutines are not portable. Likewise a function
640+
* that returns a closure is not portable. The concept of
641+
* portability is useful in cases where the code is run
642+
* in a different execution context than where
643+
* the return value will be used. */
644+
645+
PyAPI_FUNC(int) _PyCode_CheckNoInternalState(PyCodeObject *, const char **);
646+
PyAPI_FUNC(int) _PyCode_CheckNoExternalState(
647+
PyCodeObject *,
648+
_PyCode_var_counts_t *,
649+
const char **);
650+
PyAPI_FUNC(int) _PyCode_VerifyStateless(
651+
PyThreadState *,
652+
PyCodeObject *,
653+
PyObject *globalnames,
654+
PyObject *globalsns,
655+
PyObject *builtinsns);
656+
657+
PyAPI_FUNC(int) _PyCode_CheckPureFunction(PyCodeObject *, const char **);
617658
PyAPI_FUNC(int) _PyCode_ReturnsOnlyNone(PyCodeObject *);
618659

619660

Include/internal/pycore_function.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ PyFunctionObject *_PyFunction_LookupByVersion(uint32_t version, PyObject **p_cod
3535
extern PyObject *_Py_set_function_type_params(
3636
PyThreadState* unused, PyObject *func, PyObject *type_params);
3737

38+
39+
/* See pycore_code.h for explanation about what "stateless" means. */
40+
41+
PyAPI_FUNC(int)
42+
_PyFunction_VerifyStateless(PyThreadState *, PyObject *);
43+
44+
3845
#ifdef __cplusplus
3946
}
4047
#endif

Include/internal/pycore_opcode_utils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ extern "C" {
5656

5757
#define IS_RETURN_OPCODE(opcode) \
5858
(opcode == RETURN_VALUE)
59+
#define IS_RAISE_OPCODE(opcode) \
60+
(opcode == RAISE_VARARGS || opcode == RERAISE)
5961

6062

6163
/* Flags used in the oparg for MAKE_FUNCTION */

Lib/test/_code_definitions.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,32 @@ def ham_C_closure(z):
178178
*NESTED_FUNCTIONS,
179179
]
180180

181+
STATELESS_FUNCTIONS = [
182+
spam,
183+
spam_minimal,
184+
spam_with_builtins,
185+
spam_args_attrs_and_builtins,
186+
spam_returns_arg,
187+
spam_annotated,
188+
spam_with_inner_not_closure,
189+
spam_with_inner_closure,
190+
spam_N,
191+
spam_C,
192+
spam_NN,
193+
spam_NC,
194+
spam_CN,
195+
spam_CC,
196+
eggs_nested,
197+
eggs_nested_N,
198+
ham_nested,
199+
ham_C_nested
200+
]
201+
STATELESS_CODE = [
202+
*STATELESS_FUNCTIONS,
203+
spam_with_globals_and_builtins,
204+
spam_full,
205+
]
206+
181207

182208
# generators
183209

Lib/test/test_code.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@
220220
import _testinternalcapi
221221
except ModuleNotFoundError:
222222
_testinternalcapi = None
223+
import test._code_definitions as defs
223224

224225
COPY_FREE_VARS = opmap['COPY_FREE_VARS']
225226

@@ -671,7 +672,6 @@ def test_local_kinds(self):
671672
VARARGS = CO_FAST_LOCAL | CO_FAST_ARG_VAR | CO_FAST_ARG_POS
672673
VARKWARGS = CO_FAST_LOCAL | CO_FAST_ARG_VAR | CO_FAST_ARG_KW
673674

674-
import test._code_definitions as defs
675675
funcs = {
676676
defs.spam_minimal: {},
677677
defs.spam_with_builtins: {
@@ -897,7 +897,6 @@ def new_var_counts(*,
897897
},
898898
}
899899

900-
import test._code_definitions as defs
901900
funcs = {
902901
defs.spam_minimal: new_var_counts(),
903902
defs.spam_with_builtins: new_var_counts(
@@ -1025,55 +1024,70 @@ def new_var_counts(*,
10251024
counts = _testinternalcapi.get_code_var_counts(func.__code__)
10261025
self.assertEqual(counts, expected)
10271026

1028-
def func_with_globals_and_builtins():
1029-
mod1 = _testinternalcapi
1030-
mod2 = dis
1031-
mods = (mod1, mod2)
1032-
checks = tuple(callable(m) for m in mods)
1033-
return callable(mod2), tuple(mods), list(mods), checks
1034-
1035-
func = func_with_globals_and_builtins
1027+
func = defs.spam_with_globals_and_builtins
10361028
with self.subTest(f'{func} code'):
10371029
expected = new_var_counts(
1038-
purelocals=4,
1039-
globalvars=5,
1030+
purelocals=5,
1031+
globalvars=6,
10401032
)
10411033
counts = _testinternalcapi.get_code_var_counts(func.__code__)
10421034
self.assertEqual(counts, expected)
10431035

10441036
with self.subTest(f'{func} with own globals and builtins'):
10451037
expected = new_var_counts(
1046-
purelocals=4,
1047-
globalvars=(2, 3),
1038+
purelocals=5,
1039+
globalvars=(2, 4),
10481040
)
10491041
counts = _testinternalcapi.get_code_var_counts(func)
10501042
self.assertEqual(counts, expected)
10511043

10521044
with self.subTest(f'{func} without globals'):
10531045
expected = new_var_counts(
1054-
purelocals=4,
1055-
globalvars=(0, 3, 2),
1046+
purelocals=5,
1047+
globalvars=(0, 4, 2),
10561048
)
10571049
counts = _testinternalcapi.get_code_var_counts(func, globalsns={})
10581050
self.assertEqual(counts, expected)
10591051

10601052
with self.subTest(f'{func} without both'):
10611053
expected = new_var_counts(
1062-
purelocals=4,
1063-
globalvars=5,
1054+
purelocals=5,
1055+
globalvars=6,
10641056
)
10651057
counts = _testinternalcapi.get_code_var_counts(func, globalsns={},
10661058
builtinsns={})
10671059
self.assertEqual(counts, expected)
10681060

10691061
with self.subTest(f'{func} without builtins'):
10701062
expected = new_var_counts(
1071-
purelocals=4,
1072-
globalvars=(2, 0, 3),
1063+
purelocals=5,
1064+
globalvars=(2, 0, 4),
10731065
)
10741066
counts = _testinternalcapi.get_code_var_counts(func, builtinsns={})
10751067
self.assertEqual(counts, expected)
10761068

1069+
@unittest.skipIf(_testinternalcapi is None, "missing _testinternalcapi")
1070+
def test_stateless(self):
1071+
self.maxDiff = None
1072+
1073+
for func in defs.STATELESS_CODE:
1074+
with self.subTest((func, '(code)')):
1075+
_testinternalcapi.verify_stateless_code(func.__code__)
1076+
for func in defs.STATELESS_FUNCTIONS:
1077+
with self.subTest((func, '(func)')):
1078+
_testinternalcapi.verify_stateless_code(func)
1079+
1080+
for func in defs.FUNCTIONS:
1081+
if func not in defs.STATELESS_CODE:
1082+
with self.subTest((func, '(code)')):
1083+
with self.assertRaises(Exception):
1084+
_testinternalcapi.verify_stateless_code(func.__code__)
1085+
1086+
if func not in defs.STATELESS_FUNCTIONS:
1087+
with self.subTest((func, '(func)')):
1088+
with self.assertRaises(Exception):
1089+
_testinternalcapi.verify_stateless_code(func)
1090+
10771091

10781092
def isinterned(s):
10791093
return s is sys.intern(('_' + s + '_')[1:-1])

Modules/_testinternalcapi.c

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,6 +1165,47 @@ get_code_var_counts(PyObject *self, PyObject *_args, PyObject *_kwargs)
11651165
return NULL;
11661166
}
11671167

1168+
static PyObject *
1169+
verify_stateless_code(PyObject *self, PyObject *args, PyObject *kwargs)
1170+
{
1171+
PyThreadState *tstate = _PyThreadState_GET();
1172+
PyObject *codearg;
1173+
PyObject *globalnames = NULL;
1174+
PyObject *globalsns = NULL;
1175+
PyObject *builtinsns = NULL;
1176+
static char *kwlist[] = {"code", "globalnames",
1177+
"globalsns", "builtinsns", NULL};
1178+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
1179+
"O|O!O!O!:get_code_var_counts", kwlist,
1180+
&codearg, &PySet_Type, &globalnames,
1181+
&PyDict_Type, &globalsns, &PyDict_Type, &builtinsns))
1182+
{
1183+
return NULL;
1184+
}
1185+
if (PyFunction_Check(codearg)) {
1186+
if (globalsns == NULL) {
1187+
globalsns = PyFunction_GET_GLOBALS(codearg);
1188+
}
1189+
if (builtinsns == NULL) {
1190+
builtinsns = PyFunction_GET_BUILTINS(codearg);
1191+
}
1192+
codearg = PyFunction_GET_CODE(codearg);
1193+
}
1194+
else if (!PyCode_Check(codearg)) {
1195+
PyErr_SetString(PyExc_TypeError,
1196+
"argument must be a code object or a function");
1197+
return NULL;
1198+
}
1199+
PyCodeObject *code = (PyCodeObject *)codearg;
1200+
1201+
if (_PyCode_VerifyStateless(
1202+
tstate, code, globalnames, globalsns, builtinsns) < 0)
1203+
{
1204+
return NULL;
1205+
}
1206+
Py_RETURN_NONE;
1207+
}
1208+
11681209
#ifdef _Py_TIER2
11691210

11701211
static PyObject *
@@ -2292,6 +2333,8 @@ static PyMethodDef module_functions[] = {
22922333
{"get_co_localskinds", get_co_localskinds, METH_O, NULL},
22932334
{"get_code_var_counts", _PyCFunction_CAST(get_code_var_counts),
22942335
METH_VARARGS | METH_KEYWORDS, NULL},
2336+
{"verify_stateless_code", _PyCFunction_CAST(verify_stateless_code),
2337+
METH_VARARGS | METH_KEYWORDS, NULL},
22952338
#ifdef _Py_TIER2
22962339
{"add_executor_dependency", add_executor_dependency, METH_VARARGS, NULL},
22972340
{"invalidate_executors", invalidate_executors, METH_O, NULL},

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy