• BLOG

  • Upgrade vers Django 2.0 - Retour d'expérience (FR)

    lun. 25 décembre 2017 Dan Lousqui

    Share on: Twitter - Facebook - Google+

    django

    Pas besoin de présenter Django, framework web "usine à gaz" en Python ne comprenant qu'entre autre toute la panoplie pour faire une application web (routing, vue, ...), un ORM, moteur de template, gestion de formulaire, j'en passe et des meilleurs.

    Chez Seald, nous utilisons Django pour servir notre backend applicatif. Et comme nous aimons faire les choses bien, nous mettons souvent à jour nos bibliothèques ! Ce fût le cas dernièrement pour Django, qui vient de passer à sa version 2.0. Voici le récit de mon périple.

    Quoi de neuf Django ?

    Si nous regardons le changelog (Django 2.0 release notes), les principales nouveautés sont :

    • Abandon du support de Python 2.7: Super, nous utilisons Python 3.6 !
    • Nouvelle syntaxe pour l'écriture des routes d'URL: On reste rétro compatible avec l'ancienne syntaxe, pas de soucis.
    • contrib.admin compatible mobile: Chouette... nous ne l'utilisons pas...
    • Window expressions: Chouette... nous ne l'utilisons pas...

    Bref, à part l'abandon de compatibilité avec Python 2.7, rien de bien neuf. L'upgrade devrait se faire sans soucis.

    Bumping Django version to 2.0

    On prend le projet, on va dans le fichier requirements.txt, et on passe de 1.11.7 à 2.0 (Ca fait toujours bizarre de mettre une version "ronde" dans un requirement).

    Comme les choses sont bien faites, l'intégration continue devrait vite me péter au visage si des choses manquent. Et toujours comme les choses sont bien faites, les ne tests unitaires devraient également pas passer si la version 2.0 n'est pas compatible avec notre application.

    Et là... c'est le drame.

    1

    La cascade des migrations

    Donc l'intégration continue se met en marche :

    • Etape 1: On récupère les sources du projet: Ok !
    • Etape 2: On lance les migrations: KO :(
    $ python3 manage.py migrate
    Traceback (most recent call last):
      File "manage.py", line 10, in <module>
        execute_from_command_line(sys.argv)
      File "/usr/local/lib/python3.6/site-packages/django/core/management/__init__.py", line 371, in execute_from_command_line
        utility.execute()
      File "/usr/local/lib/python3.6/site-packages/django/core/management/__init__.py", line 347, in execute
        django.setup()
      File "/usr/local/lib/python3.6/site-packages/django/__init__.py", line 24, in setup
        apps.populate(settings.INSTALLED_APPS)
      File "/usr/local/lib/python3.6/site-packages/django/apps/registry.py", line 112, in populate
        app_config.import_models()
      File "/usr/local/lib/python3.6/site-packages/django/apps/config.py", line 198, in import_models
        self.models_module = import_module(models_module_name)
      File "/usr/local/lib/python3.6/importlib/__init__.py", line 126, in import_module
        return _bootstrap._gcd_import(name[level:], package, level)
      File "<frozen importlib._bootstrap>", line 994, in _gcd_import
      File "<frozen importlib._bootstrap>", line 971, in _find_and_load
      File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
      File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
      File "<frozen importlib._bootstrap_external>", line 678, in exec_module
      File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
      File "/builds/XXXX/XXXX/XXXX/models.py", line 18, in <module>
        class XXXX(models.Model):
      File "/builds/XXXX/XXXX/XXXX/models.py", line 21, in XXXX
        XXXX = models.ForeignKey(XXXX)
    TypeError: __init__() missing 1 required positional argument: 'on_delete'
    ERROR: Job failed: exit code 1
    

    Super... C'est quoi ça ? Un souci avec on_delete apparemment. Allons dans le changelog, voir si on en parle.

    The on_delete argument for ForeignKey and OneToOneField is now required in models and migrations. Consider squashing migrations so that you have fewer of them to update.

    Chouette ! required in models and migrations ! Autant pour les modèles, c'est pas trop un souci, il me suffit d'ajouter un on_delete=models.CASCADE ou on_delete=models.SET_NULL pour chaque ForeignKey, par contre pour les migrations... on me demande de les modifier à la main ou de les squash ?

    Ce n'est pas possible de les squash (j'ai besoin de pouvoir mettre l'applicatif à une version spécifique au besoin), je dois donc modifier toutes mes migrations à la main... soit.

    20 minutes (un sed me semblait trop risqué) et 2/3 bêtises liées à PEP8 de réglées plus tard, on relance l'intégration continue :

    1

    les middlewares

    Donc l'intégration continue se met en marche :

    • Etape 1: On récupère les sources du projet: Ok !
    • Etape 2: On lance les migrations: Ok !
    • Etape 3: On lance les tests unitaires: Ko :-(

    La, c'est vraiment le drame ! Les tests unitaires ne passent pas, je sens que je vais passer légèrement plus de temps que prévu pour cette simple mise à jour...

    L'erreur est la suivante (présente pour à peu près tous les tests):

    Traceback (most recent call last):
    [...]
      File "/usr/local/lib/python3.6/site-packages/rest_framework/viewsets.py", line 95, in view
        return self.dispatch(request, *args, **kwargs)
      File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py", line 494, in dispatch
        response = self.handle_exception(exc)
      File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py", line 454, in handle_exception
        self.raise_uncaught_exception(exc)
      File "/usr/local/lib/python3.6/site-packages/rest_framework/views.py", line 491, in dispatch
        response = handler(request, *args, **kwargs)
      File "/usr/local/lib/python3.6/contextlib.py", line 52, in inner
        return func(*args, **kwds)
      File "/builds/XXXX/XXXX/XXXX/views/user.py", line 67, in create
        request.session['XXXX'] = XXXX
      File "/usr/local/lib/python3.6/site-packages/rest_framework/request.py", line 412, in __getattr__
        return self.__getattribute__(attr)
    AttributeError: 'Request' object has no attribute 'session'
    

    Qu'est ce que c'est que ça ? Request n'as plus d'attribut session ? Et je fais comment moi maintenant ?

    Qu'à cela ne tienne, on va regarder dans le changelog s'il y a quelque chose au sujet des sessions dans cette mise à jour :

    The SessionAuthenticationMiddleware class is removed. It provided no functionality since session authentication is unconditionally enabled in Django 1.10.

    Ca tombe bien ! Cette dépendance était dans mon settings.py, dans la variable settings.MIDDLEWARE_CLASSES.

    Je supprime la dépendance, relance l'intégration continue... et idem... ça ne passe pas, même erreurs aux tests unitaires.

    30 minutes de troobleshooting plus tard, je vois ça à la fin du changelog:

    Support for old-style middleware using settings.MIDDLEWARE_CLASSES is removed.

    Je renomme settings.MIDDLEWARE_CLASSES en settings.MIDDLEWARE, je lance l'intégration continue:

    1

    La découverte d'une breaking change non documentée

    Donc l'intégration continue se met en marche :

    • Etape 1: On récupère les sources du projet: Ok !
    • Etape 2: On lance les migrations: Ok !
    • Etape 3: On lance les tests unitaires: Ko :-(

    Cependant, cette fois, nous n'avons pas les mêmes exceptions:

    Traceback (most recent call last):
      File "/usr/local/lib/python3.6/site-packages/rest_framework/test.py", line 291, in get
        response = super(APIClient, self).get(path, data=data, **extra)
      File "/usr/local/lib/python3.6/site-packages/rest_framework/test.py", line 208, in get
        return self.generic('GET', path, **r)
      File "/usr/local/lib/python3.6/site-packages/rest_framework/test.py", line 237, in generic
        method, path, data, content_type, secure, **extra)
      File "/usr/local/lib/python3.6/site-packages/django/test/client.py", line 404, in generic
        return self.request(**r)
      File "/usr/local/lib/python3.6/site-packages/rest_framework/test.py", line 288, in request
        return super(APIClient, self).request(**kwargs)
      File "/usr/local/lib/python3.6/site-packages/rest_framework/test.py", line 240, in request
        request = super(APIRequestFactory, self).request(**kwargs)
      File "/usr/local/lib/python3.6/site-packages/django/test/client.py", line 467, in request
        response = self.handler(environ)
      File "/usr/local/lib/python3.6/site-packages/django/test/client.py", line 125, in __call__
        self.load_middleware()
      File "/usr/local/lib/python3.6/site-packages/django/core/handlers/base.py", line 39, in load_middleware
        mw_instance = middleware(handler)
    TypeError: object() takes no parameters
    

    Youpi ! Encore une erreur mystique ! Refaisons un joli ctrl + f sur le changelog, pour middleware (ça ressemble à ça), mais rien...

    Un petit google avec "TypeError: object() takes no parameters" middleware django, et nous tombons sur le meilleur ami du développeur : Django exception middleware: TypeError: object() takes no parameters

    Alors en effet, dans ce projet, il y a un middleware "fait maison", qui ressemble à cela:

    class MyMiddleware(object):
        def process_exception(self, request, exception):
            do_something()
    

    En le remplaçant par:

    from django.utils.deprecation import MiddlewareMixin
    class MyMiddleware(MiddlewareMixin):
        def process_exception(self, request, exception):
            do_something()
    

    je lance l'intégration continue:

    2

    Woohoo \o/ (Sympa la breaking change non documentée ...)

    Conclusion

    Pour conclure, j'espère que ces péripéties sur l'art du troubleshooting vous aurons fait prendre conscience que l'update d'un framework, même si cela semble à la base trivial, peut amener de nombreux soucis.

    J'espère que si des gens font face aux même problèmes que moi, qu'ils trouveront facilement ce billet, et que cela les aidera dans la résolution de leur soucis.

    Sur ce... merry christmas and happy new year !

    newyear

  • Comments