summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIrit Katriel <1055913+iritkatriel@users.noreply.github.com>2024-03-26 15:18:17 (GMT)
committerGitHub <noreply@github.com>2024-03-26 15:18:17 (GMT)
commit79be75735c9d77972112cecc8d7e1af28c176ed0 (patch)
tree243cf7ff18185b4aeb4f213c4571bc7e26793116
parent70969d53a77a8a190c40a30419e772bc874a4f62 (diff)
downloadcpython-79be75735c9d77972112cecc8d7e1af28c176ed0.zip
cpython-79be75735c9d77972112cecc8d7e1af28c176ed0.tar.gz
cpython-79be75735c9d77972112cecc8d7e1af28c176ed0.tar.bz2
gh-115775: Compiler adds __static_attributes__ field to classes (#115913)
-rw-r--r--Include/internal/pycore_global_objects_fini_generated.h1
-rw-r--r--Include/internal/pycore_global_strings.h1
-rw-r--r--Include/internal/pycore_runtime_init_generated.h1
-rw-r--r--Include/internal/pycore_unicodeobject_generated.h3
-rw-r--r--Lib/enum.py3
-rwxr-xr-xLib/pydoc.py3
-rw-r--r--Lib/test/test_compile.py58
-rw-r--r--Lib/test/test_descr.py8
-rw-r--r--Lib/test/test_io.py2
-rw-r--r--Lib/test/test_metaclass.py8
-rw-r--r--Lib/typing.py2
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2024-02-25-14-17-25.gh-issue-115775.CNbGbJ.rst3
-rw-r--r--Python/compile.c54
13 files changed, 136 insertions, 11 deletions
diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h
index d228768..9aa34f5 100644
--- a/Include/internal/pycore_global_objects_fini_generated.h
+++ b/Include/internal/pycore_global_objects_fini_generated.h
@@ -724,6 +724,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__slotnames__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__slots__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__spec__));
+ _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__static_attributes__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__str__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__sub__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__subclasscheck__));
diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h
index fb9ec44..9a0d42f 100644
--- a/Include/internal/pycore_global_strings.h
+++ b/Include/internal/pycore_global_strings.h
@@ -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__)
diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h
index 658bf80..d75f0f8 100644
--- a/Include/internal/pycore_runtime_init_generated.h
+++ b/Include/internal/pycore_runtime_init_generated.h
@@ -722,6 +722,7 @@ extern "C" {
INIT_ID(__slotnames__), \
INIT_ID(__slots__), \
INIT_ID(__spec__), \
+ INIT_ID(__static_attributes__), \
INIT_ID(__str__), \
INIT_ID(__sub__), \
INIT_ID(__subclasscheck__), \
diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h
index d72353d..7f67e67 100644
--- a/Include/internal/pycore_unicodeobject_generated.h
+++ b/Include/internal/pycore_unicodeobject_generated.h
@@ -480,6 +480,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
string = &_Py_ID(__spec__);
assert(_PyUnicode_CheckConsistency(string, 1));
_PyUnicode_InternInPlace(interp, &string);
+ string = &_Py_ID(__static_attributes__);
+ assert(_PyUnicode_CheckConsistency(string, 1));
+ _PyUnicode_InternInPlace(interp, &string);
string = &_Py_ID(__str__);
assert(_PyUnicode_CheckConsistency(string, 1));
_PyUnicode_InternInPlace(interp, &string);
diff --git a/Lib/enum.py b/Lib/enum.py
index 5c5e711..2a135e1 100644
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -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:
diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index 08fd7ab..d9cf03f 100755
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -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
diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py
index d3e69bf..9d5f721 100644
--- a/Lib/test/test_compile.py
+++ b/Lib/test/test_compile.py
@@ -1960,6 +1960,64 @@ class TestSourcePositions(unittest.TestCase):
)
+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
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
index 5404d8d..097ca38 100644
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -5080,7 +5080,8 @@ class DictProxyTests(unittest.TestCase):
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__')
@@ -5089,7 +5090,7 @@ class DictProxyTests(unittest.TestCase):
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__')
@@ -5100,7 +5101,8 @@ class DictProxyTests(unittest.TestCase):
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...
diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py
index 5491c05..4ea1ef1 100644
--- a/Lib/test/test_io.py
+++ b/Lib/test/test_io.py
@@ -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):
diff --git a/Lib/test/test_metaclass.py b/Lib/test/test_metaclass.py
index 36e8ab4..70f9c5d 100644
--- a/Lib/test/test_metaclass.py
+++ b/Lib/test/test_metaclass.py
@@ -167,6 +167,7 @@ Use a __prepare__ method that returns an instrumented dict.
d['foo'] = 4
d['foo'] = 42
d['bar'] = 123
+ d['__static_attributes__'] = ()
>>>
Use a metaclass that doesn't derive from type.
@@ -182,12 +183,12 @@ Use a metaclass that doesn't derive from type.
... 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.
@@ -208,8 +209,9 @@ And again, with a __prepare__ attribute.
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')]
>>>
diff --git a/Lib/typing.py b/Lib/typing.py
index 533b640..581d187 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -1717,7 +1717,7 @@ _SPECIAL_NAMES = frozenset({
'__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.
diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-02-25-14-17-25.gh-issue-115775.CNbGbJ.rst b/Misc/NEWS.d/next/Core and Builtins/2024-02-25-14-17-25.gh-issue-115775.CNbGbJ.rst
new file mode 100644
index 0000000..78bef74
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2024-02-25-14-17-25.gh-issue-115775.CNbGbJ.rst
@@ -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.
diff --git a/Python/compile.c b/Python/compile.c
index 3291d31..e9507e4 100644
--- a/Python/compile.c
+++ b/Python/compile.c
@@ -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 */
@@ -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)
{
@@ -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) {
@@ -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) {
@@ -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;
@@ -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);