Skip to content

Commit 79be757

Browse files
authored
gh-115775: Compiler adds __static_attributes__ field to classes (#115913)
1 parent 70969d5 commit 79be757

13 files changed

+136
-11
lines changed

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ struct _Py_global_strings {
213213
STRUCT_FOR_ID(__slotnames__)
214214
STRUCT_FOR_ID(__slots__)
215215
STRUCT_FOR_ID(__spec__)
216+
STRUCT_FOR_ID(__static_attributes__)
216217
STRUCT_FOR_ID(__str__)
217218
STRUCT_FOR_ID(__sub__)
218219
STRUCT_FOR_ID(__subclasscheck__)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/enum.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2018,7 +2018,8 @@ def _test_simple_enum(checked_enum, simple_enum):
20182018
+ list(simple_enum._member_map_.keys())
20192019
)
20202020
for key in set(checked_keys + simple_keys):
2021-
if key in ('__module__', '_member_map_', '_value2member_map_', '__doc__'):
2021+
if key in ('__module__', '_member_map_', '_value2member_map_', '__doc__',
2022+
'__static_attributes__'):
20222023
# keys known to be different, or very long
20232024
continue
20242025
elif key in member_names:

Lib/pydoc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,8 @@ def visiblename(name, all=None, obj=None):
313313
if name in {'__author__', '__builtins__', '__cached__', '__credits__',
314314
'__date__', '__doc__', '__file__', '__spec__',
315315
'__loader__', '__module__', '__name__', '__package__',
316-
'__path__', '__qualname__', '__slots__', '__version__'}:
316+
'__path__', '__qualname__', '__slots__', '__version__',
317+
'__static_attributes__'}:
317318
return 0
318319
# Private names are hidden, but special names are displayed.
319320
if name.startswith('__') and name.endswith('__'): return 1

Lib/test/test_compile.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1960,6 +1960,64 @@ def test_load_super_attr(self):
19601960
)
19611961

19621962

1963+
class TestExpectedAttributes(unittest.TestCase):
1964+
1965+
def test_basic(self):
1966+
class C:
1967+
def f(self):
1968+
self.a = self.b = 42
1969+
1970+
self.assertIsInstance(C.__static_attributes__, tuple)
1971+
self.assertEqual(sorted(C.__static_attributes__), ['a', 'b'])
1972+
1973+
def test_nested_function(self):
1974+
class C:
1975+
def f(self):
1976+
self.x = 1
1977+
self.y = 2
1978+
self.x = 3 # check deduplication
1979+
1980+
def g(self, obj):
1981+
self.y = 4
1982+
self.z = 5
1983+
1984+
def h(self, a):
1985+
self.u = 6
1986+
self.v = 7
1987+
1988+
obj.self = 8
1989+
1990+
self.assertEqual(sorted(C.__static_attributes__), ['u', 'v', 'x', 'y', 'z'])
1991+
1992+
def test_nested_class(self):
1993+
class C:
1994+
def f(self):
1995+
self.x = 42
1996+
self.y = 42
1997+
1998+
class D:
1999+
def g(self):
2000+
self.y = 42
2001+
self.z = 42
2002+
2003+
self.assertEqual(sorted(C.__static_attributes__), ['x', 'y'])
2004+
self.assertEqual(sorted(C.D.__static_attributes__), ['y', 'z'])
2005+
2006+
def test_subclass(self):
2007+
class C:
2008+
def f(self):
2009+
self.x = 42
2010+
self.y = 42
2011+
2012+
class D(C):
2013+
def g(self):
2014+
self.y = 42
2015+
self.z = 42
2016+
2017+
self.assertEqual(sorted(C.__static_attributes__), ['x', 'y'])
2018+
self.assertEqual(sorted(D.__static_attributes__), ['y', 'z'])
2019+
2020+
19632021
class TestExpressionStackSize(unittest.TestCase):
19642022
# These tests check that the computed stack size for a code object
19652023
# stays within reasonable bounds (see issue #21523 for an example

Lib/test/test_descr.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5080,7 +5080,8 @@ def test_iter_keys(self):
50805080
keys = list(it)
50815081
keys.sort()
50825082
self.assertEqual(keys, ['__dict__', '__doc__', '__module__',
5083-
'__weakref__', 'meth'])
5083+
'__static_attributes__', '__weakref__',
5084+
'meth'])
50845085

50855086
@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
50865087
'trace function introduces __local__')
@@ -5089,7 +5090,7 @@ def test_iter_values(self):
50895090
it = self.C.__dict__.values()
50905091
self.assertNotIsInstance(it, list)
50915092
values = list(it)
5092-
self.assertEqual(len(values), 5)
5093+
self.assertEqual(len(values), 6)
50935094

50945095
@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
50955096
'trace function introduces __local__')
@@ -5100,7 +5101,8 @@ def test_iter_items(self):
51005101
keys = [item[0] for item in it]
51015102
keys.sort()
51025103
self.assertEqual(keys, ['__dict__', '__doc__', '__module__',
5103-
'__weakref__', 'meth'])
5104+
'__static_attributes__', '__weakref__',
5105+
'meth'])
51045106

51055107
def test_dict_type_with_metaclass(self):
51065108
# Testing type of __dict__ when metaclass set...

Lib/test/test_io.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1160,7 +1160,7 @@ class APIMismatchTest(unittest.TestCase):
11601160
def test_RawIOBase_io_in_pyio_match(self):
11611161
"""Test that pyio RawIOBase class has all c RawIOBase methods"""
11621162
mismatch = support.detect_api_mismatch(pyio.RawIOBase, io.RawIOBase,
1163-
ignore=('__weakref__',))
1163+
ignore=('__weakref__', '__static_attributes__'))
11641164
self.assertEqual(mismatch, set(), msg='Python RawIOBase does not have all C RawIOBase methods')
11651165

11661166
def test_RawIOBase_pyio_in_io_match(self):

Lib/test/test_metaclass.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@
167167
d['foo'] = 4
168168
d['foo'] = 42
169169
d['bar'] = 123
170+
d['__static_attributes__'] = ()
170171
>>>
171172
172173
Use a metaclass that doesn't derive from type.
@@ -182,12 +183,12 @@
182183
... b = 24
183184
...
184185
meta: C ()
185-
ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('a', 42), ('b', 24)]
186+
ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
186187
kw: []
187188
>>> type(C) is dict
188189
True
189190
>>> print(sorted(C.items()))
190-
[('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('a', 42), ('b', 24)]
191+
[('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
191192
>>>
192193
193194
And again, with a __prepare__ attribute.
@@ -208,8 +209,9 @@
208209
d['a'] = 1
209210
d['a'] = 2
210211
d['b'] = 3
212+
d['__static_attributes__'] = ()
211213
meta: C ()
212-
ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('a', 2), ('b', 3)]
214+
ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 2), ('b', 3)]
213215
kw: [('other', 'booh')]
214216
>>>
215217

Lib/typing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1717,7 +1717,7 @@ class _TypingEllipsis:
17171717
'__abstractmethods__', '__annotations__', '__dict__', '__doc__',
17181718
'__init__', '__module__', '__new__', '__slots__',
17191719
'__subclasshook__', '__weakref__', '__class_getitem__',
1720-
'__match_args__',
1720+
'__match_args__', '__static_attributes__',
17211721
})
17221722

17231723
# These special attributes will be not collected as protocol members.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Compiler populates the new ``__static_attributes__`` field on a class with
2+
the names of attributes of this class which are accessed through self.X from
3+
any function in its body.

Python/compile.c

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,8 @@ struct compiler_unit {
358358

359359
int u_scope_type;
360360

361-
PyObject *u_private; /* for private name mangling */
361+
PyObject *u_private; /* for private name mangling */
362+
PyObject *u_static_attributes; /* for class: attributes accessed via self.X */
362363

363364
instr_sequence u_instr_sequence; /* codegen output */
364365

@@ -690,9 +691,26 @@ compiler_unit_free(struct compiler_unit *u)
690691
Py_CLEAR(u->u_metadata.u_cellvars);
691692
Py_CLEAR(u->u_metadata.u_fasthidden);
692693
Py_CLEAR(u->u_private);
694+
Py_CLEAR(u->u_static_attributes);
693695
PyMem_Free(u);
694696
}
695697

698+
static struct compiler_unit *
699+
get_class_compiler_unit(struct compiler *c)
700+
{
701+
Py_ssize_t stack_size = PyList_GET_SIZE(c->c_stack);
702+
for (Py_ssize_t i = stack_size - 1; i >= 0; i--) {
703+
PyObject *capsule = PyList_GET_ITEM(c->c_stack, i);
704+
struct compiler_unit *u = (struct compiler_unit *)PyCapsule_GetPointer(
705+
capsule, CAPSULE_NAME);
706+
assert(u);
707+
if (u->u_scope_type == COMPILER_SCOPE_CLASS) {
708+
return u;
709+
}
710+
}
711+
return NULL;
712+
}
713+
696714
static int
697715
compiler_set_qualname(struct compiler *c)
698716
{
@@ -1336,6 +1354,16 @@ compiler_enter_scope(struct compiler *c, identifier name,
13361354
}
13371355

13381356
u->u_private = NULL;
1357+
if (scope_type == COMPILER_SCOPE_CLASS) {
1358+
u->u_static_attributes = PySet_New(0);
1359+
if (!u->u_static_attributes) {
1360+
compiler_unit_free(u);
1361+
return ERROR;
1362+
}
1363+
}
1364+
else {
1365+
u->u_static_attributes = NULL;
1366+
}
13391367

13401368
/* Push the old compiler_unit on the stack. */
13411369
if (c->u) {
@@ -2517,6 +2545,18 @@ compiler_class_body(struct compiler *c, stmt_ty s, int firstlineno)
25172545
compiler_exit_scope(c);
25182546
return ERROR;
25192547
}
2548+
assert(c->u->u_static_attributes);
2549+
PyObject *static_attributes = PySequence_Tuple(c->u->u_static_attributes);
2550+
if (static_attributes == NULL) {
2551+
compiler_exit_scope(c);
2552+
return ERROR;
2553+
}
2554+
ADDOP_LOAD_CONST(c, NO_LOCATION, static_attributes);
2555+
Py_CLEAR(static_attributes);
2556+
if (compiler_nameop(c, NO_LOCATION, &_Py_ID(__static_attributes__), Store) < 0) {
2557+
compiler_exit_scope(c);
2558+
return ERROR;
2559+
}
25202560
/* The following code is artificial */
25212561
/* Set __classdictcell__ if necessary */
25222562
if (c->u->u_ste->ste_needs_classdict) {
@@ -2657,6 +2697,7 @@ compiler_class(struct compiler *c, stmt_ty s)
26572697
s->v.ClassDef.keywords));
26582698

26592699
PyCodeObject *co = optimize_and_assemble(c, 0);
2700+
26602701
compiler_exit_scope(c);
26612702
if (co == NULL) {
26622703
return ERROR;
@@ -6246,6 +6287,17 @@ compiler_visit_expr1(struct compiler *c, expr_ty e)
62466287
ADDOP(c, loc, NOP);
62476288
return SUCCESS;
62486289
}
6290+
if (e->v.Attribute.value->kind == Name_kind &&
6291+
_PyUnicode_EqualToASCIIString(e->v.Attribute.value->v.Name.id, "self"))
6292+
{
6293+
struct compiler_unit *class_u = get_class_compiler_unit(c);
6294+
if (class_u != NULL) {
6295+
assert(class_u->u_scope_type == COMPILER_SCOPE_CLASS);
6296+
assert(class_u->u_static_attributes);
6297+
RETURN_IF_ERROR(
6298+
PySet_Add(class_u->u_static_attributes, e->v.Attribute.attr));
6299+
}
6300+
}
62496301
VISIT(c, expr, e->v.Attribute.value);
62506302
loc = LOC(e);
62516303
loc = update_start_location_to_match_attr(c, loc, e);

0 commit comments

Comments
 (0)