Skip to content

Commit

Permalink
Move descriptor handling in replace to fix bugs and simplify resolve()
Browse files Browse the repository at this point in the history
  • Loading branch information
cjw296 committed Feb 8, 2023
1 parent 0c480e5 commit bac8fa4
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 21 deletions.
4 changes: 4 additions & 0 deletions testfixtures/replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ def __call__(self, target: Any, replacement: R, strict: bool = True,

if isinstance(resolved.container, type):

# if we have a descriptor, don't accidentally use the result of its __get__ method:
if resolved.name in resolved.container.__dict__:
resolved.found = resolved.container.__dict__[resolved.name]

if not_same_descriptor(resolved.found, replacement, classmethod):
replacement_to_use = classmethod(replacement)

Expand Down
38 changes: 17 additions & 21 deletions testfixtures/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,28 @@ def resolve(dotted_name: str, container: Optional[Any] = None) -> Resolved:
container = found
used += '.' + name
try:
found = found.__dict__[name] # only safe way to get class descriptors
found = getattr(found, name)
setter = setattr
except (AttributeError, KeyError):
except AttributeError:
try:
found = getattr(found, name)
setter = setattr
except AttributeError:
__import__(used)
except ImportError:
setter = setitem
try:
__import__(used)
except ImportError:
setter = setitem
found = found[name] # pragma: no branch
except KeyError:
found = not_there # pragma: no branch
except TypeError:
try:
name = int(name)
except ValueError:
setter = setattr
found = not_there
else:
found = found[name] # pragma: no branch
except KeyError:
found = not_there # pragma: no branch
except TypeError:
try:
name = int(name)
except ValueError:
setter = setattr
found = not_there
else:
found = found[name] # pragma: no branch
else:
found = getattr(found, name)
setter = getattr
else:
found = getattr(found, name)
setter = getattr
return Resolved(container, setter, name, found)


Expand Down
26 changes: 26 additions & 0 deletions testfixtures/tests/test_replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,13 +724,15 @@ class SampleClass:
def method(self, x):
return x*2

original = SampleClass.__dict__['method']
sample = SampleClass()

with Replacer() as replace:
replace.on_class(SampleClass.method, lambda self, x: x*3)
compare(sample.method(1), expected=3)

compare(sample.method(1), expected=2)
assert SampleClass.__dict__['method'] is original

def test_method_on_instance(self):

Expand All @@ -739,6 +741,7 @@ class SampleClass:
def method(self, x):
return x*2

original = SampleClass.__dict__['method']
sample = SampleClass()

with Replacer() as replace:
Expand All @@ -751,6 +754,7 @@ def method(self, x):
compare(sample.method(1), expected=3)

compare(sample.method(1), expected=2)
assert SampleClass.__dict__['method'] is original

def test_badly_decorated_method(self):

Expand All @@ -765,6 +769,7 @@ class SampleClass:
def method(self, x):
return x*2

original = SampleClass.__dict__['method']
sample = SampleClass()

with Replacer() as replace:
Expand All @@ -780,6 +785,7 @@ def method(self, x):
compare(sample.method(1), expected=3)

compare(sample.method(1), expected=2)
assert SampleClass.__dict__['method'] is original

def test_classmethod(self):

Expand All @@ -789,11 +795,14 @@ class SampleClass:
def method(cls, x):
return x*2

original = SampleClass.__dict__['method']

with Replacer() as replace:
replace.on_class(SampleClass.method, classmethod(lambda cls, x: x*3))
compare(SampleClass.method(1), expected=3)

compare(SampleClass.method(1), expected=2)
assert SampleClass.__dict__['method'] is original

def test_staticmethod(self):

Expand All @@ -803,11 +812,14 @@ class SampleClass:
def method(x):
return x*2

original = SampleClass.__dict__['method']

with Replacer() as replace:
replace.on_class(SampleClass.method, lambda x: x*3)
compare(SampleClass.method(1), expected=3)

compare(SampleClass.method(1), expected=2)
assert SampleClass.__dict__['method'] is original

def test_not_callable(self):

Expand All @@ -824,58 +836,72 @@ class SampleClass:

def test_method_on_class_in_module(self):
sample = X()
original = X.__dict__['y']

with Replacer() as replace:
replace.on_class(X.y, lambda self_: 'replacement y')
compare(sample.y(), expected='replacement y')

compare(sample.y(), expected='original y')
assert X.__dict__['y'] is original

def test_method_on_instance_in_module(self):

sample = X()
original = X.__dict__['y']

with Replacer() as replace:
replace(sample.y, lambda: 'replacement y', container=sample, strict=False)
compare(sample.y(), expected='replacement y')

compare(sample.y(), expected='original y')
assert X.__dict__['y'] is original

def test_classmethod_on_class_in_module(self):

original = X.__dict__['aMethod']

with Replacer() as replace:
replace.on_class(X.aMethod, classmethod(lambda cls: (cls, cls)))
compare(X.aMethod(), expected=(X, X))

compare(X.aMethod(), expected=X)
assert X.__dict__['aMethod'] is original

def test_classmethod_on_instance_in_module(self):

sample = X()
original = X.__dict__['aMethod']

with Replacer() as replace:
replace.on_class(sample.aMethod, classmethod(lambda cls: (cls, cls)))
compare(sample.aMethod(), expected=(X, X))

compare(sample.aMethod(), expected=X)
assert X.__dict__['aMethod'] is original

def test_staticmethod_on_class_in_module(self):

original = X.__dict__['bMethod']

with Replacer() as replace:
replace.on_class(X.bMethod, lambda: 3)
compare(X.bMethod(), expected=3)

compare(X.bMethod(), expected=2)
assert X.__dict__['bMethod'] is original

def test_staticmethod_on_instance_in_module(self):

original = X.__dict__['bMethod']
sample = X()

with Replacer() as replace:
replace(sample.bMethod, lambda: 3, container=sample, strict=False)
compare(sample.bMethod(), expected=3)

compare(X.bMethod(), expected=2)
assert X.__dict__['bMethod'] is original


class TestInModule:
Expand Down

0 comments on commit bac8fa4

Please sign in to comment.