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-102192: Replace PyErr_Fetch/Restore etc by more efficient alternatives #102619

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Doc/library/dataclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ Module contents
:func:`astuple` raises :exc:`TypeError` if ``obj`` is not a dataclass
instance.

.. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False)
.. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False, module=None)

Creates a new dataclass with name ``cls_name``, fields as defined
in ``fields``, base classes as given in ``bases``, and initialized
Expand All @@ -401,6 +401,10 @@ Module contents
``match_args``, ``kw_only``, ``slots``, and ``weakref_slot`` have
the same meaning as they do in :func:`dataclass`.

If ``module`` is defined, the ``__module__`` attribute
of the dataclass is set to that value.
By default, it is set to the module name of the caller.

This function is not strictly required, because any Python
mechanism for creating a new class with ``__annotations__`` can
then apply the :func:`dataclass` function to convert that class to
Expand Down
6 changes: 3 additions & 3 deletions Doc/library/dis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ the following command can be used to display the disassembly of
2 0 RESUME 0
<BLANKLINE>
3 2 LOAD_GLOBAL 1 (NULL + len)
14 LOAD_FAST 0 (alist)
16 CALL 1
26 RETURN_VALUE
12 LOAD_FAST 0 (alist)
14 CALL 1
24 RETURN_VALUE

(The "2" is a line number).

Expand Down
28 changes: 26 additions & 2 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1440,8 +1440,8 @@ code execution::
pass


Current State of Generators and Coroutines
------------------------------------------
Current State of Generators, Coroutines, and Asynchronous Generators
--------------------------------------------------------------------

When implementing coroutine schedulers and for other advanced uses of
generators, it is useful to determine whether a generator is currently
Expand Down Expand Up @@ -1476,6 +1476,22 @@ generator to be determined easily.

.. versionadded:: 3.5

.. function:: getasyncgenstate(agen)

Get current state of an asynchronous generator object. The function is
intended to be used with asynchronous iterator objects created by
:keyword:`async def` functions which use the :keyword:`yield` statement,
but will accept any asynchronous generator-like object that has
``ag_running`` and ``ag_frame`` attributes.

Possible states are:
* AGEN_CREATED: Waiting to start execution.
* AGEN_RUNNING: Currently being executed by the interpreter.
* AGEN_SUSPENDED: Currently suspended at a yield expression.
* AGEN_CLOSED: Execution has completed.

.. versionadded:: 3.12

The current internal state of the generator can also be queried. This is
mostly useful for testing purposes, to ensure that internal state is being
updated as expected:
Expand Down Expand Up @@ -1507,6 +1523,14 @@ updated as expected:

.. versionadded:: 3.5

.. function:: getasyncgenlocals(agen)

This function is analogous to :func:`~inspect.getgeneratorlocals`, but
works for asynchronous generator objects created by :keyword:`async def`
functions which use the :keyword:`yield` statement.

.. versionadded:: 3.12


.. _inspect-module-co-flags:

Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ inspect
a :term:`coroutine` for use with :func:`iscoroutinefunction`.
(Contributed Carlton Gibson in :gh:`99247`.)

* Add :func:`inspect.getasyncgenstate` and :func:`inspect.getasyncgenlocals`
for determining the current state of asynchronous generators.
(Contributed by Thomas Krennwallner in :issue:`35759`.)

pathlib
-------

Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_code.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ extern "C" {
typedef struct {
uint16_t counter;
uint16_t index;
uint16_t module_keys_version[2];
uint16_t module_keys_version;
uint16_t builtin_keys_version;
} _PyLoadGlobalCache;

Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ extern void _PyObject_FreeInstanceAttributes(PyObject *obj);
extern int _PyObject_IsInstanceDictEmpty(PyObject *);
extern int _PyType_HasSubclasses(PyTypeObject *);
extern PyObject* _PyType_GetSubclasses(PyTypeObject *);
extern PyObject* _PyObject_GenericTryGetAttr(PyObject *, PyObject *);

// Access macro to the members which are floating "behind" the object
static inline PyMemberDef* _PyHeapType_GET_MEMBERS(PyHeapTypeObject *etype) {
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_opcode.h

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

29 changes: 20 additions & 9 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,21 +616,19 @@ def _repr_fn(fields, globals):
def _frozen_get_del_attr(cls, fields, globals):
locals = {'cls': cls,
'FrozenInstanceError': FrozenInstanceError}
condition = 'type(self) is cls'
if fields:
fields_str = '(' + ','.join(repr(f.name) for f in fields) + ',)'
else:
# Special case for the zero-length tuple.
fields_str = '()'
condition += ' or name in {' + ', '.join(repr(f.name) for f in fields) + '}'
return (_create_fn('__setattr__',
('self', 'name', 'value'),
(f'if type(self) is cls or name in {fields_str}:',
(f'if {condition}:',
' raise FrozenInstanceError(f"cannot assign to field {name!r}")',
f'super(cls, self).__setattr__(name, value)'),
locals=locals,
globals=globals),
_create_fn('__delattr__',
('self', 'name'),
(f'if type(self) is cls or name in {fields_str}:',
(f'if {condition}:',
' raise FrozenInstanceError(f"cannot delete field {name!r}")',
f'super(cls, self).__delattr__(name)'),
locals=locals,
Expand Down Expand Up @@ -1283,7 +1281,7 @@ class C:
If given, 'dict_factory' will be used instead of built-in dict.
The function applies recursively to field values that are
dataclass instances. This will also look into built-in containers:
tuples, lists, and dicts.
tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'.
"""
if not _is_dataclass_instance(obj):
raise TypeError("asdict() should be called on dataclass instances")
Expand Down Expand Up @@ -1355,7 +1353,7 @@ class C:
If given, 'tuple_factory' will be used instead of built-in tuple.
The function applies recursively to field values that are
dataclass instances. This will also look into built-in containers:
tuples, lists, and dicts.
tuples, lists, and dicts. Other objects are copied with 'copy.deepcopy()'.
"""

if not _is_dataclass_instance(obj):
Expand Down Expand Up @@ -1393,7 +1391,7 @@ def _astuple_inner(obj, tuple_factory):
def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True,
repr=True, eq=True, order=False, unsafe_hash=False,
frozen=False, match_args=True, kw_only=False, slots=False,
weakref_slot=False):
weakref_slot=False, module=None):
"""Return a new dynamically created dataclass.

The dataclass name will be 'cls_name'. 'fields' is an iterable
Expand Down Expand Up @@ -1457,6 +1455,19 @@ def exec_body_callback(ns):
# of generic dataclasses.
cls = types.new_class(cls_name, bases, {}, exec_body_callback)

# For pickling to work, the __module__ variable needs to be set to the frame
# where the dataclass is created.
if module is None:
try:
module = sys._getframemodulename(1) or '__main__'
except AttributeError:
try:
module = sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
if module is not None:
cls.__module__ = module

# Apply the normal decorator.
return dataclass(cls, init=init, repr=repr, eq=eq, order=order,
unsafe_hash=unsafe_hash, frozen=frozen,
Expand Down
13 changes: 9 additions & 4 deletions Lib/importlib/_bootstrap_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,12 +431,17 @@ def _write_atomic(path, data, mode=0o666):
# Python 3.12a5 3515 (Embed jump mask in COMPARE_OP oparg)
# Python 3.12a5 3516 (Add COMPARE_AND_BRANCH instruction)
# Python 3.12a5 3517 (Change YIELD_VALUE oparg to exception block depth)
# Python 3.12a5 3518 (Add RETURN_CONST instruction)
# Python 3.12a5 3519 (Modify SEND instruction)
# Python 3.12a5 3520 (Remove PREP_RERAISE_STAR, add CALL_INTRINSIC_2)
# Python 3.12a6 3518 (Add RETURN_CONST instruction)
# Python 3.12a6 3519 (Modify SEND instruction)
# Python 3.12a6 3520 (Remove PREP_RERAISE_STAR, add CALL_INTRINSIC_2)
# Python 3.12a7 3521 (Shrink the LOAD_GLOBAL caches)

# Python 3.13 will start with 3550

# Please don't copy-paste the same pre-release tag for new entries above!!!
# You should always use the *upcoming* tag. For example, if 3.12a6 came out
# a week ago, I should put "Python 3.12a7" next to my new magic number.

# MAGIC must change whenever the bytecode emitted by the compiler may no
# longer be understood by older implementations of the eval loop (usually
# due to the addition of new opcodes).
Expand All @@ -446,7 +451,7 @@ def _write_atomic(path, data, mode=0o666):
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
# in PC/launcher.c must also be updated.

MAGIC_NUMBER = (3520).to_bytes(2, 'little') + b'\r\n'
MAGIC_NUMBER = (3521).to_bytes(2, 'little') + b'\r\n'

_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c

Expand Down
50 changes: 50 additions & 0 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
'Yury Selivanov <yselivanov@sprymix.com>')

__all__ = [
"AGEN_CLOSED",
"AGEN_CREATED",
"AGEN_RUNNING",
"AGEN_SUSPENDED",
"ArgInfo",
"Arguments",
"Attribute",
Expand Down Expand Up @@ -77,6 +81,8 @@
"getabsfile",
"getargs",
"getargvalues",
"getasyncgenlocals",
"getasyncgenstate",
"getattr_static",
"getblock",
"getcallargs",
Expand Down Expand Up @@ -1935,6 +1941,50 @@ def getcoroutinelocals(coroutine):
return {}


# ----------------------------------- asynchronous generator introspection

AGEN_CREATED = 'AGEN_CREATED'
AGEN_RUNNING = 'AGEN_RUNNING'
AGEN_SUSPENDED = 'AGEN_SUSPENDED'
AGEN_CLOSED = 'AGEN_CLOSED'


def getasyncgenstate(agen):
"""Get current state of an asynchronous generator object.

Possible states are:
AGEN_CREATED: Waiting to start execution.
AGEN_RUNNING: Currently being executed by the interpreter.
AGEN_SUSPENDED: Currently suspended at a yield expression.
AGEN_CLOSED: Execution has completed.
"""
if agen.ag_running:
return AGEN_RUNNING
if agen.ag_suspended:
return AGEN_SUSPENDED
if agen.ag_frame is None:
return AGEN_CLOSED
return AGEN_CREATED


def getasyncgenlocals(agen):
"""
Get the mapping of asynchronous generator local variables to their current
values.

A dict is returned, with the keys the local variable names and values the
bound values."""

if not isasyncgen(agen):
raise TypeError(f"{agen!r} is not a Python async generator")

frame = getattr(agen, "ag_frame", None)
if frame is not None:
return agen.ag_frame.f_locals
else:
return {}


###############################################################################
### Function Signature Object (PEP 362)
###############################################################################
Expand Down
2 changes: 1 addition & 1 deletion Lib/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def pseudo_op(name, op, real_ops):
"LOAD_GLOBAL": {
"counter": 1,
"index": 1,
"module_keys_version": 2,
"module_keys_version": 1,
"builtin_keys_version": 1,
},
"BINARY_OP": {
Expand Down
8 changes: 5 additions & 3 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,9 @@ def _from_parsed_parts(cls, drv, root, parts):
def _format_parsed_parts(cls, drv, root, parts):
if drv or root:
return drv + root + cls._flavour.sep.join(parts[1:])
else:
return cls._flavour.sep.join(parts)
elif parts and cls._flavour.splitdrive(parts[0])[0]:
parts = ['.'] + parts
return cls._flavour.sep.join(parts)

def __str__(self):
"""Return the string representation of the path, suitable for
Expand Down Expand Up @@ -1188,7 +1189,8 @@ def expanduser(self):
homedir = self._flavour.expanduser(self._parts[0])
if homedir[:1] == "~":
raise RuntimeError("Could not determine home directory.")
return self._from_parts([homedir] + self._parts[1:])
drv, root, parts = self._parse_parts((homedir,))
return self._from_parsed_parts(drv, root, parts + self._parts[1:])

return self

Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from textwrap import dedent

from test import support
from test.support import os_helper, script_helper
from test.support.ast_helper import ASTTestMixin

def to_tuple(t):
Expand Down Expand Up @@ -2564,6 +2565,25 @@ def test_subinterpreter(self):
self.assertEqual(res, 0)


class ASTMainTests(unittest.TestCase):
# Tests `ast.main()` function.

def test_cli_file_input(self):
code = "print(1, 2, 3)"
expected = ast.dump(ast.parse(code), indent=3)

with os_helper.temp_dir() as tmp_dir:
filename = os.path.join(tmp_dir, "test_module.py")
with open(filename, 'w', encoding='utf-8') as f:
f.write(code)
res, _ = script_helper.run_python_until_end("-m", "ast", filename)

self.assertEqual(res.err, b"")
self.assertEqual(expected.splitlines(),
res.out.decode("utf8").splitlines())
self.assertEqual(res.rc, 0)


def main():
if __name__ != '__main__':
return
Expand Down
Loading