Skip to content

Commit ddbe9bc

Browse files
Optionally identify globals/builtins counts.
1 parent c8181cc commit ddbe9bc

File tree

4 files changed

+188
-53
lines changed

4 files changed

+188
-53
lines changed

Include/internal/pycore_code.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,12 @@ typedef struct {
592592
int numfree; // nonlocal
593593
struct co_unbound_counts {
594594
int total;
595-
int numglobal;
595+
struct {
596+
int total;
597+
int numglobal;
598+
int numbuiltin;
599+
int numunknown;
600+
} globals;
596601
int numattrs;
597602
int numunknown;
598603
} unbound;
@@ -604,8 +609,10 @@ PyAPI_FUNC(void) _PyCode_GetVarCounts(
604609
PyAPI_FUNC(int) _PyCode_SetUnboundVarCounts(
605610
PyCodeObject *,
606611
_PyCode_var_counts_t *,
607-
PyObject *global,
608-
PyObject *attrs);
612+
PyObject *globalnames,
613+
PyObject *attrnames,
614+
PyObject *globalsns,
615+
PyObject *builtinsns);
609616

610617
PyAPI_FUNC(int) _PyCode_ReturnsOnlyNone(PyCodeObject *);
611618

Lib/test/test_code.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,36 @@ def new_var_counts(*,
796796
):
797797
nargvars = posonly + posorkw + kwonly + varargs + varkwargs
798798
nlocals = nargvars + purelocals + othercells
799-
unbound = globalvars + attrs + unknown
799+
if isinstance(globalvars, int):
800+
globalvars = {
801+
'total': globalvars,
802+
'numglobal': 0,
803+
'numbuiltin': 0,
804+
'numunknown': globalvars,
805+
}
806+
else:
807+
g_numunknown = 0
808+
if isinstance(globalvars, dict):
809+
numglobal = globalvars['numglobal']
810+
numbuiltin = globalvars['numbuiltin']
811+
size = 2
812+
if 'numunknown' in globalvars:
813+
g_numunknown = globalvars['numunknown']
814+
size += 1
815+
assert len(globalvars) == size, globalvars
816+
else:
817+
assert not isinstance(globalvars, str), repr(globalvars)
818+
try:
819+
numglobal, numbuiltin = globalvars
820+
except ValueError:
821+
numglobal, numbuiltin, g_numunknown = globalvars
822+
globalvars = {
823+
'total': numglobal + numbuiltin + g_numunknown,
824+
'numglobal': numglobal,
825+
'numbuiltin': numbuiltin,
826+
'numunknown': g_numunknown,
827+
}
828+
unbound = globalvars['total'] + attrs + unknown
800829
return {
801830
'total': nlocals + freevars + unbound,
802831
'locals': {
@@ -824,7 +853,7 @@ def new_var_counts(*,
824853
'numfree': freevars,
825854
'unbound': {
826855
'total': unbound,
827-
'numglobal': globalvars,
856+
'globals': globalvars,
828857
'numattrs': attrs,
829858
'numunknown': unknown,
830859
},
@@ -929,6 +958,55 @@ def new_var_counts(*,
929958
counts = _testinternalcapi.get_code_var_counts(func.__code__)
930959
self.assertEqual(counts, expected)
931960

961+
def func_with_globals_and_builtins():
962+
mod1 = _testinternalcapi
963+
mod2 = dis
964+
mods = (mod1, mod2)
965+
checks = tuple(callable(m) for m in mods)
966+
return callable(mod2), tuple(mods), list(mods), checks
967+
968+
func = func_with_globals_and_builtins
969+
with self.subTest(f'{func} code'):
970+
expected = new_var_counts(
971+
purelocals=4,
972+
globalvars=5,
973+
)
974+
counts = _testinternalcapi.get_code_var_counts(func.__code__)
975+
self.assertEqual(counts, expected)
976+
977+
with self.subTest(f'{func} with own globals and builtins'):
978+
expected = new_var_counts(
979+
purelocals=4,
980+
globalvars=(2, 3),
981+
)
982+
counts = _testinternalcapi.get_code_var_counts(func)
983+
self.assertEqual(counts, expected)
984+
985+
with self.subTest(f'{func} without globals'):
986+
expected = new_var_counts(
987+
purelocals=4,
988+
globalvars=(0, 3, 2),
989+
)
990+
counts = _testinternalcapi.get_code_var_counts(func, globalsns={})
991+
self.assertEqual(counts, expected)
992+
993+
with self.subTest(f'{func} without both'):
994+
expected = new_var_counts(
995+
purelocals=4,
996+
globalvars=5,
997+
)
998+
counts = _testinternalcapi.get_code_var_counts(func, globalsns={},
999+
builtinsns={})
1000+
self.assertEqual(counts, expected)
1001+
1002+
with self.subTest(f'{func} without builtins'):
1003+
expected = new_var_counts(
1004+
purelocals=4,
1005+
globalvars=(2, 0, 3),
1006+
)
1007+
counts = _testinternalcapi.get_code_var_counts(func, builtinsns={})
1008+
self.assertEqual(counts, expected)
1009+
9321010

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

Modules/_testinternalcapi.c

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,19 +1002,41 @@ get_co_localskinds(PyObject *self, PyObject *arg)
10021002
static PyObject *
10031003
get_code_var_counts(PyObject *self, PyObject *_args, PyObject *_kwargs)
10041004
{
1005-
const char *codearg;
1006-
static char *kwlist[] = {"code", NULL};
1005+
PyObject *codearg;
1006+
PyObject *globalnames = NULL;
1007+
PyObject *attrnames = NULL;
1008+
PyObject *globalsns = NULL;
1009+
PyObject *builtinsns = NULL;
1010+
static char *kwlist[] = {"code", "globalnames", "attrnames", "globalsns",
1011+
"builtinsns", NULL};
10071012
if (!PyArg_ParseTupleAndKeywords(_args, _kwargs,
1008-
"O!:get_code_var_counts", kwlist,
1009-
&PyCode_Type, &codearg))
1013+
"O|OOO!O!:get_code_var_counts", kwlist,
1014+
&codearg, &globalnames, &attrnames,
1015+
&PyDict_Type, &globalsns, &PyDict_Type, &builtinsns))
10101016
{
10111017
return NULL;
10121018
}
1019+
if (PyFunction_Check(codearg)) {
1020+
if (globalsns == NULL) {
1021+
globalsns = PyFunction_GET_GLOBALS(codearg);
1022+
}
1023+
if (builtinsns == NULL) {
1024+
builtinsns = PyFunction_GET_BUILTINS(codearg);
1025+
}
1026+
codearg = PyFunction_GET_CODE(codearg);
1027+
}
1028+
else if (!PyCode_Check(codearg)) {
1029+
PyErr_SetString(PyExc_TypeError,
1030+
"argument must be a code object or a function");
1031+
return NULL;
1032+
}
10131033
PyCodeObject *code = (PyCodeObject *)codearg;
10141034

10151035
_PyCode_var_counts_t counts = {0};
10161036
_PyCode_GetVarCounts(code, &counts);
1017-
if (_PyCode_SetUnboundVarCounts(code, &counts, NULL, NULL) < 0) {
1037+
if (_PyCode_SetUnboundVarCounts(
1038+
code, &counts, globalnames, attrnames, globalsns, builtinsns) < 0)
1039+
{
10181040
return NULL;
10191041
}
10201042

@@ -1033,6 +1055,7 @@ get_code_var_counts(PyObject *self, PyObject *_args, PyObject *_kwargs)
10331055
PyObject *cells = NULL;
10341056
PyObject *hidden = NULL;
10351057
PyObject *unbound = NULL;
1058+
PyObject *globals = NULL;
10361059
PyObject *countsobj = PyDict_New();
10371060
if (countsobj == NULL) {
10381061
return NULL;
@@ -1103,17 +1126,30 @@ get_code_var_counts(PyObject *self, PyObject *_args, PyObject *_kwargs)
11031126
goto error;
11041127
}
11051128
SET_COUNT(unbound, counts.unbound, total);
1106-
SET_COUNT(unbound, counts.unbound, numglobal);
11071129
SET_COUNT(unbound, counts.unbound, numattrs);
11081130
SET_COUNT(unbound, counts.unbound, numunknown);
11091131

1132+
// unbound.globals
1133+
globals = PyDict_New();
1134+
if (globals == NULL) {
1135+
goto error;
1136+
}
1137+
if (PyDict_SetItemString(unbound, "globals", globals) < 0) {
1138+
goto error;
1139+
}
1140+
SET_COUNT(globals, counts.unbound.globals, total);
1141+
SET_COUNT(globals, counts.unbound.globals, numglobal);
1142+
SET_COUNT(globals, counts.unbound.globals, numbuiltin);
1143+
SET_COUNT(globals, counts.unbound.globals, numunknown);
1144+
11101145
#undef SET_COUNT
11111146

11121147
Py_DECREF(locals);
11131148
Py_DECREF(args);
11141149
Py_DECREF(cells);
11151150
Py_DECREF(hidden);
11161151
Py_DECREF(unbound);
1152+
Py_DECREF(globals);
11171153
return countsobj;
11181154

11191155
error:
@@ -1123,6 +1159,7 @@ get_code_var_counts(PyObject *self, PyObject *_args, PyObject *_kwargs)
11231159
Py_XDECREF(cells);
11241160
Py_XDECREF(hidden);
11251161
Py_XDECREF(unbound);
1162+
Py_XDECREF(globals);
11261163
return NULL;
11271164
}
11281165

Objects/codeobject.c

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,26 +1692,29 @@ PyCode_GetFreevars(PyCodeObject *code)
16921692

16931693
static int
16941694
identify_unbound_names(PyCodeObject *co,
1695-
PyObject *global, PyObject *attrs,
1695+
PyObject *globalnames, PyObject *attrnames,
1696+
PyObject *globalsns, PyObject *builtinsns,
16961697
struct co_unbound_counts *counts)
16971698
{
16981699
// This function is inspired by inspect.getclosurevars().
16991700
// It would be nicer if we had something similar to co_localspluskinds,
17001701
// but for co_names.
1701-
assert(global != NULL);
1702-
assert(PySet_Check(global));
1703-
assert(PySet_GET_SIZE(global) == 0 || counts != NULL);
1704-
assert(attrs != NULL);
1705-
assert(PySet_Check(attrs));
1706-
assert(PySet_GET_SIZE(attrs) == 0 || counts != NULL);
1702+
assert(globalnames != NULL);
1703+
assert(PySet_Check(globalnames));
1704+
assert(PySet_GET_SIZE(globalnames) == 0 || counts != NULL);
1705+
assert(attrnames != NULL);
1706+
assert(PySet_Check(attrnames));
1707+
assert(PySet_GET_SIZE(attrnames) == 0 || counts != NULL);
1708+
assert(globalsns == NULL || PyDict_Check(globalsns));
1709+
assert(builtinsns == NULL || PyDict_Check(builtinsns));
17071710
assert(counts == NULL || counts->total == 0);
17081711
Py_ssize_t len = Py_SIZE(co);
17091712
for (int i = 0; i < len; i++) {
17101713
_Py_CODEUNIT inst = _Py_GetBaseCodeUnit(co, i);
17111714
if (inst.op.code == LOAD_ATTR) {
17121715
PyObject *name = PyTuple_GET_ITEM(co->co_names, inst.op.arg>>1);
17131716
if (counts != NULL) {
1714-
if (PySet_Contains(attrs, name)) {
1717+
if (PySet_Contains(attrnames, name)) {
17151718
if (PyErr_Occurred()) {
17161719
return -1;
17171720
}
@@ -1720,23 +1723,38 @@ identify_unbound_names(PyCodeObject *co,
17201723
counts->total += 1;
17211724
counts->numattrs += 1;
17221725
}
1723-
if (PySet_Add(attrs, name) < 0) {
1726+
if (PySet_Add(attrnames, name) < 0) {
17241727
return -1;
17251728
}
17261729
}
17271730
else if (inst.op.code == LOAD_GLOBAL) {
17281731
PyObject *name = PyTuple_GET_ITEM(co->co_names, inst.op.arg>>1);
17291732
if (counts != NULL) {
1730-
if (PySet_Contains(global, name)) {
1733+
if (PySet_Contains(globalnames, name)) {
17311734
if (PyErr_Occurred()) {
17321735
return -1;
17331736
}
17341737
continue;
17351738
}
17361739
counts->total += 1;
1737-
counts->numglobal += 1;
1740+
counts->globals.total += 1;
1741+
counts->globals.numunknown += 1;
1742+
if (globalsns != NULL && PyDict_Contains(globalsns, name)) {
1743+
if (PyErr_Occurred()) {
1744+
return -1;
1745+
}
1746+
counts->globals.numglobal += 1;
1747+
counts->globals.numunknown -= 1;
1748+
}
1749+
if (builtinsns != NULL && PyDict_Contains(builtinsns, name)) {
1750+
if (PyErr_Occurred()) {
1751+
return -1;
1752+
}
1753+
counts->globals.numbuiltin += 1;
1754+
counts->globals.numunknown -= 1;
1755+
}
17381756
}
1739-
if (PySet_Add(global, name) < 0) {
1757+
if (PySet_Add(globalnames, name) < 0) {
17401758
return -1;
17411759
}
17421760
}
@@ -1849,46 +1867,41 @@ _PyCode_GetVarCounts(PyCodeObject *co, _PyCode_var_counts_t *counts)
18491867

18501868
int
18511869
_PyCode_SetUnboundVarCounts(PyCodeObject *co, _PyCode_var_counts_t *counts,
1852-
PyObject *globalarg, PyObject *attrsarg)
1870+
PyObject *globalnames, PyObject *attrnames,
1871+
PyObject *globalsns, PyObject *builtinsns)
18531872
{
18541873
int res = -1;
1855-
PyObject *global = NULL;
1856-
PyObject *attrs = NULL;
1857-
PyObject *global_owned = NULL;
1858-
PyObject *attrs_owned = NULL;
1859-
if (globalarg != NULL) {
1860-
if (!PySet_Check(globalarg)) {
1861-
PyErr_Format(PyExc_TypeError,
1862-
"expected a set for \"global\", got %R", global);
1874+
PyObject *globalnames_owned = NULL;
1875+
PyObject *attrnames_owned = NULL;
1876+
if (globalnames == NULL) {
1877+
globalnames_owned = PySet_New(NULL);
1878+
if (globalnames_owned == NULL) {
18631879
goto finally;
18641880
}
1865-
global = globalarg;
1881+
globalnames = globalnames_owned;
18661882
}
1867-
else {
1868-
global_owned = PySet_New(NULL);
1869-
if (global_owned == NULL) {
1870-
goto finally;
1871-
}
1872-
global = global_owned;
1883+
else if (!PySet_Check(globalnames)) {
1884+
PyErr_Format(PyExc_TypeError,
1885+
"expected a set for \"globalnames\", got %R", globalnames);
1886+
goto finally;
18731887
}
1874-
if (attrsarg != NULL) {
1875-
if (!PySet_Check(attrsarg)) {
1876-
PyErr_Format(PyExc_TypeError,
1877-
"expected a set for \"attrs\", got %R", attrs);
1888+
if (attrnames == NULL) {
1889+
attrnames_owned = PySet_New(NULL);
1890+
if (attrnames_owned == NULL) {
18781891
goto finally;
18791892
}
1880-
attrs = attrsarg;
1893+
attrnames = attrnames_owned;
18811894
}
1882-
else {
1883-
attrs_owned = PySet_New(NULL);
1884-
if (attrs_owned == NULL) {
1885-
goto finally;
1886-
}
1887-
attrs = attrs_owned;
1895+
else if (!PySet_Check(attrnames)) {
1896+
PyErr_Format(PyExc_TypeError,
1897+
"expected a set for \"attrnames\", got %R", attrnames);
1898+
goto finally;
18881899
}
18891900

18901901
struct co_unbound_counts unbound = {0};
1891-
if (identify_unbound_names(co, global, attrs, &unbound) < 0) {
1902+
if (identify_unbound_names(
1903+
co, globalnames, attrnames, globalsns, builtinsns, &unbound) < 0)
1904+
{
18921905
goto finally;
18931906
}
18941907
assert(unbound.numunknown == 0);
@@ -1900,8 +1913,8 @@ _PyCode_SetUnboundVarCounts(PyCodeObject *co, _PyCode_var_counts_t *counts,
19001913
res = 0;
19011914

19021915
finally:
1903-
Py_XDECREF(global_owned);
1904-
Py_XDECREF(attrs_owned);
1916+
Py_XDECREF(globalnames_owned);
1917+
Py_XDECREF(attrnames_owned);
19051918
return res;
19061919
}
19071920

0 commit comments

Comments
 (0)