Skip to content
Merged
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
73 changes: 40 additions & 33 deletions Android/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,19 @@
+ (".bat" if os.name == "nt" else "")
)

logcat_started = False
# Whether we've seen any output from Python yet.
python_started = False

# Buffer for verbose output which will be displayed only if a test fails and
# there has been no output from Python.
hidden_output = []


def log_verbose(context, line, stream=sys.stdout):
if context.verbose:
stream.write(line)
else:
hidden_output.append((stream, line))


def delete_glob(pattern):
Expand Down Expand Up @@ -118,7 +130,7 @@ def android_env(host):
env_script = ANDROID_DIR / "android-env.sh"
env_output = subprocess.run(
f"set -eu; "
f"export HOST={host}; "
f"HOST={host}; "
f"PREFIX={prefix}; "
f". {env_script}; "
f"export",
Expand Down Expand Up @@ -453,17 +465,19 @@ async def logcat_task(context, initial_devices):

# `--pid` requires API level 24 or higher.
args = [adb, "-s", serial, "logcat", "--pid", pid, "--format", "tag"]
hidden_output = []
logcat_started = False
async with async_process(
*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
) as process:
while line := (await process.stdout.readline()).decode(*DECODE_ARGS):
if match := re.fullmatch(r"([A-Z])/(.*)", line, re.DOTALL):
logcat_started = True
level, message = match.groups()
else:
# If the regex doesn't match, this is probably the second or
# subsequent line of a multi-line message. Python won't produce
# such messages, but other components might.
# If the regex doesn't match, this is either a logcat startup
# error, or the second or subsequent line of a multi-line
# message. Python won't produce multi-line messages, but other
# components might.
level, message = None, line

# Exclude high-volume messages which are rarely useful.
Expand All @@ -483,25 +497,22 @@ async def logcat_task(context, initial_devices):
# tag indicators from Python's stdout and stderr.
for prefix in ["python.stdout: ", "python.stderr: "]:
if message.startswith(prefix):
global logcat_started
logcat_started = True
global python_started
python_started = True
stream.write(message.removeprefix(prefix))
break
else:
if context.verbose:
# Non-Python messages add a lot of noise, but they may
# sometimes help explain a failure.
stream.write(line)
else:
hidden_output.append(line)
# Non-Python messages add a lot of noise, but they may
# sometimes help explain a failure.
log_verbose(context, line, stream)

# If the device disconnects while logcat is running, which always
# happens in --managed mode, some versions of adb return non-zero.
# Distinguish this from a logcat startup error by checking whether we've
# received a message from Python yet.
# received any logcat messages yet.
status = await wait_for(process.wait(), timeout=1)
if status != 0 and not logcat_started:
raise CalledProcessError(status, args, "".join(hidden_output))
raise CalledProcessError(status, args)


def stop_app(serial):
Expand All @@ -516,16 +527,6 @@ async def gradle_task(context):
task_prefix = "connected"
env["ANDROID_SERIAL"] = context.connected

hidden_output = []

def log(line):
# Gradle may take several minutes to install SDK packages, so it's worth
# showing those messages even in non-verbose mode.
if context.verbose or line.startswith('Preparing "Install'):
sys.stdout.write(line)
else:
hidden_output.append(line)

if context.command:
mode = "-c"
module = context.command
Expand All @@ -550,27 +551,27 @@ def log(line):
]
if context.verbose >= 2:
args.append("--info")
log("> " + join_command(args))
log_verbose(context, f"> {join_command(args)}\n")

try:
async with async_process(
*args, cwd=TESTBED_DIR, env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
) as process:
while line := (await process.stdout.readline()).decode(*DECODE_ARGS):
log(line)
# Gradle may take several minutes to install SDK packages, so
# it's worth showing those messages even in non-verbose mode.
if line.startswith('Preparing "Install'):
sys.stdout.write(line)
else:
log_verbose(context, line)

status = await wait_for(process.wait(), timeout=1)
if status == 0:
exit(0)
else:
raise CalledProcessError(status, args)
finally:
# If logcat never started, then something has gone badly wrong, so the
# user probably wants to see the Gradle output even in non-verbose mode.
if hidden_output and not logcat_started:
sys.stdout.write("".join(hidden_output))

# Gradle does not stop the tests when interrupted.
if context.connected:
stop_app(context.connected)
Expand Down Expand Up @@ -600,6 +601,12 @@ async def run_testbed(context):
except* MySystemExit as e:
raise SystemExit(*e.exceptions[0].args) from None
except* CalledProcessError as e:
# If Python produced no output, then the user probably wants to see the
# verbose output to explain why the test failed.
if not python_started:
for stream, line in hidden_output:
stream.write(line)

# Extract it from the ExceptionGroup so it can be handled by `main`.
raise e.exceptions[0]

Expand Down
15 changes: 15 additions & 0 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,13 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
or _update_func_cell_for__class__(member.fdel, cls, newcls)):
break

# gh-135228: Make sure the original class can be garbage collected.
# Bypass mapping proxy to allow __dict__ to be removed
old_cls_dict = cls.__dict__ | _deproxier
old_cls_dict.pop('__dict__', None)
if "__weakref__" in cls.__dict__:
del cls.__weakref__

return newcls


Expand Down Expand Up @@ -1732,3 +1739,11 @@ def _replace(self, /, **changes):
# changes that aren't fields, this will correctly raise a
# TypeError.
return self.__class__(**changes)


# Hack to the get the underlying dict out of a mappingproxy
# Use it with: cls.__dict__ | _deproxier
class _Deproxier:
def __ror__(self, other):
return other
_deproxier = _Deproxier()
35 changes: 35 additions & 0 deletions Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3804,6 +3804,41 @@ class WithCorrectSuper(CorrectSuper):
# that we create internally.
self.assertEqual(CorrectSuper.args, ["default", "default"])

def test_original_class_is_gced(self):
# gh-135228: Make sure when we replace the class with slots=True, the original class
# gets garbage collected.
def make_simple():
@dataclass(slots=True)
class SlotsTest:
pass

return SlotsTest

def make_with_annotations():
@dataclass(slots=True)
class SlotsTest:
x: int

return SlotsTest

def make_with_annotations_and_method():
@dataclass(slots=True)
class SlotsTest:
x: int

def method(self) -> int:
return self.x

return SlotsTest

for make in (make_simple, make_with_annotations, make_with_annotations_and_method):
with self.subTest(make=make):
C = make()
support.gc_collect()
candidates = [cls for cls in object.__subclasses__() if cls.__name__ == 'SlotsTest'
and cls.__firstlineno__ == make.__code__.co_firstlineno + 1]
self.assertEqual(candidates, [C])


class TestDescriptors(unittest.TestCase):
def test_set_name(self):
Expand Down
4 changes: 2 additions & 2 deletions Misc/NEWS.d/3.14.0b1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1756,7 +1756,7 @@ Add support for macOS multi-arch builds with the JIT enabled
.. nonce: q9fvyM
.. section: Core and Builtins

PyREPL now supports syntax highlighing. Contributed by Łukasz Langa.
PyREPL now supports syntax highlighting. Contributed by Łukasz Langa.

..

Expand Down Expand Up @@ -1797,7 +1797,7 @@ non-``None`` ``closure``. Patch by Bartosz Sławecki.
.. nonce: Uj7lyY
.. section: Core and Builtins

Fix a bug that was allowing newlines inconsitently in format specifiers for
Fix a bug that was allowing newlines inconsistently in format specifiers for
single-quoted f-strings. Patch by Pablo Galindo.

..
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Fix PyREPL syntax highlightning on match cases after multi-line case. Contributed by Olga Matoula.
Fix PyREPL syntax highlighting on match cases after multi-line case. Contributed by Olga Matoula.
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Fix pickling failures for protocols 0 and 1 for many objects realted to
Fix pickling failures for protocols 0 and 1 for many objects related to
subinterpreters.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
When :mod:`dataclasses` replaces a class with a slotted dataclass, the
original class is now garbage collected again. Earlier changes in Python
3.14 caused this class to remain in existence together with the replacement
class synthesized by :mod:`dataclasses`.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
The cases generator no longer accepts type annotations on stack items.
Conversions to non-default types are now done explictly in bytecodes.c and
Conversions to non-default types are now done explicitly in bytecodes.c and
optimizer_bytecodes.c. This will simplify code generation for top-of-stack
caching and other future features.
Loading