2010-08-10 13 views
8

Estoy desarrollando un marco de prueba de documentación, básicamente pruebas unitarias para archivos PDF. Las pruebas son métodos (decorados) de instancias de clases definidas por el marco, y se ubican e instancian en tiempo de ejecución y los métodos se invocan para ejecutar las pruebas.Cómo ingresar a un método Python sin aceptarlo explícitamente

Mi objetivo es reducir la cantidad de sintaxis peculiar de Python que las personas que escribirán las pruebas deben preocuparse, ya que estas personas pueden o no ser programadores de Python, o incluso muy programadores. Así que me gustaría que puedan escribir "def foo():" en lugar de "def foo (self):" para los métodos, pero aún así poder usar "self" para acceder a los miembros.

En un programa normal, consideraría esta una idea horrible, pero en un tipo de programa de dominio específico del idioma como este, parece que vale la pena intentarlo.

He eliminado con éxito el auto de la firma del método usando un decorador (en realidad, ya que estoy usando un decorador para los casos de prueba, simplemente lo enrollaría), pero "sí mismo" no se refiere a cualquier cosa en el método de caso de prueba.

He considerado utilizar un global para mí mismo, e incluso presentar una implementación que más o menos funciona, pero prefiero contaminar el espacio de nombres más pequeño posible, por lo que preferiría inyectar la variable directamente en el espacio de nombres local del método de caso de prueba. ¿Alguna idea?

+10

Simplemente enséñeles que 'def foo (self):' es parte de la plantilla que debe cumplirse en cada función. No te centres en el porqué, solo enfatiza que DEBE estar ahí, y probablemente estés bien. – derekerdmann

+0

¡Probablemente tengas razón, pero todavía estoy interesado en ver lo que se le ocurre a la gente! – kindall

+0

¿Por qué no simplemente hace que sus módulos de clases y sus métodos funcionen en el módulo? Menos repetitivo y cosas para hacer. 'py.test' hace de manera muy efectiva. –

Respuesta

4

Aquí es un decorador método de una línea que parece hacer el trabajo sin modificar ninguna Special attributes of Callable types* marked Read-only:

# method decorator -- makes undeclared 'self' argument available to method 
injectself = lambda f: lambda self: eval(f.func_code, dict(self=self)) 

class TestClass: 
    def __init__(self, thing): 
     self.attr = thing 

    @injectself 
    def method(): 
     print 'in TestClass::method(): self.attr = %r' % self.attr 
     return 42 

test = TestClass("attribute's value") 
ret = test.method() 
print 'return value:', ret 

# output: 
# in TestClass::method(): self.attr = "attribute's value" 
# return value: 42 

Tenga en cuenta que a menos que tome precauciones para evitar que, una side-effect de la función eval() puede ser que la adición de unas pocas entradas - tal como una referencia al módulo __builtin__ en la clave __builtins__ - automáticamente a la dict pasó a él.

@kendall: Por su comentario acerca de cómo está usando esto con los métodos en clases contenedor (pero ignorando la inyección de variables adicionales por el momento) - ¿es lo siguiente algo así como lo que está haciendo? Es difícil para mí entender cómo se dividen las cosas entre el marco y lo que escriben los usuarios. Me parece un patrón de diseño interesante.

# method decorator -- makes undeclared 'self' argument available to method 
injectself = lambda f: lambda self: eval(f.func_code, dict(self=self)) 

class methodclass: 
    def __call__(): 
     print 'in methodclass::__call__(): self.attr = %r' % self.attr 
     return 42 

class TestClass: 
    def __init__(self, thing): 
     self.attr = thing 

    method = injectself(methodclass.__call__) 

test = TestClass("attribute's value") 
ret = test.method() 
print 'return value:', ret 

# output 
# in methodclass::__call__(): self.attr = "attribute's value" 
# return value: 42 
+0

lamentablemente no creo que esto se extienda a los métodos con argumentos (al menos no limpiamente). La solución que he publicado (por mala que sea) lo hará. El problema parece ser una limitación fundamental de eval. – aaronasterling

+0

también, 'func_globals' es de solo lectura en el sentido de que no se puede asignar, es decir, hecho para apuntar a un dict diferente. Puede ser claramente modificado. – aaronasterling

+0

Este es un enfoque interesante, pero si pasas tu propio dictado de globales, no puedes acceder a ningún otro globo terráqueo. Intenté pasar a los locales, pero eso no funcionó en absoluto. Podría crear una copia de 'func_globals' de la función y actualizarla con mis nuevas variables, supongo, y pasar eso. – kindall

2

Esto podría ser un caso de uso para decorators - les da un pequeño conjunto de ladrillos lego para construir funciones, y el complicado material del marco se conecta a través de @testcase o somesuch.

Editar: No ha publicado ningún código, por lo que esto va a ser incompleto, pero no es necesario escribir métodos. Pueden escribir funciones ordinarias sin "yo", y se puede utilizar decoradores como en este ejemplo del artículo de vinculé:

class myDecorator(object): 

    def __init__(self, f): 
     print "inside myDecorator.__init__()" 
     f() # Prove that function definition has completed 

    def __call__(self): 
     print "inside myDecorator.__call__()" 

@myDecorator 
def aFunction(): 
    print "inside aFunction()" 
+0

Sí, ya uso decoradores para marcar métodos como casos de prueba, porque necesito saber 1) qué métodos ejecutar y 2) qué orden ejecutarlos. La mayor parte del marco está funcionando; Solo quiero deshacerme de ese molesto "yo". – kindall

+0

Editado, dio ejemplo de cómo usar decoradores sin la necesidad de usar 'self' en la función. – chryss

+0

pero uno mismo necesita referirse a algo dentro de las funciones definidas. esto no logra eso – aaronasterling

5

Mi respuesta aceptada a esta pregunta fue bastante tonto, pero yo estaba empezando a cabo. Aquí hay una manera mucho mejor. Esto solo se prueba escasamente, pero es bueno para una demostración de la forma correcta de hacer esto que no es adecuada. Funciona en 2.6.5 seguro. No he probado ninguna otra versión, pero los códigos de operación no están codificados, por lo que debería ser tan portátil como la mayoría de los otros códigos 2.x.

add_self se puede aplicar como decorador, pero eso sería contrario al propósito (¿por qué no simplemente escriba 'self'?) Sería fácil adaptar la metaclase de mi otra respuesta para aplicar esta función.

import opcode 
import types 



def instructions(code): 
    """Iterates over a code string yielding integer [op, arg] pairs 

    If the opcode does not take an argument, just put None in the second part 
    """ 
    code = map(ord, code) 
    i, L = 0, len(code) 
    extended_arg = 0 
    while i < L: 
     op = code[i] 
     i+= 1 
     if op < opcode.HAVE_ARGUMENT: 
      yield [op, None] 
      continue 
     oparg = code[i] + (code[i+1] << 8) + extended_arg 
     extended_arg = 0 
     i += 2 
     if op == opcode.EXTENDED_ARG: 
      extended_arg = oparg << 16 
      continue 
     yield [op, oparg] 


def write_instruction(inst): 
    """Takes an integer [op, arg] pair and returns a list of character bytecodes""" 
    op, oparg = inst 
    if oparg is None: 
     return [chr(op)] 
    elif oparg <= 65536L: 
     return [chr(op), chr(oparg & 255), chr((oparg >> 8) & 255)] 
    elif oparg <= 4294967296L: 
     # The argument is large enough to need 4 bytes and the EXTENDED_ARG opcode 
     return [chr(opcode.EXTENDED_ARG), 
       chr((oparg >> 16) & 255), 
       chr((oparg >> 24) & 255), 
       chr(op), 
       chr(oparg & 255), 
       chr((oparg >> 8) & 255)] 
    else: 
     raise ValueError("Invalid oparg: {0} is too large".format(oparg)) 


def add_self(f): 
    """Add self to a method 

    Creates a new function by prepending the name 'self' to co_varnames, and  
    incrementing co_argcount and co_nlocals. Increase the index of all other locals 
    by 1 to compensate. Also removes 'self' from co_names and decrease the index of 
    all names that occur after it by 1. Finally, replace all occurrences of 
    `LOAD_GLOBAL i,j` that make reference to the old 'self' with 'LOAD_FAST 0,0'. 

    Essentially, just create a code object that is exactly the same but has one more 
    argument. 
    """ 
    code_obj = f.func_code 
    try: 
     self_index = code_obj.co_names.index('self') 
    except ValueError: 
     raise NotImplementedError("self is not a global") 

    # The arguments are just the first co_argcount co_varnames 
    varnames = ('self',) + code_obj.co_varnames 
    names = tuple(name for name in code_obj.co_names if name != 'self') 

    code = [] 

    for inst in instructions(code_obj.co_code): 
     op = inst[0] 
     if op in opcode.haslocal: 
      # The index is now one greater because we added 'self' at the head of 
      # the tuple 
      inst[1] += 1 
     elif op in opcode.hasname: 
      arg = inst[1] 
      if arg == self_index: 
       # This refers to the old global 'self' 
       if op == opcode.opmap['LOAD_GLOBAL']: 
        inst[0] = opcode.opmap['LOAD_FAST'] 
        inst[1] = 0 
       else: 
        # If `self` is used as an attribute, real global, module 
        # name, module attribute, or gets looked at funny, bail out. 
        raise NotImplementedError("Abnormal use of self") 
      elif arg > self_index: 
       # This rewrites the index to account for the old global 'self' 
       # having been removed. 
       inst[1] -= 1 

     code += write_instruction(inst) 

    code = ''.join(code) 

    # type help(types.CodeType) at the interpreter prompt for this one 
    new_code_obj = types.CodeType(code_obj.co_argcount + 1, 
            code_obj.co_nlocals + 1, 
            code_obj.co_stacksize, 
            code_obj.co_flags, 
            code, 
            code_obj.co_consts, 
            names, 
            varnames, 
            '<OpcodeCity>', 
            code_obj.co_name, 
            code_obj.co_firstlineno, 
            code_obj.co_lnotab, 
            code_obj.co_freevars, 
            code_obj.co_cellvars) 


    # help(types.FunctionType) 
    return types.FunctionType(new_code_obj, f.func_globals) 



class Test(object): 

    msg = 'Foo' 

    @add_self 
    def show(msg): 
     print self.msg + msg 


t = Test() 
t.show('Bar') 
+0

Aaron, su sugerencia original funciona bien para mi caso de uso (solo habrá una instancia de una clase determinada a la vez), pero siempre es bueno ver una manera de hacerlo mejor. Más al grano, estoy seguro de que aprenderé mucho sobre qué diablos has hecho aquí. Estoy bastante sorprendido e impresionado de que tanto tú como Martineau hayan seguido mordiendo este problema. :-) – kindall

+0

@Kindall Al hacerlo así, se me ocurrió cuando resolví [este problema] (http://stackoverflow.com/questions/3908335/python-function-local-name-binding-from-an-outer-scope/3913185 # 3913185). Esta es una aplicación bastante obvia. Había estado perezosa y luego Martineau comenzó a prestar atención al hilo. – aaronasterling

+0

@Kindall, actualizó los comentarios. No me había dado cuenta de lo miserables que eran. Debería ser más fácil de entender ahora. – aaronasterling

3

El truco es añadir 'yo' a f.func_globals. Esto funciona en python2.6. Realmente debería instalar otras versiones para probar cosas como esta. Perdón por el muro de código, pero cubro dos casos: hacerlo con una metaclase y hacerlo con un decorador. Para su caso de uso, creo que la metaclase es mejor ya que el objetivo de este ejercicio es proteger a los usuarios de la sintaxis.

import new, functools 

class TestMeta(type): 
    def __new__(meta, classname, bases, classdict): 
     for item in classdict: 
      if hasattr(classdict[item], '__call__'): 
       classdict[item] = wrap(classdict[item]) 
     return type.__new__(meta, classname, bases, classdict) 

def wrap(f): 
    @functools.wraps(f) 
    def wrapper(self): 
     f.func_globals['self'] = self   
     return f() 
    return wrapper 

def testdec(f): 
    @functools.wraps(f) 
    def wrapper(): 
     return f() 
    return wrapper 

class Test(object): 
    __metaclass__ = TestMeta 
    message = 'You can do anything in python' 
    def test(): 
     print self.message 

    @testdec 
    def test2(): 
     print self.message + ' but the wrapper funcion can\'t take a self argument either or you get a TypeError' 

class Test2(object): 
    message = 'It also works as a decorator but (to me at least) feels better as a metaclass' 
    @wrap 
    def test(): 
     print self.message 


t = Test() 
t2 = Test2() 
t.test() 
t.test2() 
t2.test() 
+0

¡Gracias, parece casi exactamente lo que necesitaba saber! – kindall

+0

Bueno ... o más bien me hace doler la cabeza. En el buen sentido. – chryss

+0

¿Qué tal eso parece ignorar el hecho de que 'func_globals' es un atributo especial de tipo que se puede llamar de solo lectura según el [docs] (http://docs.python.org/reference/datamodel.html?highlight=func_globals # the-standard-type-hierarchy)? ¿O eso significa que el atributo en sí mismo es de solo lectura pero no el contenido de lo que se refiere? – martineau

4

pequeña mejora para la solución de aaronasterling (No tengo la reputación suficiente para comentarlo):

def wrap(f): 
    @functools.wraps(f) 
    def wrapper(self,*arg,**kw): 
     f.func_globals['self'] = self   
     return f(*arg,**kw) 
    return wrapper 

pero tanto estas soluciones a trabajar impredecible si la función f se llama de forma recursiva para diferentes instancia, por lo usted tiene que clonar esta manera:

import types 
class wrap(object): 
    def __init__(self,func): 
     self.func = func 
    def __get__(self,obj,type): 
     new_globals = self.func.func_globals.copy() 
     new_globals['self'] = obj 
     return types.FunctionType(self.func.func_code,new_globals) 
class C(object): 
    def __init__(self,word): 
     self.greeting = word 
    @wrap 
    def greet(name): 
     print(self.greeting+' , ' + name+ '!') 
C('Hello').greet('kindall') 
+0

Bonito adorno, gracias. Lástima que solo puedas marcar una mejor respuesta. – kindall

+0

Creo que tanto esta versión como el original de @aaronasterling podrían tener problemas si alguna vez se llamara un método similarmente envuelto de otra instancia de clase del mismo módulo, ya que el enlace "self" global se cambiaría y no se restauraría antes. devoluciones. – martineau

Cuestiones relacionadas