Décorateur de classe et arguments

ou alors un autre pattern

Le problème exposé dans ce sujet a été résolu.

Bonjour,

je suis en train de créer un outil qui utilise selenium pour faire des tests. Dans le cadre de cet outil, je voudrais ajouter un certains nombres de fonctionnalités qui peuvent se passer lors du succès ou de l'échec du test.

Ces opérations sont configurables statiquement, c'est à dire qu'on peut –par classe de test– définir les opérations qui seront réalisées ou non.

Pour faire simple, on se réduira pour l'instant à une seule opération : la prise de screenshot, qu'on peut coder ainsi :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class TestCallback:
    """
    This represent action that will be executed at the end of a test
    """
    def on_success(self, test):
        pass

    def on_failure(self, test):
        pass

    def on_error(self, test):
        pass

    def on_skip(self, test):
        pass


class TakeScreenshotCallback(TestCallback):
    def __init__(self, filepath_format):
        self.filepath_format = filepath_format

    def on_failure(self, test):
        if hasattr(test, "driver"):
            test.driver.save_screenshot(self.filepath_format.format(**{
                "time": int(time.time()),
                "name": test.__class__.__name__,
                "status": "fail"
            }))

    def on_error(self, test):
        if hasattr(test, "driver"):
            test.driver.save_screenshot(self.filepath_format.format(**{
                "time": int(time.time()),
                "name": test.id(),
                "status": "fail"

Seulement voilà, j'aimerai pouvoir ajouter facilement cette capacité à mes tests.

Bien que je n'ai qu'une maîtrise relative du concept, j'ai (peut être trop) rapidement pensé aux décorateurs, mon idée serait alors de dire :

1
2
3
4
@with_callback(TakeScreenshotCallback("/var/log/screenshots/{name}.png"))
class MonTest(TestSelenium):
    def test_smth(self):
        pass

Sachant que la classe TestSelenium est liée à une instance de TestResult qui s'assure qu'on a bien un appel aux callbacks.

Actuellement, mon décorateur ressemble à ce qui suit :

1
2
3
4
5
6
7
8
def with_callback(cls, callback):
    class NewTest(cls):
        def __init__(self, *args, **kwargs):
            super(NewTest, self).__init__(*args, **kwargs)
            if not getattr(self, "callbacks"):
                self.callbacks = []
            self.callbacks.append(callback)
    return NewTest

Seul problème : quand je fais @with_callback(TakeScreenshotCallback("/var/log/screenshots/{name}.png")), python m'engueule car en donnant un argument à with_callback.

Ma question est donc : comment régler ce problème? Est-ce au moins possible avec les décorateurs? Est-ce au moins souhaitable de le faire avec des décorateurs? Toute autre idée m'intéresse.

Si ton décorateur prend un argument, il faut ajouter une couche de plus :

1
2
3
4
5
6
def decorateur(arg1, arg2):
    def deco(func):
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
     return deco
+4 -0

edit: mince grillé.

Ton problème viens de ta supposition sur comment fonctionne les décorateur avec arguments qui prendraient a la fois la classe et les paramètres.

En réalité si ton call-back prend des argument, il ne doit prendre que eux et fournir un nouveau décorateur qui va faire la transformation réel. Ça devient vite tordu mais ça devrait marcher :

1
2
3
4
5
6
7
# ça c'est le décorateur avec argument
def with_callback(callback):
    le décorateur réel
    def inner_decorator(cls):
        # ton code
        return NewTest
    return inner_decorator
+2 -0

Salut,

Peut être que j'ai pas bien compris, mais si tu veux pouvoir appliquer ton décorateur comme ça:

1
2
3
4
@with_callback(TakeScreenshotCallback("/var/log/screenshots/{name}.png"))
class MonTest(TestSelenium):
    def test_smth(self):
        pass

… ton décorateur with_callback devrait ressembler à quelque chose comme ça:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def with_callback(callback):
    def _class_wrapper(cls):
        class NewTest(cls):
            def __init__(self, *args, **kwargs):
                super(NewTest, self).__init__(*args, **kwargs)
                if not getattr(self, "callbacks"):
                    self.callbacks = []
                self.callbacks.append(callback)
        return NewTest
    return _class_wrapper

Je connais pas du tout Selenium, mais en théorie ça devrait être ça.

Par contre y'a d'autres trucs bizarres dans ton code:

  • Pourquoi tu fais du unpacking sur un dictionnaire litéral ?
1
2
3
4
5
test.driver.save_screenshot(self.filepath_format.format(**{
                "time": int(time.time()),
                "name": test.__class__.__name__,
                "status": "fail"
            }))

peut s'écrire plus simplement:

1
2
3
4
test.driver.save_screenshot(self.filepath_format.format(
                time=int(time.time()),
                name=test.__class__.__name__,
                status="fail"))
  • Pourquoi tu utilises getattr avec un string literal en second argument ?

getattr(objet, 'callbacks') est strictement équivalent à objet.callbacks.

  • Ton code dans NewTest.__init__ va forcément lever une exception si la classe ne définit pas un attribut callbacks parce que si tu ne donne pas de troisième argument à getattr, ça lève une exception si l'attribut n'est pas trouvé. Personnellement j'écrirai ça comme ça:
1
2
3
4
if hasattr(self, 'callbacks'):
    self.callbacks.append(callback)
else:
    self.callbacks = []

Edit: je me suis fait devancé mais je poste quand même pour les autres trucs bizarres dans ton code.

+1 -0

@AlphaZest : pour la partie getattr, c'est une typo, merci de me l'avoir montrée.

@everyoneelse : Merci pour le coup de main. Je passe en résolu.

peur s'écrire plus simplement:

car ce dico est écrit en litéral que pendant les tests. D'ailleurs, il y a des choses que je changerai. L'important c'était la partie décorateur en fait.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte