views:

45

answers:

2

Given an object A, which contains a callable object B, is there a way to determine "A" from inside B.__call__()?

This is for use in test injection, where A.B is originally a method.

The code is something like this:

# Class to be tested.
class Outer(object):
  def __init__(self, arg):
    self.arg = arg
  def func(self, *args, **kwargs):
    print ('Outer self: %r' % (self,))

# Framework-provided:
class Injected(object):
  def __init__(self):
    self._replay = False
    self._calls = []
  def replay(self):
    self._replay = True
    self._calls.reverse()
  def __call__(self, *args, **kwargs):
    if not self._replay:
      expected = (args[0], args[1:], kwargs)
      self._calls.append(expected)
    else:
      expected_s, expected_a, expected_k = self._calls.pop()
      ok = True
      # verify args, similar for expected_k == kwargs
      ok = reduce(lambda x, comp: x and comp[0](comp[1]),
                  zip(expected_a, args),
                  ok)
      # what to do for expected_s == self?
      # ok = ok and expected_s(this) ### <= need "this". ###
      if not ok:
        raise Exception('Expectations not matched.')

# Inject:
setattr(Outer, 'func', Injected())
# Set expectations:
# - One object, initialised with "3", must pass "here" as 2nd arg.
# - One object, initialised with "4", must pass "whatever" as 1st arg.
Outer.func(lambda x: x.arg == 3, lambda x: True, lambda x: x=='here')
Outer.func(lambda x: x.arg == 4, lambda x: x=='whatever', lambda x: True)
Outer.func.replay()
# Invoke other code, which ends up doing:
o = Outer(3)
p = Outer(5)
o.func('something', 'here')
p.func('whatever', 'next')  # <- This should fail, 5 != 4

The question is: Is there a way (black magic is fine) within Injected.__call__() to access what "self" would have been in non-overwritten Outer.func(), to use as "this" (line marked with "###")?

Naturally, the code is a bit more complex (the calls can be configured to be in arbitrary order, return values can be set, etc.), but this is the minimal example I could come up with that demonstrates both the problem and most of the constraints.

I cannot inject a function with a default argument instead of Outer.func - that breaks recording (if a function were injected, it'd be an unbound method and require an instance of "Outer" as its first argument, rather than a comparator/validator).

Theoretically, I could mock out "Outer" completely for its caller. However, setting up the expectations there would probably be more code, and not reusable elsewhere - any similar case/project would also have to reimplement "Outer" (or its equivalent) as a mock.

+2  A: 

Given an object A, which contains a callable object B, is there a way to determine "A" from inside B.call()?

Not in the general case, i.e., with the unbounded generality you require in this text -- unless B keeps a reference to A in some way, Python most surely doesn't keep it on B's behalf.

For your intended use case, it's even worse: it wouldn't help even if B itself did keep a reference to A (as a method object would to its class), since you're trampling all over B without recourse with that setattr, which is equivalent to an assignment. There is no trace left in class Outer that, back in happier times, it had a func attribute with certain characteristics: that attribute is no more, obliterated by your setattr.

This isn't really dependency injection (a design pattern requiring cooperation from the injected-into object), it's monkey patching, one of my pet peeves, and its destructive, non-recoverable nature (that you're currently struggling with) is part of why I peeve about it.

Alex Martelli
You're right, it's not dependency injection, it's stubbing out. Anyway, it seems to be as hard as I feared. I'd need to replace the method *and* would need to replace `__init__` to replace the method again at instantiation, this time with a proxying method which would pass the containing object to `B`... doable, but not maintainable, so best left as a Gedankenexperiment.
Gabe
A: 

Perhaps you want the Python descriptor protocol?

llasram