Nous avons franchi le pont de Python 2.7.15 à 3.6.6 !
Cela nous a pris 1 an et demi avec l'aide d'Antoine et Léa. Un grand merci à toutes les équipes et personnes qui ont eu un impact de près ou de loin pour le travail acharné ☺

Nous avons fait le choix de ne pas utiliser Python 3.7 tout de suite car certains modules n'étaient pas encore compatible.

Avant de pouvoir faire cette grosse mise à jour, nous avions (et avons encore, à moindre mesure) une dette technique assez lourde. Il a été nécessaire de mettre à jour d'autres parties du code, utiliser des modules compatibles avec Python 2 et 3 et modifier des grosses fonctionnalités comme l'installeur multi-platforme et la mise à jour automatique (cx_Freeze/Esky -> PyInstaller), toute la partie graphique (HTML/Js2Py -> QML) et enfin le framework PyQt 4 devait aussi subir un gros lifting vers PyQt 5.

Hormis la transition unicode -> bytes, que je prévoyais plus compliquée, voici ce qu'il faut retenir.


Adaptations

Nous nous sommes grandement fait aidés par Python à l'aide de ces arguments pour la CLI :

  • -bb pour traquer les comparaisons bytes/bytearray <-> str ou int.
  • -W all nous a permis de mettre à jour les appels à des fonctions/méthodes qui seront supprimées dans un futur proche et dénicher quelques incohérences par-ci, par-là. Idéalement, nous aurions préféré utiliser -W error mais cette issue avec pytest bloquait les tests.

Bon à savoir

# Python 2
super(NotificationService, self).__init__()
# Python 3
super()__init__()



Méta-classes


La syntaxe suivante est valable pour Python 2 seulement :
class Options(object):
    __metaclass__ = MetaOptions


Les méta-classes s'utilisent de cette manière en Python 3 :
class Options(metaclass=MetaOptions): ...



DateTime


datetime.utcfromtimestamp() ne lève plus l'exception ValueError mais OverflowError si le timestamp est hors des limites supportées par le système.

De plus, sous Windows seulement (issue bpo-29097), si le timestamp est compris entre 0 et 86 400 vous aurez l'erreur OSError: [Errno 22] Invalid argument. Il y a un fix temporaire et crade dont je tairais le code ☻

Dictionnaires


Pour commencer, les dictionnaires respectent dorénavant l'ordre d'insertion des éléments. C'est officiel !

Ensuite, en Python 2, dict.items() renvoie une copie du dictionnaire sous forme d'une liste de tuple (clef, valeur). Mais en Python 3, elle revoie une vue sur le dictionnaire.
Ce qui implique que l'on ne peut plus accéder aux données par leur index :
>>> snakes = {'taïpan': 'dangerous', 'python': 'best', 'orvet': 'lol'}
>>> ze_best = snakes.items()[1][0]
TypeError: 'dict_items' object does not support indexing

# Fix
>>> ze_best = list(snakes.items())[1][0]
>>> ze_best
'python'



Il faut aussi faire attention à ne pas modifier le dictionnaire pendant que vous itérez dessus :
>>> for adj, name in snakes.items():
...     if adj == 'dangerous':
...         del snakes[adj]
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration

# Fix
>>> for adj, name in list(snakes.items()):
...     if adj == 'dangerous':
...         del snakes[adj]
... 
>>> snakes
{'best': 'python', 'lol': 'orvet'}



Enfin, une nouveauté intéressante comme tout : le dict unpacking. Prenons cette méthode :
def get_metrics(self):
    metrics = super().get_metrics()
    metrics.update(self._metrics)
    return metrics


Nous pouvons désormais la refactoriser tel que :
def get_metrics(self):
    metrics = super().get_metrics()
    return {**metrics, **self._metrics}



Plateforme


platform.linux_distribution() est dépréciée et sera bientôt supprimée :
>>> import platform
>>> platform.linux_distribution()
('Ubuntu', '16.04', 'xenial')
PendingDeprecationWarning: dist() and linux_distribution() functions are deprecated in Python 3.5



Nous avons corrigé le tir en utilisant le module distro qui renvoie même des informations plus précises :
>>> import distro
>>> distro.linux_distribution()
('Ubuntu', '16.04', 'Xenial Xerus')



Attributs étendus


Une erreur liée à l'utilisation de xattr et sys.platform nous a donné du fil à retordre :
File "xattr/__init__.py", line 184, in setxattr
    return xattr(f).set(attr, value, options=options)
  File "xattr/__init__.py", line 78, in set
    return self._call(_setxattr, _fsetxattr, name, value, 0, options | self.options)
  File "xattr/__init__.py", line 60, in _call
    return name_func(self.value, *args)
  File "xattr/lib.py", line 82, in _setxattr
    raise error(path)
  File "xattr/lib.py", line 33, in error
    raise IOError(errno, strerror, path)
OSError: [Errno 95] Operation not supported: b'/home/tiger-222/test_file'


Cette erreur est liée au nom de l'attribut qui n'est pas valable. Sous GNU/Linux, l'attribut doit commencer par user.. Hors, pour déterminer le nom de l'attribut, nous avons différentes conditions pour prendre en charge GNU/Linux et macOS. Mais comme sys.platform ne renvoie plus la valeur attendue => ☠
Pour renforcer le code et éviter ce genre de déboire à l'avenir, nous avons remplacé toutes les occurrences à sys.platform par les constantes LINUX, MAC et WINDOWS.



Python 3, c'est surtout...


  • L'utilisation de l'encodage UTF-8 de base sur tous les OS (surtout Windows), ça sauve des vies !

  • f-strings : c'est str.format() sous emphétamines tout en étant bien plus lisible.

  • Les type annotations grâce au module typing, à consommer sans modération.

  • Les data classes (Python 3.7 seulement).


Suppress


L'introduction du context manager suppress. Pratique comme tout, il permet de remplacer ce genre de code :
try:
    os.remove(file)
except (FileNotFoundError, TypeError):
    pass


Par quelque chose de plus conçis :
with suppress(FileNotFoundError, TypeError):
    os.remove(file)





Packaging


Dernier point sur lequel je comptais beaucoup pour la distribution de Nuxeo Drive sur Windows : Python embarqué.
Il s'agit d'un ZIP contenant Python et le strict minimum au niveau des modules. Décompressé, on arrive à moins de 15 Mo, ce qui petit et très appréciable.

Mais il fallait bien que ça couine quelque part : impossible d'installer un module tiers sans wheel. Ce qui veut dire que si le mainteneur d'un module ne fourni pas de fichier .whl pour Windows, l'installation ne sera pas possible.
J'avais ouvert le ticket bpo-33903: Can't use lib2to3 with embeddable zip file mais la conclusion n'est pas satisfaisante de mon point de vue. Donc le problème reste bien présent. La meilleure solution sera de proposer un patch si je veux que ça avance.



Patches


Durant le processus, nous avons pu remonter plusieurs anomalies et améliorations dans divers projets. Parce que c'est surtout ça le monde de l'open-source ☮




Sources :