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 une influence 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
- sys.platform() renvoie désormais linux au lieu de linux2.
- EnvironmentError, IOError, WindowsError, socket.error, select.error et mmap.error n'existent plus qu'en tant qu'alias de
OSError
. - Plus besoin de spécifier
object
dans la déclarations de vos classes, c'est le comportement de base. Créer une classe devient aussi simple queclass Foo: ...
. - L'utilisation de
super()
est moins verbeuse :
# 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 ☮
- pytest-timeout : Fix Python 3 DeprecationWarning: type argument to addoption() is a string 'choice'
- PyInstaller : Bootloader: OSX: Respect Info.plist options
- PyInstaller : Hooks: Fix missing SSL libraries on Windows with PyQt5.QtNetwork
- PyInstaller : Windows: The machinery to emulate app in tests is broken
- PyInstaller : Fix several ResourceWarnings and DeprecationWarnings
- PyCryptodome : Fix use of the deprecated imp module in Python 3
- PyCryptodome : Missing macOS and Windows wheels for Python 3.7
- PyCryptodome : Fix DeprecationWarning: invalid escape sequence
- tzlocal : Fix BytesWarning: Comparison between bytes and string in unix.py
- PyPAC : Fix DeprecationWarning: invalid escape sequence
- psutil : Fix DeprecationWarning: invalid escape sequence
- PyObjC : Fix DeprecationWarning: invalid escape sequence in _bridgesupport.py
- Js2Py : Fix DeprecationWarning: invalid escape sequence
- pre-commit : Fix several ResourceWarning: unclosed file
Sources :
- Script d'aide à la conversion PyQt4 vers PyQt5
- NXDRIVE-730 : Move to PyInstaller
- NXDRIVE-1143 : New auto-update framework
- NXDRIVE-1223 : Use a fork of universal-analytics-python that supports Python 3
- NXDRIVE-969 : Switch to QML for UI
- NXDRIVE-691 : Upgrade to Python 3.7
- NXDRIVE-692 : Upgrade from PyQt4 to PyQt5