Skip to content

Commit c81fa2b

Browse files
gh-132775: Add _PyCode_GetScriptXIData() (gh-133480)
This converts functions, code, str, bytes, bytearray, and memoryview objects to PyCodeObject, and ensure that the object looks like a script. That means no args, no return, and no closure. _PyCode_GetPureScriptXIData() takes it a step further and ensures there are no globals. We also add _PyObject_SupportedAsScript() to the internal C-API.
1 parent f0f93ba commit c81fa2b

File tree

8 files changed

+366
-0
lines changed

8 files changed

+366
-0
lines changed

Include/internal/pycore_crossinterp.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ PyAPI_FUNC(int) _PyCode_GetXIData(
191191
PyThreadState *,
192192
PyObject *,
193193
_PyXIData_t *);
194+
PyAPI_FUNC(int) _PyCode_GetScriptXIData(
195+
PyThreadState *,
196+
PyObject *,
197+
_PyXIData_t *);
198+
PyAPI_FUNC(int) _PyCode_GetPureScriptXIData(
199+
PyThreadState *,
200+
PyObject *,
201+
_PyXIData_t *);
194202

195203

196204
/* using cross-interpreter data */

Include/internal/pycore_pythonrun.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ extern int _PyRun_InteractiveLoopObject(
2525
PyObject *filename,
2626
PyCompilerFlags *flags);
2727

28+
extern int _PyObject_SupportedAsScript(PyObject *);
2829
extern const char* _Py_SourceAsString(
2930
PyObject *cmd,
3031
const char *funcname,

Lib/test/_code_definitions.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
11

2+
def simple_script():
3+
assert True
4+
5+
6+
def complex_script():
7+
obj = 'a string'
8+
pickle = __import__('pickle')
9+
def spam_minimal():
10+
pass
11+
spam_minimal()
12+
data = pickle.dumps(obj)
13+
res = pickle.loads(data)
14+
assert res == obj, (res, obj)
15+
16+
17+
def script_with_globals():
18+
obj1, obj2 = spam(42)
19+
assert obj1 == 42
20+
assert obj2 is None
21+
22+
23+
def script_with_explicit_empty_return():
24+
return None
25+
26+
27+
def script_with_return():
28+
return True
29+
230

331
def spam_minimal():
432
# no arg defaults or kwarg defaults
@@ -141,6 +169,11 @@ def ham_C_closure(z):
141169

142170
TOP_FUNCTIONS = [
143171
# shallow
172+
simple_script,
173+
complex_script,
174+
script_with_globals,
175+
script_with_explicit_empty_return,
176+
script_with_return,
144177
spam_minimal,
145178
spam_with_builtins,
146179
spam_with_globals_and_builtins,
@@ -179,6 +212,10 @@ def ham_C_closure(z):
179212
]
180213

181214
STATELESS_FUNCTIONS = [
215+
simple_script,
216+
complex_script,
217+
script_with_explicit_empty_return,
218+
script_with_return,
182219
spam,
183220
spam_minimal,
184221
spam_with_builtins,
@@ -200,10 +237,26 @@ def ham_C_closure(z):
200237
]
201238
STATELESS_CODE = [
202239
*STATELESS_FUNCTIONS,
240+
script_with_globals,
203241
spam_with_globals_and_builtins,
204242
spam_full,
205243
]
206244

245+
PURE_SCRIPT_FUNCTIONS = [
246+
simple_script,
247+
complex_script,
248+
script_with_explicit_empty_return,
249+
spam_minimal,
250+
spam_with_builtins,
251+
spam_with_inner_not_closure,
252+
spam_with_inner_closure,
253+
]
254+
SCRIPT_FUNCTIONS = [
255+
*PURE_SCRIPT_FUNCTIONS,
256+
script_with_globals,
257+
spam_with_globals_and_builtins,
258+
]
259+
207260

208261
# generators
209262

Lib/test/test_code.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,20 @@ def test_local_kinds(self):
673673
VARKWARGS = CO_FAST_LOCAL | CO_FAST_ARG_VAR | CO_FAST_ARG_KW
674674

675675
funcs = {
676+
defs.simple_script: {},
677+
defs.complex_script: {
678+
'obj': CO_FAST_LOCAL,
679+
'pickle': CO_FAST_LOCAL,
680+
'spam_minimal': CO_FAST_LOCAL,
681+
'data': CO_FAST_LOCAL,
682+
'res': CO_FAST_LOCAL,
683+
},
684+
defs.script_with_globals: {
685+
'obj1': CO_FAST_LOCAL,
686+
'obj2': CO_FAST_LOCAL,
687+
},
688+
defs.script_with_explicit_empty_return: {},
689+
defs.script_with_return: {},
676690
defs.spam_minimal: {},
677691
defs.spam_with_builtins: {
678692
'x': CO_FAST_LOCAL,
@@ -898,6 +912,19 @@ def new_var_counts(*,
898912
}
899913

900914
funcs = {
915+
defs.simple_script: new_var_counts(),
916+
defs.complex_script: new_var_counts(
917+
purelocals=5,
918+
globalvars=1,
919+
attrs=2,
920+
),
921+
defs.script_with_globals: new_var_counts(
922+
purelocals=2,
923+
globalvars=1,
924+
),
925+
defs.script_with_explicit_empty_return: new_var_counts(),
926+
defs.script_with_return: new_var_counts(),
927+
defs.spam_minimal: new_var_counts(),
901928
defs.spam_minimal: new_var_counts(),
902929
defs.spam_with_builtins: new_var_counts(
903930
purelocals=4,

Lib/test/test_crossinterp.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,126 @@ def test_other_objects(self):
758758
])
759759

760760

761+
class PureShareableScriptTests(_GetXIDataTests):
762+
763+
MODE = 'script-pure'
764+
765+
VALID_SCRIPTS = [
766+
'',
767+
'spam',
768+
'# a comment',
769+
'print("spam")',
770+
'raise Exception("spam")',
771+
"""if True:
772+
do_something()
773+
""",
774+
"""if True:
775+
def spam(x):
776+
return x
777+
class Spam:
778+
def eggs(self):
779+
return 42
780+
x = Spam().eggs()
781+
raise ValueError(spam(x))
782+
""",
783+
]
784+
INVALID_SCRIPTS = [
785+
' pass', # IndentationError
786+
'----', # SyntaxError
787+
"""if True:
788+
def spam():
789+
# no body
790+
spam()
791+
""", # IndentationError
792+
]
793+
794+
def test_valid_str(self):
795+
self.assert_roundtrip_not_equal([
796+
*self.VALID_SCRIPTS,
797+
], expecttype=types.CodeType)
798+
799+
def test_invalid_str(self):
800+
self.assert_not_shareable([
801+
*self.INVALID_SCRIPTS,
802+
])
803+
804+
def test_valid_bytes(self):
805+
self.assert_roundtrip_not_equal([
806+
*(s.encode('utf8') for s in self.VALID_SCRIPTS),
807+
], expecttype=types.CodeType)
808+
809+
def test_invalid_bytes(self):
810+
self.assert_not_shareable([
811+
*(s.encode('utf8') for s in self.INVALID_SCRIPTS),
812+
])
813+
814+
def test_pure_script_code(self):
815+
self.assert_roundtrip_equal_not_identical([
816+
*(f.__code__ for f in defs.PURE_SCRIPT_FUNCTIONS),
817+
])
818+
819+
def test_impure_script_code(self):
820+
self.assert_not_shareable([
821+
*(f.__code__ for f in defs.SCRIPT_FUNCTIONS
822+
if f not in defs.PURE_SCRIPT_FUNCTIONS),
823+
])
824+
825+
def test_other_code(self):
826+
self.assert_not_shareable([
827+
*(f.__code__ for f in defs.FUNCTIONS
828+
if f not in defs.SCRIPT_FUNCTIONS),
829+
*(f.__code__ for f in defs.FUNCTION_LIKE),
830+
])
831+
832+
def test_pure_script_function(self):
833+
self.assert_roundtrip_not_equal([
834+
*defs.PURE_SCRIPT_FUNCTIONS,
835+
], expecttype=types.CodeType)
836+
837+
def test_impure_script_function(self):
838+
self.assert_not_shareable([
839+
*(f for f in defs.SCRIPT_FUNCTIONS
840+
if f not in defs.PURE_SCRIPT_FUNCTIONS),
841+
])
842+
843+
def test_other_function(self):
844+
self.assert_not_shareable([
845+
*(f for f in defs.FUNCTIONS
846+
if f not in defs.SCRIPT_FUNCTIONS),
847+
*defs.FUNCTION_LIKE,
848+
])
849+
850+
def test_other_objects(self):
851+
self.assert_not_shareable([
852+
None,
853+
True,
854+
False,
855+
Ellipsis,
856+
NotImplemented,
857+
(),
858+
[],
859+
{},
860+
object(),
861+
])
862+
863+
864+
class ShareableScriptTests(PureShareableScriptTests):
865+
866+
MODE = 'script'
867+
868+
def test_impure_script_code(self):
869+
self.assert_roundtrip_equal_not_identical([
870+
*(f.__code__ for f in defs.SCRIPT_FUNCTIONS
871+
if f not in defs.PURE_SCRIPT_FUNCTIONS),
872+
])
873+
874+
def test_impure_script_function(self):
875+
self.assert_roundtrip_not_equal([
876+
*(f for f in defs.SCRIPT_FUNCTIONS
877+
if f not in defs.PURE_SCRIPT_FUNCTIONS),
878+
], expecttype=types.CodeType)
879+
880+
761881
class ShareableTypeTests(_GetXIDataTests):
762882

763883
MODE = 'xidata'

Modules/_testinternalcapi.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1989,6 +1989,16 @@ get_crossinterp_data(PyObject *self, PyObject *args, PyObject *kwargs)
19891989
goto error;
19901990
}
19911991
}
1992+
else if (strcmp(mode, "script") == 0) {
1993+
if (_PyCode_GetScriptXIData(tstate, obj, xidata) != 0) {
1994+
goto error;
1995+
}
1996+
}
1997+
else if (strcmp(mode, "script-pure") == 0) {
1998+
if (_PyCode_GetPureScriptXIData(tstate, obj, xidata) != 0) {
1999+
goto error;
2000+
}
2001+
}
19922002
else {
19932003
PyErr_Format(PyExc_ValueError, "unsupported mode %R", modeobj);
19942004
goto error;

0 commit comments

Comments
 (0)