0

I periodically find myself attempting to slightly abuse some of the dinamicity allowed by python (I'm using python3 here, but there shouldn't be many differences).

In this case, I wanted to split a single test_ method in my unittest.TestCase, to several methods created at runtime.

(this was a Kata about roman numerals, but I actually didn't TDD: I wrote the test later)

This is the test:

class TestNumbers(TestCase):
    def test_number(self):
            for i in range(3000):
                    self.assertEqual(i, roman_to_int(int_to_roman(i)))

this is how I tried to write it:

from functools import partial
from types import MethodType

class TestNumbers(TestCase):
    pass

def test_number(self, n):
    self.assertEqual(n, roman_to_int(int_to_roman(n)))

for i in range(3000):
    name = str(i)
    test_method = MethodType(partial(test_number, n=i), TestNumbers)
    setattr(TestNumbers, "test" + name, test_method)

(alternatively, I also tried to dinamically create lots of TestCase subclasses and setattr(globals(), ...) them)

I know: this doesn't really has much purpose, it's also probably slower, etc.etc. but this is just a POC and I'm trying to understand how I can get it to work

by using MethodType, the test becomes a bound method, but inside, assertEqual apparently becomes a function, and when trying to call it it'll fail with TypeError: assertEqual() takes at least 3 arguments (2 given)

I tried to change test_number to

def test_number(self, n):
    self.assertEqual(self, n, roman_to_int(int_to_roman(n)))

but this will only unearth the same problem deeper in hidden TestCase methods: TypeError: _getAssertEqualityFunc() takes exactly 3 arguments (2 given)

I looked here on stackoverflow and found similar questions (like Python: Bind an Unbound Method? ), but none of those deal with binding a method that inside of it calls other bound methods of the target class

I also tried to look into metaclasses ( http://docs.python.org/py3k/reference/datamodel.html#customizing-class-creation ) but it doesn't seem to match with what I'm trying to do

Community
  • 1
  • 1
berdario
  • 1,733
  • 15
  • 28

2 Answers2

1

If you're adding the method directly to the class then there's no need to bind it yourself.

class C(object):
  def __init__(self):
    self.foo = 42

def getfoo(self):
  return self.foo

C.getfoo = getfoo
c=C()
print(c.getfoo())
Ignacio Vazquez-Abrams
  • 699,552
  • 132
  • 1,235
  • 1,283
  • mhn, yes... even If I'm calling it from a subclass and dinamically setting its name, it works http://paste.pocoo.org/show/584190/ but when trying to do it on unittest.TestCase it'll fail... maybe it's due to the way the testrunner instantiates the classes? I'll look into it later and update the question eventually – berdario Apr 19 '12 at 18:04
1

On Python 2 there are functions, unbound and bound methods. Binding a method to a class as a instance doesn't make it an unbound method, is makes it the equivalent of a classmethod or a metaclass-method.

On Python 3 there are no longer bound and unbound methods, just functions and methods, so if you're getting assertEqual as a function, it means your testx method is not being bound to the instance and that's the real problem.

On Python 2 all you have to do is assign None to the instance on the MethodType call and it would work.

So, replace:

test_method = MethodType(partial(test_number, n=i), TestNumbers)

For:

test_method = MethodType(partial(test_number, n=i), None, TestNumbers)

On Python 3 just assigning the function to the class would work, like the other answer suggests, but the real issue in your case is that partial objects don't become methods.

An easy solution for your case is to use lambda instead of partial.

Instead of:

test_method = MethodType(partial(test_number, n=i), TestNumbers)

Use:

test_method = lambda self: test_number(self, i)

And it should work...

The real neat solution would be to rewrite partial in order to return a real function with the parameters you want. You can create a instance of function with everything from the older one and the extra default argument. Something like this:

code = test_number.__code__
globals = test_number.__globals__
closure = test_number.__closure__

testx = FunctionType(code, globals, "test"+name, (i,), closure)
setattr(TestNumbers, "test" + name, testx)
Pedro Werneck
  • 38,032
  • 6
  • 53
  • 74
  • assertEqual **does** become a function apparently: http://paste.pocoo.org/show/584485/ and in python3 MethodType only has 2 arguments: http://paste.pocoo.org/show/584486/ – berdario Apr 20 '12 at 08:30
  • On Python 3 there are no unbound and bound methods, just methods. I changed the answer to fit accordingly. – Pedro Werneck Apr 20 '12 at 14:21
  • _partial objects don't become methods_ I totally didn't think about that! thanks :D – berdario Apr 20 '12 at 14:45