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

Remove work-arounds for setting cell_contents #1201

Merged
merged 3 commits into from
Nov 11, 2023
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
107 changes: 0 additions & 107 deletions src/attr/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import platform
import sys
import threading
import types
import warnings

from collections.abc import Mapping, Sequence # noqa: F401
from typing import _GenericAlias
Expand All @@ -26,16 +24,6 @@
from typing import Protocol # noqa: F401


def just_warn(*args, **kw):
warnings.warn(
"Running interpreter doesn't sufficiently support code object "
"introspection. Some features like bare super() or accessing "
"__class__ will not work with slotted classes.",
RuntimeWarning,
stacklevel=2,
)


class _AnnotationExtractor:
"""
Extract type annotations from a callable, returning None whenever there
Expand Down Expand Up @@ -76,101 +64,6 @@ def get_return_type(self):
return None


def make_set_closure_cell():
"""Return a function of two arguments (cell, value) which sets
the value stored in the closure cell `cell` to `value`.
"""
# pypy makes this easy. (It also supports the logic below, but
# why not do the easy/fast thing?)
if PYPY:

def set_closure_cell(cell, value):
cell.__setstate__((value,))

return set_closure_cell

# Otherwise gotta do it the hard way.

try:
if sys.version_info >= (3, 8):

def set_closure_cell(cell, value):
cell.cell_contents = value

else:
# Create a function that will set its first cellvar to `value`.
def set_first_cellvar_to(value):
x = value
return

# This function will be eliminated as dead code, but
# not before its reference to `x` forces `x` to be
# represented as a closure cell rather than a local.
def force_x_to_be_a_cell(): # pragma: no cover
return x

# Extract the code object and make sure our assumptions about
# the closure behavior are correct.
co = set_first_cellvar_to.__code__
if co.co_cellvars != ("x",) or co.co_freevars != ():
raise AssertionError # pragma: no cover

# Convert this code object to a code object that sets the
# function's first _freevar_ (not cellvar) to the argument.
args = [co.co_argcount]
args.append(co.co_kwonlyargcount)
args.extend(
[
co.co_nlocals,
co.co_stacksize,
co.co_flags,
co.co_code,
co.co_consts,
co.co_names,
co.co_varnames,
co.co_filename,
co.co_name,
co.co_firstlineno,
co.co_lnotab,
# These two arguments are reversed:
co.co_cellvars,
co.co_freevars,
]
)
set_first_freevar_code = types.CodeType(*args)

def set_closure_cell(cell, value):
# Create a function using the set_first_freevar_code,
# whose first closure cell is `cell`. Calling it will
# change the value of that cell.
setter = types.FunctionType(
set_first_freevar_code, {}, "setter", (), (cell,)
)
# And call it to set the cell.
setter(value)

# Make sure it works on this interpreter:
def make_func_with_cell():
x = None

def func():
return x # pragma: no cover

return func

cell = make_func_with_cell().__closure__[0]
set_closure_cell(cell, 100)
if cell.cell_contents != 100:
raise AssertionError # pragma: no cover

except Exception: # noqa: BLE001
return just_warn
else:
return set_closure_cell


set_closure_cell = make_set_closure_cell()

# Thread-local global to track attrs instances which are already being repr'd.
# This is needed because there is no other (thread-safe) way to pass info
# about the instances that are already being repr'd through the call stack
Expand Down
3 changes: 1 addition & 2 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
PY310,
_AnnotationExtractor,
get_generic_base,
set_closure_cell,
)
from .exceptions import (
DefaultAlreadySetError,
Expand Down Expand Up @@ -909,7 +908,7 @@ def _create_slots_class(self):
pass
else:
if match:
set_closure_cell(cell, cls)
cell.cell_contents = cls

return cls

Expand Down
34 changes: 1 addition & 33 deletions tests/test_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
"""

import pickle
import sys
import types
import weakref

from unittest import mock
Expand All @@ -15,7 +13,7 @@

import attr

from attr._compat import PYPY, just_warn, make_set_closure_cell
from attr._compat import PYPY


# Pympler doesn't work on PyPy.
Expand Down Expand Up @@ -478,36 +476,6 @@ def statmethod():

assert D.statmethod() is D

@pytest.mark.skipif(PYPY, reason="set_closure_cell always works on PyPy")
@pytest.mark.skipif(
sys.version_info >= (3, 8),
reason="can't break CodeType.replace() via monkeypatch",
)
def test_code_hack_failure(self, monkeypatch):
"""
Keeps working if function/code object introspection doesn't work
on this (nonstandard) interpreter.

A warning is emitted that points to the actual code.
"""
# This is a pretty good approximation of the behavior of
# the actual types.CodeType on Brython.
monkeypatch.setattr(types, "CodeType", lambda: None)
func = make_set_closure_cell()

with pytest.warns(RuntimeWarning) as wr:
func()

w = wr.pop()
assert __file__ == w.filename
assert (
"Running interpreter doesn't sufficiently support code object "
"introspection. Some features like bare super() or accessing "
"__class__ will not work with slotted classes.",
) == w.message.args

assert just_warn is func


@pytest.mark.skipif(PYPY, reason="__slots__ only block weakref on CPython")
def test_not_weakrefable():
Expand Down