Skip to content

Commit e8e3d32

Browse files
committed
Merge branch 'main' into gh-148874/with-signal-exit-skip
2 parents 8fc75f0 + c1940bc commit e8e3d32

24 files changed

Lines changed: 559 additions & 194 deletions

Doc/glossary.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ Glossary
3939
ABCs with the :mod:`abc` module.
4040

4141
annotate function
42-
A function that can be called to retrieve the :term:`annotations <annotation>`
43-
of an object. This function is accessible as the :attr:`~object.__annotate__`
44-
attribute of functions, classes, and modules. Annotate functions are a
45-
subset of :term:`evaluate functions <evaluate function>`.
42+
A callable that can be called to retrieve the :term:`annotations <annotation>` of
43+
an object. Annotate functions are usually :term:`functions <function>`,
44+
automatically generated as the :attr:`~object.__annotate__` attribute of functions,
45+
classes, and modules. Annotate functions are a subset of
46+
:term:`evaluate functions <evaluate function>`.
4647

4748
annotation
4849
A label associated with a variable, a class

Doc/library/annotationlib.rst

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,81 @@ annotations from the class and puts them in a separate attribute:
510510
return typ
511511
512512
513+
Creating a custom callable annotate function
514+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
515+
516+
Custom :term:`annotate functions <annotate function>` may be literal functions like those
517+
automatically generated for functions, classes, and modules. Or, they may wish to utilise
518+
the encapsulation provided by classes, in which case any :term:`callable` can be used as
519+
an :term:`annotate function`.
520+
521+
To provide the :attr:`~Format.VALUE`, :attr:`~Format.STRING`, or
522+
:attr:`~Format.FORWARDREF` formats directly, an :term:`annotate function` must provide
523+
the following attribute:
524+
525+
* A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that does not
526+
raise a :exc:`NotImplementedError` when called with a supported format.
527+
528+
To provide the :attr:`~Format.VALUE_WITH_FAKE_GLOBALS` format, which is used to
529+
automatically generate :attr:`~Format.STRING` or :attr:`~Format.FORWARDREF` if they are
530+
not supported directly, :term:`annotate functions <annotate function>` must provide the
531+
following attributes:
532+
533+
* A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that does not
534+
raise a :exc:`NotImplementedError` when called with
535+
:attr:`~Format.VALUE_WITH_FAKE_GLOBALS`.
536+
* A :ref:`code object <code-objects>` ``__code__`` containing the compiled code for the
537+
annotate function.
538+
* Optional: A tuple of the function's positional defaults ``__kwdefaults__``, if the
539+
function represented by ``__code__`` uses any positional defaults.
540+
* Optional: A dict of the function's keyword defaults ``__defaults__``, if the function
541+
represented by ``__code__`` uses any keyword defaults.
542+
* Optional: All other :ref:`function attributes <inspect-types>`.
543+
544+
.. code-block:: python
545+
546+
class Annotate:
547+
called_formats = []
548+
549+
def __call__(self, format=None, /, *, _self=None):
550+
# When called with fake globals, `_self` will be the
551+
# actual self value, and `self` will be the format.
552+
if _self is not None:
553+
self, format = _self, self
554+
555+
self.called_formats.append(format)
556+
if format <= 2: # VALUE or VALUE_WITH_FAKE_GLOBALS
557+
return {"x": MyType}
558+
raise NotImplementedError
559+
560+
__code__ = __call__.__code__
561+
__defaults__ = (None,)
562+
__kwdefaults__ = property(lambda self: dict(_self=self))
563+
564+
__globals__ = {}
565+
__builtins__ = {}
566+
__closure__ = None
567+
568+
This can then be called with:
569+
570+
.. code-block:: pycon
571+
572+
>>> from annotationlib import call_annotate_function, Format
573+
>>> call_annotate_function(Annotate(), format=Format.STRING)
574+
{'x': 'MyType'}
575+
576+
Or used as the annotate function for an object:
577+
578+
.. code-block:: pycon
579+
580+
>>> from annotationlib import get_annotations, Format
581+
>>> class C:
582+
... pass
583+
>>> C.__annotate__ = Annotate()
584+
>>> get_annotations(Annotate(), format=Format.STRING)
585+
{'x': 'MyType'}
586+
587+
513588
Limitations of the ``STRING`` format
514589
------------------------------------
515590

Doc/library/typing.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2361,6 +2361,12 @@ without the dedicated syntax, as documented below.
23612361
>>> Alias.__module__
23622362
'__main__'
23632363

2364+
This attribute is writable.
2365+
2366+
.. versionchanged:: 3.15
2367+
2368+
The attribute is now writable.
2369+
23642370
.. attribute:: __type_params__
23652371

23662372
The type parameters of the type alias, or an empty tuple if the alias is

Include/internal/pycore_opcode_utils.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ extern "C" {
7575
#define CONSTANT_BUILTIN_ANY 4
7676
#define CONSTANT_BUILTIN_LIST 5
7777
#define CONSTANT_BUILTIN_SET 6
78-
#define NUM_COMMON_CONSTANTS 7
78+
#define CONSTANT_NONE 7
79+
#define CONSTANT_EMPTY_STR 8
80+
#define CONSTANT_TRUE 9
81+
#define CONSTANT_FALSE 10
82+
#define CONSTANT_MINUS_ONE 11
83+
#define NUM_COMMON_CONSTANTS 12
7984

8085
/* Values used in the oparg for RESUME */
8186
#define RESUME_AT_FUNC_START 0

Lib/_pyrepl/reader.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@
3838
)
3939
from .layout import LayoutMap, LayoutResult, LayoutRow, WrappedRow, layout_content_lines
4040
from .render import RenderCell, RenderLine, RenderedScreen, ScreenOverlay
41-
from .utils import ANSI_ESCAPE_SEQUENCE, THEME, StyleRef, wlen, gen_colors
41+
from .utils import ANSI_ESCAPE_SEQUENCE, ColorSpan, THEME, StyleRef, wlen, gen_colors
4242
from .trace import trace
4343

4444

4545
# types
4646
Command = commands.Command
47+
from collections.abc import Callable, Iterator
4748
from .types import (
4849
Callback,
4950
CommandName,
@@ -304,6 +305,7 @@ class Reader:
304305
lxy: CursorXY = field(init=False)
305306
scheduled_commands: list[CommandName] = field(default_factory=list)
306307
can_colorize: bool = False
308+
gen_colors: Callable[[str], Iterator[ColorSpan]] = gen_colors
307309
threading_hook: Callback | None = None
308310
invalidation: RefreshInvalidation = field(init=False)
309311

@@ -534,7 +536,7 @@ def _build_content_lines(
534536
prompt_from_cache: bool,
535537
) -> tuple[ContentLine, ...]:
536538
if self.can_colorize:
537-
colors = list(gen_colors(self.get_unicode()))
539+
colors = list(self.gen_colors(self.get_unicode()))
538540
else:
539541
colors = None
540542
trace("colors = {colors}", colors=colors)

Lib/dis.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,7 @@ def get_argval_argrepr(self, op, arg, offset):
651651
argrepr = str(argval)
652652
elif deop == LOAD_COMMON_CONSTANT:
653653
obj = _common_constants[arg]
654+
argval = obj
654655
if isinstance(obj, type):
655656
argrepr = obj.__name__
656657
else:
@@ -700,10 +701,15 @@ def _get_const_value(op, arg, co_consts):
700701
Otherwise (if it is a LOAD_CONST and co_consts is not
701702
provided) returns the dis.UNKNOWN sentinel.
702703
"""
703-
assert op in hasconst or op == LOAD_SMALL_INT
704+
assert op in hasconst or op == LOAD_SMALL_INT or op == LOAD_COMMON_CONSTANT
704705

705706
if op == LOAD_SMALL_INT:
706707
return arg
708+
if op == LOAD_COMMON_CONSTANT:
709+
# Opargs 0-6 are callables; 7-11 are literal values.
710+
if 7 <= arg <= 11:
711+
return _common_constants[arg]
712+
return UNKNOWN
707713
argval = UNKNOWN
708714
if co_consts is not None:
709715
argval = co_consts[arg]
@@ -1023,8 +1029,9 @@ def _find_imports(co):
10231029
if op == IMPORT_NAME and i >= 2:
10241030
from_op = opargs[i-1]
10251031
level_op = opargs[i-2]
1026-
if (from_op[0] in hasconst and
1027-
(level_op[0] in hasconst or level_op[0] == LOAD_SMALL_INT)):
1032+
if ((from_op[0] in hasconst or from_op[0] == LOAD_COMMON_CONSTANT) and
1033+
(level_op[0] in hasconst or level_op[0] == LOAD_SMALL_INT or
1034+
level_op[0] == LOAD_COMMON_CONSTANT)):
10281035
level = _get_const_value(level_op[0], level_op[1], consts)
10291036
fromlist = _get_const_value(from_op[0], from_op[1], consts)
10301037
# IMPORT_NAME encodes lazy/eager flags in bits 0-1,

Lib/opcode.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@
4141
_special_method_names = _opcode.get_special_method_names()
4242
_common_constants = [builtins.AssertionError, builtins.NotImplementedError,
4343
builtins.tuple, builtins.all, builtins.any, builtins.list,
44-
builtins.set]
44+
builtins.set,
45+
# Append-only — must match CONSTANT_* in
46+
# Include/internal/pycore_opcode_utils.h.
47+
None, "", True, False, -1]
4548
_nb_ops = _opcode.get_nb_ops()
4649

4750
hascompare = [opmap["COMPARE_OP"]]

Lib/test/test_annotationlib.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,6 +1619,84 @@ def annotate(format, /):
16191619
# Some non-Format value
16201620
annotationlib.call_annotate_function(annotate, 7)
16211621

1622+
def test_basic_non_function_annotate(self):
1623+
class Annotate:
1624+
def __call__(self, format, /, __Format=Format,
1625+
__NotImplementedError=NotImplementedError):
1626+
if format == __Format.VALUE:
1627+
return {'x': str}
1628+
elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
1629+
return {'x': int}
1630+
elif format == __Format.STRING:
1631+
return {'x': "float"}
1632+
else:
1633+
raise __NotImplementedError(format)
1634+
1635+
annotations = annotationlib.call_annotate_function(Annotate(), Format.VALUE)
1636+
self.assertEqual(annotations, {"x": str})
1637+
1638+
annotations = annotationlib.call_annotate_function(Annotate(), Format.STRING)
1639+
self.assertEqual(annotations, {"x": "float"})
1640+
1641+
with self.assertRaises(AttributeError) as cm:
1642+
annotations = annotationlib.call_annotate_function(
1643+
Annotate(), Format.FORWARDREF
1644+
)
1645+
1646+
self.assertEqual(cm.exception.name, "__builtins__")
1647+
self.assertIsInstance(cm.exception.obj, Annotate)
1648+
1649+
def test_full_non_function_annotate(self):
1650+
def outer():
1651+
local = str
1652+
1653+
class Annotate:
1654+
called_formats = []
1655+
1656+
def __call__(self, format=None, *, _self=None):
1657+
nonlocal local
1658+
if _self is not None:
1659+
self, format = _self, self
1660+
1661+
self.called_formats.append(format)
1662+
if format == 1: # VALUE
1663+
return {"x": MyClass, "y": int, "z": local}
1664+
if format == 2: # VALUE_WITH_FAKE_GLOBALS
1665+
return {"w": unknown, "x": MyClass, "y": int, "z": local}
1666+
raise NotImplementedError
1667+
1668+
__globals__ = {"MyClass": MyClass}
1669+
__builtins__ = {"int": int}
1670+
__closure__ = (types.CellType(str),)
1671+
__defaults__ = (None,)
1672+
1673+
__kwdefaults__ = property(lambda self: dict(_self=self))
1674+
__code__ = property(lambda self: self.__call__.__code__)
1675+
1676+
return Annotate()
1677+
1678+
annotate = outer()
1679+
1680+
self.assertEqual(
1681+
annotationlib.call_annotate_function(annotate, Format.VALUE),
1682+
{"x": MyClass, "y": int, "z": str}
1683+
)
1684+
self.assertEqual(annotate.called_formats[-1], Format.VALUE)
1685+
1686+
self.assertEqual(
1687+
annotationlib.call_annotate_function(annotate, Format.STRING),
1688+
{"w": "unknown", "x": "MyClass", "y": "int", "z": "local"}
1689+
)
1690+
self.assertIn(Format.STRING, annotate.called_formats)
1691+
self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS)
1692+
1693+
self.assertEqual(
1694+
annotationlib.call_annotate_function(annotate, Format.FORWARDREF),
1695+
{"w": support.EqualToForwardRef("unknown"), "x": MyClass, "y": int, "z": str}
1696+
)
1697+
self.assertIn(Format.FORWARDREF, annotate.called_formats)
1698+
self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS)
1699+
16221700
def test_error_from_value_raised(self):
16231701
# Test that the error from format.VALUE is raised
16241702
# if all formats fail

Lib/test/test_ast/test_ast.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2685,11 +2685,12 @@ def test_get_docstring(self):
26852685

26862686
def get_load_const(self, tree):
26872687
# Compile to bytecode, disassemble and get parameter of LOAD_CONST
2688-
# instructions
2688+
# and LOAD_COMMON_CONSTANT instructions
26892689
co = compile(tree, '<string>', 'exec')
26902690
consts = []
26912691
for instr in dis.get_instructions(co):
2692-
if instr.opcode in dis.hasconst:
2692+
if instr.opcode in dis.hasconst or \
2693+
instr.opname == 'LOAD_COMMON_CONSTANT':
26932694
consts.append(instr.argval)
26942695
return consts
26952696

Lib/test/test_capi/test_opt.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3446,6 +3446,27 @@ def testfunc(n):
34463446
self.assertIn("_BUILD_LIST", uops)
34473447
self.assertNotIn("_LOAD_COMMON_CONSTANT", uops)
34483448

3449+
def test_load_common_constant_new_literals(self):
3450+
def testfunc(n):
3451+
x = None
3452+
s = ""
3453+
t = True
3454+
f = False
3455+
m = -1
3456+
for _ in range(n):
3457+
x = None
3458+
s = ""
3459+
t = True
3460+
f = False
3461+
m = -1
3462+
return x, s, t, f, m
3463+
res, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD)
3464+
self.assertEqual(res, (None, "", True, False, -1))
3465+
self.assertIsNotNone(ex)
3466+
uops = get_opnames(ex)
3467+
self.assertNotIn("_LOAD_COMMON_CONSTANT", uops)
3468+
self.assertIn("_LOAD_CONST_INLINE_BORROW", uops)
3469+
34493470
def test_load_small_int(self):
34503471
def testfunc(n):
34513472
x = 0

0 commit comments

Comments
 (0)