Skip to content
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

gh-115775: Compiler adds __static_attributes__ field to classes #115913

Merged
merged 13 commits into from
Mar 26, 2024
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(__slotnames__)
STRUCT_FOR_ID(__slots__)
STRUCT_FOR_ID(__spec__)
STRUCT_FOR_ID(__static_attributes__)
STRUCT_FOR_ID(__str__)
STRUCT_FOR_ID(__sub__)
STRUCT_FOR_ID(__subclasscheck__)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -2018,7 +2018,8 @@ def _test_simple_enum(checked_enum, simple_enum):
+ list(simple_enum._member_map_.keys())
)
for key in set(checked_keys + simple_keys):
if key in ('__module__', '_member_map_', '_value2member_map_', '__doc__'):
if key in ('__module__', '_member_map_', '_value2member_map_', '__doc__',
'__static_attributes__'):
# keys known to be different, or very long
continue
elif key in member_names:
Expand Down
3 changes: 2 additions & 1 deletion Lib/pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@ def visiblename(name, all=None, obj=None):
if name in {'__author__', '__builtins__', '__cached__', '__credits__',
'__date__', '__doc__', '__file__', '__spec__',
'__loader__', '__module__', '__name__', '__package__',
'__path__', '__qualname__', '__slots__', '__version__'}:
'__path__', '__qualname__', '__slots__', '__version__',
'__static_attributes__'}:
return 0
# Private names are hidden, but special names are displayed.
if name.startswith('__') and name.endswith('__'): return 1
Expand Down
58 changes: 58 additions & 0 deletions Lib/test/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1960,6 +1960,64 @@ def test_load_super_attr(self):
)


class TestExpectedAttributes(unittest.TestCase):

def test_basic(self):
class C:
def f(self):
self.a = self.b = 42

self.assertIsInstance(C.__static_attributes__, tuple)
self.assertEqual(sorted(C.__static_attributes__), ['a', 'b'])

def test_nested_function(self):
class C:
def f(self):
self.x = 1
self.y = 2
self.x = 3 # check deduplication

def g(self, obj):
self.y = 4
self.z = 5

def h(self, a):
self.u = 6
self.v = 7

obj.self = 8

self.assertEqual(sorted(C.__static_attributes__), ['u', 'v', 'x', 'y', 'z'])

def test_nested_class(self):
class C:
def f(self):
self.x = 42
self.y = 42

class D:
def g(self):
self.y = 42
self.z = 42

self.assertEqual(sorted(C.__static_attributes__), ['x', 'y'])
self.assertEqual(sorted(C.D.__static_attributes__), ['y', 'z'])

def test_subclass(self):
class C:
def f(self):
self.x = 42
self.y = 42

class D(C):
def g(self):
self.y = 42
self.z = 42

self.assertEqual(sorted(C.__static_attributes__), ['x', 'y'])
self.assertEqual(sorted(D.__static_attributes__), ['y', 'z'])


class TestExpressionStackSize(unittest.TestCase):
# These tests check that the computed stack size for a code object
# stays within reasonable bounds (see issue #21523 for an example
Expand Down
8 changes: 5 additions & 3 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5080,7 +5080,8 @@ def test_iter_keys(self):
keys = list(it)
keys.sort()
self.assertEqual(keys, ['__dict__', '__doc__', '__module__',
'__weakref__', 'meth'])
'__static_attributes__', '__weakref__',
'meth'])

@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
'trace function introduces __local__')
Expand All @@ -5089,7 +5090,7 @@ def test_iter_values(self):
it = self.C.__dict__.values()
self.assertNotIsInstance(it, list)
values = list(it)
self.assertEqual(len(values), 5)
self.assertEqual(len(values), 6)

@unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(),
'trace function introduces __local__')
Expand All @@ -5100,7 +5101,8 @@ def test_iter_items(self):
keys = [item[0] for item in it]
keys.sort()
self.assertEqual(keys, ['__dict__', '__doc__', '__module__',
'__weakref__', 'meth'])
'__static_attributes__', '__weakref__',
'meth'])

def test_dict_type_with_metaclass(self):
# Testing type of __dict__ when metaclass set...
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -1160,7 +1160,7 @@ class APIMismatchTest(unittest.TestCase):
def test_RawIOBase_io_in_pyio_match(self):
"""Test that pyio RawIOBase class has all c RawIOBase methods"""
mismatch = support.detect_api_mismatch(pyio.RawIOBase, io.RawIOBase,
ignore=('__weakref__',))
ignore=('__weakref__', '__static_attributes__'))
self.assertEqual(mismatch, set(), msg='Python RawIOBase does not have all C RawIOBase methods')

def test_RawIOBase_pyio_in_io_match(self):
Expand Down
8 changes: 5 additions & 3 deletions Lib/test/test_metaclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
d['foo'] = 4
d['foo'] = 42
d['bar'] = 123
d['__static_attributes__'] = ()
>>>

Use a metaclass that doesn't derive from type.
Expand All @@ -182,12 +183,12 @@
... b = 24
...
meta: C ()
ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('a', 42), ('b', 24)]
ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
kw: []
>>> type(C) is dict
True
>>> print(sorted(C.items()))
[('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('a', 42), ('b', 24)]
[('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 42), ('b', 24)]
>>>

And again, with a __prepare__ attribute.
Expand All @@ -208,8 +209,9 @@
d['a'] = 1
d['a'] = 2
d['b'] = 3
d['__static_attributes__'] = ()
meta: C ()
ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('a', 2), ('b', 3)]
ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('__static_attributes__', ()), ('a', 2), ('b', 3)]
kw: [('other', 'booh')]
>>>

Expand Down
2 changes: 1 addition & 1 deletion Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1717,7 +1717,7 @@ class _TypingEllipsis:
'__abstractmethods__', '__annotations__', '__dict__', '__doc__',
'__init__', '__module__', '__new__', '__slots__',
'__subclasshook__', '__weakref__', '__class_getitem__',
'__match_args__',
'__match_args__', '__static_attributes__',
})

# These special attributes will be not collected as protocol members.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Compiler populates the new ``__static_attributes__`` field on a class with
the names of attributes of this class which are accessed through self.X from
any function in its body.
54 changes: 53 additions & 1 deletion Python/compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,8 @@ struct compiler_unit {

int u_scope_type;

PyObject *u_private; /* for private name mangling */
PyObject *u_private; /* for private name mangling */
PyObject *u_static_attributes; /* for class: attributes accessed via self.X */

instr_sequence u_instr_sequence; /* codegen output */

Expand Down Expand Up @@ -690,9 +691,26 @@ compiler_unit_free(struct compiler_unit *u)
Py_CLEAR(u->u_metadata.u_cellvars);
Py_CLEAR(u->u_metadata.u_fasthidden);
Py_CLEAR(u->u_private);
Py_CLEAR(u->u_static_attributes);
PyMem_Free(u);
}

static struct compiler_unit *
get_class_compiler_unit(struct compiler *c)
{
Py_ssize_t stack_size = PyList_GET_SIZE(c->c_stack);
for (Py_ssize_t i = stack_size - 1; i >= 0; i--) {
PyObject *capsule = PyList_GET_ITEM(c->c_stack, i);
struct compiler_unit *u = (struct compiler_unit *)PyCapsule_GetPointer(
capsule, CAPSULE_NAME);
assert(u);
if (u->u_scope_type == COMPILER_SCOPE_CLASS) {
return u;
}
}
return NULL;
}

static int
compiler_set_qualname(struct compiler *c)
{
Expand Down Expand Up @@ -1336,6 +1354,16 @@ compiler_enter_scope(struct compiler *c, identifier name,
}

u->u_private = NULL;
if (scope_type == COMPILER_SCOPE_CLASS) {
u->u_static_attributes = PySet_New(0);
if (!u->u_static_attributes) {
compiler_unit_free(u);
return ERROR;
}
}
else {
u->u_static_attributes = NULL;
}

/* Push the old compiler_unit on the stack. */
if (c->u) {
Expand Down Expand Up @@ -2517,6 +2545,18 @@ compiler_class_body(struct compiler *c, stmt_ty s, int firstlineno)
compiler_exit_scope(c);
return ERROR;
}
assert(c->u->u_static_attributes);
PyObject *static_attributes = PySequence_Tuple(c->u->u_static_attributes);
if (static_attributes == NULL) {
compiler_exit_scope(c);
return ERROR;
}
ADDOP_LOAD_CONST(c, NO_LOCATION, static_attributes);
Py_CLEAR(static_attributes);
if (compiler_nameop(c, NO_LOCATION, &_Py_ID(__static_attributes__), Store) < 0) {
compiler_exit_scope(c);
return ERROR;
}
/* The following code is artificial */
/* Set __classdictcell__ if necessary */
if (c->u->u_ste->ste_needs_classdict) {
Expand Down Expand Up @@ -2657,6 +2697,7 @@ compiler_class(struct compiler *c, stmt_ty s)
s->v.ClassDef.keywords));

PyCodeObject *co = optimize_and_assemble(c, 0);

compiler_exit_scope(c);
if (co == NULL) {
return ERROR;
Expand Down Expand Up @@ -6246,6 +6287,17 @@ compiler_visit_expr1(struct compiler *c, expr_ty e)
ADDOP(c, loc, NOP);
return SUCCESS;
}
if (e->v.Attribute.value->kind == Name_kind &&
_PyUnicode_EqualToASCIIString(e->v.Attribute.value->v.Name.id, "self"))
{
struct compiler_unit *class_u = get_class_compiler_unit(c);
if (class_u != NULL) {
assert(class_u->u_scope_type == COMPILER_SCOPE_CLASS);
assert(class_u->u_static_attributes);
RETURN_IF_ERROR(
PySet_Add(class_u->u_static_attributes, e->v.Attribute.attr));
}
}
VISIT(c, expr, e->v.Attribute.value);
loc = LOC(e);
loc = update_start_location_to_match_attr(c, loc, e);
Expand Down