AttributeError 'DefaultMemberAdapter' object has no attribute '

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

Bonsoir à tous,

Je m'en remets à vous pour un problème que je ne comprends pas. Je tenté de simplifier mon problème pour que vous puissiez le comprendre facilement.

J'ai un fichier adapters.py avec une classe et une méthode :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class DefaultMemberAdapter(object):
    def get_login_redirect_url(self, request, url=None, redirect_field_name="next"):
        pass

    def perform_login(self, request, user, redirect_url):
        pass

    def perform_logout(self, request):
        pass


def get_adapter():
    """
    Gets the right adapter according to the value specified
    in the settings of the member module.
    """
    return import_attribute(app_settings.ADAPTER)()

Je n'utilise jamais directement la classe DefaultMemberAdapter au profit de la méthode get_adapter qui va m'importer ma classe. Dans mes fichiers qui veulent utiliser l'adaptateur, je l'importe comme suit : from .adapters import get_adapter.

Le problème c'est que je parviens à accéder aux méthodes perform_login et get_login_redirect_url sans problème mais impossible d'accéder à la méthode perform_logout. Lorsque je tente d'y accéder, j'ai l'erreur suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Traceback (most recent call last):
  File "/Users/Gerard/.virtualenvs/zdsenv/lib/python2.7/site-packages/django/core/handlers/base.py", line 112, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/Gerard/.virtualenvs/zdsenv/lib/python2.7/site-packages/django/views/generic/base.py", line 69, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/Gerard/.virtualenvs/zdsenv/lib/python2.7/site-packages/django/views/generic/base.py", line 87, in dispatch
    return handler(request, *args, **kwargs)
  File "/Users/Gerard/Documents/workspace-django/zds-site/zds/member/views.py", line 666, in post
    return get_adapter().perform_logout(self.request)
AttributeError: 'DefaultMemberAdapter' object has no attribute 'perform_logout'

Si quelqu'un à une idée, je suis preneur parce que je commence à sécher.

Une information avec son importante : J'ai exécuter les instructions suivantes.

1
2
print dir(DefaultMemberAdapter)
print dir(get_adapter())

Le résultat du premier :

1
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'get_login_redirect_url', 'perform_login', 'perform_logout`]

Le résultat du second :

1
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'get_login_redirect_url', 'perform_login']

J'ai fais exprès d'omettre ces informations pour rester simple. Voici les informations que tu me demandes :

J'ai d'abord une classe Settings dans settings.py où je définis toutes les propriétés nécessaires de mon module des membres.

 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
# coding: utf-8

from django.conf import settings


class Settings(object):
    def _setting(self, name, default_value):
        getter = getattr(settings,
                         'MEMBER_SETTING_GETTER',
                         lambda name, default_value: getattr(settings, name, default_value))
        return getter(name, default_value)

    @property
    def ADAPTER(self):
        return self._setting('ADAPTER', 'zds.member.adapter.DefaultMemberAdapter')

    @property
    def LOGIN_REDIRECT_URL(self):
        return self._setting('LOGIN_REDIRECT_URL', 'zds.pages.views.home')

    @property
    def FORMS(self):
        return self._setting('FORMS', {})


app_settings = Settings()

Dans un fichier qui regroupe des fonctions utilitaires, j'ai la méthode import_attribute :

1
2
3
4
5
def import_attribute(path):
    assert isinstance(path, six.string_types)
    pkg, attr = path.rsplit('.', 1)
    ret = getattr(importlib.import_module(pkg), attr)
    return ret

Alors je ne comprends pas encore tout, et il est un poil tard pour faire des tests =)

Mais si j'étais toi, je mettrai des print(dir(...)) de partout pour voir à quel endroit parmi tout ces getattr imbriqués la méthode disparaît. A l'instinct je dirais ici : getattr(importlib.import_module(pkg), attr) parce que c'est quand même violent comme manière de récupérer un objet ^^ Mais mon instinct n'est pas très bon pour ce genre de trucs …

Du coup, est-ce que en le découpant en plusieurs bout ça plante toujours ?

1
2
3
4
5
6
7
def import_attribute(path):
    assert isinstance(path, six.string_types)
    pkg, attr = path.rsplit('.', 1)
    module = importlib.import_module(pkg)
    print dir(module.ADAPTER)
    ret = getattr(module, attr)
    return ret
+0 -0

J'ai pas accès au code là mais ADAPTER ne fait pas parti du module importé par importlib.import_module(pkg). Il me semble qu'il a fallu que je fasse module.app_settings.ADAPTER. Après, j'ai du mal à comprendre l'intérêt de la modification dans la méthode.

Je n'arrive pas a reproduire ce bug chez moi. J'ai créé un projet django tout neuf, appelé project, et dedans une application app. J'ai ajouté la ligne

1
ADAPTER = 'app.adapters.DefaultMemberAdapter'

à la fin de project/settings.py.

Puis j'ai mis les fichiers suivants dans app :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# app/adapters.py
from .utils import import_attribute
from .app_settings import app_settings

class DefaultMemberAdapter(object):
    def get_login_redirect_url(self, request, url=None, redirect_field_name="next"):
        pass

    def perform_login(self, request, user, redirect_url):
        pass

    def perform_logout(self, request):
        pass


def get_adapter():
    """
    Gets the right adapter according to the value specified
    in the settings of the member module.
    """
    return import_attribute(app_settings.ADAPTER)()
 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
# app/app_settings.py
# coding: utf-8

from django.conf import settings

class Settings(object):
    def _setting(self, name, default_value):
        getter = getattr(settings,
                         'MEMBER_SETTING_GETTER',
                         lambda name, default_value: getattr(settings, name, default_value))
        return getter(name, default_value)

    @property
    def ADAPTER(self):
        return self._setting('ADAPTER', 'zds.member.adapter.DefaultMemberAdapter')

    @property
    def LOGIN_REDIRECT_URL(self):
        return self._setting('LOGIN_REDIRECT_URL', 'zds.pages.views.home')

    @property
    def FORMS(self):
        return self._setting('FORMS', {})


app_settings = Settings()
1
2
3
4
5
6
7
8
9
# app/utils.py

import six, importlib

def import_attribute(path):
    assert isinstance(path, six.string_types)
    pkg, attr = path.rsplit('.', 1)
    ret = getattr(importlib.import_module(pkg), attr)
    return ret

Puis dans un ./manage.py shell

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> from app.adapters import get_adapter
>>> adapter = get_adapter()
>>> dir(adapter)
['__class__',
 '__delattr__',
 '__dict__',
 '__doc__',
 '__format__',
 '__getattribute__',
 '__hash__',
 '__init__',
 '__module__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'get_login_redirect_url',
 'perform_login',
 'perform_logout']

Mon python est en version 2.7.8.

Sinon, il est sans doutes possible de prendre le système à l'envers : que cherches-tu à faire ?

+0 -0

Si je fais exactement, les mêmes commandes que toi dans le shell, j'obtiens bien le bug :

1
2
3
4
>>> from zds.member.adapter import get_adapter
>>> adapter = get_adapter()
>>> dir (adapter)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'get_login_redirect_url', 'perform_login']

Par contre, je ne comprends pas comme ça peut marcher chez toi :

  • Pourquoi rajouter ADAPTER = 'app.adapters.DefaultMemberAdapter dans ton project/settings.py ?
  • La propriété ADAPTER dans ta classe Settings référence un module qui n'existe pas puisque tu as créé app et non pas zds.member. Comment ça se fait qu'il parvient à retrouver le bon ADAPTER dans ton shell ?

Pour répondre à ta question, je tente simplement d'abstraire l'adapter pour pouvoir l'interchanger facilement en renseignant un autre adapter si nécessaire.

Comment ça se fait qu'il parvient à retrouver le bon ADAPTER dans ton shell ?

Parce que ces lignes de code vont le chercher :

1
2
3
4
getter = getattr(settings,
                         'MEMBER_SETTING_GETTER',
                         lambda name, default_value: getattr(settings, name, default_value))
        return getter(name, default_value)

getter commence par chercher un attribut MEMBER_SETTING_GETTER dans settings, n'en trouve pas, et se réfère donc à la valeur par défaut lambda name, default_value: getattr(settings, name, default_value). Ici, name vaut ADAPTER, et c'est bien la bonne classe qui est retournée.

Mais quand je vois le nombre d'indirection ici, je me dis qu'un truc beaucoup plus simple a base de dictionnaire ferait aussi l'affaire, non ? Du style

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# settings.py
MY_SETTINGS = {
    "ADAPTER": MaClasseDejaImportée,
    "LOGIN_REDIRECT_URL": "une.chaine.ça.marche.Aussi"
}

#utils.py
from django.conf import settings
def get_adapter():
    return _get_class(settings.MY_SETTINGS["ADAPTER"], "zds...DefaultMemberAdapter")

def _get_class(value, default=None):
   if isinstance(value, type):
      return value
   if isinstance(value, basestring):
      try:
         return __import__(value)
      except ImportError:
         if default is not None:
            return get_class(default)
         else:
            raise
+0 -0

Bien, hier soir j'ai dégagé un peu de temps pour pouvoir me pencher sur ce problème et j'ai trouvé la solution.

D'abord, merci Luthaf. Effectivement, utiliser un dictionnaire aurait été la solution si je n'étais pas parvenu à faire quelque chose de plus propre. Du coup, je l'ai quand même tenté quand ça fonctionnait pas encore et le problème persistait. Je comprenais vraiment pas alors j'ai vérifié l'orthographe … Voilà, j'ai honte il manquait un "s" à "adapter" dans "zds.member.adapter.DefaultMemberAdapter".

Vous voyez le coin sombre là bas ? J'y suis aller pour pleurer toute la nuit d'hier …

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