Plone pour les développeurs Version 1.0.0 plone-fr 17-April-2017 Table des matières 1 2 3 Rappel sur Python 1.1 Définition . . . . . . . . . . . . . 1.2 Savoir . . . . . . . . . . . . . . . 1.3 Aide intégrée à Python . . . . . . 1.4 Attributs par défaut d’une classe . 1.5 set . . . . . . . . . . . . . . . . . 1.6 Le __call__ d’une classe . . . . . 1.7 Manipulation de chaines . . . . . 1.8 Différence entre unicode et string 1.9 Comprehensive list et generator . 1.10 Decorateur . . . . . . . . . . . . 1.11 Methode de classe . . . . . . . . 1.12 Property . . . . . . . . . . . . . 1.13 Template . . . . . . . . . . . . . 1.14 Debogueur pdb . . . . . . . . . . 1.15 Profiler . . . . . . . . . . . . . . 1.16 Ressources . . . . . . . . . . . . 1.17 Exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 3 3 4 4 4 5 5 5 6 7 7 7 8 8 8 9 Presentation de Python 2.1 Introduction . . . . . . . . . . . . . . . . . . . . 2.2 Installation . . . . . . . . . . . . . . . . . . . . . 2.3 La console Python . . . . . . . . . . . . . . . . . 2.4 Le contrôle du flux d’instructions . . . . . . . . . 2.5 Les conteneurs standard . . . . . . . . . . . . . . 2.6 Fonctions et espaces de noms . . . . . . . . . . . 2.7 Modules et packages . . . . . . . . . . . . . . . . 2.8 La programmation Orientée Objet . . . . . . . . . 2.9 Quelques Techniques avancées de programmation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 15 15 30 36 48 54 59 67 ZODB - Une Base de données objet native pour python 3.1 Avertissement . . . . . . . . . . . . . . . . . . . . 3.2 Licence . . . . . . . . . . . . . . . . . . . . . . . . 3.3 Résumé . . . . . . . . . . . . . . . . . . . . . . . . 3.4 Documentation . . . . . . . . . . . . . . . . . . . . 3.5 Téléchargements . . . . . . . . . . . . . . . . . . . 3.6 Communauté et contribution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 77 77 77 78 87 88 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i 4 5 6 7 8 9 Le guide complet de l’Architecture de Composants de Zope 4.1 Mise en route . . . . . . . . . . . . . . . . . . . . . . . 4.2 Un exemple . . . . . . . . . . . . . . . . . . . . . . . . 4.3 Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . 4.4 Adaptateurs . . . . . . . . . . . . . . . . . . . . . . . . 4.5 Utilitaires . . . . . . . . . . . . . . . . . . . . . . . . . 4.6 Adaptateurs avancés . . . . . . . . . . . . . . . . . . . 4.7 Usage de la ZCA dans Zope . . . . . . . . . . . . . . . 4.8 Étude de cas . . . . . . . . . . . . . . . . . . . . . . . 4.9 Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 89 93 96 100 104 106 110 114 125 Le langage ZCML 5.1 Introduction au ZCML . . . . 5.2 Traitement des fichiers ZCML 5.3 Les directives ZCML . . . . . 5.4 Pour aller plus loin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 163 164 166 168 Utiliser la ZCA avec Grok 6.1 Introduction . . . . . . 6.2 Installation de five.grok 6.3 Utiliser Grok . . . . . . 6.4 Ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 173 173 174 175 Modélisation UML 7.1 Objectifs . . . . . . . . . . . . . . . 7.2 Savoir . . . . . . . . . . . . . . . . . 7.3 Qu’est ce qu’UML . . . . . . . . . . 7.4 Les éléments . . . . . . . . . . . . . 7.5 Les relations . . . . . . . . . . . . . 7.6 Les diagrammes que nous utiliserons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 177 177 177 178 183 184 Génerer un composant Plone avec ArgoUML et ArchgenXML 8.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.2 Savoir . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.3 Installation d’ArgoUML . . . . . . . . . . . . . . . . . . . . . . 8.4 Installation de ArchGenXML . . . . . . . . . . . . . . . . . . . 8.5 Configuration d’ArgoUML pour utiliser le profil d’ArchGenXML 8.6 Création du buildout de notre module . . . . . . . . . . . . . . . 8.7 Création du plan documentaire . . . . . . . . . . . . . . . . . . . 8.8 Création des workflows . . . . . . . . . . . . . . . . . . . . . . 8.9 exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189 189 189 189 189 190 192 192 199 212 Nouvelles fonctionnalités de ArchGenXML 9.1 Définition . . . . . . . . . . . . . . . . 9.2 Savoir . . . . . . . . . . . . . . . . . . 9.3 Modélisation d’un portlet . . . . . . . 9.4 Modélisation de vue zope 3 . . . . . . 9.5 Exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Création d’un thème Plone 10.1 Installation de Gloworm . . . . . . 10.2 Création d’un theme . . . . . . . . 10.3 Exercice 1 . . . . . . . . . . . . . 10.4 Exercice 2 : Création d’une viewlet 10.5 Portlets . . . . . . . . . . . . . . . 10.6 GenericSetup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213 213 213 213 213 214 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 215 215 216 216 217 219 11 Les viewlets 221 11.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221 ii 11.2 11.3 11.4 11.5 11.6 Gestion interactive des viewlets L’enregistrement en ZCML . . Le step GenericSetup . . . . . . Exemple . . . . . . . . . . . . Pour aller plus loin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222 223 226 227 228 12 Les portlets 12.1 Personnalisation . . . . . . . . . . . . . . . . . . . . . 12.2 Portlets disponibles en standard . . . . . . . . . . . . . 12.3 Retour sur le paramétrage d’une portlet . . . . . . . . . 12.4 Habillage graphique . . . . . . . . . . . . . . . . . . . 12.5 Export/Import Generic setup du paramétrage des portlets 12.6 Création d’une portlet personnelle . . . . . . . . . . . . 12.7 Et le résultat... . . . . . . . . . . . . . . . . . . . . . . 12.8 Créer votre propre portlet . . . . . . . . . . . . . . . . 12.9 Pour aller plus loin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231 231 232 234 234 234 234 237 239 239 13 Les vues standard 13.1 Qu’est-ce que c’est ? . . . . . . . 13.2 Pourquoi utiliser ces vues ? . . . 13.3 Quelles vues sont proposées ? . . 13.4 Accéder aux ressources des vues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 241 241 241 242 14 Internationalisation d’un composant 14.1 Exemples de la formation . . . . . 14.2 Internationaliser un composant . . . 14.3 Localiser un composant . . . . . . 14.4 La syntaxe des po en quelques mots 14.5 Modifier les traductions de Plone . 14.6 Mise à jour des traductions . . . . . 14.7 Ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 249 250 253 254 254 255 255 15 Gestion des utilisateurs avec PlonePAS 15.1 Présentation . . . . . . . . . . . . 15.2 Architecture fonctionnelle . . . . . 15.3 PlonePAS en ZMI . . . . . . . . . 15.4 Les plugins standard . . . . . . . . 15.5 Les plugins de tierce partie . . . . . 15.6 Votre plugin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257 257 258 259 264 264 264 16 L’environnement de développement 16.1 Installation de Python 2.4 . . . . . . . . . . . 16.2 Configuration de l’éditeur vim . . . . . . . . . 16.3 Configuration de l’autocomplétion dans Python 16.4 Activation de la recherche dans l’historique . . 16.5 Installation de ArgoUML et ArchGenXML . . 16.6 Checkout svn.zope.org . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 265 265 265 266 266 267 17 La gestion des sources avec subversion 17.1 Savoir . . . . . . . . . . . . . . . . . . 17.2 Création d’un dépôt via Apache . . . . 17.3 Installation du client subversion . . . . 17.4 Administration d’un dépôt subversion . 17.5 Mettre à jour la branche de production . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269 269 269 270 270 270 . . . . . . . . . . . . . . . . . . . . 18 Migration des composants développés pour Plone 2.5 273 18.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273 18.2 Savoir . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273 18.3 Outils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273 iii 18.4 18.5 18.6 18.7 18.8 18.9 18.10 18.11 Outils pour la preview des documents . . . . Installation du produit AttachmentField . . . Génération du produit AgileKnowledgeBase Configuration du calendrier . . . . . . . . . Création d’un nouveau Plone Site . . . . . . Migration du contenu . . . . . . . . . . . . Ajout nouvelles fonctionnalités . . . . . . . Exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273 274 274 275 275 275 276 277 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 279 279 279 280 20 Programmation dirigée par la documentation 20.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.2 Savoir . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.3 Exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281 281 281 281 21 Sensibilisation aux méthodes agiles 21.1 Savoir . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.2 Ressources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.3 Exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283 283 283 283 22 Glossaire 285 23 Indexes et tables 291 24 Contributeurs 293 25 Evolutions planifiées de ce document 295 Index 297 19 Programmation dirigée par les tests 19.1 Définition . . . . . . . . . . . . 19.2 Savoir . . . . . . . . . . . . . . 19.3 Test unitaire . . . . . . . . . . 19.4 Exercice . . . . . . . . . . . . iv . . . . . . . . . . . . . . . . . . . . . . . . . . . . Plone pour les développeurs, Version 1.0.0 Astuce : Pour voir cette documentation dans son site original et accéder à son moteur de recherche, veuillez vous rendre ici. Lire la dernière version de ce document au format PDF Ce document constitué à partir de contributions de la communauté Plone francophone, notamment : – Le manuel de formation intégrateur Plone par Vincent Fretin (Ecréall) – Le cours sur le développement pour Plone par Michael Launay (Ecréall) – La traduction par Christophe Combelles du livre de Baiju Muthukadan sur la Zope Component Architecture – Les travaux, les relectures et les très nombreuses contributions de Gilles Lenfant et Thomas Desvenain – Le cours sur Python 3 de Robert Cordeau Cette création est mise à disposition selon le Contrat Paternité-Partage des Conditions Initiales à l’Identique 2.0 France disponible en ligne http ://creativecommons.org/licenses/by-sa/2.0/fr/ ou par courrier postal à Creative Commons, 171 Second Street, Suite 300, San Francisco, California 94105, USA. Table des matières 1 Plone pour les développeurs, Version 1.0.0 2 Table des matières CHAPITRE 1 Rappel sur Python 1.1 Définition Le but est de maîtriser le langage python, de comprendre et respecter les standards utilisés par la communauté Plone. 1.2 Savoir – – – – – – – – – – Installation de python Votre premier programme Python Types prédéfinis Le pouvoir de l’introspection Les objets et l’orienté objet Traitement des exceptions et utilisation de fichiers Expressions régulières Traitement du HTML Traitement de données XML Des scripts et des flots de données (streams) 1.3 Aide intégrée à Python Dans un Python interactif, vous pouvez lire la documentation d’un module avec la commande help, par exemple si vous voulez la documentation sur le module os. >>> help("os") ou bien >>> import os >>> help(os) Il existe aussi une commande spéciale pour avoir de la documentation sur un sujet donné. >>> help(’topics’) 3 Plone pour les développeurs, Version 1.0.0 1.4 Attributs par défaut d’une classe Dans un shell Python : >>> class MyClass(object): ... myattr = 2 ... >>> a = MyClass() >>> a.myattr 2 >>> a.__dict__ {} >>> a.myattr = 3 >>> a.myattr 3 >>> a.__dict__ {’myattr’: 3} >>> b = MyClass() >>> b.myattr 2 >>> MyClass.myattr = 4 >>> b.myattr 4 >>> a.myattr 3 1.5 set C’est une liste à éléments uniques : >>> s set([1,2,3,1,5]) >>> s = set([1,2,3,1,5]) >>> s set([1, 2, 3, 5]) >>> s.add(6) >>> s set([1, 2, 3, 5, 6]) >>> s2 = frozenset([1,2]) >>> s2.add(3) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: ’frozenset’ object has no attribute ’add’ >>> 1.6 Le __call__ d’une classe Une classe peut être appelé : >>> class A(object): ... def __call__(self): ... print "bonjour" ... >>> a = A() >>> a() bonjour 4 Chapitre 1. Rappel sur Python Plone pour les développeurs, Version 1.0.0 1.7 Manipulation de chaines >>> "bonJour".capitalize() ’Bonjour’ >>> "bonjour"[0] ’b’ >>> "bonjour"[-1] ’r’ >>> "bonjour"[0:3] ’bon’ >>> "bonjour"[:3] ’bon’ >>> "bonjour"[1:3] ’on’ >>> "bonjour"[-2:-1] ’u’ >>> "bonjour"[-2:] ’ur’ >>> "bonjour"[1:5:2] ’oj’ 1.8 Différence entre unicode et string – http ://www.stereoplex.com/two-voices/python-unicode-and-unicodedecodeerror 1.9 Comprehensive list et generator >>> [i*2 for i in range(10)] [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] >>> (i*2 for i in range(10)) <generator object at 0x1745ef0> >>> g = (i*2 for i in range(10)) >>> g.next() 0 >>> g.next() 2 >>> g.next() 4 >>> g.next() 6 >>> g.next() 8 >>> for i in g: ... print i ... 10 12 14 16 18 >>> g.next() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration 1.7. Manipulation de chaines 5 Plone pour les développeurs, Version 1.0.0 1.10 Decorateur Copiez cet exemple dans un fichier test.py : import functools def log_calls_decorator(f): """My decorator. """ @functools.wraps(f) def wrapped(*args, **kwargs): """My wrapped function. """ call_string = "%s called with *args: %r, **kwargs: %r " % (f.__name__, args, kwargs) try: retval = f(*args, **kwargs) call_string += " --> " + repr(retval) return retval finally: print call_string return wrapped @log_calls_decorator def add(operand1, operand2): """Return the sum of operand1 and operand2. """ return operand1 + operand2 La construction est équivalente à : add = log_calls_decorator(add) Lancez le : python -i test.py >>> print add.__name__ add >>> print add.__doc__ Return the sum of operand1 and operand2. Le @functools.wraps(f) permet de garder le __name__ et __doc__ de la fonction add d’origine. Enlevez le et recommencez le test. Pour plus d’informations : >>> help(functools.wraps) >>> help(functools.update_wrapper) >>> functools.WRAPPER_ASSIGNMENTS (’__module__’, ’__name__’, ’__doc__’) >>> functools.WRAPPER_UPDATES (’__dict__’,) Source 6 Chapitre 1. Rappel sur Python Plone pour les développeurs, Version 1.0.0 1.11 Methode de classe >>> class A(object): ... @classmethod ... def m(cls): ... print "classmethod" ... def n(self): ... print "dans n" ... >>> a = A() >>> a.n() dans n >>> A.m() classmethod >>> a.m() classmethod >>> A.n() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unbound method n() must be called with A instance as first argument (got nothing instea 1.12 Property Exemple : >>> ... ... >>> >>> >>> 5 >>> ... ... ... ... ... ... >>> >>> >>> 6 class A(object): myattr = 3 a = A() a.myattr = 5 a.myattr class A(object): def setMyAttr(self, value): self._myattr = value * 2 def getMyAttr(self): return self._myattr myattr = property(getMyAttr, setMyAttr) b = A() b.myattr = 3 b.myattr 1.13 Template En Python 2.4-2.6 : >>> "Bonjour %(name)s" % {’name’: ’Vincent’} ’Bonjour Vincent’ En Python 2.6/3.x : >>> "Bonjour {name}".format(name=’Vincent’) ’Bonjour Vincent’ 1.11. Methode de classe 7 Plone pour les développeurs, Version 1.0.0 1.14 Debogueur pdb À insérer dans le code source à l’endroit où vous voulez vous arrêter : import pdb; pdb.set_trace () (Ne mettez pas l’espace avant les parenthèses) À partir de là, vous avez les commandes suivantes : – s/step : entre dans la fonction – n/next : instruction suivante – c/continue : continuer jusqu’au prochain point d’arrêt – l/list : affiche le contexte – u/up : monter dans la pile d’appel – d/down : descendre dans la pile d’appel – b/break num_line : ajouter un point d’arrêt à la ligne num_line – a : affiche la liste des paramètres passés à la fonction – r : continue jusqu’au return de la fonction – q : quitter la session de debogage – Taper juste sur la touche entrée exécute la dernière commande réalisée. – Préfixer par ” !” si vous voulez afficher la variable “a” ou “b” plutôt que d’exécuter la commande pdb associé. Lire un exemple concret d’une session de debogage (en anglais) 1.15 Profiler Installez le package Ubuntu pour avoir les modules profile et pstats : $ sudo apt-get install python-profiler Dans votre code, pour obtenir des statistiques sur l’appel de la methode : self.method(arg1, arg2) remplacez la ligne par : import profile p = profile.Profile() p.runcall(self.method, arg1, arg2) p.dump_stats(’/tmp/stats’) Les résultats seront sauvegardés dans le fichier /tmp/stats. Pour ensuite afficher les résultats, ouvrez un Python shell et exécutez : import pstats p = pstats.Stats(’/tmp/stats’) p.strip_dirs().sort_stats(-1).print_stats() Plus d’informations sur le module profile. 1.16 Ressources – livre Plongez au coeur de Python 8 Chapitre 1. Rappel sur Python Plone pour les développeurs, Version 1.0.0 1.17 Exercice Lecture d’un fichier, traitement avec une expression régulière et écriture des résultats dans un fichier. 1.17. Exercice 9 Plone pour les développeurs, Version 1.0.0 10 Chapitre 1. Rappel sur Python CHAPITRE 2 Presentation de Python Le but est de connaître le langage python, de comprendre les standards utilisés par la communauté Plone. Nous avons pris le parti de présenter Python 3 bien que Plone 4 soit développé avec Python 2.6, les différences entre les deux versions seront présentées. Ce cours est issu du cours de Robert Cordeau http ://www.afpy.org/Members/bcordeau/Python3v1-1.pdf/download disponible à l’adresse Afin de respecter la licence initiale définie par Robert Cordeau, cette présentation de Python est sous les termes de la licence Paternité-Pas d’Utilisation Commerciale-Partage des Conditions Initiales à l’Identique 2.0 France accessible à http ://creativecommons.org/licenses/by-nc-sa/2.0/fr 2.1 Introduction Ce premier chapitre introduit les grandes caractéristiques du langage Python, replace Python dans l’histoire des langages, donne les particularités de production des scripts, défini la notion si importante d’algorithme et conclut sur les divers implémentations disponibles. 2.1.1 Principales caractéristiques du langage Python Historique – 1991 : Guido van Rossum publie Python au CWI (Pays-Bas) à partir du langage ABC et du projet AMOEBA (système d’exploitation distribué) – 1996 : sortie de Numerical Python – 2001 : naissance de de la PSF (Python Software Fundation) – Les versions se succèdent... Un grand choix de modules disponibles, des colloques annuels sont organisés, Python est enseigné dans plusieurs universités et est utilisé en entreprise... – Fin 2008 : sorties simultanées de Python 2.6 et de Python 3 Langage Open Source – Licence Open Source CNRI, compatible GPL, mais sans la restriction copyleft. Python est libre et gratuit même pour les usages commerciaux – GvR (Guido van Rossum) est le “BDFL” (dictateur bénévole à vie !) – Importante communauté de développeurs – Nombreux outils standard disponibles : Batteries included 11 Plone pour les développeurs, Version 1.0.0 Travail interactif – – – – – Nombreux interpréteurs interactifs disponibles Importantes documentations en ligne Développement rapide et incrémentiel Tests et débogage faciles Analyse interactive de données Langage interprété rapide – Interprétation du bytecode compilé – De nombreux modules sont disponibles à partir de bibliothèques optimisées écrites en C, C++ ou FORTRAN Simplicité du langage (cf. Zen of Python p. 101) – – – – Syntaxe claire et cohérente Indentation significative Gestion automatique de la mémoire (garbage collecteur) Typage dynamique fort : pas de déclaration Orientation objet – Modèle objet puissant mais pas obligatoire – Structuration multifichier très facile des applications : facilite les modifications et les extensions – Les classes, les fonctions et les méthodes sont des objets dits de première classe. Ces objets sont traités comme tous les autres (on peut les affecter, les passer en paramètre) Ouverture au monde – Interfaçable avec C/C++/FORTRAN – Langage de script de plusieurs applications importantes – Excellente portabilité Disponibilité de bibliothèques – Plusieurs milliers de packages sont disponibles dans tous les domaines 2.1.2 Environnements matériel et logiciel L’ordinateur On peut simplifier la définition de l’ordinateur de la façon suivante : L’ordinateur comprend entre autres : – un microprocesseur avec une UC (Unité de Contrôle), une UAL (Unité Arithmétique et Logique), une horloge, une mémoire cache rapide ; – de la mémoire volatile (dite vive ou RAM), contenant les instructions et les données nécessaires à l’exécution des programmes. La RAM est formée de cellules binaires (bits) organisées en mots de 8 bits (octets) ; – des périphériques : entrées/sorties, mémoires permanentes (dite mortes : disque dur, clé USB, CD-ROM...), réseau... 12 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 Deux sortes de programmes On distingue, pour faire rapide : – Le système d’exploitation : ensemble des programmes qui gèrent les ressources matérielles et logicielles. Il propose une aide au dialogue entre l’utilisateur et l’ordinateur : l’interface textuelle (interprèteur de commande) ou graphique (gestionnaire de fenêtres). Il est souvent multitâche et parfois multiutilisateur ; – les programmes applicatifs sont dédiés à des tâches particulières. Ils sont formés d’une série de commandes contenues dans un programme source qui est transformé pour être exécuté par l’ordinateur. 2.1.3 Les langages Des langages de différents niveaux – Chaque processeur possède un langage propre, directement exécutable : le langage machine. Il est formé de 0 et de 1 et n’est pas portable, mais c’est le seul que l’ordinateur puisse utiliser ; – le langage d’assemblage est un codage alphanumérique du langage machine. Il est plus lisible que le langage machine, mais n’est toujours pas portable. On le traduit en langage machine par un assembleur ; – les langages de haut niveau. Souvent normalisés, ils permettent le portage d’une machine à l’autre. Ils sont traduits en langage machine par un compilateur ou un interpréteur. Bref historique des langages – – – – – – Années 50 (approches expérimentales) : FORTRAN, LISP, COBOL, ALGOL... Années 60 (langages universels) : PL/1, Simula, Smalltalk, Basic... Années 70 (génie logiciel) : C, PASCAL, ADA, MODULA-2... Années 80 (programmation objet) : C++, LabView, Eiffel, Perl, VisualBasic... Années 90 (langages interprétés objet) : Java, tcl/Tk, Ruby, Python... Années 2000 (langages commerciaux propriétaires) : C#, VB.NET... Des centaines de langages ont été créés, mais l’industrie n’en utilise qu’une minorité. 2.1.4 Production des programmes Deux techniques de production des programmes La compilation est la traduction du source en langage objet. Elle comprend au moins quatre phases (trois phases d’analyse | lexicale, syntaxique et sémantique | et une phase de production de code objet). Pour générer le langage machine il faut encore une phase particulière : l’édition de liens. La compilation est contraignante mais offre au final une grande vitesse d’exécution. F IGURE 2.1 – Chaîne de compilation Dans la technique de l’interprétation chaque ligne du source analysé est traduite au fur et à mesure en instructions directement exécutées. Aucun programme objet n’est généré. Cette technique est très souple mais les codes générés sont peu performants : l’interpréteur doit être utilisé à chaque exécution... F IGURE 2.2 – Technique de l’interprétation 2.1. Introduction 13 Plone pour les développeurs, Version 1.0.0 Technique de production de Python – Technique mixte : l’interprétation du bytecode compilé. Bon compromis entre la facilité de développement et la rapidité d’exécution ; – le bytecode (forme intermédiaire) est portable sur tout ordinateur muni de la machine virtuelle Python. F IGURE 2.3 – Interprétation du bytecode compilé La construction des programmes Le génie logiciel étudie les méthodes de construction des programmes. Plusieurs modèles sont envisageables, entre autres : – la méthodologie procédurale. On emploie l’analyse descendante (division des problèmes) et remontante (réutilisation d’un maximum de sous algorithmes). On s’efforce ainsi de décomposer un problème complexe en sousprogrammes plus simples. Ce modèle structure d’abord les actions ; – la méthodologie objet. On conçoit des fabriques (classes) qui servent à produire des composants (objets) qui contiennent des données (attributs) et des actions (méthodes). Les classes dérivent (héritage et polymorphisme) de classes de base dans une construction hiérarchique. Python offre les deux techniques. 2.1.5 Algorithme et programme Définitions Algorithme Ensemble des étapes permettant d’atteindre un but en répétant un nombre fini de fois un nombre fini d’instructions. Un algorithme se termine en un temps fini. Programme un programme est la traduction d’un algorithme en un langage compilable ou interprétable par un ordinateur. Il est souvent écrit en plusieurs parties dont une qui pilote les autres : le programme principal. La présentation des programmes Un programme source est destiné à l’être humain. Pour en faciliter la lecture, il doit être judicieusement commenté. La signification de parties non triviales (et uniquement celles-là) doit être expliquée par un commentaire. Un commentaire commence par le caractère # et s’étend jusqu’à la fin de la ligne : #--------------------# Voici un commentaire #--------------------x = 9 + 2 # En voici un autre 2.1.6 Les implémentations de Python – – – – – – – CPython : Classic Python, codé en C, portable sur différents systèmes Python3000 : Python 3, la nouvelle implémentation de CPython Jython : ciblé pour la JVM (utilise le bytecode de JAVA) IronPython : Python.NET, écrit en C#, utilise le MSIL (MicroSoft Intermediate Language) Stackless Python : élimine l’utilisation de la pile du langage C (permet de récurser tant que l’on veut) Pypy : projet de recherche européen d’un interpréteur Python écrit en Python Unladen Swallow est un nouveau projet de Google utilisant les techniques de compilation JIT de Java. Ce projet, encore au stade de développement multipliera par 5 la vitesse d’exécution de Python. 14 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 2.2 Installation L’installation est simple : Si vous êtes sur Linux ou MacOSX, votre système a probablement une version de python déjà installée. Pour connaître sa version il vous suffit de faire python -V. Pour installer une autre version vous devez utiliser le gestionnaire de paquet de votre distribution. MacOSX n’intègre pas en natif de gestionnaire de paquets. Nous vous recommandons d’installer le gestionnaire de paquets MacPorts. Pour Windows et autres Unix, il vous suffit d’ouvrir votre navigateur sur la page http ://www.python.org/download et de choisir la version correspondant à votre choix. 2.3 La console Python Comme tout langage, Python permet de manipuler des données grâce à un vocabulaire de mots réservés et grâce à des types de données - approximation des ensembles définis en mathématique. Ce chapitre présente les règles de construction des identifiants, les types de données simples (les conteneurs seront examinés au chapitre Les conteneurs standard) ainsi que les types chaîne de caractères (Unicode et binaires). Enfin, last but not the least, ce chapitre s’étend sur les notions non triviales de variables, de références d’objet et d’affectation. 2.3.1 Les modes d’exécution Les deux modes d’exécution d’un code Python – Soit on enregistre un ensemble de commandes Python dans un fichier grâce à un éditeur (on parle alors d’un script Python) que l’on exécute par une touche du menu de l’éditeur ; – soit on utilise un interpréteur (par exemple IDLE) pour obtenir un résultat immédiat grâce à l’interpréteur Python embarqué dans IDLE qui exécute la boucle d’évaluation (cf. Fig. 2.1) F IGURE 2.4 – La boucle d’évaluation de IDLE 2.3.2 Identifiants et mots clés Identifiants Comme tout langage, Python utilise un identifiant pour nommer chaque objet. Sachant que : – un caractère de début peut être n’importe quelle lettre UNICODE. – un caractère de continuation est une lettre UNICODE, un chiffre, un point ou un tiret bas _. 2.2. Installation 15 Plone pour les développeurs, Version 1.0.0 Note : En python 2.6, les caractères ne peuvent être que des lettres ASCII, c’est à dire sans accent Warning : Les identifiants sont sensibles à la casse et ne doivent pas être un mot clé. Astuce : Pour être certain de ne pas empiéter sur le vocabulaire (mot clés et bibliothèque standard de Python), essayez ceci en mode interactif : >>> # Un symbole valable doit produire les erreurs suivantes >>> symbole Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name ’symbole’ is not defined >>> import symbole Traceback (most recent call last): File "<stdin>", line 1, in <module> ImportError: No module named symbole >>> # Un symbole non valable doit produire Une erreur de syntaxe >>> with File "<stdin>", line 1 with ^ SyntaxError: invalid syntax >>> # Un import de symbole non valable ne produit pas d’erreur >>> import calendar Styles de nommage Il est important d’utiliser une politique cohérente de nommage des identifiants. Voici les styles préconisés : – UPPERCASE ou UPPER_CASE pour les constantes ; – TitleCase pour les classes ; – camelCase pour les fonctions, les méthodes et les interfaces graphiques ; – lowercase ou lower_case pour tous les autres identifiants. Exemples : NB_ITEMS = 12 # UPPER_CASE class MaClasse: pass # TitleCase def maFonction(): pass # camelCase mon_id = 5 # lower_case Les mots réservés de Python 3 La version 3.1.1 de Python compte 33 mots clés : – False – None – True – and – as – assert – break – class – continue – def – del 16 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 – – – – – – – – – – – – – – – – – – – – – – – elif else except finally for from global if import in is lambda nonlocal 1 not or pass print 2 raise return try while with yield 2.3.3 Notion d’expression Exemples de deux expressions simples et d’une expression complexe : id1 = 15.3 id2 = maFonction(id1) if id2 > 0: id3 = math.sqrt(id2) else: id4 = id1 - 5.5*id2 2.3.4 Les types de données entiers Python 3 offre deux types entiers standard : int et bool. Le type int Le type int n’est limité en taille que par la mémoire de la machine. Les entiers littéraux sont décimaux par défaut, mais on peut aussi utiliser les bases suivantes : >>> 2009 # décimal 2009 >>> 0b11111011001 # binaire 2009 >>> 0o3731 # octal 2009 >>> 0x7d9 # hexadecimal 2009 1. N’est pas supporté par Python 2.x. 2. N’est supporté que par Python 2.x en tant que mot clé. 2.3. La console Python 17 Plone pour les développeurs, Version 1.0.0 Opérations arithmétiques Les principales opérations : >>> 20 + 3 23 >>> 20 - 3 17 >>> 20 * 3 60 >>> 20 ** 3 8000 >>> 20 / 3 6.666666666666667 >>> 20 // 3 # division entière 6 >>> 20 % 3 # modulo 2 >>> abs(3 - 20) # valeur absolue 17 Bien remarquer le rôle des deux opérateurs de division : – / : produit une division flottante ; – // : produit une division entière. Note : En python 2.6, les nombres dépassants 2 puissance 63 - 1 étaient des long. La notation des longs est marquée par un L ex : 9223372036854775808L Le type bool – Deux valeurs possibles : False, True. – Opérateurs de comparaison : ==, !=, >, >=, < et <= : >>> 2 > 8 False >>> 2 <= 8 < 15 True – Opérateurs logiques (concept de shortcut) : not, or et and. En observant les tables de vérité des opérateurs and et or (cf. p. 108), on remarque que : – dès qu’un premier membre à la valeur False, l’expression False and expression2 vaudra False. On n’a donc pas besoin de l’évaluer ; – de même dès qu’un premier membre à la valeur True, l’expression True or expression2 vaudra True. Cette optimisation est appelée “principe du shortcut” : >>> (3 == 3) or (9 > 24) # True (dès le premier membre) True >>> (9 > 24) and (3 == 3) # False (dès le premier membre) False – Les opérations logiques et de comparaisons sont évaluées afin de donner des résultats booléens dans False, True. Les expressions booléennes Une expression booléenne a deux valeurs possibles : False ou True. Python attribut à une expression booléennes la valeur False si c’est : – la constante False ; – la constante None ; – une séquence ou une collection vide ; 18 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 – une donnée numérique de valeur 0. Tout le reste vaut True. Exemple : >>> l = ( None, "N’est pas False", "", (), (1), [], [1] , {}, {’a’:’A’}, ... True, False, 0, 1) >>> for e in l: ... print("{!r} est {}".format(e, e and "VRAI" or "FAUX")) ... None est FAUX "N’est pas False" est VRAI ’’ est FAUX () est FAUX 1 est VRAI [] est FAUX [1] est VRAI {} est FAUX {’a’: ’A’} est VRAI True est VRAI False est FAUX 0 est FAUX 1 est VRAI 2.3.5 Les types de données flottants Le type float – Un float est noté avec un point décimal ou en notation exponentielle : >>> float() 0.0 >>> float(1) # Construit avec un entier = conversion 1.0 >>> 2.718 2.718 >>> .02 0.02 >>> 3e8 300000000.0 >>> 6.023e23 6.0229999999999998e+23 – Les flottants (float) supportent les mêmes opérations que les entiers. – Les float ont une précision finie indiquée dans sys.float_info.epsilon. – L’import du module math autorise toutes les opérations mathématiques usuelles : >>> import math >>> math.sin(math.pi/4) 0.70710678118654746 >>> math.degrees(math.pi) 180.0 >>> math.factorial(9) 362880 >>> math.log(1024, 2) 10.0 2.3.6 Le type complex – Les complexes sont écrits en notation cartésienne formée de deux flottants. 2.3. La console Python 19 Plone pour les développeurs, Version 1.0.0 – La partie imaginaire est suffixée par j : >>> 1j 1j >>> (2+3j) + (4-7j) (6-4j) >>> (9+5j).real 9.0 >>> (9+5j).imag 5.0 >>> abs(3+4j) 5.0 – Un module mathématique spécifique (cmath) leur est réservé : >>> import cmath >>> cmath.phase(-1 + 0j) 3.1415926535897931 >>> cmath.polar(3 + 4j) (5.0, 0.92729521800161219) >>> cmath.rect(1., cmath.pi/4) (0.70710678118654757+0.70710678118654746j) 2.3.7 Variables et affectation Les variables Dès que l’on possède des types de données, on a besoin des variables pour stocker les données. En réalité, Python n’offre pas la notion de variable, mais plutôt celle de référence d’objet. Tant que l’objet n’est pas modifiable (comme les entiers, les flottants, etc.), il n’y a pas de différence notable. On verra que la situation change dans le cas des objets modifiables... L’affectation La valeur d’une variable, comme son nom l’indique, peut évoluer au cours de chaque affectation (la valeur antérieure est perdue) : >>> a = a + 1 # 3 (incrémentation) >>> a = a - 1 # 2 (décrémentation) Affecter n’est pas comparer ! Affectation L’ affectation a un effet (elle modifie l’état interne du programme en cours d’exécution) mais n’a pas de valeur (on ne peut pas l’utiliser dans une expression) : >>> a = 2 >>> x = (a = 3) + 2 SyntaxError: invalid syntax 20 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 Comparaison La comparaison a une valeur utilisable dans une expression mais n’a pas d’effet (l’automate interne représentant l’évolution du programme n’est pas modifié) : >>> x = (a == 3) + 2 >>> x 2 Les variantes de l’affectation Outre l’affectation simple, on peut aussi utiliser les formes suivantes : # affectation simple v = 4 # affectation augmentée v += 2 # idem à : v = v + 2 si v est déjà référencé # affectation de droite à gauche c = d = 8 # cibles multiples # affectations parallèles d’une séquence e, f = 2.7, 5.1 # tuple g, h, i = [’G’, ’H’, ’I’] # liste x, y = coordonneesSouris() # retour multiple d’une fonction Les affectations (explications graphiques) Dans les schémas des figures ci-dessous, les cercles représentent les identificateurs alors que les rectangles représentent les données. Les affectations relient les identificateurs aux données : si une donnée en mémoire n’est plus reliée, le ramassemiettes (garbage collector) de Python la supprime automatiquement : F IGURE 2.5 – Exemple (a) : Trois affectations 2.3. La console Python 21 Plone pour les développeurs, Version 1.0.0 F IGURE 2.6 – Exemple (b) : La donnée ‘c’ est supprimée F IGURE 2.7 – Exemple (c) : La donnée ‘a’ est supprimée 22 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 2.3.8 Les chaînes de caractères Les chaînes de caractères : présentation Chaîne de caractères Le type de données non modifiable str représente une séquence de caractères Unicode. Non modifiable signifie qu’une donnée, une fois crée en mémoire, ne pourra plus être changée. Trois syntaxes de chaînes sont disponibles. Remarquez que l’on peut aussi utiliser le ‘ à la place de ”, ce qui permet d’inclure une notation dans l’autre : syntaxe1 = "Première forme avec un retour à la ligne \n" syntaxe2 = r"Deuxième forme sans retour à la ligne \n" syntaxe3 = """ Troisième forme multi-ligne """ guillemets = "L’eau vive" apostrophes = ’Forme "avec des apostrophes"’ Note : En python 2.6 “une chaine” est en faite une suite d’octets, contrairement à la chaîne unicode u”Avec un é” qui correspond à la chaine de caractères de python 3. Il faut passer de la chaine d’octets vers l’unicode en utilisant la méthode decode et en précisant l’encodage exemple : >>> print "Le \xc3\xa9 en utf8 est cod\xc3\xa9 sur 2 octets".decode("utf-8") Le é en utf8 est codé sur 2 octets >>> print "Le \xe9 en iso 8859-15 est cod\xe9 sur 1 octet".decode("iso 8859-15") Le é en iso 8859-15 est codé sur 1 octet Pour passer d’un encodage à un autre directement depuis une chaine d’octets, on utilise la méthode encode en lui précisant en paramètre l’encodage de destination et l’encodage source : >>> ’le symbole euro \xe2\x82\xac’.encode("iso 8859-15", "utf8") ’le symbole euro \xa4’ Attention : Un caractère unicode occupe 4 octets en mémoire, il s’agit d’un chiffre dont la valeur numérique correspond au symbole a représenter. Ainsi le é a pour valeur 233 (e9) soit en mémoire “00 00 00 E9”, or en iso 8859-15 le é a aussi pour valeur 233 (e9), mais en mémoire il prendra simplement un octet soit “E9”. Beaucoup d’erreurs de compréhension viennent de cette intersection dans la représentation des unicodes et des iso 8859-15. Les chaînes de caractères : opérations – Longueur : >>> s = "abcde" >>> len(s) 5 – Concaténation : >>> s1 = "abc" >>> s2 = "defg" >>> s3 = s1 + s2 >>> s3 abcdefg – Répétition : 2.3. La console Python 23 Plone pour les développeurs, Version 1.0.0 >>> >>> >>> Fi! s4 = "Fi! " s5 = s4 * 3 s5 Fi! Fi! Les chaînes de caractères : fonctions vs méthodes On peut agir sur une chaîne (et plus généralement sur une séquence) en utilisant des fonctions (notion procédurale) ou des méthodes (notion objet). – Pour appliquer une fonction, on utilise l’opérateur () appliqué à la fonction : >>> ch1 = "abc" >>> long = len(ch1) 3 – On applique une méthode à un objet en utilisant la notation pointée entre la donnée/variable à laquelle on applique la méthode, et le nom de la méthode suivi de l’opérateur () appliqué à la méthode : >>> ch2 = "abracadabra" >>> ch3 = ch2.upper() ABRACADABRA Méthodes de test de l’état d’une chaîne ch Les méthodes suivantes sont à valeur booléennes, c’est-à-dire qu’elles retournent la valeur True ou False. La notation [xxx] indique un élément optionnel que l’on peut donc omettre lors de l’utilisation de la méthode. – isupper() et islower() : retournent True si ch ne contient respectivement que des majuscules/minuscules : >>> print("cHAise basSe".isupper()) False – istitle() : retourne True si seule la première lettre de chaque mot de ch est en majuscule : >>> print("Chaise Basse".istitle()) True – isalnum(), isalpha(), isdigit() et isspace() : retournent True si ch ne contient respectivement que des caractères alphanumériques, alphabétiques, numériques ou des espaces : >>> print("3 chaises basses".isalpha()) False >>> print("54762".isdigit()) True – startswith(prefix[, start[, stop]]) et endswith(suffix[, start[, stop]]) testent si la sous-chaîne définie par start et stop commence respectivement par prefix ou finit par suffix : >>> print("abracadabra".startswith(’ab’)) True >>> print("abracadabra".endswith(’ara’)) False 24 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 Méthodes retournant une nouvelle chaîne – lower(), upper(), capitalize() et swapcase() : retournent respectivement une chaîne en minuscule, en majuscule, en minuscule commençant par une majuscule, ou en casse inversée : >>> # s sera notre chaîne de test pour toutes les méthodes >>> s = "cHAise basSe" >>> print(s.lower()) chaise basse >>> print(s.upper()) CHAISE BASSE >>> print(s.capitalize()) Chaise basse >>> print(s.swapcase()) ChaISE BASsE – expandtabs([tabsize]) : remplace les tabulations par tabsize espaces (8 par défaut). – center(width[, fillchar]), ljust(width[, fillchar]) et rjust(width[, fillchar]) : retournent respectivement une chaîne centrée, justifiée à gauche ou à droite, de largeur maximale fixée par le paramètre width complétée par le caractère fillchar (ou par l’espace par défaut) : >>> print(s.center(20, ’-’)) ----cHAise basSe--->>> print(s.rjust(20, ’@’)) @@@@@@@@cHAise basSe – zfill(width) : complète ch à gauche avec des 0 jusqu’à une longueur maximale de width : >>> print(s.zfill(20)) 00000000cHAise basSe – strip([chars]), lstrip([chars]) et rstrip([chars]) : suppriment toute les combinaisons de chars (ou l’espace par défaut) respectivement au début et en fin, au début, ou en fin d’une chaîne : >>> print(s.strip(’ce’)) HAise basS – find(sub[, start[, stop]]) : renvoie l’indice de la chaîne sub dans la sous-chaîne start à stop, sinon renvoie -1. rfind() effectue le même travail en commençant par la fin. index() et rindex() font de même mais produisent une erreur (exception) si la chaîne n’est pas trouvée : >>> print(s.find(’se b’)) 4 – replace(old[, new[, count]]) : remplace count instances (toutes pas défaut) de old par new : >>> print(s.replace(’HA’, ’ha’)) chaise basSe – split(seps[, maxsplit]) : découpe la chaîne en maxsplit morceaux (tous par défaut). rsplit() effectue la même chose en commençant par la fin et striplines() effectue ce travail avec les caractères de fin de ligne : >>> print(s.split()) [’cHAise’, ’basSe’] – join(seq) : concatène les chaînes du conteneur seq en intercalant la chaîne sur laquelle la méthode est appliquée : >>> print("**".join([’cHAise’, ’basSe’])) cHAise**basSe 2.3. La console Python 25 Plone pour les développeurs, Version 1.0.0 Les chaînes de caractères : indiçage simple Pour indicer une chaîne, on utilise l’opérateur [x] dans lequel l’indice x, un entier signé qui commence à 0 indique la position d’un caractère : >>> s = "Rayon X" >>> len(s) 7 >>> print(s[0]) R >>>>> print(s[2]) y >>> print(s[-1]) X >>> print(s[-3]) n F IGURE 2.8 – L’indiçage d’une chaîne Extraction de sous-chaînes L’opérateur [ ] avec 2 ou 3 indices séparés par le caractère permet d’extraire des sous-chaînes (ou tranches) d’une chaîne. La littérrature anglophone appelle cette notation le “slice” : >>> s = "Rayon X" >>> len(s) 7 >>> s[1:4] # (de l’indice 1 compris à 4 non compris) ’ayo’ >>> s[-2:] # (de l’indice -2 compris à la fin) ’ X’ >>> s[:3] # (du début à 3 non compris) ’Ray’ >>> s[3:] # (de l’indice 3 compris à la fin) ’on X’ >>> s[::2] # (du début à la fin, de 2 en 2) ’RynX’ 26 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 F IGURE 2.9 – L’extraction de sous-chaînes. 2.3.9 Les données binaires Les types binaires Python 3 propose deux types de données binaires : byte (non modifiable) et bytearray (modifiable). Une donnée binaire contient une suite de zéro ou plusieurs octets, c’est-à-dire d’entiers non-signés sur 8 bits (compris dans l’intervalle [0...255]). Ces types à la C sont bien adaptés pour stocker de grandes quantités de données. De plus Python fournit des moyens de manipulation efficaces de ces types. Les deux types sont assez semblables au type str et possèdent la plupart de ses méthodes. Le type modifiable bytearray possède des méthodes communes au type list. Exemples de données binaires et de méthodes : >>> b_mot = b"Animal" # chaîne préfixée par b : type byte >>> print(b_mot) b’Animal’ >>> for b in b_mot: ... print(b, end=" ") # (cf. table ASCII) ... 65 110 105 109 97 108 >>> bMot = bytearray(b_mot) # retourne un nouveau tableau de bytes... >>> bMot.pop() # ...qui possède les méthodes usuelles 108 >>> bMot bytearray(b’Anima’) >>> data = b"5 Hills \x35\x20\x48\x69\x6C\x6C\x73" >>> data.upper() b’5 HILLS 5 HILLS’ >>> data.replace(b"ill", b"at") b’5 Hats 5 Hats’ Bien différencier les codes, glyphes, caractères et octets ! 2.3. La console Python 27 Plone pour les développeurs, Version 1.0.0 F IGURE 2.10 – Codes, glyphes, caractères et octets. 28 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 2.3.10 Les entrées-sorties L’utilisateur à besoin d’interagir avec le programme. En mode “console”, on doit pouvoir saisir ou entrer des informations, ce qui est généralement fait depuis une lecture au clavier. Inversement, on doit pouvoir afficher ou sortir des informations, ce qui correspond généralement à une écriture sur l’écran. F IGURE 2.11 – Les entrées-sorties. Les entrées Il s’agit de réaliser une saisie à l’écran : la fonction standard input() interrompt le programme, afficher une éventuelle invite et attend que l’utilisateur entre une donnée et la valide par Entrée. La fonction standard input() effectue toujours une saisie en mode texte (la saisie est une chaîne) dont on peut ensuite changer le type (on dit aussi transtyper) : >>> nb_etudiant = input("Entrez le nombre d’étudiants : ") >>> type(nb_etudiant) # (c’est une chaîne) <class ’str’> >>> f1 = input("\nEntrez un flottant : ") >>> f1 = float(f1) # transtypage en flottant >>> # ou plus brièvement : >>> f2 = float(input("Entrez un autre flottant : ")) >>> type(f2)) <class ’float’> Les sorties En mode “Console”, Python lit-évalue-affiche, mais la fonction print() reste indispensable aux affichages dans les scripts : 2.3. La console Python 29 Plone pour les développeurs, Version 1.0.0 >>> import sys >>> a, b = 2, 5 >>> print(a, b) 2 5 >>> print("Somme :", a + b) Somme : 7 >>> print(a - b, "est la différence") -3 est la différence >>> print("Le produit de", a, "par", b, "vaut :", a * b) Le produit de 2 par 5 vaut : 10 >>> print() # affiche une nouvelle ligne >>> # pour afficher un espace à la place de la nouvelle ligne: >>> print(a, end=" ") 2 # (et ne va pas à la ligne) >>> print("\nErreur fatale !", file=sys.stderr) # dans un fichier >>> print("On a <", 2**32, "> cas !", sep="###") On a <###4294967296###> cas ! Attention : La fonction print n’est pas active par défaut en Python 2.6. Voir la note à cette adresse : http ://docs.python.org/library/functions.html#print Les séquences d’échappement À l’intérieur d’une chaîne, le caractère antislash \ permet de donner une signification spéciale à certaines séquences : Séquence \ \\ \’ \" \a \b \f \n \r \t \v \Nnom \uhhhh \Uhhhhhhhh \ooo \xhh Signification saut_ligne saut de ligne ignoré affiche un antislash apostrophe guillemet sonnerie (bip) retour arrière saut de page saut de ligne retour en début de ligne tabulation horizontale tabulation verticale caractère sous forme de code Unicode nommé caractère sous forme de code Unicode 16 bits caractère sous forme de code Unicode 32 bits caractère sous forme de code octal caractère sous forme de code hexadécimal Exemples : >>> print("\N{pound sign} \u00A3 \U000000A3") £ £ £ >>> print("d \144 \x64") d d d >>> print("d \144 \x64") #séquence d’échapement inactive dans les chaînes brutes d \144 \x64 2.4 Le contrôle du flux d’instructions Un script Python est formé d’une suite d’instructions exécutées en séquence de haut en bas. 30 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 Chaque ligne d’instructions est formée d’une ou plusieurs lignes physiques qui peuvent être continuées par un antislash \\ ou un caractère ouvrant [ ou ( ou { pas encore fermé. Cette exécution en séquence peut être modifiée pour choisir ou répéter des portions de code. C’est l’objet principal de ce chapitre. 2.4.1 Les instructions composées Instruction composée Une instruction composée est constituée : – d’une ligne d’en-tête terminée par deux-points ; – d’un bloc d’instructions indenté par rapport à la ligne d’en-tête. Important : Toutes les instructions au même niveau d’indentation appartiennent au même bloc Exemple : somme = 0.0 nb_valeurs = 0 for v in valeurs: nb_valeurs = nb_valeurs + 1 somme = somme + valeurs moyenne = somme / nb_valeurs Astuce : N’indentez pas avec des tabulations. Bien que Python admette les tabulations pour indenter le code source, il est vivement recommandé d’utiliser 4 espaces à cette fin. On a souvent besoin d’imbriquer les instructions composées : if a == 0: if b != 0: print("\nx = {:.2f}".format(-c/b)) else: print("\nPas de solution.") else: delta = b**2 - 4*a*c if delta > 0.0: rac_delta = sqrt(delta) print("\nx1 = {:.2f} \t x2 = {:.2f}" .format((-b-rac_delta)/(2*a), (-b+rac_delta)/(2*a))) elif delta < 0.0: print("\nPas de racine réelle.") else: print("\nx = {:.2f}".format(-b/(2*a))) 2.4.2 Choisir Choisir : if - [elif] - [else] Contrôler une alternative : if x < 0: print("x est négatif") elif x % 2: print("x est positif et impair") else: print("x n’est pas négatif et est pair") 2.4. Le contrôle du flux d’instructions 31 Plone pour les développeurs, Version 1.0.0 F IGURE 2.12 – Les instructions composées. 32 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 Test d’une valeur booléenne : if x: # mieux que (if x is True:) ou que (if x == True:) pass Syntaxe compacte d’une alternative Pour trouver, par exemple, le minimum de deux nombres, on peut utiliser l’opérateur ternaire (repris du C) : x, y = 4, 3 # Ecriture classique : if x < y: plus_petit = x else: plus_petit = y # Utilisation de l’opérateur ternaire : plus_petit = x if x < y else y print("Plus petit : ", plus_petit) # 3 2.4.3 Boucler Boucler : while Répéter une portion de code : x, cpt = 257, 0 print("L’approximation de log2 de", x, "est", end=" ") while x > 1: x //= 2 # division avec troncature cpt += 1 # incrémentation print(cpt, "\n") # 8 Utilisation classique : la saisie filtrée d’une valeur numérique (on doit préciser le type car input() saisit une chaîne) : n = int(input(’Entrez un entier [1 .. 10] : ’)) while not(1 <= n <= 10): n = int(input(’Entrez un entier [1 .. 10], S.V.P. : ’)) Parcourir : for Parcourir un itérable (tout conteneur que l’on peut parcourir élément par élément, dans l’ordre ou non, suivant son type) : for lettre in "ciao": print(lettre, end=" ") # c i a o for x in [2, ’a’, 3.14]: print(x, end=" ") # 2 a 3.14 for i in range(5): print(i, end=" ") # 0 1 2 3 4 2.4. Le contrôle du flux d’instructions 33 Plone pour les développeurs, Version 1.0.0 2.4.4 Ruptures de séquences Interrompre une boucle : break Sort immédiatement de la boucle for ou while en cours contenant l’instruction : for x in range(1, 11): # 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 if x == 5: break print(x, end=" ") print("\nBoucle interrompue pour x =", x) # affiche : # 1 2 3 4 # Boucle interrompue pour x = 5 Court-circuiter une boucle : continue Passe immédiatement à l’itération suivante de la boucle for ou while en cours contenant l’instruction ; reprend à la ligne de l’en-tête de la boucle : for x in range(1, 11): # 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 if x == 5: continue print(x, end=" ") print("\nLa boucle a sauté la valeur 5") # affiche : # 1 2 3 4 6 7 8 9 10 # La boucle a sauté la valeur 5 Syntaxe complète des boucles while - else Les boucles while et for peuvent posséder une clause else qui ne s’exécute que si la boucle se termine normalement, c’est-à-dire sans interruption : y = int(input("Entrez un entier positif : ")) while not(y > 0): y = int(input(’Entrez un entier positif, S.V.P. : ’)) x = y // 2 while x > 1: if y % x == 0: print("{} a pour facteur {}".format(y, x)) break # voici l’interruption ! x -= 1 else: print(y, "est premier.") for - else Un exemple avec le parcours d’une liste : une_sequence = [2, 5, 9, 7, 11] cible = int(input("Entrez un entier : ")) for i in une_sequence: if i == cible: sauve = i break # voici l’interruption ! else: print(cible, "n’est pas dans", une_sequence) sauve = None 34 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 # sauve vaut donc cible ou None : print("On obtient sauve =", sauve) Exceptions Afin de rendre les applications plus robustes, il est nécessaire de gérer les erreurs d’exécution des parties sensibles du code. Le mécanisme des exceptions sépare d’un côté la séquence d’instructions à exécuter lorsque tout se passe bien, et d’un autre côté, une ou plusieurs séquences d’instructions à exécuter en cas d’erreur. Lorsqu’une erreur survient, un objet exception est passé au mécanisme de propagation des exceptions, et l’exécution est transférée à la séquence de traitement ad doc. Le mécanisme s’effectue en deux phases : – la levée d’exception lors de la détection d’erreur ; – le traitement approprié. Syntaxe La séquence normale d’instructions est placée dans un bloc try. Si une erreur est détectée (levée d’exception), elle est traitée dans le bloc except approprié (le gestionnaire d’exception). from math import sin for x in range(-4, 5): # -4, -3, -2, -1, 0, 1, 2, 3, 4 try: print(’{:.3f}’.format(sin(x)/x), end=" ") except ZeroDivisionError: # toujours fournir une exception print(1.0, end=" ") # gère l’exception en 0 # -0.189 0.047 0.455 0.841 1.0 0.841 0.455 0.047 -0.189 Toutes les exceptions levées par Python sont des instances de sous-classe de la classe Exception. La hiérarchie des sous-classes offre une vingtaine d’exceptions standard. L’instruction raise permet de lever volontairement une exception : x = 2 if not(0 <= x <= 1): raise ValueError("x n’est pas dans [0 .. 1]") Syntaxe complète d’une exception : try: ... # séquence normale d’exécution except <exception_1>: ... # traitement de l’exception 1 except <exception_2>: ... # traitement de l’exception 2 ... else: ... # clause exécutée en l’absence d’erreur finally: ... # clause toujours exécutée Note : En python 2.6, il est encore possible de lever une exception avec une simple chaîne 2.4. Le contrôle du flux d’instructions 35 Plone pour les développeurs, Version 1.0.0 >>> raise "Une exception" Traceback (most recent call last): File ... Une exception Contrairement à Python 3, où il faut impérativement une instance dérivée de BaseException. 2.5 Les conteneurs standard Le chapitre “La console Python” a présenté les types de données simples, mais Python offre beaucoup plus : les conteneurs. De façon générale, un conteneur est un objet composite destiné à contenir d’autres objets. Ce chapitre détaille les séquences, les tableaux associatifs, les ensembles et les fichiers textuels. 2.5.1 Les séquences Qu’est-ce qu’une séquence ? Sequence Une séquence est un conteneur ordonné d’éléments indicés par des entiers. Python dispose de trois types prédéfinis de séquences : – les chaînes (vues précédemment) ; – les listes ; – les tuples. 2.5.2 Les listes Définition, syntaxe et exemples Collection Collection ordonnée et modifiable d’éléments éventuellement hétérogènes. Syntaxe Éléments séparés par des virgules, et entourés de crochets. >>> couleurs = [’trèfle’, ’carreau’, ’coeur’, ’pique’] >>> print(couleurs) [’trèfle’, ’carreau’, ’coeur’, ’pique’] >>> couleurs[1] = 14 >>> print(couleurs) [’trèfle’, 14, ’coeur’, ’pique’] >>> list1 = [’a’, ’b’] >>> list2 = [4, 2.718] >>> list3 = [list1, list2] # liste de listes >>> print(list3) [[’a’, ’b’], [4, 2.718]] Initialisations et tests Utilisation de la répétition, de l’opérateur d’appartenance in et de l’itérateur range() : 36 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 >>> truc, machin = [], [0.0] * 3 >>> print(truc) # (liste vide) [] >>> print(machin) [0.0, 0.0, 0.0] >>> l1 = list(range(4)) >>> print("l1 =", l1) l1 = [0, 1, 2, 3] >>> l2 = list(range(4, 8)) >>> print("l2 =", l2) l2 = [4, 5, 6, 7] >>> l3 = list(range(2, 9, 2)) >>> print("l3 =", l3) l3 = [2, 4, 6, 8] >>> print(2 in l1, 8 in l2, 6 in l3) True False True >>> for i in range(len(l3)): ... print(i, l3[i], sep="-", end=" ") 0-2 1-4 2-6 3-8 Méthodes Quelques méthodes de modification des listes : >>> nombres = [17, 38, 10, 25, 72] >>> nombres.sort() >>> print(nombres) [10, 17, 25, 38, 72] >>> nombres.append(12) >>> nombres.reverse() >>> nombres.remove(38) >>> print(nombres) [12, 72, 25, 17, 10] >>> print(nombres.index(17)) 3 >>> nombres[0] = 11 >>> nombres[1:3] = [14, 17, 2] >>> print(nombres.pop()) 10 >>> print(nombres) [11, 14, 17, 2, 17] >>> print(nombres.count(17)) 2 >>> nombres.extend([1, 2, 3]) >>> print(nombres) [11, 14, 17, 2, 17, 1, 2, 3] Manipulation des “tranches” Syntaxe Si on veut supprimer, remplacer ou insérer plusieurs éléments d’une liste, il faut indiquer une tranche dans le membre de gauche d’une affectation et fournir une liste dans le membre de droite. >>> mots = [’jambon’, ’sel’, ’miel’, ’confiture’, ’beurre’] >>> mots[2:4] = [] # effacement par affectation d’une liste vide >>> print(mots) [’jambon’, ’sel’, ’beurre’] >>> mots[1:3] = [’salade’] 2.5. Les conteneurs standard 37 Plone pour les développeurs, Version 1.0.0 >>> print(mots) [’jambon’, ’salade’] >>> mots[1:] = [’mayonnaise’, ’poulet’, ’tomate’] >>> print(mots) [’jambon’, ’mayonnaise’, ’poulet’, ’tomate’] >>> mots[2:2] = [’miel’] # insertion en 3è position >>> print(mots) [’jambon’, ’mayonnaise’, ’miel’, ’poulet’, ’tomate’] 2.5.3 Les listes en intension Exemple Soit la fonction f(x) retournant l’ensemble des entiers qui à x associe x au carré pour tout x compris entre 2 et 10 inclus. En Python on écrit : >>> [x**2 for x in range(2, 11)] [4, 9, 16, 25, 36, 49, 64, 81, 100] Une liste en intension est une expression qui permet de générer une liste de manière très compacte. Cette notation reprend la définition mathématique d’un ensemble en intension. Liste en intension Une liste en intension est équivalente à une boucle for qui construirait la même liste en utilisant la méthode append(). Les listes en intension sont utilisables sous trois formes. Première forme expression d’une liste simple de valeurs : result1 = [x+1 for x in une_seq] # a le même effet que : result2 = [] for x in une_seq: result2.append(x+1) Deuxième forme expression d’une liste de valeurs avec filtrage : result3 = [x+1 for x in une_seq if x > 23] # a le même effet que : result4 = [] for x in une_seq: if x > 23: result4.append(x+1) Troisième forme expression d’une combinaison de listes de valeurs : result5 = [x+y for x in une_seq for y in une_autre] # a le même effet que : result6 = [] for x in une_seq: for y in une_autre: result6.append(x+y) Des utilisations très pythoniques : >>> >>> >>> >>> >>> 38 valeurs_s = ["12", "78", "671"] # conversion d’une liste de chaînes en liste d’entier valeurs_i = [int(i) for i in valeurs_s] # [12, 78, 671] # calcul de la somme de la liste avec la fonction intégrée sum print(sum([int(i) for i in valeurs_s])) Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 761 >>> # a le même effet que : >>> s = 0 >>> for i in valeurs_s: ... s = s + int(i) >>> print(s) 761 >>> # Initialisation d’une liste 2D >>> multi_liste = [[0]*2 for ligne in range(3)] >>> print(multi_liste) [[0, 0], [0, 0], [0, 0]] 2.5.4 Les tuples Tuple Collection ordonnée et non modifiable d’éléments éventuellement hétérogènes. Syntaxe Éléments séparés par des virgules, et entourés de parenthèses. >>> mon_tuple = (’a’, 2, [1, 3]) – Les tuples s’utilisent comme les listes mais leur parcours est plus rapide ; – Les tuples consomment moins de mémoire ; – Ils sont utiles pour définir des constantes. Attention : Comme les chaînes de caractères, les tuples ne sont pas modifiables ! 2.5.5 Retour sur les références Nous avons déjà vu que l’opération d’affectation, apparemment innocente, est une réelle difficulté de Python. >>> i = 1 >>> msg = "Quoi de neuf ?" >>> e = 2.718 Dans l’exemple ci-dessus, les affectations réalisent plusieurs opérations : – crée en mémoire un objet du type ad hoc (membre de droite) ; – stocke la donnée dans l’objet créé ; – crée un nom de variable (membre de gauche) ; – associe ce nom de variable à l’objet contenant la valeur. Une conséquence de ce mécanisme est que, si un objet modifiable est affecté, tout changement sur un objet modifiera l’autre : >>> fable = ["Je", "plie", "mais", "ne", "romps", "point"] >>> phrase = fable >>> phrase[4] = "casse" >>> print(fable) [’Je’, ’plie’, ’mais’, ’ne’, ’casse’, ’point’] Si l’on désire réaliser une vraie copie d’un objet, on doit utiliser le module copy : >>> >>> >>> >>> >>> import copy a = [1, 2, 3] b = a # une référence b.append(4) print(a) 2.5. Les conteneurs standard 39 Plone pour les développeurs, Version 1.0.0 [1, >>> >>> >>> [1, >>> [1, 2, 3, 4] c = copy.copy(a) # une copie de "surface" c.append(5) print(c) 2, 3, 4, 5] print(a) 2, 3, 4] Dans les rares occasions où l’on veut aussi que chaque élément et attribut de l’objet soit copié séparément et de façon récursive, on emploie la fonction copy.deepcopy(). Complément graphique sur l’assignation Assignation augmentée d’un objet non modifiable (cas d’un entier). On a représenté en gris clair l’addition intermédiaire : F IGURE 2.13 – Exemple (a) Assignation d’un entier F IGURE 2.14 – Exemple (b) Addition intermédiaire Assignation augmentée d’un objet modifiable (cas d’une liste). On a représenté en gris clair la création de la liste intermédiaire : 40 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 F IGURE 2.15 – Exemple (c) Assignation augmentée F IGURE 2.16 – Exemple (a) Assignation d’une liste F IGURE 2.17 – Exemple (b) Création intermédiaire en mémoire 2.5. Les conteneurs standard 41 Plone pour les développeurs, Version 1.0.0 F IGURE 2.18 – Exemple (c) Assignation augmentée 2.5.6 Les tableaux associatifs Les types tableaux associatifs Tableau associatif Un tableau associatif est un type de données permettant de stocker des couples cle : valeur, avec un accès très rapide à la valeur à partir de la clé, la clé ne pouvant être présente qu’une seule fois dans le tableau. Il possède les caractéristiques suivantes : – l’opérateur d’appartenance d’une clé in ; – la fonction taille len() donnant le nombre de couples stockés ; – il est itérable (on peut le parcourir) mais n’est pas ordonné. Python propose le type standard dict. Les dictionnaires (dict) Syntaxe Collection de couples cle : valeur entourée d’accolades. Les dictionnaires constituent un type composite mais ils n’appartiennent pas aux séquences. Comme les listes, les dictionnaires sont modifiables, mais les couples enregistrés n’occupent pas un ordre immuable, leur emplacement est géré par un algorithme spécifique (Cf. les fonctions de hachage). Une clé pourra être alphabétique, numérique...en fait tout type hachable. Les valeurs pourront être des valeurs numériques, des séquences, des dictionnaires, mais aussi des fonctions, des classes ou des instances. Exemples de création : >>> # insertion de clés/valeurs une à une >>> d1 = {} # dictionnaire vide >>> d1["nom"] = 3 >>> d1["taille"] = 176 >>> print(d1) {’nom’: 3, ’taille’: 176} >>> # définition en extension >>> d2 = {"nom": 3, "taille": 176} >>> print(d2) {’nom’: 3, ’taille’: 176} 42 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 >>> # définition en intension >>> d3 = {x: x**2 for x in (2, 4, 6)} >>> print(d3) {2: 4, 4: 16, 6: 36} >>> # utilisation de paramètres nommés >>> d4 = dict(nom=3, taille=176) >>> print(d4) {’taille’: 176, ’nom’: 3} >>> # utilisation d’une liste de couples clés/valeurs >>> d5 = dict([("nom", 3), ("taille", 176)]) >>> print(d5) {’nom’: 3, ’taille’: 176} Méthodes Quelques méthodes applicables aux dictionnaires : >>> tel = {’jack’: 4098, ’sape’: 4139} >>> tel[’guido’] = 4127 >>> print(tel) {’sape’: 4139, ’jack’: 4098, ’guido’: 4127} >>> print(tel[’jack’]) 4098 >>> del tel[’sape’] >>> tel[’irv’] = 4127 >>> print(tel) {’jack’: 4098, ’irv’: 4127, ’guido’: 4127} >>> print(list(tel.keys())) [’jack’, ’irv’, ’guido’] >>> print(sorted(tel.keys())) [’guido’, ’irv’, ’jack’] >>> print(sorted(tel.values())) [4098, 4127, 4127] >>> print(’guido’ in tel, ’jack’ not in tel) True False 2.5.7 Les ensembles (set) set Collection itérable non ordonnée d’éléments hachables distincts. >>> X, Y = set(’abcd’), set(’sbds’) >>> print("X =", X) X = {’a’, ’c’, ’b’, ’d’} >>> print("Y =", Y) Y = {’s’, ’b’, ’d’} : un seul élément ’s’ >>> print(’c’ in X) True >>> print(’a’ in Y) False >>> print(X - Y) {’a’, ’c’} >>> print(Y - X) {’s’} >>> print(X | Y) {’a’, ’c’, ’b’, ’d’, ’s’} >>> print(X & Y) {’b’, ’d’} Note : La notation d’ensembles avec des accolades est une nouveauté de Python 3. en Python 2, l’affichage serait : 2.5. Les conteneurs standard 43 Plone pour les développeurs, Version 1.0.0 F IGURE 2.19 – Opérations sur les ensembles >>> X = set(’abcd’) >>> print X set([’a’, ’c’, ’b’, ’d’]) 2.5.8 Les fichiers textuels Les fichiers : introduction On rappelle que l’ordinateur n’exécute que les programmes présents dans sa mémoire volatile (la RAM). Mais, pour conserver durablement des informations, il faut utiliser une mémoire permanente comme par exemple le dique dur, la clé USB, le DVD... Comme la plupart des langages, Python utilise classiquement la notion de fichier. C’est un type pré-défini en Python, qui ne nessécite donc pas d’importer de module externe. Nous nous limiterons aux fichiers textuels (portables, lisible par un éditeur), mais signalons que les fichiers stockés en codage binaire sont plus compacts et plus rapides à gérer. Gestion des fichiers Ouverture et fermeture des fichiers Principaux modes d’ouverture des fichiers textuels : >>> f1 = open("monFichier_1", "r") # en lecture >>> f2 = open("monFichier_2", "w") # en écriture >>> f3 = open("monFichier_3", "a") # en ajout Python utilise les fichiers en mode texte par défaut (noté t) (pour les fichiers binaires, il faut préciser le mode b). Tant que le fichier n’est pas fermé, son contenu n’est pas garanti sur le disque. Une seule méthode de fermeture : 44 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 >>> f1.close() Écriture séquentielle Méthodes d’écriture : >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> f = open("truc.txt", "w") s = ’toto\n’ f.write(s) # écrit la chaîne s dans f l = [’a’, ’b’, ’c’] f.writelines(l) # écrit les chaînes de la liste l dans f f.close() # utilisation de l’option file de print f2 = open("truc2.txt", "w") print("abcd", file=f2) f2.close() Regardez maintenant votre fichier truc.txt avec votre éditeur de texte favori : toto abc Lecture séquentielle Méthodes de lecture : >>> f = open("truc.txt", "r") >>> s = f.read() # lit tout le fichier --> string >>> s = f.read(3) # lit au plus n octets --> string >>> s = f.readline() # lit la ligne suivante --> string >>> s = f.readlines() # lit tout le fichier --> liste de strings >>> f.close() >>> # Affichage des lignes d’un fichier une à une >>> f = open("truc.txt") # mode "r" par défaut >>> for ligne in f: ... print(ligne[:-1]) # pour sauter le retour à la ligne toto abc >>> f.close() 2.5.9 Itérer sur les conteneurs Les techniques suivantes sont classiques et très utiles. Obtenir clés et valeurs en bouclant sur un dictionnaire : >>> knights = {"Gallahad": "the pure", "Robin": "the brave"} >>> for k, v in knights.items(): ... print(k, v) Gallahad the pure Robin the brave Obtenir clés et valeurs en bouclant sur une liste : >>> for i, v in enumerate(["tic", "tac", "toe"]): ... print(i, v, end=" ", sep="->") 0->tic 1->tac 2->toe Boucler sur deux séquences (ou plus) appariées : 2.5. Les conteneurs standard 45 Plone pour les développeurs, Version 1.0.0 >>> question = ["name", "quest", "favorite color"] >>> answers = ["Lancelot", "the Holy Grail", "blue"] >>> for q, a in zip(question, answers): ... print("What is your {}? It is {}.".format(q, a)) What is your name? It is Lancelot. What is your quest? It is the Holy Grail. What is your favorite color? It is blue. Boucler sur une séquence inversée (la séquence initiale est inchangée) : >>> print() >>> for i in reversed(range(1, 10, 2)): ... print(i, end=" ") 9 7 5 3 1 Boucler sur une séquence triée à éléments uniques (la séquence initiale est inchangée) : >>> print() >>> basket = ["apple", "orange", "apple", "pear", "orange", "banana"] >>> for f in sorted(set(basket)): ... print(f, end=" ") apple banana orange pear 2.5.10 L’affichage formaté Attention : Python 2.6 : Contrairement à Python 3, les arguments positionnels d’une chaîne de formatage doit être explicite. En d’autres termes l’utilisation du slot de formatage {} est sanctionné d’un message d’erreur. >>> # Python 3 >>> print("Je m’appelle {}".format("Bob")) Je m’appelle Bob >>> # Python 2.6 >>> print("Je m’appelle {0}".format("Bob")) Je m’appelle Bob La méthode format permet de contrôler finement toutes sortes d’affichages. Remplacements simples : >>> print("Je m’appelle {}".format("Bob")) Je m’appelle Bob >>> print("Je m’appelle {{{}}}".format("Bob")) Je m’appelle {Bob} >>> print("{}".format("-"*10)) ---------- Remplacements avec champs nommés : >>> a, b = 5, 3 >>> print("The story of {c} and {d}".format(c=a+b, d=a-b)) The story of 8 and 2 Formatages à l’aide de liste : >>> stock = [’papier’, ’enveloppe’, ’chemise’, ’encre’, ’buvard’] >>> print("Nous avons de l’{0[3]} et du {0[0]} en stock\n".format(stock)) Nous avons de l’encre et du papier en stock Formatages à l’aide de dictionnaire : 46 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 >>> >>> >>> >>> >>> print("My name is {0[name]}".format(dict(name=’Fred’))) # My name is Fred d = dict(animal = ’éléphant’, poids = 12000) print("L’{0[animal]} pèse {0[poids]} kg\n".format(d)) # L’éléphant pèse 12000 kg Remplacement avec attributs nommés : >>> import math >>> import sys >>> print("math.pi = {.pi}, epsilon = {.float_info.epsilon}".format(math, sys)) math.pi = 3.14159265359, epsilon = 2.22044604925e-16 Conversions str() et repr() : >>> print("{0!s} {0!r}".format("Une chaîne")) Une chaîne ’Une chaîne’ Formatages numériques : >>> n = 100 >>> pi = 3.1415926535897931 >>> print("{0}, et {1}".format(n, pi)) 100, et 3.14159265359 >>> print("{}, et {}".format(n, pi)) 100, et 3.14159265359 >>> print("{0}, {1} et {0}".format(n, pi)) 100, 3.14159265359 et 100 >>> print("{:.4e}".format(pi)) 3.1416e+00 >>> print("{:g}".format(pi)) 3.14159 >>> msg = "Résultat sur {:d} échantillons : {:.2f}".format(n, pi) >>> print(msg) Résultat sur 100 échantillons : 3.14 Formatages divers : >>> s = "The sword of truth" >>> print("[{}]".format(s)) [The sword of truth] >>> print("[{:25}]".format(s)) [The sword of truth ] >>> print("[{:>25}]".format(s)) [ The sword of truth] >>> print("[{:^25}]".format(s)) [ The sword of truth ] >>> print("[{:-^25}]".format(s)) [---The sword of truth----] >>> print("[{:.<25}]".format(s)) [The sword of truth.......] >>> long = 12 >>> print("[{}]".format(s[:long])) [The sword of] >>> m = 123456789 >>> print("{:0=12}".format(m)) 000123456789 >>> print("{:#=12}".format(m)) ###123456789 2.5. Les conteneurs standard 47 Plone pour les développeurs, Version 1.0.0 2.6 Fonctions et espaces de noms Note : Les fonctions sont les éléments structurants de base de tout langage procédural. Elles offrent différents avantages : – Évite la répétition : on peut < factoriser > une portion de code qui se répète lors de l’exécution en séquence d’un script ; – Met en relief les données et les résultats : entrées et sorties de la fonction ; – Permet la réutilisation : mécanisme de l’import ; – Décompose une tâche complexe en tâches plus simples : conception de l’application. Ces avantages sont illustrés ci-dessous : F IGURE 2.20 – Exemple (a) Évite la duplication de code. F IGURE 2.21 – Exemple (b) Met en relief entrées et sorties. 48 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 F IGURE 2.22 – Exemple (c) L’import permet la réutilisation. F IGURE 2.23 – Exemple (d) Améliore la conception. 2.6.1 Définition et syntaxe Fonction Groupe d’instructions regroupé sous un nom (définition) et s’exécutant à la demande (appel). Syntaxe C’est une instruction composée : def nomFonction(parametre_1, ...): """Documentation de la fonction.""" # <bloc_instructions>* Le bloc d’instructions est obligatoire. S’il est vide, on emploie l’instruction pass. La documentation (facultative) est fortement conseillée. 2.6.2 Passage des arguments Mécanisme général Note : Passage par affectation : chaque argument de la définition de la fonction correspond, dans l’ordre, à un paramètre de l’appel. La correspondance se fait par affectation. Un ou plusieurs paramètres, pas de retour Exemple sans l’instruction return, ce qu’on appelle souvent une procédure. Dans ce cas la fonction renvoie implicitement la valeur None : def table(base, debut, fin): """Affiche la table des <base> de <debut> à <fin>.""" n = debut while n <= fin: 2.6. Fonctions et espaces de noms 49 Plone pour les développeurs, Version 1.0.0 F IGURE 2.24 – Passage des arguments par affectation. print(n, ’x’, base, ’=’, n * base, end=" ") n += 1 # exemple d’appel : table(7, 2, 11) # 2 x 7 = 14 3 x 7 = 21 4 x 7 = 28 5 x 7 = 35 6 x 7 = 42 # 7 x 7 = 49 8 x 7 = 56 9 x 7 = 63 10 x 7 = 70 11 x 7 = 77 Un ou plusieurs paramètres, utilisation du retour Exemple avec utilisation d’un return unique : from math import pi def cube(x): return x**3 def volumeSphere(r): return 4.0 * pi * cube(r) / 3.0 # Saisie du rayon et affichage du volume rayon = float(input(’Rayon : ’)) print("Volume de la sphère =", volumeSphere(rayon)) Exemple avec utilisation d’un return multiple : import math def surfaceVolumeSphere(r): surf = 4.0 * math.pi * r**2 vol = surf * r/3 return surf, vol # programme principal 50 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 rayon = float(input(’Rayon : ’)) s, v = surfaceVolumeSphere(rayon) print("Sphère de surface {:g} et de volume {:g}".format(s, v)) Passage d’une fonction en paramètre def tabuler(fonction, borneInf, borneSup, nbPas): """Affichage des valeurs de <fonction>. On doit avoir (borneInf < borneSup) et (nbPas > 0)""" h, x = (borneSup - borneInf) / float(nbPas), borneInf while x <= borneSup: y = fonction(x) print("f({:.2f}) = {:.2f}".format(x, y)) x += h # ... def maFonction(x): return 2*x**3 + x - 5 tabuler(maFonction, -5, 5, 10) # f(-5.00) = -260.00 # f(-4.00) = -137.00 # ... # f(5.00) = 250.00 Paramètres avec valeur par défaut On utilise de préférence des valeurs par défaut non modifiables car la modification d’un paramètre par un premier appel est visible les fois suivantes : def initPort(speed=9600, parity="paire", data=8, stops=1): print("Init. à", speed, "bits/s", "parité :", parity) print(data, "bits de données", stops, "bits d’arrêt") # Appels possibles : initPort() # Init. à 9600 bits/s parité : paire # 8 bits de données 1 bits d’arrêt initPort(parity="nulle") # Init. à 9600 bits/s parité : nulle # 8 bits de données 1 bits d’arrêt initPort(2400, "paire", 7, 2) # Init. à 2400 bits/s parité : paire # 7 bits de données 2 bits d’arrêt Nombre d’arguments arbitraire : passage d’un tuple def somme(*args): """Renvoie la somme de <tuple>.""" resultat = 0 for nombre in args: resultat += nombre return resultat # Exemples d’appel : print(somme(23)) # 23 print(somme(23, 42, 13)) # 78 Note : Si la fonction possède plusieurs arguments, le tuple est en dernière position. 2.6. Fonctions et espaces de noms 51 Plone pour les développeurs, Version 1.0.0 Il est aussi possible de passer un tuple (en fait une séquence) à l’appel qui sera décompressé en une liste de paramètres d’une fonction < classique > : def somme(a, b, c): return a+b+c # Exemple d’appel : elements = (2, 4, 6) print(somme(*elements)) # 12 Nombre d’arguments arbitraire : passage d’un dictionnaire def unDict(**kargs): return kargs # Exemples d’appels ## par des paramètres nommés : print(unDict(a=23, b=42)) # {’a’: 23, ’b’: 42} ## en fournissant un dictionnaire : mots = {’d’: 85, ’e’: 14, ’f’:9} print(unDict(**mots)) # {’e’: 14, ’d’: 85, ’f’: 9} Note : Si la fonction possède plusieurs arguments, le dictionnaire est en toute dernière position (après un éventuel tuple). 2.6.3 Espaces de noms Portée des objets Note : Portée : les noms des objets sont créés lors de leur première affectation, mais ne sont visibles que dans certaines régions de la mémoire. On distingue : – La portée globale : celle du module __main__. Un dictionnaire gère les objets globaux : l’instruction globals() fournit les couples variable :valeur ; – La portée locale : les objets internes aux fonctions (et aux classes) sont locaux. Les objets globaux ne sont pas modifiables dans les portées locales. L’instruction locals() fournit les couples variable :valeur. Résolution des noms : règle LGI La recherche des noms est d’abord locale (L), puis globale (G), enfin interne (I) (cf. Figure ci-dessous) : Exemples de portée : # x et fonc sont affectés dans le module : globaux def fonc(y): # y et z sont affectés dans fonc : locaux global x # permet de modifier x ligne suivante x += 2 z = x + y return z x = 99 print(fonc(1)) # 102 # x et fonc sont affectés dans le module : globaux def fonc(y): # y et z sont affectés dans fonc : locaux # dans fonc : portée locale z = x + y return z 52 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 F IGURE 2.25 – Règle LGI 2.6. Fonctions et espaces de noms 53 Plone pour les développeurs, Version 1.0.0 x = 99 print(fonc(1)) # 100 # x et fonc sont affectés dans le module : globaux def fonc(y): # y, x et z sont affectés dans fonc : locaux x = 3 # ce nouvel x est local et masque le x global z = x + y return z x = 99 print(fonc(1)) # 4 2.7 Modules et packages Un programme Python est généralement composé de plusieurs fichiers sources, appelés modules. Leur nom est suffixé :samp‘.py‘. S’ils sont correctement codés les modules doivent être indépendants les uns des autres pour être réutilisés à la demande dans d’autres programmes. Ce chapitre explique comment coder et importer des modules dans un autre. Nous verrons également la notion de package qui permet de grouper plusieurs modules. 2.7.1 Modules Module Fichier indépendant permettant de scinder un programme en plusieurs scripts. Ce mécanisme permet d’élaborer efficacement des bibliothèques de fonctions ou de classes. Avantages des modules : – réutilisation du code ; – la documentation et les tests peuvent être intégrés au module ; – réalisation de services ou de données partagés ; – partition de l’espace de noms du système. Import d’un module Deux syntaxes possibles : – la commande import nom_module importe la totalité des objets du module : import tkinter – la commande from nom_module import obj1, obj2... n’importe que les objets obj1, obj2... du module : from math import pi, sin, log Il est conseillé d’importer dans l’ordre : – les modules de la bibliothèque standard ; – les modules des bibliothèques tierces ; – Les modules personnels. Exemples Notion d’auto-test Un module cube_m.py. Remarquez l’utilisation de l’auto-test qui permet de tester le module seul : 54 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 def cube(y): """Calcule le cube du paramètre <y>.""" return y**3 # Auto-test ---------------------------------------------------if __name__ == "__main__": # False lors d’un import ==> ignoré help(cube) # affiche le docstring de la fonction print("cube de 9 :", cube(9)) # cube de 9 : 729 Utilisation de ce module. On importe la fonction cube() incluse dans le fichier cube_m.py : from cube_m import cube for i in range(1, 4): print("cube de", i, "=", cube(i), end=" ") # cube de 1 = 1 cube de 2 = 8 cube de 3 = 27 Une interface à gnuplot L’application libre gnuplot permet d’afficher des courbes. La fonction suivante est une interface d’appel qui permet d’afficher des données issues de fichiers : import os def plotFic(courbes): dem = open("_.dem", "w") # fichier réutilisé à chaque tracé dem.write("set grid\n") plot_data = ["’%s’ with %s" % (c[0], c[1]) for c in courbes] dem.write("plot " + ’,’.join(plot_data)) dem.write(’\npause -1 "\’Entrée\’ pour continuer"\n’) dem.write("reset") dem.close() os.system("wgnuplot _.dem") L’auto-test suivant illustre son utilisation : if __name__ == ’__main__’: f, g, h = open("d1.dat", "w"), open("d2.dat", "w"), open("d3.dat", "w") for i in range(201): x = 0.1*i - 5.0 y = x**3 - 20*x**2 f.write("%g %g\n" %(x, y)) y = x**3 - 30*x**2 g.write("%g %g\n" %(x, y)) y = x**3 - 40*x**2 h.write("%g %g\n" %(x, y)) h.close(); g.close(); f.close() plotFic([(’d1.dat’, ’points’)]) plotFic([(’d1.dat’, ’lines’), (’d2.dat’, ’points’), (’d3.dat’, ’lines’)]) 2.7.2 Bibliothèque standard La bibliothèque standard On dit souvent que Python est livré piles comprises (batteries included) tant sa bibliothèque standard, riche de plus de 200 packages et modules, répond aux problèmes courants les plus variés. Ce survol présente quelques fonctionnalités utiles. 2.7. Modules et packages 55 Plone pour les développeurs, Version 1.0.0 La gestion des chaînes Le module string fournit des constantes comme ascii_lowercase, digits...et la classe Formatter qui peut être spécialisée en sous-classes spécialisées de formateurs de chaînes. Le module textwrap est utilisé pour formater un texte : longueur de chaque ligne, contrôle de l’indentation. Le module struct permet de convertir des nombres, booléens et des chaînes en leur représentation binaire afin de communiquer avec des bibliothèques de bas niveau (souvent en C). Le module difflib permet la comparaison de séquences et fournit des sorties au format standard diff ou en HTML. Enfin on ne peut oublier le module re qui offre à Python la puissance des expressions régulières. Exemple : le module io.StringIO Ce module fournit des objets compatibles avec l’interface des objets fichiers. Exemple de gestion ligne à ligne d’un fichier ou d’une chaîne avec la même fonction scanner utilisant le même traitement : def scanner(objet_fichier, gestionnaire_ligne): for ligne in objet_fichier: gestionnaire_ligne(ligne) if __name__==’__main__’: def premierMot(ligne): print(ligne.split()[0]) fic = open("data.dat") scanner(fic, premierMot) import io chaine = io.StringIO("un\ndeux xxx\ntrois\n") scanner(chaine, premierMot) La gestion de la ligne de commande La gestion est assurée par deux modules : getopt, le module historique hérité du C et optparse, un module récent beaucoup plus puissant : from optparse import OptionParser parser = OptionParser() parser.add_option("-f", "--file", dest="filename", help="write report to FILE", metavar="FILE") parser.add_option("-q", "--quiet", action="store_false", dest="verbose", default=True, help="don’t print status messages to stdout") (options, args) = parser.parse_args() Les lignes de commande : $ python 6_025.py -h ou : $ python 6_025.py --help produisent la même documentation : Usage: 6_025.py [options] Options: -h, --help show this help message and exit -f FILE, --file=FILE write report to FILE -q, --quiet don’t print status messages to stdout 56 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 Bibliothèques mathématiques et types numériques En standard, Python propose les modules fraction et decimal : >>> from fractions import Fraction >>> import decimal as d >>> print(Fraction(16, -10)) -8/5 >>> print(Fraction(123)) 123 >>> print(Fraction(’ -3/7 ’)) -3/7 >>> print(Fraction(’-.125’)) -1/8 >>> print(Fraction(’7e-6’)) 7/1000000 >>> d.getcontext().prec = 6 >>> print(d.Decimal(1) / d.Decimal(7)) 0.142857 >>> d.getcontext().prec = 18 >>> print(d.Decimal(1) / d.Decimal(7)) 0.142857142857142857 En plus des bibliothèques math et cmath déjà vues, la bibliothèque random propose plusieurs fonctions de nombres aléatoires. La gestion du temps et des dates Les modules calendar, time et datetime fournissent les fonctions courantes de gestion du temps et des durées : >>> import calendar, datetime, time >>> moon_apollo11 = datetime.datetime(1969, 7, 20, 20, 17, 40) >>> print(moon_apollo11) >>> print(time.asctime(time.gmtime(0))) Thu Jan 01 00:00:00 1970 ("epoch" UNIX) >>> vendredi_precedent = datetime.date.today() >>> un_jour = datetime.timedelta(days=1) >>> while vendredi_precedent.weekday() != calendar.FRIDAY: ... vendredi_precedent -= un_jour >>> print(vendredi_precedent.strftime("%A, %d-%b-%Y")) Friday, 09-Oct-2009 Algorithmes et types de données collection Le module bisect fournit des fonctions de recherche de séquences triées. Le module array propose un type semblable à la liste, mais plus rapide car de contenu homogène. Le module heapq gère des tas dans lesquels l’élément d’indice 0 est toujours le plus petit : >>> >>> >>> >>> ... >>> [2, import heapq import random heap = [] for i in range(10): heapq.heappush(heap, random.randint(2, 9)) print(heap) 3, 5, 4, 6, 6, 7, 8, 7, 8] À l’instar des structures C, Python propose désormais, via le module collections, la notion de type tuple nommé : 2.7. Modules et packages 57 Plone pour les développeurs, Version 1.0.0 >>> import collections >>> # description du type : >>> Point = collections.namedtuple("Point", "x y z") >>> # on instancie un point : >>> point = Point(1.2, 2.3, 3.4) >>> # on l’affiche : >>> print("point : [{}, {}, {}]".format(point.x, point.y, point.z)) point : [1.2, 2.3, 3.4] Il est bien sûr possible d’avoir des tuples nommés emboîtés. Le type defaultdict permet des utilisations avancées : >>> from collections import defaultdict >>> s = [(’y’, 1), (’b’, 2), (’y’, 3), (’b’, 4), (’r’, 1)] >>> d = defaultdict(list) >>> for k, v in s: ... d[k].append(v) >>> print(d.items()) dict_items([(’y’, [1, 3]), (’r’, [1]), (’b’, [2, 4])]) >>> s = ’mississippi’ >>> d = defaultdict(int) >>> for k in s: ... d[k] += 1 >>> print(d.items()) dict_items([(’i’, 4), (’p’, 2), (’s’, 4), (’m’, 1)]) Et tant d’autres domaines... Beaucoup d’autres domaines pourraient être explorés : – accès au système ; – utilitaires fichiers ; – programmation réseau ; – persistance ; – les fichiers XML ; – la compression ; – ... 2.7.3 Bibliothèques tierces Une grande diversité Outre les modules intégrés à la distribution standard de Python, on trouve des bibliothèques dans tous les domaines : – scientifique ; – bases de données ; – tests fonctionnels et contrôle de qualité ; – 3D ; – ... Le site http ://pypi.python.org/pypi (The Python Package Index) recense des milliers de modules et de packages ! Un exemple : la bibliothèque Unum Elle permet de calculer en tenant compte des unités du système S.I. Voici un exemple de session interactive : 58 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 -- Welcome in Unum Calculator (ver 04.00) ->>> d = 1609 * M >>> t = 11.7 * S >>> v = d/t >>> v 137.521367521 [m/s] >>> a = v/t >>> a 11.753963036 [m/s2] 2.7.4 Packages Package Un package est un module contenant d’autres modules. Les modules d’un package peuvent être des sous-packages, ce qui donne une structure arborescente. En résumé, un package est simplement un répertoire qui contient des modules et un fichier __init__.py décrivant l’arborescence du package. 2.8 La programmation Orientée Objet La Programmation Orientée Objet : – la POO permet de mieux modéliser la réalité en concevant des ensembles d’objets, les classes. – Ces classes permettent de construire des objets interactifs entre eux et avec le monde extérieur. – Les objets sont créés indépendamment les uns des autres, grâce à l’encapsulation, mécanisme qui permet d’embarquer leurs propriétés. – Les classes permettent d’éviter au maximum l’emploi des variables globales. – Enfin les classes offrent un moyen économique et puissant de construire de nouveaux objets à partir d’objets préexistants. 2.8.1 Insuffisance de l’approche procédurale Un exemple : On veut représenter un cercle, ce qui nécessite au minimum trois informations, les coordonnées du centre et le rayon : cercle = (11, 60, 8) Mais comment interpréter ces trois données ? cercle = (x, y, rayon) ou bien cercle = (rayon, x, y) Pour résoudre ce problème et améliorer la lisibilité, on peut utiliser des tuples nommés : from collection import namedtuple Cercle = namedtuple("Cercle", "x y rayon") cercle = Cercle(11, 60, 8) # exemple d’utilisation : distance = distance_origine(cercle.x, cercle.y) Par contre, il reste le problème des données invalides, ici un rayon négatif : cercle = Cercle(11, 60, -8) 2.8. La programmation Orientée Objet 59 Plone pour les développeurs, Version 1.0.0 Si les cercles doivent changer de caractéristiques, il faut opter pour un type modifiable, liste ou dictionnaire ce qui ne règle toujours pas le problème des données invalides... On a donc besoin d’un mécanisme pour empaqueter les données nécessaires pour représenter un cercle et pour empaqueter les méthodes applicables à ce nouveau type de données (la classe), de telle sorte que seules les opérations valides soient utilisables. 2.8.2 Terminologie Le vocabulaire de la POO Une classe est donc équivalente à un nouveau type de données. On connaît déjà par exemple int ou str. Un objet ou une instance est un exemplaire particulier d’une classe. Par exemple "truc" est une instance de la classe str. La plupart des classes encapsulent à la fois les données et les méthodes applicables aux objets. Par exemple un objet str contient une chaîne de caractères Unicode (les données) et de nombreuses méthodes comme upper. On pourrait définir un objet comme une capsule, à savoir un “paquet” contenant des attributs et des méthodes : objet = [attributs + méthodes] Beaucoup de classes offrent des caractéristiques supplémentaires comme par exemple la concaténation des chaînes en utilisant simplement l’opérateur +. Ceci est obtenu grâce aux méthodes spéciales. Par exemple l’opérateur + est utilisable car on a redéfini la méthode __add__. Les objets ont généralement deux sortes d’attributs : les données nommées simplement attributs et les fonctions applicables appelées méthodes. Par exemple un objet de la classe complex possède : – imag et real, ses attributs ; – beaucoup de méthodes, comme conjugate ; – des méthodes spéciales pour le support des opérateurs : +, -, / ... Les attributs sont normalement implémentés comme des variables d’instance, particulières à chaque instance d’objet. Le mécanisme de property permet un accès contrôlé aux données, ce qui permet de les valider et de les sécuriser. Un avantage décisif de la POO est qu’une classe Python peut toujours être spécialisée en une classe fille qui hérite alors de tous les attributs (données et méthodes) de sa supper classe. Comme tous les attributs peuvent être redéfinis, une méthode de la classe fille et de la classe mère peut posséder le même nom mais effectuer des traitements différents (surcharge) et Python s’adaptera dynamiquement, dès l’affectation. En proposant d’utiliser un même nom de méthode pour plusieurs types d’objets différents, le polymorphisme permet une programmation beaucoup plus générique. Le développeur n’a pas à savoir, lorsqu’il programme une méthode, le type précis de l’objet sur lequel la méthode va s’appliquer. Il lui suffit de savoir que cet objet implémentera la méthode. Enfin Python supporte également le duck typing : “s’il marche comme un canard et cancane comme un canard, alors c’est un canard !”. Ce qui signifie que Python ne s’intéresse qu’au comportement des objets. Par exemple un objet fichier peut être créé par open ou par une instance de io.StringIO. Les deux approches offrent la même API (interface de programmation), c’est-à-dire les mêmes méthodes. 60 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 2.8.3 Classes et instanciation d’objets L’instruction class Syntaxe Instruction composée : en-tête (avec docstring) + corps indenté : class C: """Documentation de la classe.""" x = 23 Dans cet exemple, C est le nom de la classe (qui commence conventionnellement par une majuscule), et x est un attribut de classe, local à C. L’instanciation et ses attributs – Les classes sont des fabriques d’objets : on construit d’abord l’usine avant de produire des objets ! – On instancie un objet (i.e. création, production depuis l’usine) en appelant le nom de sa classe : >>> a = C() # a est un objet de la classe C >>> print(dir(a)) # affiche les attributs de l’objet a [’__class__’, ’__delattr__’, ’__dict__’, ’__doc__’, ’__format__’, ..., ’x’] >>> print(a.x) # x est un attribut de classe 23 >>> a.x = 12 # modifie l’attribut d’instance (attention...) >>> print(C.x) # l’attribut de classe est inchangé 23 >>> a.y = 44 # nouvel attribut d’instance >>> b = C() # b est un autre objet de la classe C >>> print(b.x) # b connaît son attribut de classe, mais... 23 >>> print(b.y) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: ’C’ object has no attribute ’y’ L’introspection. Plusieurs commandes magiques : – dir fonction d’affichage des membres d’un objet, – is opérateur testant si les deux membres sont la même instance, – isinstance fonction testant si une instance est bien d’un type donné, – help fonction d’affichage de l’aide sur un objet, – globals permet d’afficher les variables globales, – locals permet d’afficher les variables locales d’une fonction ou methode. Plusieurs attributs : – __class__ pointe vers la classe de l’objet. – __dict__ si l’objet est un mutable, ce dictionnaire contient la liste des membres de l’instance. Retour sur les espaces de noms Tout comme les fonctions, les classes possèdent leurs espaces de noms : – Chaque classe possède son propre espace de noms. Les variables qui en font partie sont appelées attributs de classe. – Chaque objet instance (créé à partir d’une classe) obtient son propre espace de noms. Les variables qui en font partie sont appelées attributs d’instance. 2.8. La programmation Orientée Objet 61 Plone pour les développeurs, Version 1.0.0 – Les classes peuvent utiliser (mais pas modifier) les variables définies au niveau principal. – Les instances peuvent utiliser (mais pas modifier) les variables définies au niveau de la classe et les variables définies au niveau principal. Les espaces de noms sont implémentés par des dictionnaires pour les modules, les classes et les instances. – Noms non qualifiés (exemple dimension) l’affectation crée ou change le nom dans la portée locale courante. Ils sont cherchés suivant la règle LGI. – Noms qualifiés (exemple dimension.hauteur) l’affectation crée ou modifie l’attribut dans l’espace de noms de l’objet. Un attribut est cherché dans l’objet, puis dans toutes les classes dont l’objet dépend (mais pas dans les modules). L’exemple suivant affiche le dictionnaire lié à la classe C puis la liste des attributs liés à une instance de C : >>> class C: ... x = 20 >>> print(C.__dict__) {’__dict__’: <attribute ’__dict__’ of ’C’ objects>, ’x’: 20, ’__module__’: ’__main__’, ’__weakref__’: <attribute ’__weakref__’ of ’C’ objects>, ’__doc__’: None} >>> a = C() >>> print(dir(a)) [’__class__’, ’__delattr__’, ’__dict__’, ’__doc__’, ’ __getattribute__’, ’__hash__’, ’__init__’, ’__module__’, ’__new__’, ’ __reduce__’, ’__reduce_ex__’, ’__repr__’, ’__setattr__’, ’__str__’, ’ __weakref__’, ’x’] 2.8.4 Méthodes Syntaxe Une méthode s’écrit comme une fonction du corps de la classe avec un premier paramètre self obligatoire, où self représente l’objet sur lequel la méthode sera appliquée. 3 Autrement dit self est la référence d’instance. class C: x = 23 # x et y : attributs de classe y = x + 5 def affiche(self): # méthode affiche() self.z = 42 # attribut d’instance print(C.y) # dans une méthode, on qualifie un attribut de classe, print(self.z) # mais pas un attribut d’instance ob = C() # instanciation de l’objet ob ob.affiche() # 28 42 (à l’appel, ob affecte self) 2.8.5 Méthodes spéciales Les méthodes spéciales Ces méthodes portent des noms pré-définis, précédés et suivis de deux caractères de soulignement. Elles servent : – à initialiser l’objet instancié ; – à modifier son affichage ; – à surcharger ses opérateurs ; – ... 3. L’utilisation du terme self est une convention, contrairement à Javascript qui impose le terme this. 62 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 L’initialisateur Lors de l’instanciation d’un objet, la méthode __init__ est automatiquement invoquée. Elle permet d’effectuer toutes les initialisations nécessaires : >>> class C: ... def __init__(self, n): ... self.x = n # initialisation de l’attribut d’instance x >>> une_instance = C(42) # paramètre obligatoire, affecté à n >>> print(une_instance.x) 42 C’est une procédure automatiquement invoquée lors de l’instanciation : elle ne contient jamais l’instruction return. Le cas échéant, celle-ci est ignorée. Surcharge des opérateurs La surcharge permet à un opérateur de posséder un sens différent suivant le type de leurs opérandes. Par exemple, l’opérateur + permet : x = 7 + 9 # addition entière s = ’ab’ + ’cd’ # concaténation Python possède des méthodes de surcharge pour : – tous les types (__call__, __str__, ...) ; – les nombres (__add__, __div__, ...) ; – les séquences (__len__, __iter__, ...). Soient deux instances, obj1 et obj2, les méthodes spéciales suivantes permettent d’effectuer les opérations arithmétiques courantes : Nom opposé addition soustraction multiplication division Méthode spéciale __neg__ __add__ __sub__ __mul__ __div__ Utilisation -obj1 obj1 + obj2 obj1 - obj2 obj1 * obj2 obj1 / obj2 Exemple de surcharge class Vecteur2D: def __init__(self, x, y): self.x = x self.y = y def __add__(self, autre): # addition vectorielle return Vecteur2D(self.x + autre.x, self.y + autre.y) def __str__(self): # affichage d’un Vecteur2D return "Vecteur({:g}, {:g})" % (self.x, self.y) v1 = Vecteur2D(1.2, 2.3) v2 = Vecteur2D(3.4, 4.5) print(v1 + v2) # Vecteur(4.6, 6.8) 2.8. La programmation Orientée Objet 63 Plone pour les développeurs, Version 1.0.0 2.8.6 Héritage et polymorphisme Héritage et polymorphisme Héritage L’héritage est le mécanisme qui permet de se servir d’une classe préexistante pour en créer une nouvelle qui possédera des fonctionnalités différentes ou supplémentaires. Polymorphisme Le polymorphisme est la faculté pour une méthode portant le même nom mais appartenant à des classes distinctes héritées d’effectuer un travail différent. Cette propriété est acquise par la technique de la surcharge. Exemple d’héritage et de polymorphisme Dans l’exemple suivant, la classe Carre hérite de la classe Rectangle, et la méthode __init__ est polymorphe : class Rectangle: def __init__(self, longueur=30, largeur=15): self.L, self.l, self.nom = longueur, largeur, "rectangle" class Carre(Rectangle): def __init__(self, cote=10): Rectangle.__init__(self, cote, cote) self.nom = "carré" r = Rectangle() print(r.nom) # ’rectangle’ c = Carre() print(c.nom) # ’carré’ 2.8.7 Retour sur l’exemple initial La classe Cercle : conception Nous allons tout d’abord concevoir une classe Point héritant de la classe mère object. Puis nous pourrons l’utiliser comme classe de base de la classe Cercle. Dans les schémas UML (Unified Modeling Language ) ci-dessous, les attributs en italiques sont hérités, ceux en casse normale sont nouveaux et ceux en gras sont redéfinis (surchargés). La classe Cercle Voici le code de la classe Point : class Point: def __init__(self, x=0, y=0): self.x, self.y = x, y @property def distance_origine(self): return math.hypot(self.x, self.y) def __eq__(self, other): return self.x == other.x and self.y == other.y def __str__(self): return "({0.x!s}, {0.y!s})".format(self) 64 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 F IGURE 2.26 – Conception UML de la classe Cercle. L’utilisation du décorateur property permet un accès en lecture seule au résultat de la méthode distance_origine considérée alors comme un simple attribut (car il n’y a pas de parenthèse) : if __name__ == "__main__": p1, p2 = Point(), Point(3, 4) print(p1 == p2) # False print(p2, p2.distance_origine) # (3, 4) 5.0 De nouveau, les méthodes renvoyant un simple flottant seront utilisées comme des attributs grâce à property : class Cercle(Point): def __init__(self, rayon, x=0, y=0): super().__init__(x, y) self.rayon = rayon @property def aire(self): return math.pi * (self.rayon ** 2) @property def circonference(self): return 2 * math.pi * self.rayon @property def distance_bord_origine(self): return abs(self.distance_origine - self.rayon) Voici la syntaxe permettant d’utiliser la méthode rayon comme un attribut en lecture-écriture. Remarquez que la méthode rayon retourne l’attribut protégé : __rayon qui sera modifié par le setter (la méthode modificatrice) : class Cercle(Cercle): @property def rayon(self): return self.__rayon 2.8. La programmation Orientée Objet 65 Plone pour les développeurs, Version 1.0.0 @rayon.setter def rayon(self, rayon): assert rayon > 0, "rayon strictement positif" self.__rayon = rayon Exemple d’utilisation des instances de Cercle : class Cercle(Cercle): def __eq__(self, other): return (self.rayon == other.rayon and super().__eq__(other)) def __str__(self): return ("{0.__class__.__name__}({0.rayon!s}, {0.x!s}, " "{0.y!s})".format(self)) if __name__ == "__main__": c1 = Cercle(2, 3, 4) print(c1, c1.aire, c1.circonference) # Cercle(2, 3, 4) 12.5663706144 12.5663706144 print(c1.distance_bord_origine, c1.rayon) # 3.0 2 c1.rayon = 1 # modification du rayon print(c1.distance_bord_origine, c1.rayon) # 4.0 1 2.8.8 Notion de Conception Orientée Objet Suivant les relations que l’on va établir entre les objets de notre application, on peut concevoir nos classes de deux façons possibles : – la composition qui repose sur la relation a-un ou sur la relation utilise-un ; – la dérivation qui repose sur la relation est-un. Bien sûr, ces deux conceptions peuvent cohabiter, et c’est souvent le cas ! Composition Composition La composition est la collaboration de plusieurs classes distinctes via une association (utilise-un) ou une aggrégation (a-un). La classe composite bénéficie de l’ajout de fonctionnalités d’autres classes qui n’ont rien en commun. L’implémentation Python utilisée est généralement l’instanciation de classes dans le constructeur de la classe composite. Exemple : class Point: def __init__(self, x, y): self.px, self.py = x, y class Segment: """Classe composite utilisant la classe distincte Point.""" def __init__(self, x1, y1, x2, y2): self.orig = Point(x1, y1) # Segment "a-un" Point origine, self.extrem = Point(x2, y2) # et "a-un" Point extrémité def __str__(self): return ("Segment : [({:g}, {:g}), ({:g}, {:g})]" .format(self.orig.px, self.orig.py, self.extrem.px, self.extrem.py)) 66 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 s = Segment(1.0, 2.0, 3.0, 4.0) print(s) # Segment : [(1, 2), (3, 4)] 2.8.9 Dérivation Dérivation La dérivation décrit la création de sous-classes par spécialisation. On utilise dans ce cas le mécanisme de l’héritage. L’implémentation Python utilisée est généralement l’appel dans le constructeur de la classe dérivée du constructeur de la classe parente, soit nommément, soit grâce à l’instruction super. Exemple : class Rectangle: def __init__(self, longueur=30, largeur=15): self.L, self.l, self.nom = longueur, largeur, "rectangle" class Carre(Rectangle): # héritage simple """Sous-classe spécialisée de la super-classe Rectangle.""" def __init__(self, cote=20): # appel au constructeur de la super-classe de Carre : super().__init__(cote, cote) self.nom = "carré" # surcharge d’attribut 2.9 Quelques Techniques avancées de programmation Ce chapitre présente quelques exemples de techniques avancées dans les trois paradigmes que supporte Python, les programmations procédurale, objet et fonctionnelle. 2.9.1 Techniques procédurales Améliorer la documentation La fonction utilitaire printApi filtre les méthodes disponibles de element, et affiche les docstrings associés sous une forme plus lisible que help : def printApi(element): methods = [el for el in dir(element) if not el.startswith(’_’)] for meth in methods: print(getattr(element, meth).__doc__) if __name__ == "__main__": printApi([]) Son exécution produit : L.append(object) -- append object to end L.count(value) -> integer -- return number of occurrences of value L.extend(iterable) -- extend list by appending elements from the iterable L.index(value, [start, [stop]]) -> integer -- return first index of value. Raises ValueError if the value is not present. L.insert(index, object) -- insert object before index L.pop([index]) -> item -- remove and return item at index (default last). Raises IndexError if list is empty or index is out of range. L.remove(value) -- remove first occurrence of value. Raises ValueError if the value is not present. L.reverse() -- reverse *IN PLACE* 2.9. Quelques Techniques avancées de programmation 67 Plone pour les développeurs, Version 1.0.0 L.sort(cmp=None, key=None, reverse=False) -- stable sort *IN PLACE*; cmp(x, y) -> -1, 0, 1 Faire des menus avec un dictionnaire On peut utiliser le couple clé : valeur d’un dictionnaire pour implémenter un menu, de la façon suivante : – on donne à cle le nom d’un élément du menu ; – la valeur correspondante est une référence : l’appel à une procédure sans argument. L’exemple proposé est la programmation d’une queue FIFO, une structure de données illustrée par la file d’attente à un guichet : le premier arrivé est le premier servi. Source du module de fonctions : """Module de gestion d’une queue FIFO.""" queue = [] # initialisation def enQueue(): queue.append(int(input("Entrez un entier : "))) def deQueue(): if len(queue) == 0: print("\nImpossible : la queue est vide !") else: print("\nElément ’%d’ supprimé" % queue.pop(0)) def afficheQueue(): print("\nqueue :", queue) Source du menu de gestion : """Implémentation d’une queue FIFO avec une liste. Un menu (utilisant un dictionnaire) appelle des procédures sans argument. """ # import from queue_FIFO_menu_m import enQueue, deQueue, afficheQueue # programme principal ----------------------------------------------afficheQueue() CMDs = {’a’:enQueue, ’v’:afficheQueue, ’s’:deQueue} menu = """ (A)jouter (V)oir (S)upprimer (Q)uitter Votre choix ? """ while True: while True: try: choix = input(menu).strip()[0].lower() except: choix = ’q’ if choix not in ’avsq’: print("Option invalide ! Réessayez") else: 68 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 break if choix == ’q’: print("\nAu revoir !") break CMDs[choix]() Expressions régulières Le module re permet d’accéder à la richesse des expressions rationnelles (“régulière” est une traduction malheureuse du terme anglais “regular”). Expression rationnelle C’est une suite de caractères qui permet de décrire d’autres chaînes de caractères (les motifs) afin de les retrouver dans un bloc de caractères. La construction de l’expression repose sur l’usage de caractères qui seront interprétés comme joker, répéteur ou groupe et des autres caractères interprétés comme motif. Ainsi : – (experssion) indique un groupe, – [caractères] indique un ensemble de caractère, ex : – [aez] soit a, soit e, soit z – [a-z] tout de a à z – [^caractères] indique tout caractère autres que ceux de l’ensemble, ex [^a-c] tout sauf a, b, c, – . tout caractère, – | indique une alternative, – ? indique que le motif précédent peut être ou non présent, – * indique la possibilité de répéter le motif indéfiniment ou aucune fois, – + indique la possibilité de répéter le motif au moins une fois. Les symboles peuvent être échappés par exemple : [[] indique le caractère [ Beaucoup d’autres symboles ont un sens. Le module sre plus riche en donne la liste. Une fois l’expression construite nous allons l’utiliser pour trouver les motifs d’une chaine : >>> import re >>> expression_rationnelle = "[a-zA-Z]*" >>> phrase = "Cette phrase contient cinq motifs" >>> [phrase[x.start(): x.end()] for x in ... re.finditer(expression_rationnelle, phrase) if x.end() != x.start()] [’Cette’, ’phrase’, ’contient’, ’cinq’, ’motifs’] >>> re.match(expression_rationnelle, phrase) <_sre.SRE_Match object at ...> La fonction finditer permet de retourner un liste d’objet Match. Les objets Match permettent de connaitre le début et la fin d’un motif, si la fin est égale au début c’est qu’il ne s’agit pas d’un motif reconnu. L’expression rationnelle est analysée à chaque demande ce qui consomme de la puissance pour rien. On peut alors optimiser en compilant l’expression : >>> import re >>> expression_rationnelle = "[a-zA-Z]*" >>> phrase = "Cette phrase contient cinq motifs" >>> erc = re.compile(expression_rationnelle) >>> re.match(erc, phrase) <_sre.SRE_Match object at ... Les fonctions récursives Fonction récursive Une fonction récursive peut s’appeler elle-même. 2.9. Quelques Techniques avancées de programmation 69 Plone pour les développeurs, Version 1.0.0 Par exemple, trier un tableau de N éléments par ordre croissant c’est extraire le plus petit élément puis trier le tableau restant à N - 1 éléments. Bien que cette méthode soit souvent plus difficile à comprendre et à coder que la méthode classique dite itérative, elle est, dans certains cas, l’application la plus directe de sa définition mathématique. Voici l’exemple classique de définition par récurrence de la fonction factorielle : n! = | 1 si n = 0 | n (n - 1)! si n > 1 Note : Son code est très exactement calqué sur sa définition mathématique : def factorielle(n): if n == 0: # cas de base : condition terminale return 1 else # cas récursif return n*factorielle(n-1) Dans cette définition, la valeur de n n’est pas connue tant que l’on n’a pas atteint la condition terminale (ici n == 0). Le programme empile les appels récursifs jusqu’à atteindre la condition terminale puis dépile les valeurs. Les générateurs et les expressions génératrices Les générateurs Les générateurs fournissent un moyen de générer des exécutions paresseuses 4 , ce qui signifie qu’elles ne calculent que les valeurs réellement demandées. Les générateurs s’appuient sur le mot-clé yield. Cette instruction suspend l’exécution de la fonction qui l’exécute, à la façon de return et retourne l’argument à sa droite. Lors de l’appel suivant, l’exécution reprend à la suite de la ligne contenant le yield. Ceci peut s’avérer beaucoup plus efficace (en terme de mémoire) que le calcul, par exemple, d’une énorme liste en une seule fois. Voici un exemple de générateur qui fournit autant de valeurs que demandées : def quarters(next_quarter=0.0): while True: yield next_quarter next_quarter += 0.25 if __name__ == "__main__": result = [] for x in quarters(): result.append(x) if x == 1.0: break print("Liste résultante : ", result) # Liste résultante : [0.0, 0.25, 0.5, 0.75, 1.0] Il est aussi possible de passer une initialisation au générateur : import sys def quarters(next_quarter=0.0): while True: received = (yield next_quarter) if received is None: 4. “Paresseux” est une traduction littérale du terme anglais “lazy”. 70 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 next_quarter += 0.25 else: next_quarter += received if __name__ == "__main__": result = [] generator = quarters() while len(result) < 5: x = next(generator) if abs(x - 0.5) < sys.float_info.epsilon: x = generator.send(1.0) # réinitialise le générarteur result.append(x) print("Liste résultante : ", result) # Liste résultante : [0.0, 0.25, 1.5, 1.75, 2.0] Syntaxe Une expression génératrice possède une syntaxe presque identique à celle des listes en intension ; la différence est qu’une expression génératrice est entourée de parenthèses. Utilisation Les expressions génératrices sont aux générateurs ce que les listes en intension sont aux fonctions. Les fonctions incluses La syntaxe de définition des fonctions en Python permet tout à fait d’emboîter leur définition. Distinguons deux cas d’emplois : – Idiome de la fonction fabrique renvoyant une fermeture : def creer_plus(ajout): """Fonction ’fabrique’.""" def plus(increment): """Fonction ’fermeture’ : utilise des noms locaux à creer_plus().""" return increment + ajout return plus # Programme principal ----------------------------------------------## création de deux fabriques distinctes p = creer_plus(23) q = creer_plus(42) ## utilisation print("p(100) =", p(100)) print("q(100) =", q(100)) Fonction fabrique renvoyant une classe : # classes class CasNormal(object): def uneMethode(self): print("normal") class CasSpecial(object): def uneMethode(self): print("spécial") # fonction def casQuiConvient(estNormal=True): 2.9. Quelques Techniques avancées de programmation 71 Plone pour les développeurs, Version 1.0.0 """Fonction fabrique renvoyant une classe.""" return CasNormal() if estNormal else CasSpecial() # Programme principal ----------------------------------------------une_instance = casQuiConvient() une_instance.uneMethode() # normal autre_instance = casQuiConvient(False) autre_instance.uneMethode() # spécial Les décorateurs Note : On utilise un décorateur lorsqu’on a besoin d’effectuer un prétraitement lors de l’appel d’une fonction ou un post-traitement à la suite de l’appel d’une fonction. Soit le prétraitement suivant : def pretraitement(fonction): fonction.__doc__ += "(fonction décorée)." return fonction def traitement(): """ma fonction """ print("traitement") traitement = pretraitement(traitement) print(traitement.__doc__) # ma fonction (fonction décorée). Nous obtenons le même résultat en utilisant un décorateur : def pretraitement(fonction): fonction.__doc__ += "(fonction décorée)." return fonction @pretraitement def traitement(): """ma fonction """ print("traitement") print(traitement.__doc__) # ma fonction (fonction décorée). Enfin il est possible d’enchaîner les décorateurs (à l’image de la composition des fonctions en mathématique) : @f1 @f2 @f3 def fonction(): pass # Notation équivalente à : fonction = f1(f2(f3(fonction))) 2.9.2 Techniques objets Comme nous l’avons vu lors du chapitre précédent, Python est un langage complètement objet. Tous les types de base ou dérivés sont en réalité des types abstrait de données implémentés sous forme de classe. Toutes ces classes dérivent d’une unique classe de base, ancêtre de tous les autres : la classe object. 72 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 __slot__ et __dict__ Examinons le code suivant : >>> class Point: ... __slots__ = ("x", "y") ... def __init__(self, x=0, y=0): ... self.x = x ... self.y = y >>> p=Point() >>> p.v = "Interdit" Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: ’Point’ object has no attribute ’v’ Quand une classe est créée sans utiliser l’attribut __slots__, ce que nous avons fait jusqu’à maintenant, Python crée de manière transparente un dictionnaire privé appelé __dict__ pour chaque instance de la classe, et ce dictionnaire contient les attributs de l’instance. Voici pourquoi il est possible d’ajouter ou de retirer des attributs d’un objet. Mais si l’on se contente d’objets sur lesquels nous accédons aux attributs sans en ajouter ou en ôter, nous pouvons créer des classes sans dictionnaire privé, ce qui économisera de la mémoire à chaque instanciation. C’est ce qui est réalisé dans l’exemple ci-dessus en définissant un attribut de classe __slots__ dont la valeur est un tuple formé des noms des attributs. Warning : Sous python 2 la classe utilisant slots doit être dérivée de object. Functor En Python un objet fonction ou fonctor est une référence à tout objet appelable (callable) : fonction, fonction lambda, méthode, classe. La fonction prédéfinie callable permet de tester cette propriété : >>> def maFonction(): ... print(’Ceci est "appelable"’) ... >>> callable(maFonction) True >>> chaine = ’Ceci est "appelable"’ >>> callable(chaine) False Il est possible de transformer les instances d’une classe en functor si la méthode spéciale __call__ est définie dans la la classe : >>> class A: def __call__(self, un , deux): return un + deux >>> a = A() >>> callable(a) True >>> a(1, 6) 7 Les gestionnaires de contexte Les gestionnaires de contexte simplifient nos codes en assurant que certaines opérations sont effectuées avant et après un bloc d’instruction donné. 2.9. Quelques Techniques avancées de programmation 73 Plone pour les développeurs, Version 1.0.0 Syntaxe On utilise l’instruction with. with_block ::= “with” expression “as” symbol ”:” suite Une utilisation classique est d’assurer la fermeture d’un fichier : with open("hello.txt") as f: for line in f: print line L’ <expression> retourne un gestionnaire de contexte associé à <variable>. un gestionnaire de contexte est un objet quelconque fournissant les méthodes __enter__ et __exit__. La méthode __enter__ de l’objet retourné par l’expression est exécutée par le mot-clé with. L’objet retourné par cette méthode est associé au symbole suivant le mot-clé as. Généralement, il s’agit du gestionnaire de contexte lui-même (donc return self). Lors de la cloture du bloc commencé par with, la méthode __exit__ du gestionnaire de ocontexte est appellée, avec les informations sur une éventuelle exception survenue dans le bloc. Le rôle de cette méthode est d’effectuer l’éventuel “nettoyage” associé à la fin d’utilisation du gestionnaire de contexte. 2.9.3 Techniques fonctionnelles Directive lambda Issue de langages fonctionnels (comme Lisp), la directive lambda permet de définir un objet fonction anonyme dont le retour est limité à une expression. Syntaxe lambda_stmt ::= “lambda” [parametres] ”:” expression Par exemple cette fonction retourne “s” si son argument est différent de 1, une chaîne vide sinon : >>> s = lambda x: "" if x == 1 else "s" >>> s(0) ’s’ >>> s(1) ’’ >>> Dans la présentation des interfaces graphiques, on verra que les callbacks sont souvent codés ainsi : f = lambda x=1, y=1, z=0: 3*x + 2*y - z print(f()) # 5 def make_increment(n): return lambda x: x + n f2, f6 = make_increment(2), make_increment(6) print(f2(42), f6(42)) # 44 48 lc = [lambda x: x**2, lambda x: x**3, lambda x: x**4] for f in lc: print(f(2), end=" ") # 4 8 16 74 Chapitre 2. Presentation de Python Plone pour les développeurs, Version 1.0.0 Les fonctions map, filter et reduce La programmation fonctionnelle est un paradigme de programmation qui considère le calcul en tant qu’évaluation de fonctions mathématiques et rejette le changement d’état et la mutation des données. Elle souligne l’application des fonctions, contrairement au modèle de programmation impérative qui met en avant les changements d’état. Le paradigme fonctionnel n’utilise pas de machine d’états pour décrire un programme, mais un emboîtement de fonctions que l’on peut voir comme des “boîtes noires” que l’on peut imbriquer les unes dans les autres. Chaque boîte possédant plusieurs paramètres en entrée mais une seule sortie, elle ne peut sortir qu’une seule valeur possible pour chaque n-uplet de valeurs présentées en entrée. Ainsi, les fonctions n’introduisent pas d’effets de bord. Un programme est donc une application, au sens mathématique, qui ne donne qu’un seul résultat pour chaque ensemble de valeurs en entrée. La programmation fonctionnelle repose sur trois concepts : mapping, filtering et reducing qui sont implémentésen Python par trois fonctions : map, filter et reduce. La fonction map() map applique une fonction à chaque élément d’une séquence et retourne un itérateur : >>> def renvoiTitres(element): ... return element.title() >>> map(renvoiTitres, [’fiat lux’, "lord of the fly"]) [’Fiat Lux’, ’Lord Of The Fly’] >>> >>> [element.title() for element in [’fiat lux’, "lord of the fly"]] [’Fiat Lux’, ’Lord Of The Fly’] On remarque que map peut être remplacée par une liste en intension. Voir Les listes en intension. La fonction filter() filter construit et renvoie un itérateur sur une liste qui contient tous les éléments de la séquence initiale répondant au critère function(element) == True : >>> [1, >>> >>> [1, filter(lambda x: x > 0, [1, -2, 3, -4]) 3] [x for x in [1, -2, 3, -4] if x > 0] 3] On remarque que filter peut être remplacée par une liste en intension avec un test. La fonction reduce() reduce est une fonction du module functools. Elle applique de façon cumulative une fonction de deux arguments aux éléments d’une séquence, de gauche à droite, de façon à réduire cette séquence à une seule valeur qu’elle renvoie : >>> def somme(x, y): ... print x, ’+’, y ... return x + y >>> reduce(somme, [1, 2, 3, 4]) 1 + 2 2.9. Quelques Techniques avancées de programmation 75 Plone pour les développeurs, Version 1.0.0 3 + 3 6 + 4 10 >>> >>> sum([1, 2, 3, 4]) 10 On remarque que reduce peut être remplacée par une des fonctions suivantes : all, any, func :max, min ou func :sum. Les applications partielles de fonctions Issue de la programmation fonctionnelle, une PFA (application partielle de fonction) de n paramètres prend le premier argument comme paramètre fixe et retourne un objet fonction (ou instance) utilisant les n-1 arguments restants. Exemple simple : >>> from functools import partial >>> baseTwo = partial(int, base=2) >>> baseTwo(’10010’) 18 Les PFA sont très utiles pour fournir des modèles partiels de widgets, qui ont souvent de nombreux paramètres. Dans l’exemple suivant, on redéfinie la classe Button en fixant certains de ses attributs (qui peuvent toujours être surchargés) : from functools import partial import tkinter as tk root = tk.Tk() # instanciation partielle de classe : MonBouton = partial(tk.Button, root, fg=’purple’, bg=’green’) MonBouton(text="Bouton 1").pack() MonBouton(text="Bouton 2").pack() MonBouton(text="QUITTER", bg=’red’, fg=’black’, command=root.quit).pack(fill=tk.X, expand=True) root.title("PFA !") root.mainloop() 2.9.4 Exercice Lecture d’un fichier, traitement avec une expression régulière et écriture des résultats dans un fichier. 76 Chapitre 2. Presentation de Python CHAPITRE 3 ZODB - Une Base de données objet native pour python N’éclatez pas vos objets dans des tables : stockez les dans une base objet. 3.1 Avertissement Ce document est une adaptation libre de la documentation accessible à http ://www.zodb.org/ par Michael Launay. 3.2 Licence Ce document et régi par la licence Creative Common BY SA, vous pouvez donc le partager, le modifier tant que vous citez votre source initiale et que vous conservez les termes de la licence initiale. 3.3 Résumé Les programmes Python sont généralement écrits en suivant le paradigme orienté objet. Vous pouvez utiliser des objets qui se référencent les uns les autres sans contraintes de type ou d’architecture : les objets ne sont liés à aucun schéma spécifique et peuvent porter n’importe quelle information. Stocker ces objets dans une base de données relationnelle exige que vous sacrifiez votre liberté d’architecture et la puissance des références. Les contraintes des bases relationnelles réduisent votre capacité à écrire du code orienté objet. La ZODB est une base de données objet native, qui stocke vos objets et vous permet de travailler selon n’importe lequel des paradigmes possibles en Python. En conséquence votre code est plus simple, plus robuste et plus compréhensible. De plus il n’y a pas de gap entre la base de données et le programme : pas de code “glue” à écrire, ni de mappings à configurer. Le tutoriel vous montre à quel point c’est facile à utiliser. Voici quelques- unes des fonctionnalités que la ZODB vous apporte : – Persistance transparente pour les objets Python. – Support des transactions ACID (avec les points de sauvegardes). – Historique et fonctionnalité de défaire refaire (undo). – Support efficace pour les objets binaires volumineux (BLOBs). – Dépôts pluggables. – Architecture adaptable. 77 Plone pour les développeurs, Version 1.0.0 3.4 Documentation 3.4.1 Introduction Ce guide explique comment écrire des programmes Python qui utilise la Z Objects Database (ZODB) et le Zope Enterprise Objects (ZEO). La dernière version de ce guide est accessible en anglais à http ://www.zope.org/Wikis/ZODB/guide/index.html. Qu’est ce que la ZODB ? La ZODB est un système de persistance pour les objets Python. Les langages de programmation persistants offrent la possibilité d’écrire automatiquement les objets vers le disque et de les relire quand ils sont requis par le programme. En installant la ZODB, vous ajoutez cette possibilité à Python. Il vous est possible de créer votre propre système de persistance des objets Python. Le point de départ habituel d’une telle construction est d’utiliser le module pickle, qui convertit les objets en une représentation texte, ainsi que différents modules de base de données, tels que les modules gdbm ou bsdbd, qui fournissent un moyen d’écrire des chaînes vers le disque et de les relire. Il est très puissant de combiner le module pickle et un module de base de données pour stocker et restituer les objets ; le module shelve qui fait partie de la bibliothèque standard de Python, fait exactement cela. La difficulté pour le développeur est de gérer explicitement les objets, lire un objet quand il en a besoin et l’écrire sur le disque lorsque l’objet n’est plus requis. La ZODB gère les objets pour vous, les gardes en cache, les écrit sur le disque lorsqu’ils sont modifiés et les supprime du cache s’ils n’ont pas été utilisés depuis un moment. OODBs vs. DBs relationnelles La ZODB est une base de données orientée objet (OODB) spécifique à Python. Les bases de données objets commerciales pour le C++ et Java réclament souvent de surmonter certains obstacles tels que l’usage d’un préprocesseur dédié ou que vous n’utilisiez pas certains types. Comme nous allons le voir, la ZODB a aussi quelques pièges à éviter, mais en comparaison la simplicité de la ZODB est à couper le souffle. Les bases de données relationnelles (RDBs) sont plus communes que les OOBDs. Les bases de données relationnelles stockent les informations dans des tables. Une table consiste en un ensemble de lignes, chaque ligne contient plusieurs colonnes d’informations. (Les lignes sont généralement appelées des relations, et c’est de là que vient le nom de base de données relationnelles). Mais regardons un cas concret. Notre exemple est issu d’une journée de travail de l’auteur initial à MEMS Exchange, en version simplifiée. Le travail consiste à tracer l’exécution de processus, qui sont constitués d’une liste d’étapes de fabrication dans une usine de semi-conducteurs. L’exécution est réalisée par un utilisateur unique, identifié par un nom et un numéro d’identification. L’exécution est constituée par un nombre déterminé d’opérations, comme déposer quelque chose sur le wafer ou en enlever quelque chose. Les opérations peuvent avoir des paramètres, qui sont des informations additionnelles requises pour réaliser l’opération. Par exemple, si vous déposez quelque chose sur wafer, vous avez besoins de savoir deux choses : 1) qu’est ce que vous déposez, 2) quelle quantité vous devez déposer. Par exemple vous déposez 100 microns d’oxide de silicium ou 1 micron de cuivre. Mapper cette structure vers une base de données relationnelles est direct : CREATE TABLE runs ( int run_id, varchar owner, varchar title, int acct_num, primary key(run_id) ); CREATE TABLE operations ( int run_id, 78 Chapitre 3. ZODB - Une Base de données objet native pour python Plone pour les développeurs, Version 1.0.0 int step_num, varchar process_id, PRIMARY KEY(run_id, step_num), FOREIGN KEY(run_id) REFERENCES runs(run_id), ); CREATE TABLE parameters ( int run_id, int step_num, varchar param_name, varchar param_value, PRIMARY KEY(run_id, step_num, param_name) FOREIGN KEY(run_id, step_num) REFERENCES operations(run_id, step_num), ); En Python, vous pouvez écrire trois classes nommées Run, Operation, and Parameter. Je ne présenterai pas le code de définition des ces classes, car le code est en dehors du propos. Chaque classe devra contenir un constructeur __init__ qui créera les attributs et leur assignera des valeurs par défaut. Il n’est pas difficile d’écrire du code Python qui instanciera la classe Run et la remplira avec les données venant d’une base de données relationnelle, vous pouvez aussi créer directement un utilitaire pour cette tâche classiquement appelée un mapper objet-relationnel. (Voir http ://www.amk.ca/python/unmaintained/ordb.html pour un aperçu rapide d’un test de mapper objet - relationnel en Python, et http ://www.python.org/workshops/199710/proceedings/shprentz.html pour un autre qui est basé sur la même idée mais qui est utilisé en production). Néanmoins, il est difficile de réaliser un mapper objet-relationnel rapide ; une implémentation simple comme celle réalisée est très lente car elle doit réaliser plusieurs requêtes pour accéder à toutes les données des objets. Plusieurs techniques de cache peuvent optimiser les requêtes SQL et les limiter au strict nécessaire. Cela aide si vous souhaitez accéder à l’exécution numéro 123. Mais qu’en est il si vous voulez trouver toutes les exécutions dont l’une des étapes possède un paramètre nommé ‘thickness’ avec une valeur de 2,0 ? Dans une version relationnelle, vous avez deux possibilités peu attrayantes : 1. Écrire une requête SQL spécialement pour ce cas : SELECT run_id FROM operations WHERE param_name = ’thickness’ AND param_value = 2.0 Si de telles requêtes sont communes, vous pouvez vous retrouver avec énormément de requêtes de ce type. Quand vous modifierez la structure de votre base ou de votre code vous aurez à maintenir toutes vos requêtes. 2. Utiliser un mapper objet relationnel ne vous aidera pas beaucoup plus. Chercher parmi les exécutions signifie que le mapper déroulera la requête SQL pour lire l’exécution numéro 1 et qu’un code Python vérifiera si l’une de ses étapes possède le paramètre que nous cherchons. Même chose pour l’exécution 2 puis 3 etc. Ce qui aboutit a traitement de nombreuses requêtes SQL, et risque d’être extrêmement long. Une base de données comme la ZODB va simplement stocker la référence interne entre objets, en conséquence lire un objet unique est plus rapide qu’envoyer une série de requêtes et d’assembler les résultats. Scanner toute les exécutions, est également inefficace. Qu’est ce que la ZEO ? La ZODB est fournie avec quelques classes qui implémentent l’interface Storage. Ces classes sont responsables du travail d’écriture des objets Python vers un média de stockage, qui peut être un fichier sur le disque (la classe FileStorage), un fichier BerkleyDB (BDBFullStorage), une base de données relationnelle (DCOracleStorage), ou tout autre média. La ZEO ajoute ClientStorage, un nouveau Storage qui n’écrit pas vers un média physique mais qui envoie chaque requête à travers le réseau vers un serveur. Le serveur qui fait fonctionner une instance de la classe StorageServer, va simplement agir comme le ‘front end’ d’un média physique de la classe Storage. C’est une idée simple et efficace qui comme on le verra dans la suite du document ouvre énormément de possibilités. 3.4. Documentation 79 Plone pour les développeurs, Version 1.0.0 À propos de ce guide Les premiers auteurs de ce guide travaillaient sur un projet qui utilisait la ZODB et ZEO comme principale technologie de stockage. Ils utilisaient la ZODB pour y stocker les processus, les opérations, un catalogue des processus possibles, des informations utilisateurs, des informations sur les comptes et autres données. Un des objectifs de ce document et de partager notre expérience. Ce guide est un concentré de toute l’expérience acquise lors des nombreuses heures passées à essayer de résoudre un problème. Il a pour but d’éviter à d’autres de passer du temps sur les même erreurs que celles que nous avons faites lors de notre apprentissage. Les auteurs du projet ZODB sont présentés dans un article accessible ici http ://www.amk.ca/python/writing/mxarchitecture/ Ce guide et en évolution constante. Si vous souhaitez contribuer et suggérer des éclaircissements ou de nouveau chapitre, envoyez vos commentaires à [email protected]. Remerciements Andrew Kuchling est l’auteur initial de la première version anglaise de ce guide, il a fournit la première documentation de la ZODB pour les programmeur Python. Sa version a été complété de nombreuse fois par Jeremy Hylton et Tim Peters. Les auteurs remercient les personnes qui ont relevé les inexactitudes et les bogues, qui ont proposé des suggestions sur le texte, ou de nouveau sujet qui ont put être traités : Jeff Bauer, Willem Broekema, Thomas Guettler, Chris McDonough, George Runyan. Traduction en Français par Michael Launay 3.4.2 Tutoriel Ce tutoriel a pour objectif de guider les développeurs avec une introduction pas à pas sur comment développer une application qui stocke ses données dans la ZODB. Introduction Commençons par un petit morceau de code que nous allons rendre fonctionnel avec la ZODB : class Account(object): def __init__(self): self.balance = 0.0 def deposit(self, amount): self.balance += amount def cash(self, amount): assert amount < self.balance self.balance -= amount Ce code définit une classe simple qui gère le solde d’un compte en banque et fournit deux méthodes pour manipuler le solde : deposit et cash. Installation Avant d’être capable d’utiliser la ZODB nous avons à l’installer, en utilisant easy_install. Remarquez bien que le paquet s’appelle en fait “ZODB3” : $ easy_install ZODB3 ... $ python >>> import ZODB 80 Chapitre 3. ZODB - Une Base de données objet native pour python Plone pour les développeurs, Version 1.0.0 La ZODB est maintenant installée et peut être importée depuis votre installation Python. Note : Si vous ne disposez pas de la commande easy_install sur votre système, suivez les instructions d’installation EasyInstall Configuration Lorsqu’un programme souhaite utiliser la ZODB il doit établir une connexion, comme pour les autres bases données. Pour la ZODB nous avons besoins de trois éléments différents : un système de stockage, une base de données et une connexion : : >>> >>> >>> >>> >>> >>> from ZODB.FileStorage import FileStorage from ZODB.DB import DB storage = FileStorage(’Data.fs’) db = DB(storage) connection = db.open() root = connection.root() Nous créons un système de stockage appelé FileStorage, qui est le système par défaut utilisé par pratiquement tout le monde. Il garde les traces de toutes les données dans un fichier unique sur le disque nommé avec le premier paramètre fourni à l’instanciation. Enfin, nous récupérons la racine de la base de données à partir de la connexion que nous avons ouverte. Stocker des objets Pour stocker un objet dans la ZODB nous avons simplement à le lier à n’importe quel objet qui est déjà dans la base de données. En fait, l’objet racine fonctionne comme un point d’entrée. L’objet racine est un dictionnaire et vous pouvez commencer à y stocker directement vos objets de la façon suivante : :python : >>> root[’account-1’] = Account() >>> root[’account-2’] = Account() Note : Les frameworks comme Zope ne créent qu’un seul objet à la racine, qui représente l’application elle même, et sous lequel chaque sous-objet est référencé. Ils choisissent un nom comme ‘app’ pour le premier objet qu’il place dans la ZODB. Transactions Maintenant vous avez deux objets placés dans votre objet racine et dans votre base de données. Toutefois ils ne sont pas encore stockés de façon permanente. La ZODB utilise le mécanisme des transactions et pour rendre vos modifications permanentes vous devez terminer votre transaction : :python : >>> import transaction >>> transaction.commit() >>> root.keys() [’account-1’, ’account-2’] Maintenant vous pouvez arrêter et redémarrer votre application et observer votre objet racine à nouveau, vous y trouverez les entrées ‘account-1’ et ‘account-2’ encore présentes ainsi que les objets que vous avez créés. Si votre code réalise des modifications pendant une transaction et votre base se trouve dans un état non souhaité, il est possible de ne pas livrer les modifications. Vous pouvez annuler la transaction ce qui restituera les objets dans l’état d’avant le début de transaction : :python : 3.4. Documentation 81 Plone pour les développeurs, Version 1.0.0 >>> del root[’account-1’] >>> root.keys() [’account-2’] >>> transaction.abort() >>> root.keys() [’account-1’, ’account-2’] Warning : Les objets qui n’ont pas été stockés dans la ZODB ne seront pas restitués par la commande abort. Objets persistants Un des derniers aspects que nous devons couvrir est la persistance des objets par eux-mêmes. La ZODB peut stocker presque tous les objets que vous lui donnez (elle ne stocke pas les fichiers tels quels par exemple). Mais en fait pour savoir quel objet a été modifié la ZODB a besoin que ces objets collaborent avec la base de données. En général, vous vous contentez de dériver de la classe persistent.Persistent pour que cela fonctionne. Appliqué à notre classe d’exemple donne :python : import persistent class Account(persistent.Persistent): # ... same code as above ... Résumé Vous avez vu comment installer la ZODB et comment ouvrir une connexion avec la base de données dans votre application pour pouvoir stocker des objets. Nous avons également abordé deux commandes simple commit et abort. La documentation de référence contient plus d’informations sur chaque sujet. 3.4.3 Programmation ZODB Installation de la ZODB La ZODB est empaquetée à l’aide des outils standards ‘distutils’. Vous avez besoin au minimum d’une version de Python supérieure ou égale à 2.3.5. Installez la ZODB avec la commande easy_install ZODB3 sous Unix sinon installer les paquets pré-construits pour Windows. En effet, sous Unix vous aurez besoin d’un compilateur C pour construire le paquet car la ZODB possède de nombreux modules écrits en C pour des raisons de performance. Comment fonctionne la ZODB La ZODB est conceptuellement simple. Les classes Python dérivent une classe de base persistent.Persistent pour être compatibles avec la ZODB. Les instances des objets persistants sont construits, lorsque le programme en a besoin, à partir du média de stockage, qui peut être par exemple un fichier sur le disque, et sont mis en cache dans la mémoire. La ZODB est prévenue de la modification des objets, si bien que lorsque une instruction réalise une modification comme obj.size = 1, l’objet modifié est marqué ‘dirty’. En une requête tous les objets marqués ‘dirty’ sont écrits sur le stockage permanent, cette opération s’appelle valider une transaction (“committing a transaction” ou “commiter” en langue de développeur). Une transaction peut aussi être abandonnée ou annulée, ce qui à pour conséquence d’annuler toutes les modifications, les objets marqués ‘dirty’ sont remis à leur état initial d’avant le début de transaction. Le terme “transaction” a une signification spécifique en informatique. Il est extrêmement important que le contenu d’une base de données ne soit pas corrompue par les crashs logiciels ou matériels, et la plupart des logiciels de 82 Chapitre 3. ZODB - Une Base de données objet native pour python Plone pour les développeurs, Version 1.0.0 base de données offrent cette protection en ayant quatre caractéristiques : atomique, consistante, isolée, et durable (ACID) : – atomique : les opérations sont indivisibles, en cas d’échec la suite des opérations est complètement annulée quelles que soientt le nombre d’opérations effectuées (rollback), inversement en cas de succès elles sont toutes appliquées ; en cas de crash cela garantit que la base ne sera pas dans un état partiellement modifié. – cohérente : en fin de transaction, la base est de nouveau dans un état cohérent - ce qui n’est pas forcément le cas pendant la transaction. Un contenu final incohérent provoque l’annulation de la transaction. – isolée : deux transactions simultanées n’ont pas connaissance de modifications apportées à la base par l’autre tant qu’elle n’a pas été validée (commitée). – durable : une transaction validée ne peut être annulée ou écrasée par une transaction ayant démarré simultanément. Lorsque la seconde voudra écraser les données de la première elle se verra annulée. Ce qui provoque l’émission d’un message d’erreur prévenant l’auteur de la seconde transaction que sa requête n’a pu aboutir en raison d’un conflit de transaction. La ZODB fournit toutes les caractéristiques ACID. Créer une ZODB Il y a trois interfaces principales fournies par la ZODB : les classes Storage, DB, et Connection. Les interfaces DB et Connection ont toutes deux une implémentation unique, mais il y a plein de classes différentes qui implémentent l’interface Storage. – Les classes de type Storage sont les couches les plus basses, manipulant, stockant et restituant les objets depuis les différents types de stockage. Quelques types de stockage différents ont été écrits, telles les classes FileStorage, qui utilise un fichier de stockage classique, et BDBFullStorage, qui utilise le logiciel de la base de données BerkeleyDB Sleepycat. Vous pouvez écrire votre propre classes Storage qui stocke les objets dans une base de données relationnelle, par exemple, si cela convient mieux à votre application. D’autres exemples de stockage, DemoStorage, MappingStorage et RelStorage (storage MySQL), sont disponibles et peuvent servir de modèle si vous voulez écrire un nouveau système de stockage. – La classe DB chapeaute le stockage, et réalise la médiation entre les différentes connexions. Une seule instance de DB est créée par processus. – Enfin, la classe Connection réalise la mise en cache des objets, et les déplace depuis ou vers la solution de stockage. Un programme muti-threadé doit ouvrir une instance de Connection pour chaque flux d’exécution. Les différents flux d’exécution peuvent alors modifier les objets et valider leur modifications indépendamment. Projeter d’utiliser une ZODB demande trois étapes : vous avez à instancier la classe Storage, et obtenir une connexion sous forme d’une instance de Connection à partir de l’instance de :class :’DB’. Tout cela ne représente que très peu de ligne de code : : >>> >>> >>> >>> from ZODB import FileStorage, DB storage = FileStorage.FileStorage(’/tmp/test-filestorage.fs’) db = DB(storage) conn = db.open() Remarquez que vous pouvez utiliser un système de stockage complètement différent simplement en changeant la ligne qui instancie la classe du type Storage, l’exemple précédent utilise FileStorage. Dans la section zeo, “Comment fonctionne la ZEO”, vous verrez comment ZEO utilise cette possibilité. Utiliser un fichier de configuration pour la ZODB La ZODB supporte également les fichiers de configuration écrits dans le format ZConfig. Un fichier de configuration peut être utilisé pour séparer la configuration de l’applicatif. Les classes de stockage et la classe DB supportent divers arguments. Toutes ces options peuvent être spécifiées par le fichier de configuration. Le format du fichier est simple, L’exemple du chapitre précédent peut être réalisé comme suit : : <zodb> <filestorage> path /tmp/test-filestorage.fs </filestorage> </zodb> 3.4. Documentation 83 Plone pour les développeurs, Version 1.0.0 Le module ZODB.config inclut plusieurs fonctions pour ouvrir une base de données et un stockage depuis un fichier de configuration. :python : >>> import ZODB.config >>> db = ZODB.config.databaseFromURL(’/tmp/test.conf’) >>> conn = db.open() La documentation sur ZConfig est inclue dans la livraison de ZODB3, elle explique le format en détail. Chaque fichier de configuration est décrit par un schéma, qui par convention est stocké dans un fichier component.xml. ZODB, ZEO, zLOG, et zdaemon ont tous un schéma. Écriture d’une classe persistante Faire une classe persistante est assez simple ; il suffit de dériver de la classe Persistent, comme montré dans l’exemple suivant :python : >>> from persistent import Persistent >>> class User(Persistent): ... pass La classe Persistent est une classe de type ‘new-style’ c’est à dire qu’elle dérive de object. Elle est implémentée en C. Pour des raisons de simplicité, dans l’exemple la classe User sera simplement utilisée comme un support à un ensemble d’attributs. Habituellement la classe devrait définir plusieurs méthodes qui ajoutent des fonctionnalités, mais cela n’a aucun impact sur le traitement qu’en fait la ZODB. La ZODB utilise la persistance par accessibilité : à partir d’un ensemble d’objets racines tous les attributs de ces objets sont rendus persistants, qu’il s’agisse de type de données Python ou d’instances de classe. Il n’y a pas de méthode explicite pour stocker les objets dans la base ZODB : ajoutez les simplement comme attribut à un objet ou dans un dictionnaire qui soit déjà dans la base. Cette chaîne de contenance doit finir par rejoindre l’objet racine de la base de données. Comme exemple, nous allons créer une base de données d’utilisateurs simple qui permette de récupérer des instances de la classe User pour un ID d’utilisateur donné. Premièrement, nous récupérons l’objet à la racine primaire de la ZODB en utilisant la méthode root de l’instance Connection. L’objet racine se comporte comme un dictionnaire, en conséquence vous pouvez ajouter une nouvelle entrée clé/valeur pour la racine de votre application. Nous allons insérer un objet OOBTree qui va contenir toute les objets User. (Le module BTree est également inclus comme faisant partie des éléments de Zope.) :python : >>> >>> ... >>> ... >>> ... ... ... >>> dbroot = conn.root() # Ensure that a ’userdb’ key is present # in the root if not dbroot.has_key(’userdb’): from BTrees.OOBTree import OOBTree dbroot[’userdb’] = OOBTree() userdb = dbroot[’userdb’] Insérer un nouvel utilisateur est simple : créez un objet de la classe User, remplissez le avec les données, insérer le dans l’instance du BTree, et validez (commitez) la transaction. :python : >>> >>> >>> >>> ... >>> >>> >>> 84 newuser.id = ’amk’ newuser.first_name = ’Andrew’ ; newuser.last_name = ’Kuchling’ # Add object to the BTree, keyed on the ID userdb[newuser.id] = newuser # Commit the change Chapitre 3. ZODB - Une Base de données objet native pour python Plone pour les développeurs, Version 1.0.0 ... >>> transaction.commit() Le module transaction définit quelque fonction de haut niveau pour travailler avec les transactions. La fonction commit écrit tous les objets modifiés sur le disque, ce qui rend les modifications permanentes. La fonction abort annule toutes les modifications qui ont été réalisées depuis le dernier appel à commit, restaurant l’état initial des objets. Si vous êtes familier avec la sémantique des bases de données relationnelles, vous n’êtes pas dépaysé. La fonction get retourne une instance de la classe Transaction qui ont des méthodes additionnelles comme la fonction note qui ajoute une note au métadata de la transaction. Plus précisément, le module transaction expose une instance de la classe de gestion des transactions ThreadTransactionManager comme transaction.manager, et les fonctions du module transaction comme get et begin qui redirige vers des méthodes du même nom du transaction.manager. La fonction commit et abort appliquent les méthodes de même nom de l’instance de la classe Transaction retourné par transaction.manager.get(). Tout ceci pour des raisons de commodité. Il est également possible de créer votre propre gestionnaire de transaction, et de dire à DB.open() de l’utiliser à la place. Par ce que l’intégration avec Python est complète, c’est presque comme avoir une sémantique transactionnelle pour les variables de vos programme, vous pouvez expérimenter les transactions dans un interpréteur Python : :python : >>> newuser <User instance at 81b1f40> >>> newuser.first_name ’Andrew’ >>> newuser.first_name = ’Bob’ >>> newuser.first_name ’Bob’ >>> transaction.abort() >>> newuser.first_name ’Andrew’ # Print initial value # Change first name # Verify the change # Abort transaction # The value has changed back Règles d’écriture de classes persistantes Pratiquement tous les langages persistants imposent des restrictions sur le style des programmes, avertissant des constructions qu’ils ne peuvent gérer ou y ajoutent de subtiles modifications sémantiques, et la ZODB ne fait pas exception. Heureusement, les restrictions de la ZODB sont assez simples à comprendre, et dans la pratique il n’est pas douloureux de les contourner. Le résumé des règles est le suivant : – Si vous modifiez un objet mutable qui est la valeur d’un attribut d’un autre objet la ZODB ne peut le savoir, et ne marquera pas l’objet comme ‘dirty’. La solution consiste soit à positionner le drapeau ‘dirty’ vous même quand vous modifiez l’objet, soit à utiliser un ‘wrapper’ (un objet enveloppe qui fournit les services manquants) pour les listes et les dictionnaires Python (PersistentList, PersistentMapping) qui positionne le drapeau ‘dirty’ proprement. – Les versions récentes de la ZODB autorisent l’écriture de classe qui ont des méthodes __setattr__, __getattr__, ou __delattr__. Ce que ne permettaient pas du tout les anciennes versions. Si vous écrivez des méthodes __setattr__ ou __delattr__, leur code doit positionner le drapeau ‘dirty’ manuellement. – Une classe persistante ne doit pas avoir de méthode __del__. La base de données doit pouvoir déplacer librement les objets entre le système de stockage et la mémoire. Si un objet n’est pas utilisé depuis un moment, il peut être relâché et son contenu chargé depuis le système de stockage à la prochaine utilisation. Parce que l’interpréteur Python n’est pas conscient des mécanismes de persistance, il pourrait appeler la méthode __del__ chaque fois que l’objet a été libéré. Nous allons regarder chaque règle en détail. Modification des objets modifiables La ZODB utilise différents hameçons Python pour attraper les accès aux attributs, et peut détourner la majorité des façons de modifier un objet, mais pas tous. Si vous modifiez un objet de la classe User par affectation d’un de 3.4. Documentation 85 Plone pour les développeurs, Version 1.0.0 ses attributs, comme dans userobj.first_name = ’Andrew’, la ZODB va marquer l’objet comme ayant changé, et il sera écrit dans le système de stockage lors du prochain commit (validation). Le cas le plus typique qui n’est pas pris en charge par la ZODB est la liste ou le dictionnaire. Si les objets de type User ont un attribut nommé friends contenant une liste, appelant userobj.friends.append(otherUser) qui ne marque pas userobj comme étant modifié. Du point de vue de la ZODB, userobj.friends n’a été que lu, et sa valeur, ce qui arrive à une liste Python ordinaire, a été retournée. La ZODB n’est pas consciente que l’objet retourné a été modifié après. C’est l’une des quelque bizarreries dont vous devez vous rappeler quand vous utilisez la ZODB : si vous modifiez un objet modifiable attribut d’un objet en place, vous devez marquer manuellement l’objet qui a été modifié pour que son drapeau ‘dirty’ soit à vrai. Ceci est fait en positionnant l’attribut _p_changed de l’objet à vrai :python : >>> userobj.friends.append(otherUser) >>> userobj._p_changed = True Vous pouvez cacher le détails d’implémentation du marquage de l’objet comme ‘dirty’ en concevant l’API de vos classes pour qu’elles n’utilisent pas directement l’accès aux attributs. En lieu et place, vous pouvez utiliser l’approche Java des accesseurs pour tout, et positionner le drapeau de modification à l’intérieur des méthodes. Par exemple, vous pouvez interdire l’accès direct à l’attribut friends, et ajouter une méthode get_friend_list et une méthode add_friend de modification. La méthode add_friend devrait ressembler à :python : >>> def add_friend(self, friend): >>> self.friends.append(otherUser) >>> self._p_changed = True Vous pouvez aussi utiliser le mécanisme des ‘properties’ pour cacher les accesseurs à l’usage (@property). Vous pouvez également utiliser une liste ou un dictionnaire compatible avec la ZODB qui gère pour vous le drapeau de modification. La ZODB est fournie avec la classe PersistentMapping et PersistentList Vous pouvez rendre silencieuses les modifications d’un objet en changeant la valeur du drapeau de modification (_p_changed ) à False. __getattr__, __delattr__, and __setattr__ La ZODB autorise la persistance des classes qui ont des méthodes crochets comme __getattr__ et __setattr__. Il y a quatre méthodes spéciales qui contrôlent l’accès aux attributs : les règles de chacune diffèrent. La méthode __getattr__ fonctionne presque de la même façon pour les classes persistantes que pour les autres classes. Pas besoin de manipuler quoi que ce soit. Si un objet est rendu silencieux, il devra être manipulé avant l’appel à __getattr__. Les autres méthodes sont plus délicates. Elles vont surcharger les crochets fournis par la Persistent, si bien que l’utilisateur doit appeler des méthodes spéciales pour invoquer ces crochets. La méthode __getattribute__ sera appelée pour les accès aux attributs : Elle surcharge l’accès au code fourni lors de la dérivation de la classe Persistent. Une méthode __getattribute__ surchargée par l’utilisateur doit toujours faire en sorte que la classe de base Persistent ait une chance de manipuler les attributs spéciaux comme __dict__ ou __class__. La surcharge doit appeler la méthode _p_getattr, et doit lui passer comme seul argument le nom de l’attribut. Si elle retourne True, le code de la fonction surchargée par l’utilisateur doit appeler la méthode __getattribute__ de la classe Persistent pour obtenir la valeur. Sinon le code peut continuer sont exécution. Un crochet de la méthode __setattr__ va également surcharger la méthode __setattr__ de la classe Persistent et l’utilisateur doit la traiter un peu comme la précédente. Le code réalisé par l’utilisateur doit appeler la méthode _p_setattr de la classe Persistent en lui passant le nom et la valeur de l’attribut. Si la méthode retourne True, la classe Persistent gère l’attribut, sinon le code peut continuer sont exécution. Si le code de l’utilisateur modifie l’état de l’objet, le code doit positionner l’attribut _p_changed. 86 Chapitre 3. ZODB - Une Base de données objet native pour python Plone pour les développeurs, Version 1.0.0 Le crochet de la méthode meth :__delattr__ doit être implémenté de la même façon. Le code de l’utilisateur doit appeler _p_delattr, en passant le nom de l’attribut comme argument. Si l’appel renvoit True alors la classe Persistent gère l’attribut sinon c’est au code de l’utilisateur de le faire. Méthode __del__ La méthode __del__ est invoquée juste avant que la mémoire occupée par un objet Python non référencé soit libérée. Parce que la ZODB peut matérialiser ou dématérialiser un objet persistant en mémoire un nombre quelconque de fois, il y a une relation très forte entre la persistance d’un objet et la méthode __del__ qui est normalement invoquée durant le cycle de vie de l’objet. Par exemple, la méthode __del__ d’un objet persistant n’est pas invoqué uniquement dans le cas d’un objet qui n’est plus référencé par d’autres objets de la base de donnée car la méthode __del__ est aussi mise en jeu dans le cas de l’accessibilité des objets en mémoire. Pire, une méthode __del__ peut interférer avec l’objectif de la machinerie de persistance. Par exemple, de nombreux objets restent dans le cache d’une class :Connection. À plusieurs reprises, pour réduire la charge du cache, les objets qui n’ont pas été référencés récemment sont enlevés du cache. Si un objet persistant est enlevé du cache et que le cache contenait la dernière référence en mémoire de cet objet la méthode __del__ de l’objet sera appelée. Si la méthode __del__ référence n’importe quel attribut de l’objet, la ZODB devra recharger l’objet à partir de la base de données à nouveau, avant de pouvoir satisfaire la référence. Ce qui a pour conséquence de remettre l’objet dans le cache. Un tel objet est virtuellement immortel, occupant de l’espace en mémoire pour toujours, puisque chaque essai pour l’enlever du cache abouti à l’y remettre. Avec les ZODB antérieures à la version 3.2.2 cela causait le bouclage infini du code de réduction de la taille du cache. Cette boucle infinie ne se produit plus mais les objets continuent à vivre en cache pour toujours. Parce que la méthode __del__ n’a pas beaucoup de sens dans le cas d’objets persistants et peut créer des problèmes, les méthodes persistantes ne devraient pas surcharger la méthode __del__. Écrire des classes persistantes Maintenant que nous connaissons les bases de la programmation de la ZODB, nous allons regarder quelques tâches plus délicates qui sont nécessaires à tous les utilisateurs de la ZODB dans un système en production. Modifier les attributs d’une instance Idéalement, avant de rendre des classes persistantes, vous voulez définir leur interface correctement du premier coup, en conséquence de quoi il n’y aurait pas besoin d’ajouter de nouveaux attributs au cours du temps. C’est un objectif difficile et pratiquement impossible à atteindre à moins de connaître exactement vos besoins futurs. De telles demandes peuvent être réclamées par d’autres personnes, si bien que vous devez vous préparer à recevoir des demandes impliquant des changement structurels. En terminologie de base de données orientées objets, cela s’appelle une mise à jour du schéma. La ZODB n’a pas besoin de spécification du schéma, si vous changez ce que le logiciel attend comme données de la base pour un objet, vous changez implicitement le schéma. Une façon de gérer de tels changements est de réaliser des programmes qui vont chercher tous les objets de la base pour les mettre à jour selon le nouveau schéma. C’est facile si votre réseau d’objet est bien structuré, par exemple si toutes les instances de la classe User se trouvent dans un unique dictionnaire ou BTree, il suffit alors de boucler sur chaque instance User. C’est plus difficile si le graphe est moins structuré. Si vos objets User ne peuvent être trouvés comme attributs d’un faible nombre d’objets, il vous faudra écrire un traverseur qui parcourera la base et qui vérifiera que chaque objet de la ZODB est du type User ou non. Certaines OODBs supportent une fonctionnalité appelée “extends”, qui peut rapidement trouver les objets d’un type données, quelque soit le graphe des objets, malheureusement ce n’est pas le cas de la ZODB. 3.5 Téléchargements La ZODB est distribuée sous forme d’un egg (paquet) Python accessible sur le dépôt de la communauté Python Python Package Index. 3.5. Téléchargements 87 Plone pour les développeurs, Version 1.0.0 Il est à la base de Zope et donc de Plone. Vous pouvez l’installer seul à l’aide de la commande easy_install ‘setuptools’ : $ easy_install ZODB3 L’installation des anciennes versions sont possibles via le gestionnaire de version Subversion. 3.6 Communauté et contribution Les discussions ont lieux sur la mailing liste ZODB developers’ mailing list. Bug reporting, Feature requests, et le planning des version sur Launchpad. Si vous voulez contribuer, la communauté sera heureuse de recevoir de l’aide sur la documentation, l’aide aux autres développeurs et utilisateurs sur la mailing liste, soumission de rapport de bogues, propositions d’améliorations et écriture de code. La ZODB est un projet géré par la fondation Zope où vous pouvez obtenir les droits pour contribuer directement sur le code - pour cela récupérez la documentation de la communauté Zope Developer Information. 88 Chapitre 3. ZODB - Une Base de données objet native pour python CHAPITRE 4 Le guide complet de l’Architecture de Composants de Zope Author Baiju M Version 0.5.8 Printed Book http ://www.lulu.com/content/1561045 Online PDF http ://www.muthukadan.net/docs/zca.pdf Traducteur Christophe Combelles <[email protected]>, Stéphane Klein <[email protected]> Révision http ://bazaar.launchpad.net/~ccomb/zcadoc/book-fr/revision/109 Copyright (C) 2007,2008,2009 Baiju M <baiju.m.mail AT gmail.com>. Chacun est autorisé à copier, distribuer et/ou modifier ce document suivant les termes de la « GNU Free Documentation License », version 1.3 ou toute version ultérieure publiée par la Free Software Foundation. Le code source présent dans ce document est soumis aux conditions de la « Zope Public License », Version 2.1 (ZPL). THE SOURCE CODE IN THIS DOCUMENT AND THE DOCUMENT ITSELF IS PROVIDED “AS IS” AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. Remerciements De nombreuses personnes m’ont aidé à écrire ce livre. Le brouillon initial a été relu par mon collègue Brad Allen. Quand j’ai annoncé ce livre sur mon blog, j’ai reçu de nombreux encouragements pour poursuivre ce travail. Kent Tenney a modifié de nombreuses parties du livre et a également réécrit l’application servant d’exemple. De nombreuses autres personnes m’ont envoyé des corrections et des commentaires, dont Lorenzo Gil Sanchez, Michael Haubenwallner, Nando Quintana, Stephane Klein, Tim Cook, Kamal Gill et Thomas Herve. Lorenzo a traduit ce travail en espagnol et Stephane initié la traduction en français. Merci à toutes ces personnes ! 4.1 Mise en route 4.1.1 Introduction Le développement de grands systèmes logiciels est toujours une tâche ardue L’expérience montre que l’approche orientée objet est bien appropriée à l’analyse, à la modélisation et à la programmation des systèmes complexes. 89 Plone pour les développeurs, Version 1.0.0 La conception orientée composants et la programmation basée sur des composants deviennent actuellement très populaires. L’approche basée composants est d’une grande aide pour l’écriture et la maintenance de systèmes logiciels et de leurs tests unitaires. Il existe de nombreux frameworks permettant la conception basée sur des composants dans différents langages, certains étant même indépendants du langage. On peut citer comme exemple : COM de Microsoft, ou XPCOM de Mozilla. La Zope Component Architecture (ZCA) est un framework en Python qui autorise la conception et la programmation basée sur des composants. Elle est très bien adaptée au développement de grands systèmes logiciels. La ZCA n’est pas liée au serveur d’application Zope : elle peut être utilisée pour développer n’importe quelle application en Python. Peut-être devrait-elle s’appeler la « Python Component Architecture ». Le but de la ZCA est d’utiliser des objets Python de manière efficace. Les composants sont des objets réutilisables fournissant une interface que l’on peut introspecter. Une interface est un objet qui décrit comment on peut travailler avec un composant particulier. Autrement dit, un composant fournit une interface implémentée dans une classe ou tout autre objet appelable (callable). La façon dont le composant est implémenté n’a pas d’importance : ce qui compte est que celui-ci soit en accord avec l’interface. Grâce à la ZCA, vous pouvez éclater la complexité d’un système dans plusieurs composants travaillant ensemble. Elle vous aide à créer notamment deux types de composants : les « adaptateurs » et les « utilitaires ». Trois paquets composent la ZCA : – zope.interface est utilisé pour la définition d’une interface. – zope.event fournit un système simple d’événements. – zope.component sert à la création, l’inscription et la récupération des composants. Souvenez-vous que la ZCA ne fournit pas de composants par elle-même. Elle n’a pour but que de créer, inscrire et récupérer des composants. Gardez également à l’esprit qu’un adaptateur est une classe Python tout à fait normale (en général une fabrique) et qu’un utilitaire est un simple objet Python appelable. Le framework ZCA est développé comme une partie du projet Zope 3. Comme mentionné plus haut, c’est un framework purement Python et il peut être utilisé dans n’importe quelle application Python. Actuellement, Zope 3, Zope 2 et Grok l’utilisent abondamment. De nombreux autres projets l’utilisent, y compris des applications non liées au web 1 . 4.1.2 Petit historique Le développement de la ZCA a débuté en 2001 dans le cadre du projet Zope 3. Ce framework est le fruit de l’expérience acquise pendant le développement d’applications d’envergure avec Zope 2. Jim Fulton est le père de ce projet. De nombreuses personnes ont participé à sa conception et à son implémentation, notamment Stephan Richter, Philipp von Weitershausen, Guido van Rossum (aka. Python BDFL), Tres Seaver, Phillip J Eby et Martijn Faassen. À ses débuts, la ZCA définissait des types de composants supplémentaires : les services et les vues, mais les développeurs ont fini par réaliser qu’un utilitaire peut remplacer un service et qu’un multi-adaptateur peut remplacer une vue. Aujourd’hui, la ZCA comporte un nombre très réduit de types de composants de base : les utilitaires (en anglais utilities), les adaptateurs (adapters), les abonnés (subscribers) et les gestionnaires (handlers). Et encore, les abonnés et les gestionnaires sont des cas particuliers de l’adaptateur. Pendant le cycle de développement de Zope 3.2, Jim Fulton a proposé une importante simplification de la ZCA 2 . Grâce à cette simplification, une nouvelle interface unique (IComponentRegistry) a été créée pour l’inscription des composants globaux et locaux. Le paquet zope.component avait une longue liste de dépendances, dont une grande partie n’était pas nécessaire pour une application non liée à Zope 3. Pendant PyCon 2007, Jim Fulton a ajouté la fonctionnalité extras_require à setuptools pour permettre d’extraire le cœur de la ZCA des fonctionnalités annexes 3 . En mars 2009, Tres Seaver a supprimé les dépendances à zope.deferredimport et zope.proxy. Aujourd’hui, le projet ZCA est indépendant et possède son propre cycle de publications et son propre dépôt Subversion. Il est fourni avec le framework Zope. 4 . Cependant, les anomalies et les bogues sont toujours pris en 1. 2. 3. 4. 90 http ://wiki.zope.org/zope3/ComponentArchitecture http ://wiki.zope.org/zope3/LocalComponentManagementSimplification http ://peak.telecommunity.com/DevCenter/setuptools#declaring-dependencies http ://docs.zope.org/zopeframework/ Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 charge au travers du projet Zope 3 5 et la liste de diffusion zope-dev est utilisée pour débattre du développement 6 . Il existe également une liste pour les utilisateurs de Zope 3 (zope3-users) qui peut être utilisée pour toute demande au sujet de la ZCA 7 . 4.1.3 Installation Les paquets zope.component, zope.interface et zope.event constituent le cœur de la Zope Component Architecture. Ils fournissent des outils permettant de définir, inscrire et rechercher des composants. Le paquet zope.component et ses dépendances sont disponibles sous forme d’egg dans l’index des paquets Python (PyPI) 8 . Vous pouvez installer zope.component et ses dépendances avec easy_install 9 $ easy_install zope.component Cette commande télécharge zope.component et ses dépendances depuis PyPI et l’installe dans votre Python path. Une autre manière de procéder est de télécharger zope.component et ses dépendances depuis PyPI et de les installer. Il faut installer les paquets dans l’ordre donné ci-dessous. Sous Windows, vous aurez probablement besoin du paquet binaire de zope.interface. 1. zope.interface 2. zope.event 3. zope.component Pour installer ces paquet, après téléchargement, vous pouvez utiliser la commande easy_install avec les eggs comme argument (éventuellement en fournissant tous les eggs sur la même ligne de commande) $ easy_install /path/to/zope.interface-3.x.x.tar.gz $ easy_install /path/to/zope.event-3.x.x.tar.gz $ easy_install /path/to/zope.component-3.x.x.tar.gz Vous pouvez aussi installer ces paquets en les décompressant séparément. Par exemple $ $ $ $ tar zxvf /path/to/zope.interface-3.x.x.tar.gz cd zope.interface-3.x.x python setup.py build python setup.py install Ces méthodes installeront la ZCA dans votre Python système, dans le dossier site-packages, ce qui peut être problématique. Dans un message sur la liste de diffusion Zope 3, Jim Fulton déconseille d’utiliser le Python système 10 . Vous pouvez utiliser virtualenv ou zc.buildout pour jouer avec n’importe quel paquet Python, et également pour les déploiements. Attention : Cette installation n’est nécessaire que pour effectuer les exercices proposés ci-après. Zope - donc Plone - intègre en standard les composants requis. 4.1.4 Méthode pour expérimenter Il existe deux approches populaires en Python pour construire un environnement de travail isolé. La première est virtualenv créé par Ian Biking, la deuxième est zc.buildout créé par Jim Fulton. Il est même possible d’utiliser ces paquets ensemble. Ils vous aideront à installer zope.component et d’autres dépendances dans 5. 6. 7. 8. 9. 10. https ://bugs.launchpad.net/zope3 http ://mail.zope.org/mailman/listinfo/zope-dev http ://mail.zope.org/mailman/listinfo/zope3-users Dépôt des paquets Python : http ://pypi.python.org/pypi http ://peak.telecommunity.com/DevCenter/EasyInstall http ://article.gmane.org/gmane.comp.web.zope.zope3/21045 4.1. Mise en route 91 Plone pour les développeurs, Version 1.0.0 un environnement de développement isolé. Passer un peu de temps à se familiariser avec ces outils vous sera bénéfique lors des développements et des déploiements. virtualenv Vous pouvez installer virtualenv grâce à easy_install $ easy_install virtualenv Puis vous pouvez créer un nouvel environnement de cette façon $ virtualenv --no-site-packages myve Ceci crée un nouvel environnement virtuel dans le dossier myve. Maintenant, depuis ce dossier, vous pouvez installer zope.component et ses dépendances avec le script easy_install situé dans myve/bin $ cd myve $ ./bin/easy_install zope.component Ensuite vous pouvez importer zope.interface et zope.component depuis le nouvel interprète python situé dans myve/bin $ ./bin/python Cette commande démarre un interprète Python que vous pourrez utiliser pour lancer le code de ce livre. zc.buildout En combinant zc.buildout et la recette zc.recipe.egg, vous pouvez créer un interprète Python ayant accès aux eggs Python spécifiés. Tout d’abord, installez zc.buildout grâce à la commande easy_install (vous pouvez le faire à l’intérieur de l’environnement virtuel). Pour créer un nouveau buildout servant à expérimenter des eggs Python, commencez par créer un dossier, puis initialisez-le grâce à la commande buildout init $ mkdir mybuildout $ cd mybuildout $ buildout init De cette façon, le dossier mybuildout devient un « buildout ». Le fichier de configuration de buildout est par défaut buildout.cfg. Après initialisation, il doit contenir les lignes suivantes [buildout] parts = Vous pouvez le modifier pour qu’il corresponde à ceci [buildout] parts = py [py] recipe = zc.recipe.egg interpreter = python eggs = zope.component Si maintenant vous lancez la commande buildout disponible à l’intérieur du dossier mybuildout/bin, vous obtiendrez un nouvel interprète Python dans le dossier mybuildout/bin $ ./bin/buildout $ ./bin/python Cette commande démarre un interprète Python que vous pourrez utiliser pour lancer le code de ce livre. 92 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 4.2 Un exemple 4.2.1 Introduction Imaginez une application servant à enregistrer les clients d’un hôtel. En Python, il est possible de faire ceci de différentes manières. Nous allons commencer par étudier rapidement la méthode procédurale, puis nous diriger vers une approche basique orientée objet. En examinant l’approche orientée objet, nous verrons comment nous pouvons bénéficier des motifs de conception adaptateur et interface. Cela nous fera entrer dans le monde de la Zope Component Architecture. 4.2.2 Approche procédurale Dans toute application professionnelle, le stockage des données est un point critique. Pour une question de simplicité, cet exemple utilise un simple dictionnaire Python comme méthode de stockage. Nous allons générer des identifiants uniques pour le dictionnaire, la valeur associée sera elle-même un dictionnaire contenant les détails de l’enregistrement. >>> bookings_db = {} #key: unique ID, value: details in a dictionary Une implémentation minimaliste nécessite une fonction à laquelle on transmet les détails de l’enregistrement et une fonction annexe qui fournit les identifiants uniques utilisés comme clés du dictionnaire de stockage. Nous pouvons obtenir un identifiant unique de cette façon >>> def get_next_id(): ... db_keys = bookings_db.keys() ... if db_keys == []: ... next_id = 1 ... else: ... next_id = max(db_keys) + 1 ... return next_id Comme vous pouvez voir, l’implémentation de la fonction get_next_id est très simple. La fonction récupère une liste de clés et vérifie si la liste est vide. Si c’est le cas, nous savons que nous en sommes au premier enregistrement et nous renvoyons 1. Sinon, nous ajoutons 1 à la valeur maximale de la liste et nous renvoyons la nouvelle valeur en résultant. Ensuite, pour créer des enregistrements dans le dictionnaire bookings_db, nous utilisons la fonction suivante >>> def book_room(name, place): ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: name, ... ’room’: place ... } Une application de gestion des enregistrements d’hôtel a besoin de données supplémentaires : – les numéros de téléphone – les options des chambres – les méthodes de paiement – ... Et de code pour gérer les données : – annuler une réservation – Modifier une réservation – payer une chambre – stocker les données – assurer la sécurité des données – ... 4.2. Un exemple 93 Plone pour les développeurs, Version 1.0.0 Si nous devions continuer l’exemple procédural, il faudrait créer plusieurs fonctions, en renvoyant les données de l’une à l’autre. Au fur à mesure que les fonctionnalités seraient ajoutées, le code deviendrait de plus en plus difficile à maintenir et les bogues deviendraient difficiles à trouver et à corriger. Nous n’irons pas plus loin dans l’approche procédurale. Il sera beaucoup plus facile d’assurer la persistance des données, la flexibilité de la conception et l’écriture de tests unitaires en utilisant des objets. 4.2.3 Approche orientée objet Notre discussion sur la conception orientée objet nous amène à la notion de « classe » qui sert à encapsuler les données et le code qui les gère. Notre classe principale sera « FrontDesk ». Le FrontDesk et d’autres classes auxquelles il délèguera du traitement, sauront comment gérer les données de l’hôtel. Nous allons créer des instances de FrontDesk pour appliquer cette connaissance au métier de la gestion d’hôtel. L’expérience a montré qu’en consolidant le code et les contraintes sur les données via des objets, on aboutit à un code qui est plus facile à comprendre, à tester et à modifier. Observons les détails d’implémentation de la classe FrontDesk >>> class FrontDesk(object): ... ... def book_room(self, name, place): ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: name, ... ’place’: place ... } Dans cette implémentation, l’objet frontdesk (une instance de la classe FrontDesk) est capable de prendre en charge les réservations. Nous pouvons l’utiliser de la façon suivante >>> frontdesk = FrontDesk() >>> frontdesk.book_room("Jack", "Bangalore") Dans tout projet, on est confronté à des changements de besoins. Dans notre cas, la direction a décidé que chaque client devait fournit un numéro de téléphone, donc nous devons changer le code. Nous pouvons satisfaire ce nouveau besoin en ajoutant un argument à la méthode book_room qui sera ajouté au dictionnaire des valeurs >>> class FrontDesk(object): ... ... def book_room(self, name, place, phone): ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: name, ... ’place’: place, ... ’phone’: phone ... } En plus de devoir migrer les données vers le nouveau schéma, nous devons changer tous les appels à la classe FrontDesk. Si nous incorporons les coordonnées de chaque client dans un objet et que nous utilisons cet objet pour les réservations, les modifications de code pourront être minimisées. Nous pouvons maintenant changer les détails de l’objet client sans avoir à changer les appels à FrontDesk. Nous avons donc >>> class FrontDesk(object): ... ... def book_room(self, guest): ... next_id = get_next_id() 94 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 ... ... ... ... ... bookings_db[next_id] = { ’name’: guest.name, ’place’: guest.place, ’phone’: guest.phone } Nous devons toujours modifier le code pour répondre aux nouvelles demandes. C’est inévitable, cependant notre but est de minimiser ces changements et donc d’améliorer la maintenabilité. Note : Lors du développement il ne faut jamais hésiter à faire des changements sans craindre de casser l’application. L’avertissement nécessaire doit être immédiatement obtenu grâce à des tests automatisés. Avec des tests bien écrits (et un bon contrôle de versions), vous pouvez faire impunément des changements aussi importants que vous souhaitez. Une bonne source d’informations à propos de cette philosophie de programmation est l’ouvrage Extreme Programming Explained par Kent Beck. En introduisant l’objet guest, vous avez économisé un peu de temps. Mais plus important, l’abstraction apportée par l’objet guest a rendu le système plus simple et mieux compréhensible. Par conséquent, le code est plus facile à restructurer et à maintenir. 4.2.4 Le motif adaptateur Dans une vraie application, l’objet frontdesk devra gérer des fonctionnalités comme les annulations et les modifications. Avec la conception actuelle, nous devons transmettre l’objet guest au frontdesk à chaque fois que nous appelons les méthodes cancel_booking et update_booking. Nous pouvons éviter ceci si nous transmettons l’objet guest à FrontDesk.__init__() et si nous le stockons en attribut de l’instance. >>> class FrontDeskNG(object): ... ... def __init__(self, guest): ... self.guest = guest ... ... def book_room(self): ... guest = self.guest ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: guest.name, ... ’place’: guest.place, ... ’phone’: guest.phone ... } ... ... def cancel_booking(self): ... guest = self.guest ... #code for cancellations goes here ... ... ... def update_booking(self): ... guest = self.guest ... #code for updatiion goes here ... La solution que nous avons obtenue est un motif de conception courant appelé adaptateur. Le Gang of Four 11 résume ainsi l’esprit de l’adaptateur « Convertir l’interface d’une classe en une autre interface à laquelle le client s’attend. L’adaptateur permet à des classes de fonctionner ensemble même si elles ont des interfaces incompatibles. » En général, un objet adaptateur contient un objet adapté 11. http ://en.wikipedia.org/wiki/Design_Patterns 4.2. Un exemple 95 Plone pour les développeurs, Version 1.0.0 >>> class Adapter(object): ... ... def __init__(self, adaptee): ... self.adaptee = adaptee Ce motif sera utile lorsqu’on sera confronté à des détails d’implémentation qui dépendent de considérations telles que : – modification des besoins client – modification des méthodes de stockage (ZODB, RDBM, XML) – modification des méthodes de sortie (HTML, PDF, texte pur) – modification de la source utilisée (ReST, Markdown, Textile) La ZCA utilise des adaptateurs et un registre de composants pour fournir la capacité de changer les détails d’implémentation du code via de la configuration. Comme nous pouvons le constater dans la section sur les adaptateurs de la ZCA, la capacité de configurer les détails d’implémentation fournit des avantages intéressants : – on peut interchanger les implémentations – on peut ajouter de nouvelles implémentations si besoin – on améliore les possibilités de réutilisation, aussi bien d’un code existant que du code utilisant la ZCA. Ces possibilités mènent à du code qui est flexible, évolutif et réutilisable. Il y a un cependant un coût : maintenir le registre de composants ajoute un niveau de complexité à l’application. Si une application n’a pas besoin de ces avantages, la ZCA n’est pas pas nécessaire. Nous sommes maintenant prêts à étudier la Zope Component Architecture, en commençant avec les interfaces. 4.3 Interfaces 4.3.1 Introduction Le fichier README.txt 12 dans chemin/vers/zope/interface définit les interfaces de cette manière Les interfaces sont des objets qui spécifient (documentent) le comportement externe des objets qui les « fournissent ». Une interface spécifie un comportement au travers : - de documentation informelle dans une docstring - de définitions d’attributs - d’invariants qui sont des conditions posées sur les objets fournissant l’interface. L’ouvrage de référence Design Patterns 12 par le Gang of Four recommande que vous devez « programmer pour une interface, pas pour une implémentation ». Définir une interface formelle est utile dans la compréhension d’un système. De plus, les interfaces vous apportent tous les bénéfices de la ZCA. Une interface spécifie les caractéristiques d’un objet, son comportement et ses capacités. Elle décrit le « quoi » d’un objet. Pour apprendre le « comment », vous devez regarder l’implémentation. Les métaphores couramment utilisées pour les interfaces sont contrat ou plan, des termes légaux et architecturaux pour représenter un jeu de spécifications. Dans certains langages de programmation comme Java, C# ou VB.NET, les interfaces sont un aspect explicite du langage. Étant donné que Python ne possède pas nativement d’interfaces, la ZCA les implémente comme des meta-classes desquelles on hérite. Voici un exemple classique de Hello World 12. L’arborescence du code de Zope contient de nombreux fichiers README.txt qui offrent une très bonne documentation. 96 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 >>> class Host(object): ... ... def goodmorning(self, name): ... """Say good morning to guests""" ... ... return "Good morning, %s!" % name Dans la classe ci-dessus, on a défini une méthode goodmorning. Si vous appelez cette méthode depuis un objet créé en utilisant cette classe, vous obtiendrez un Good morning, ... ! >>> host = Host() >>> host.goodmorning(’Jack’) ’Good morning, Jack!’ Ici, host est l’objet réel que votre code utilise. Si vous voulez examiner les détails d’implémentation, vous devez accéder à la classe Host, soit via le code source, soit au travers d’un outil de documentation d’API 13 . Maintenant nous allons commencer à utiliser les interfaces de la ZCA. Pour la classe ci-dessus, vous pouvez définir l’interface comme suit >>> from zope.interface import Interface >>> class IHost(Interface): ... ... def goodmorning(guest): ... """Say good morning to guest""" Vous pouvez constater que l’interface hérite de zope.interface.Interface. C’est de cette façon (abusive ?) que la ZCA définit des interfaces. Le préfixe « I » pour le nom de la classe est une convention utile. 4.3.2 Déclarer des interfaces Vous avez déjà vu comment déclarer une interface en utilisant zope.interface dans la section précédente. Cette section va expliquer les concepts en détail. Prenez cette exemple d’interface >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> class IHost(Interface): ... """A host object""" ... ... name = Attribute("""Name of host""") ... ... def goodmorning(guest): ... """Say good morning to guest""" L’interface IHost possède deux attributs, name et goodmorning. Rappelez-vous qu’en Python les méthodes des classes sont aussi des attributs. L’attribut name est défini en utilisant la classe zope.interface.Attribute. Quand vous ajoutez l’attribut name à l’interface IHost, vous ne définissez pas de valeur initiale. La raison de définir l’attribut name ici est simplement d’indiquer que toute implémentation de cette interface doit fournir un attribut nommé name. Dans ce cas, vous n’indiquez même pas de quel type doit être l’attribut ! Vous pouvez juste fournir une chaîne de documentation comme premier argument à Attribute. L’autre attribut, goodmorning est une méthode définie en utilisant une fonction. Notez bien que self n’est pas nécessaire dans les interfaces, car self est un détail d’implémentation de la classe. Il est possible pour un module d’implémenter cette interface. Si un module implémente cette interface, cela signifiera qu’un attribut name et une fonction goodmorning seront définis. Et la fonction goodmorning devra accepter un argument. 13. http ://en.wikipedia.org/wiki/Application_programming_interface 4.3. Interfaces 97 Plone pour les développeurs, Version 1.0.0 Nous allons maintenant voir comment effectuer le lien entre les objets, les classes et les interfaces. Seuls les objets sont vivants : ce sont des instances de classes. Et les interfaces représentent la définition des objets, donc les classes ne sont qu’un détail d’implémentation. C’est pour cette raison que vous devez programmer pour une interface, pas pour une implémentation. Nous devons nous familiariser avec deux termes supplémentaires pour comprendre les autres concepts. Le premier est fournir, le deuxième est implémenter. Les objets fournissent des interfaces, tandis que les classes implémentent des interfaces. Autrement dit, les objets fournissent les interfaces que les classes implémentent. Dans l’exemple ci-dessus, l’objet host fournit l’interface IHost et la classe Host implémente l’interface IHost. Un objet peut fournir plusieurs interfaces. Les objets peuvent également fournir directement des interfaces, en plus de celles implémentées par leur classe. Note : Les classes sont les détails d’implémentation des objets. En Python, les classes sont des objets appelables (callable). Pourquoi un autre objet appelable ne pourrait-il pas implémenter une interface ? En fait c’est possible. Pour n’importe quel objet appelable, vous pouvez déclarer qu’il produit des objets qui fournissent une interface donnée, en disant simplement que cet objet appelable implémente l’interface. Ces objets appelables sont généralement nommés des « fabriques ». Étant donné que les fonctions sont des objets appelables, une fonction peut implémenter une interface. 4.3.3 Implémentation des interfaces Pour déclarer qu’une classe implémente une interface zope.interface.implements dans la déclaration de classe. donnée, utilisez la fonction Considérons cet exemple. Ici, Host implémente IHost >>> from zope.interface import implements >>> class Host(object): ... ... implements(IHost) ... ... name = u’’ ... ... def goodmorning(self, guest): ... """Say good morning to guest""" ... ... return "Good morning, %s!" % guest Note : Si vous vous demandez comment implements fonctionne, faites un tour sur l’article de blog de James Henstridge (http ://blogs.gnome.org/jamesh/2005/09/08/python-class-advisors/) . Dans la section sur les adaptateurs, vous allez voir une fonction adapts, celle-ci fonctionne de manière similaire. Comme Host implémente IHost, les instances de Host fournissent IHost. Il existe des méthodes permettant d’introspecter les déclarations. La déclaration peut également s’écrire en dehors de la classe. Au lieu d’écrire interface.implements(IHost) comme dans l’exemple ci-dessus, vous pouvez écrire la chose suivante après la déclaration de classe >>> from zope.interface import classImplements >>> classImplements(Host, IHost) 4.3.4 L’exemple revisité Maintenant retournons à notre application exemple. Voici comment définir l’interface de l’objet frontdesk 98 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 >>> from zope.interface import Interface >>> class IDesk(Interface): ... """A frontdesk will register object’s details""" ... ... def register(): ... """Register object’s details""" ... Nous avons d’abord importé la classe Interface depuis le module zope.interface. Si vous définissez une sous-classe de cette classe Interface, ce sera considéré comme une interface, du point de vue de l’Architecture de Composants. Une interface peut être implémentée, comme nous l’avons vu, dans une classe ou tout autre objet appelable. L’interface de frontdesk ici définie est IDesk. La chaîne de documentation de l’interface donne une idée sur la nature de l’objet. En définissant une méthode dans l’interface, vous avez passé un contrat avec le composant, imposant la présence d’une méthode du même nom. Dans la déclaration de la méthode côté interface, le premier argument ne doit pas être self, car une interface ne sera jamais instanciée et ses méthodes ne seront jamais appelées. Le rôle d’une interface est simplement de documenter quels arguments et quelles méthodes doivent apparaître dans une classe qui l’implémente, et le paramètre self n’est qu’un détail d’implémentation qui n’a pas besoin d’être documenté. Comme vous le savez, une interface peut aussi spécifier des attributs normaux >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> class IGuest(Interface): ... ... name = Attribute("Name of guest") ... place = Attribute("Place of guest") Dans cette interface, l’objet guest a deux attributs contenant de la documentation. Une interface peut spécifier à la fois des méthodes et des attributs. Une interface peut être implémentée dans une classe, un module ou tout autre objet. Par exemple une fonction peut créer dynamiquement le composant et le renvoyer, auquel cas on dit que la fonction implémente l’interface. Vous savez maintenant ce qu’est une interface, comment la créer et l’utiliser. Dans le chapitre suivant nous allons voir comment une interface peut être utilisée pour définir un composant adaptateur. 4.3.5 Interfaces marqueurs Une interface peut être utilisée pour déclarer qu’un objet appartient à un type donné. Une interface sans aucun attribut ni aucune méthode est appelée une « interface marqueur ». Voici une interface marqueur >>> from zope.interface import Interface >>> class ISpecialGuest(Interface): ... """A special guest""" Cette interface peut être utilisée pour déclarer qu’un objet est un client spécial. 4.3.6 Invariants Parfois vous aurez besoin de définir des règles ou des contraintes sur les attributs de vos composants. Ces types de règles sont appelés des invariants. Vous pouvez utiliser zope.interface.invariant pour définir des invariants sur vos objets dans leur interface. 4.3. Interfaces 99 Plone pour les développeurs, Version 1.0.0 Considérons un exemple simple, avec un objet person. Une objet person a un attribut nom, un attribut email et un attribut phone. Comment peut-on implémenter une règle de validation qui oblige à définir au choix le phone ou l’email mais pas forcément les deux ? Créez tout d’abord un objet appelable, soit une simple fonction soit une instance appelable d’une classe de la façon suivante >>> def contacts_invariant(obj): ... ... if not (obj.email or obj.phone): ... raise Exception( ... "At least one contact info is required") Ensuite définissez l’interface de l’objet person en utilisant la fonction zope.interface.invariant pour définir l’invariant >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> from zope.interface import invariant >>> class IPerson(Interface): ... ... name = Attribute("Name") ... email = Attribute("Email Address") ... phone = Attribute("Phone Number") ... ... invariant(contacts_invariant) Maintenant, utilisez la méthode validateInvariants de l’interface pour effectuer la validation >>> from zope.interface import implements >>> class Person(object): ... implements(IPerson) ... ... name = None ... email = None ... phone = None >>> jack = Person() >>> jack.email = u"[email protected]" >>> IPerson.validateInvariants(jack) >>> jill = Person() >>> IPerson.validateInvariants(jill) Traceback (most recent call last): ... Exception: At least one contact info is required Vous constatez que l’ojet jack est validé sans erreur. Mais l’objet jill n’a pas pu valider la contrainte de l’invariant, il a donc levé une exception. 4.4 Adaptateurs 4.4.1 Implémentation Cette section décrit les adaptateurs en détail. L’Architecture de Composants, comme vous l’avez remarqué, fournit une aide dans l’utilisation efficace des objets Python. Les composants adaptateurs sont l’un des composants basiques utilisés par l’Architecture de Composants de Zope pour utiliser efficacement des objets Python. Les adaptateurs sont aussi des objets Python, mais avec une interface bien définie. 100 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 Pour déclarer qu’une classe est un adaptateur, utilisez la fonction adapts définie dans le paquet zope.component. Voici un nouvel adaptateur FrontDeskNG avec une déclaration explicite d’interface >>> from zope.interface import implements >>> from zope.component import adapts >>> class FrontDeskNG(object): ... ... implements(IDesk) ... adapts(IGuest) ... ... def __init__(self, guest): ... self.guest = guest ... ... def register(self): ... guest = self.guest ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: guest.name, ... ’place’: guest.place, ... ’phone’: guest.phone ... } Ce que nous avons défini ici est un adaptateur pour IDesk, qui s’adapte à l’objet IGuest. L’interface IDesk est implémentée par la classe FrontDeskNG. Donc une instance de cette classe fournira l’interface IDesk. >>> class Guest(object): ... ... implements(IGuest) ... ... def __init__(self, name, place): ... self.name = name ... self.place = place >>> jack = Guest("Jack", "Bangalore") >>> jack_frontdesk = FrontDeskNG(jack) >>> IDesk.providedBy(jack_frontdesk) True FrontDeskNG est juste un adaptateur que nous avons créé. Vous pouvez créer d’autres adaptateurs qui prendront en charge le bureau d’enregistrement différemment. 4.4.2 Inscription Pour utiliser ce composant adaptateur, vous devez l’inscrire dans un registre de composants (appelé également « gestionnaire de site »). Un gestionnaire de site réside normalement à l’intérieur d’un site. Le site et le gestionnaire de site prendront leur importance lors du développement d’une application Zope 3. Pour l’instant vous avez juste besoin de connaître les notions de site global et de gestionnaire global de site (ou registre de composant). Un gestionnaire global de site est situé en mémoire, alors qu’un gestionnaire de site local est persistant. Pour inscrire votre composant, commencez par récupérer le gestionnaire global de site >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerAdapter(FrontDeskNG, ... (IGuest,), IDesk, ’ng’) Pour récupérer le gestionnaire global de site, vous devez appeler la fonction getGlobalSiteManager‘ disponible dans le paquet ‘‘zope.component. En fait, le gestionnaire global de site est disponible dans un attribut (globalSiteManager) du paquet zope.component. Vous pouvez donc utiliser directement l’attribut zope.component.globalSiteManager. Pour inscrire l’adaptateur comme un compo4.4. Adaptateurs 101 Plone pour les développeurs, Version 1.0.0 sant, utilisez la méthode registerAdapter du registre de composants. Le premier argument doit être votre classe ou fabrique d’adaptateur. Le deuxième argument est un tuple d’objets adaptés, c’est à dire les objets sur lesquels vous vous adaptez. Dans cet exemple, vous vous adaptez seulement à l’objet IGuest. Le troisième argument est l’interface implémentée par le composant adaptateur. Le quatrième argument est optionnel, il s’agit du nom de cet adaptateur particulier. Si vous donnez un nom à l’adaptateur, celui devient un adaptateur nommé. Si aucun nom n’est donné, la valeur transmise par défaut est une chaîne vide (‘’). Dans l’inscription ci-dessus, vous avez donné l’interface de l’objet adapté et l’interface fournie par l’adaptateur. Comme vous avez déjà donné ces informations dans l’implémentation de l’adaptateur, il est inutile de les spécifier à nouveau. En réalité, vous pouvez effectuer l’inscription de la manière suivante >>> gsm.registerAdapter(FrontDeskNG, name=’ng’) Pour effectuer l’inscription, il existe d’anciennes API que vous devriez éviter. Les fonctions de l’ancienne API commencent par provide, par exemple : provideAdapter, provideUtility, etc. Si vous développez une application Zope 3, vous pouvez utiliser le langage ZCML (Zope Configuration Markup Language) pour effectuer les inscriptions des composants. Avec Zope 3, les composants locaux (persistants) peut être inscrits depuis la ZMI (Zope Management Interface), ou bien par programmation. Vous avez inscrit FrontDeskNG avec le nom ng. De la même manière, vous pouvez inscrire d’autre adaptateurs avec différents noms. Si un composant est inscrit sans nom, son nom sera la chaîne vide par défaut. Note : Les composants locaux sont persistants mais les composants globaux sont en mémoire. Les composants globaux sont inscrits en fonction de la configuration de l’application. Les composants locaux sont récupérés dans la base de données au démarrage de l’application. 4.4.3 récupération d’un adaptateur La récupération des composants inscrits dans le registre est effectuée grâce à deux fonctions disponibles dans le paquet zope.component. La première est getAdapter, la deuxième queryAdapter. Les deux fonctions prennent les mêmes arguments. getAdapter lève une exception ComponentLookupError si la recherche de composant échoue, tandis que queryAdapter renvoie None. Vous pouvez importer ces fonctions comme ceci >>> from zope.component import getAdapter >>> from zope.component import queryAdapter Dans la section précédente, nous avons inscrit un composant qui fournit l’interface IDesk avec un nom ng, et qui s’adapte à l’objet guest. Dans la première section nous avons créé un objet nommé jack. Voici maintenant comment récupérer un composant qui s’adapte à l’interface de l’objet jack (IGuest) et qui fournit l’interface IDesk également avec le nom ng. Ici, getAdapter et queryAdapter fonctionnent de la même manière >>> getAdapter(jack, IDesk, ’ng’) <FrontDeskNG object at ...> >>> queryAdapter(jack, IDesk, ’ng’) <FrontDeskNG object at ...> Vous pouvez constater que le premier argument doit être l’objet adapté, puis l’interface qui doit être fournie par le composant et finalement le nom du composant adaptateur. Si vous essayez de récupérer le composant grâce à un nom inutilisé pour l’inscription mais pour le même objet adapté et la même interface, la recherche échouera. Voici dans ce cas comment fonctionnent les deux méthodes >>> getAdapter(jack, IDesk, ’not-exists’) Traceback (most recent call last): ... ComponentLookupError: ... >>> reg = queryAdapter(jack, 102 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 ... IDesk, ’not-exists’) >>> reg is None True getAdapter a levé une exception ComponentLookupError mais queryAdapter a renvoyé None. Le troisième argument, le nom d’inscription, est optionnel. Si le troisième argument n’est pas fourni, sa valeur par défaut est la chaîne vide (‘’). Comme il n’y a aucun composant inscrit avec la chaîne vide comme nom, getAdapter lève l’exception ComponentLookupError. De la même manière, queryAdapter renvoie None. Voyez vous-même comment cela fonctionne >>> getAdapter(jack, IDesk) Traceback (most recent call last): ... ComponentLookupError: ... >>> reg = queryAdapter(jack, IDesk) >>> reg is None True Dans cette section, vous avez appris à inscrire un simple adaptateur et à le récupérer depuis le registre de composants. Ce type d’adaptateurs est appelé un adaptateur simple, car il ne s’adapte qu’à un seul objet. Si un adaptateur s’adapte à plusieurs objets, on l’appelle un multi-adaptateur. 4.4.4 Récupérer un adaptateur en utilisant l’interface Les adaptateurs peuvent être récupérés directement en utilisant les interfaces, mais cela ne fonctionne qu’avec les adaptateurs simples. Le premier argument est l’objet adapté, le deuxième argument est un argument mot-clé. Si la recheche d’adaptateur échoue, le deuxième argument est renvoyé. >>> IDesk(jack, alternate=’default-output’) ’default-output’ Le mot-clé peut être omis : >>> IDesk(jack, ’default-output’) ’default-output’ Si le deuxième argument n’est pas fourni, une erreur TypeError est levée : >>> IDesk(jack) Traceback (most recent call last): ... TypeError: (’Could not adapt’, <Guest object at ...>, <InterfaceClass __builtin__.IDesk>) Ici, FrontDeskNG est inscrit sans nom : >>> gsm.registerAdapter(FrontDeskNG) Maintenant la récupération de l’adaptateur doit réussir : >>> IDesk(jack, ’default-output’) <FrontDeskNG object at ...> Pour les cas simples, vous pouvez utiliser l’interface pour récupérer des adaptateurs. 4.4. Adaptateurs 103 Plone pour les développeurs, Version 1.0.0 4.4.5 Motif adaptateur Le principe de l’adaptateur dans l’Architecture de Composants de Zope est très similaire au motif adaptateur classique, tel que décrit dans le livre « Design Patterns ». Mais l’objectif visé par l’utilisation des adaptateurs dans la ZCA est plus large que le principe de l’adaptateur lui-même. Le but de l’adaptateur est de convertir l’interface d’une classe en une autre interface attendue par le client. Ceci permet de faire fonctionner des classes ensemble même si elles ont des interfaces incompatibles entre elles. Mais dans la section motivations du livre « Design Patterns », le GoF dit : « souvent, l’adaptateur est responsable d’une fonctionnalité que la classe adaptée ne fournit pas ». Un adaptateur de la ZCA se concentre effectivement plus sur l’ajout de fonctionnalités que sur la création d’une nouvelle interface pour un objet adapté. L’adaptateur de la ZCA permet aux classes adaptateurs d’étendre des fonctionnalités par ajout de méthodes. (Il peut être intéressant de remarquer que l’adaptateur était appelé Fonctionnalité (feature) dans les premières étapes de conception de la ZCA.) 14 La citation du Gang of Four dans le paragraphe ci-dessus se termine de la façon suivante : « ... que la classe adaptée ne fournit pas ». Mais dans la phrase suivante nous avons utilisé « objet adapté » plutôt que « classe adaptée », car le GoF décrit deux variantes d’adaptateurs selon les implémentations. Le premier est appelé adaptateur de classe, le deuxième adaptateur d’objet. Un adaptateur de classe utilise l’héritage multiple pour adapter une interface à une autre, alors que l’adaptateur d’objet se base sur la composition d’objet. L’adaptateur de la ZCA utilise le principe de l’adaptateur d’objet, qui utilise la délégation comme mécanisme de composition. Le second principe du GoF à propos de la conception orientée objets est la suivante : « favorisez la composition d’objets plutôt que l’héritage de classes ». Pour approfondir le sujet, reportez-vous au livre « Design Patterns ». Les principaux atouts des adaptateurs de la ZCA sont les interfaces explicites et les registres de composants. Les composants adaptateurs de la ZCA sont inscrits dans le registre de composants et sont récupérés par les objets clients via leur interface et leur nom si besoin. 4.5 Utilitaires 4.5.1 Introduction Vous connaissez maintenant le principe des interfaces, des adaptateurs et du registre de composants. Parfois, il peut être utile d’inscrire un objet qui ne s’adapte à rien du tout, par exemple une connexion à une base de données, un analyseur XML ou un objet retournant des identifiants uniques. Ce type de composant fourni par la ZCA est appelé un « utilitaire » (utility). Les utilitaires sont simplement des objets qui fournissent une interface et qui sont récupérés à partir de cette interface et d’un nom. Cette approche permet de créer un registre global dans lequel des instances peuvent être inscrites et récupérées à différents endroits de votre application, sans avoir besoin de transmettre les instances comme paramètres. Vous n’avez pas besoin d’inscrire toutes les instances de composants de cette façon. Inscrivez seulement les composants que vous voulez rendre interchangeables. 4.5.2 Utilitaire simple Un utilitaire peut être inscrit avec ou sans nom. Un utilitaire inscrit avec un nom est appelé un utilitaire nommé. Vous le verrez dans la prochaine section. Avant d’implémenter l’utilitaire, comme d’habitude, définissez son interface. Voici une interface qui dit bonjour >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IGreeter(Interface): ... ... def greet(name): ... """Say hello""" 14. Thread discussing renaming of Feature to Adapter : http ://mail.zope.org/pipermail/zope3-dev/2001-December/000008.html 104 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 Comme un adaptateur, un utilitaire peut avoir plusieurs implémentations. Voici une implémentation possible de l’interface ci-dessus >>> class Greeter(object): ... ... implements(IGreeter) ... ... def greet(self, name): ... return "Hello " + name L’utilitaire réel sera une instance de cette classe. Pour utiliser cet utilitaire, vous devez l’inscrire, puis vous pourrez la récupérer en utilisant l’API de la ZCA. Vous pouvez inscrire une instance de cette classe (utilitaire) grâce à registerUtility >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> greet = Greeter() >>> gsm.registerUtility(greet, IGreeter) Dans cet exemple, vous avez inscrit l’utilitaire en fournissant l’interface IGreeter. Vous pouvez récupérer l’utilitaire avec queryUtility ou getUtility >>> from zope.component import queryUtility >>> from zope.component import getUtility >>> queryUtility(IGreeter).greet(’Jack’) ’Hello Jack’ >>> getUtility(IGreeter).greet(’Jack’) ’Hello Jack’ Comme vous pouvez le constater, alors que les adaptateurs sont habituellement des classes, les utilitaires sont habituellement des instances de classes. vous ne créez une instance d’utilitaire qu’une fois, alors que les instances d’adaptateurs sont créées à chaque fois que vous les récupérez. 4.5.3 Utilitaire nommé Lorsque vous inscrivez un composant utilitaire, de la même manière que pour les adaptateurs, vous pouvez utiliser un nom. Comme mentionné dans la section précédente, un utilitaire inscrit avec un nom est appelé un utilitaire nommé. Voici comment inscrire l’utilitaire greeter avec un nom >>> greet = Greeter() >>> gsm.registerUtility(greet, IGreeter, ’new’) Dans cet exemple, nous avons inscrit l’utilitaire avec un nom en fournissant l’interface IGreeter. Vous pouvez récupérer l’utilitaire avec queryUtility ou getUtility >>> from zope.component import queryUtility >>> from zope.component import getUtility >>> queryUtility(IGreeter, ’new’).greet(’Jill’) ’Hello Jill’ >>> getUtility(IGreeter, ’new’).greet(’Jill’) ’Hello Jill’ Remarquez que vous devez utiliser le nom de l’utilitaire comme second argument pour que la récupération fonctionne. 4.5. Utilitaires 105 Plone pour les développeurs, Version 1.0.0 Appeler getUtility sans nom (comme second argument) est équivalent à l’appeler avec un nom vide (‘’), car la valeur par défaut pour ce second argument est justement la chaîne vide. Donc le mécanisme de recherche de composant essaiera de trouver le composant ayant une chaîne vide comme nom et il échouera. Lorsqu’une recherche de composant échoue, le résultat est une exception ComponentLookupError. Souvenez-vous qu’aucun composant aléatoire avec un autre nom ne sera renvoyé. Les fonctions de récupération d’adaptateurs, getAdapter et queryUtility fonctionnent de manière similaire. 4.5.4 Fabrique Une fabrique est un composant utilitaire qui fournit l’interface IFactory. Pour créer une fabrique, commencez par définir l’interface de l’objet >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IDatabase(Interface): ... ... def getConnection(): ... """Return connection object""" Voici une implémentation factice de l’interface IDatabase >>> class FakeDb(object): ... ... implements(IDatabase) ... ... def getConnection(self): ... return "connection" Vous pouvez créer une fabrique en utilisant zope.component.factory.Factory >>> from zope.component.factory import Factory >>> factory = Factory(FakeDb, ’FakeDb’) Maintenant vous pouvez l’inscrire de cette façon >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> from zope.component.interfaces import IFactory >>> gsm.registerUtility(factory, IFactory, ’fakedb’) Pour utiliser la fabrique, vous pouvez faire comme ceci >>> from zope.component import queryUtility >>> queryUtility(IFactory, ’fakedb’)() <FakeDb object at ...> Voici l’écriture simplifiée pour utiliser une fabrique >>> from zope.component import createObject >>> createObject(’fakedb’) <FakeDb object at ...> 4.6 Adaptateurs avancés Ce chapître traite de concepts avancés comme les multi-adaptateurs, les abonnés (subscribers) et les gestionnaires (handles). 106 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 4.6.1 Multi-adaptateur Un adaptateur simple s’adapte normalement à un seul objet, mais un adaptateur peut s’adapter à plusieurs objets. Si un adaptateur s’adapte à plus d’un objet, il s’appelle un multi-adaptateur. >>> from zope.interface import Interface >>> from zope.interface import implements >>> from zope.component import adapts >>> class IAdapteeOne(Interface): ... pass >>> class IAdapteeTwo(Interface): ... pass >>> class IFunctionality(Interface): ... pass >>> class MyFunctionality(object): ... implements(IFunctionality) ... adapts(IAdapteeOne, IAdapteeTwo) ... ... def __init__(self, one, two): ... self.one = one ... self.two = two >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerAdapter(MyFunctionality) >>> class One(object): ... implements(IAdapteeOne) >>> class Two(object): ... implements(IAdapteeTwo) >>> one = One() >>> two = Two() >>> from zope.component import getMultiAdapter >>> getMultiAdapter((one,two), IFunctionality) <MyFunctionality object at ...> >>> myfunctionality = getMultiAdapter((one,two), IFunctionality) >>> myfunctionality.one <One object at ...> >>> myfunctionality.two <Two object at ...> 4.6.2 Adaptateur d’abonnement À la différence des adaptateurs habituels, les adaptateurs d’abonnement sont utilisés quand on veut récupérer tous les adaptateurs qui adaptent un objet à une interface donnée. Un adaptateur d’abonnement est également appelé abonné. Considérons un problème de validation. Nous avons des objets et nous voulons vérifier s’ils satisfont à un certain critère. Nous définissons une interface de validation 4.6. Adaptateurs avancés 107 Plone pour les développeurs, Version 1.0.0 >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> from zope.interface import implements >>> class IValidate(Interface): ... ... def validate(ob): ... """Determine whether the object is valid ... ... Return a string describing a validation problem. ... An empty string is returned to indicate that the ... object is valid. ... """ Nous avons également des documents >>> class IDocument(Interface): ... ... summary = Attribute("Document summary") ... body = Attribute("Document text") >>> class Document(object): ... ... implements(IDocument) ... ... def __init__(self, summary, body): ... self.summary, self.body = summary, body Maintenant, nous pouvons avoir besoin de préciser différentes règles de validation pour les documents. Par exemple, nous voulons que le résumé tienne sur une seule ligne >>> from zope.component import adapts >>> class SingleLineSummary: ... ... adapts(IDocument) ... implements(IValidate) ... ... def __init__(self, doc): ... self.doc = doc ... ... def validate(self): ... if ’\n’ in self.doc.summary: ... return ’Summary should only have one line’ ... else: ... return ’’ Ou bien nous voulons que le corps du document fasse au moins 1000 caractères >>> class AdequateLength(object): ... ... adapts(IDocument) ... implements(IValidate) ... ... def __init__(self, doc): ... self.doc = doc ... ... def validate(self): ... if len(self.doc.body) < 1000: ... return ’too short’ ... else: ... return ’’ 108 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 Nous pouvons inscrire ces deux composants comme des adaptateurs d’abonnement >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerSubscriptionAdapter(SingleLineSummary) >>> gsm.registerSubscriptionAdapter(AdequateLength) Nous pouvons ensuite utiliser ces abonnés pour valider les objets >>> from zope.component import subscribers >>> doc = Document("A\nDocument", "blah") >>> [adapter.validate() ... for adapter in subscribers([doc], IValidate) ... if adapter.validate()] [’Summary should only have one line’, ’too short’] >>> doc = Document("A\nDocument", "blah" * 1000) >>> [adapter.validate() ... for adapter in subscribers([doc], IValidate) ... if adapter.validate()] [’Summary should only have one line’] >>> doc = Document("A Document", "blah") >>> [adapter.validate() ... for adapter in subscribers([doc], IValidate) ... if adapter.validate()] [’too short’] 4.6.3 Gestionnaire Les gestionnaires sont des fabriques d’abonnés qui ne produisent rien du tout. Ils font la totalité de leur travail lorsqu’ils sont appelés. Les gestionnaires sont typiquement utilisés pour gérer des événements. Les gestionnaires sont aussi connus sous le nom d’abonnés à des événements ou adaptateurs d’abonnement à des événements. Les abonnés à des événements sont différents des autres abonnés car l’objet qui appelle les abonnés ne s’attend pas à interagir avec eux d’une quelconque façon. Par exemple, un publicateur d’événements ne s’attend pas à récupérer une valeur de retour. Comme les abonnés n’ont pas besoin de fournir une API aux objets qui les appellent, il est plus naturel de les définir avec des fonctions, plutôt qu’avec des classes. Par exemple, dans un système de gestion de documents, nous pouvons avoir besoin de gérer les heures de création des documents >>> import datetime >>> def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() Dans cet exemple, nous avons une fonction qui prend un événement et effectue un traitement. La fonction ne retourne en fait rien du tout. C’est un cas particulier de l’adaptateur d’abonnement qui s’adapte à un événement et ne fournit rien. Tout le travail est effectué lorsque la fabrique de l’adaptateur est appelée. Nous appelons les abonnés qui ne fabriquent rien des « gestionnaires ». Il existe des APIs spécialisées dans leur inscription et leur récupération. Pour inscrire l’abonné ci-dessus, nous définissons un événement « document créé » >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> from zope.interface import implements >>> class IDocumentCreated(Interface): ... 4.6. Adaptateurs avancés 109 Plone pour les développeurs, Version 1.0.0 ... doc = Attribute("The document that was created") >>> class DocumentCreated(object): ... ... implements(IDocumentCreated) ... ... def __init__(self, doc): ... self.doc = doc Nous modifions également notre définition du gestionnaire >>> def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() >>> from zope.component import adapter >>> @adapter(IDocumentCreated) ... def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() Ceci marque le gestionnaire comme étant un adaptateur des événements IDocumentCreated. Nous inscrivons le gestionnaire >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerHandler(documentCreated) Il est maintenant possible de créer un événement et d’utiliser la fonction handle pour appeler les gestionnaires inscrits à l’événement >>> from zope.component import handle >>> handle(DocumentCreated(doc)) >>> doc.created.__class__.__name__ ’datetime’ 4.7 Usage de la ZCA dans Zope L’Architecture de Composants de Zope est utilisée dans Zope 2 et dans Zope 3. Ce chapitre traite de l’usage de la ZCA dans Zope. 4.7.1 ZCML Le Zope Configuration Markup Language (ZCML) est un système de configuration pour l’inscription des composants. Au lieu d’utiliser l’API Python pour les inscriptions, vous pouvez utiliser le ZCML. Pour pouvoir utiliser le ZCML, vous devez installer des paquets supplémentaires. Vous pouvez installer zope.component avec la prise en charge du ZCML en utilisant easy_install comme ceci $ easy_install "zope.component [zcml]" Un fichier ZCML doit commencer par une directive configure avec la déclaration d’espace de nom appropriée <configure xmlns="http://namespaces.zope.org/zope"> ... </configure> La directive adapter peut être utilisée pour inscrire des adaptateurs 110 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 <adapter factory=".company.EmployeeSalary" provides=".interfaces.ISalary" for=".interfaces.IEmployee" /> Les attributs provides et for sont optionnels, à condition que vous ayez fait les déclaration correspondantes dans l’implémentation <adapter factory=".company.EmployeeSalary" /> Si vous voulez inscrire le composant comme un adaptateur nommé, vous pouvez utiliser l’attribut name <adapter factory=".company.EmployeeSalary" name="salary" /> Un utilitaire peut être inscrit avec la directive utility <utility component=".database.connection" provides=".interfaces.IConnection" /> L’attribut provides est optionnel, à condition que vous ayez fait la déclaration correspondante dans l’implémentation <configure xmlns="http://namespaces.zope.org/zope"> <utility component=".database.connection" /> Si vous voulez inscrire le composant comme un utilitaire nommé, vous pouvez utiliser l’attribut name <utility component=".database.connection" name="db_connection" /> Plutôt que d’utiliser directement le composant, vous pouvez aussi fournir une fabrique <utility factory=".database.Connection" /> 4.7.2 Surchargements Lorsque vous inscrivez des composants avec l’API Python (méthodes register*), le dernier composant inscrit remplace le précédent, si les deux sont inscrits avec les mêmes arguments. Considérons l’exemple suivant >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IA(Interface): ... pass >>> class IP(Interface): ... pass 4.7. Usage de la ZCA dans Zope 111 Plone pour les développeurs, Version 1.0.0 >>> from zope.interface import implements >>> from zope.component import adapts >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> class AP(object): ... ... implements(IP) ... adapts(IA) ... ... def __init__(self, context): ... self.context = context >>> class AP2(object): ... ... implements(IP) ... adapts(IA) ... ... def __init__(self, context): ... self.context = context >>> class A(object): ... ... implements(IA) >>> a = A() >>> ap = AP(a) >>> gsm.registerAdapter(AP) >>> getAdapter(a, IP) <AP object at ...> Si vous inscrivez un autre adaptateur, celui existant sera surchargé >>> gsm.registerAdapter(AP2) >>> getAdapter(a, IP) <AP2 object at ...> Mais lorsque vous inscrivez des composants en ZCML, la deuxième inscription provoquera un conflit. Ceci est fait pour éviter les surchargements accidentels d’inscriptions, qui sont difficiles à retrouver et corriger. L’utilisation de ZCML est donc un avantage pour votre application. Parfois vous aurez besoin de surcharger une inscription existante. ZCML fournit une directive includeOverrides à cet effet. Avec la directive ci-dessous, vous pouvez écrire vos surcharges dans un fichier séparé <includeOverrides file="overrides.zcml" /> 4.7.3 NameChooser Emplacement : zope.app.container.contained.NameChooser C’est un adaptateur qui choisit un nom unique pour un objet dans un conteneur. L’inscription de l’adaptateur ressemble à ceci <adapter provides=".interfaces.INameChooser" for="zope.app.container.interfaces.IWriteContainer" 112 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 factory=".contained.NameChooser" /> Dans cette inscription, vous pouvez observer qu’on s’adapte à un IWriteContainer et que l’adaptateur fournit INameChooser. Cet adaptateur fournit une fonctionnalité très pratique pour les développeurs Zope. Les implémentations principales de IWriteContainer dans Zope 3 sont zope.app.container.BTreeContainer‘ et zope.app.folder.Folder. Normalement vous hériterez de ces implémentations pour créer vos propres classes de conteneurs. Supposez qu’il n’y ait pas d’interface INameChooser ni d’adaptateur : vous seriez obligé de recréer cette fonctionnalité séparément pour chaque implémentation. 4.7.4 LocationPhysicallyLocatable Emplacement : zope.location.traversing.LocationPhysicallyLocatable Cet adaptateur est fréquemment utilisé dans les applications Zope 3, mais habituellement il est appelé au travers d’une API dans zope.traversing.api. (Certains vieux codes utilisent même les fonctions zope.app.zapi qui ne sont qu’une indirection supplémentaire). L’inscription de l’adaptateur ressemble à ceci <adapter factory="zope.location.traversing.LocationPhysicallyLocatable" /> L’interface fournie et l’interface adaptée sont données dans l’implémentation, dont voici le début class LocationPhysicallyLocatable(object): """Provide location information for location objects """ zope.component.adapts(ILocation) zope.interface.implements(IPhysicallyLocatable) ... Normalement, presque tous les objets persistants d’une application Zope 3 fournissent l’interface ILocation. Cette interface n’a que deux attributs, __parent__ et __name__. __parent__ est le parent dans la hiérarchie des objets et __name__ est le nom, vu depuis le parent. L’interface IPhysicallyLocatable a quatre méthodes : getRoot, getPath, getName, et getNearestSite. – getRoot renvoie l’objet racine physique. – getPath renvoie une chaîne donnant le chemin physique vers l’objet. – getName renvoie le dernier segment du chemin physique. – getNearestSite renvoie le site dans lequel l’objet est contenu. Si l’objet est lui-même un site, il est renvoyé. Si vous apprenez Zope 3, ce sont des choses importantes dont vous aurez besoin très souvent. Pour comprendre la beauté de ce système, vous devriez regarder comment Zope 2 récupère l’objet racine physique et comment ceci est implémenté. Il existe une méthode getPhysicalRoot virtuellement pour tous les objets conteneurs. 4.7.5 DefaultSized Emplacement : zope.size.DefaultSized Cet adaptateur n’est qu’une implémentation par défaut de l’interface ISized. Il est inscrit pour tous les types d’objets. Si vous voulez inscrire cet adaptateur pour une interface particulière, vous devez surcharger cette inscription avec votre implémentation. L’inscription de l’adaptateur ressemble à ceci 4.7. Usage de la ZCA dans Zope 113 Plone pour les développeurs, Version 1.0.0 <adapter for="*" factory="zope.size.DefaultSized" provides="zope.size.interfaces.ISized" permission="zope.View" /> Comme vous pouvez voir, l’interface adaptée est « * », pour pouvoir s’adapter à tous les types d’objets. ISized est une interface simple possédant deux méthodes class ISized(Interface): def sizeForSorting(): """Returns a tuple (basic_unit, amount) Used for sorting among different kinds of sized objects. ’amount’ need only be sortable among things that share the same basic unit.""" def sizeForDisplay(): """Returns a string giving the size. """ Vous pouvez trouver un autre adaptateur ISized pour IZPTPage dans le paquet zope.app.zptpage. 4.7.6 ZopeVersionUtility Emplacement : zope.app.applicationcontrol.ZopeVersionUtility Cet utilitaire donne la version de Zope en fonctionnement. L’inscription est la suivante <utility component=".zopeversion.ZopeVersionUtility" provides=".interfaces.IZopeVersion" /> L’interface fournie, IZopeVersion, n’a qu’une méthode nommée getZopeVersion. Cette méthode renvoie une chaîne contenant la version de Zope (avec éventuellement un changeset SVN). L’implémentation par défaut, ZopeVersionUtility, récupère le numéro de version dans un fichier version.txt du répertoire zope/app. Si Zope fonctionne depuis un espace de travail Subversion, il montre le dernier numéro de révision. Si aucun des deux ne fonctionne il renvoie DevCenter/Unknown. 4.8 Étude de cas Note : Ce chapitre n’est pas terminé. Merci d’envoyer vos suggestions ! 4.8.1 Introduction Ce chapitre est un exemple de création d’une application lourde avec la bibliothèque d’interface graphique PyGTK et la ZCA. Cette application utilise également deux types différents de mécanismes de persistance des données : le premier est une base de données objet (ZODB), l’autre est une base de données relationnelle (SQLite). Cependant, en pratique, un seul des deux mécanismes peut être utilisé dans une installation donnée. L’utilisation de deux types de persistance est là pour démontrer comment utiliser la ZCA pour brancher des composants entre eux. La majorité de code de cette application est lié à PyGTK. 114 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 Lorsque l’application grossit, vous pouvez utiliser les composants de la ZCA aux endroits où vous souhaitez fournir de la modularité ou de l’extensibilité. Utilisez des objets Python classiques quand vous n’avez pas besoin de ces propriétés. Il n’y a pas de différence entre l’utilisation de la ZCA pour une application web, pour une application graphique lourde, ou pour toute autre application ou framework. Il est préférable de suivre une convention pour l’emplacement où vous allez inscrire les composants. Cette application utilise une convention qui peut être étendue en plaçant des inscriptions ou des composants similaires dans des modules séparés et en les important plus tard depuis le module principal d’inscription. Dans cette application, le module principal pour l’inscription est register.py. Le code source de cette application peut être téléchargé sur : http ://www.muthukadan.net/downloads/zcalib.tar.bz2 4.8.2 Cas d’utilisation L’application que nous allons créer ici est un système de gestion de bibliothèque avec des fonctionnalités minimales. Les besoins peuvent être résumés comme ceci : – Ajouter des membres avec un numéro et un nom uniques. – Ajouter des livres avec un code barre, un auteur et un titre. – Prêter des livres – Récupérer des livres L’application peut être conçue de telle façon que les grandes fonctionnalités seront utilisées dans une fenêtre unique. La fenêtre principale peut être conçue de cette façon : Depuis la fenêtre des membres, l’utilisateur devrait être capable de gérer des membres. Donc il devrait être possible d’ajouter, modifier et effacer des membres comme dans l’image ci-dessous : Similaire à la fenêtre des membres, la fenêtre du catalogue permet aux utilisateurs d’ajouter, modifier et effacer des livres : 4.8. Étude de cas 115 Plone pour les développeurs, Version 1.0.0 La fenêtre de circulation doit avoir ce qu’il faut pour prêter et récupérer des livres : 4.8.3 Survol du code PyGTK Vous pouvez constater que la majorité du code est en rapport avec PyGTK. Sa structure est très similaire pour les différentes fenêtres. Les écrans de cette application sont conçus avec le créateur d’interface graphiques Glade. Vous devriez donner des noms appropriés aux widgets que vous allez utiliser dans votre code. Dans la fenêtre principale, tous les éléments de menu ont des noms du type : circulation, catalog, member, quit et about. La classe gtk.glade.XML est utilisée pour analyser le fichier Glade, et donc pour créer les objets widgets de l’interface graphique. Voici comment faire import gtk.glade xmlobj = gtk.glade.XML(’/path/to/file.glade’) widget = xmlobj.get_widget(’widget_name’) Dans mainwindow.py, vous pouvez voir le code suivant curdir = os.path.abspath(os.path.dirname(__file__)) xml = os.path.join(curdir, ’glade’, ’mainwindow.glade’) xmlobj = gtk.glade.XML(xml) self.mainwindow = xmlobj.get_widget(’mainwindow’) Le nom du widget fenêtre principal est mainwindow. Les autres widgets sont créés de la même manière circulation = xmlobj.get_widget(’circulation’) member = xmlobj.get_widget(’member’) quit = xmlobj.get_widget(’quit’) catalog = xmlobj.get_widget(’catalog’) about = xmlobj.get_widget(’about’) Ensuite ces widgets sont connectés à certains événements self.mainwindow.connect(’delete_event’, self.delete_event) quit.connect(’activate’, self.delete_event) circulation.connect(’activate’, self.on_circulation_activate) member.connect(’activate’, self.on_member_activate) catalog.connect(’activate’, self.on_catalog_activate) about.connect(’activate’, self.on_about_activate) 116 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 L’événement delete_event est celui correspondant à la fermeture de la fenêtre lors de l’appui sur le bouton de fermeture. L’événement activate est émis lorsque le menu est sélectionné. Les widgets sont connectés à des fonctions de rappel pour certains événements. Vous pouvez voir dans le code ci-dessus que la fenêtre principale est connectée à la méthode on_delete_event pour l’événement delete_event. Le widget quit est aussi connecté aux mêmes méthodes pour l’événement activate def on_delete_event(self, *args): gtk.main_quit() La fonction de rappel lance juste la fonction main_quit. 4.8.4 Le code Voici zcalib.py import registry import mainwindow if __name__ == ’__main__’: registry.initialize() try: mainwindow.main() except KeyboardInterrupt: import sys sys.exit(1) Ici, deux modules sont importés : registry et mainwindow. Ensuite le registre est initialisé et la fonction main de la fenêtre principale est appelée. Si l’utilisateur essaye de fermer l’application en appuyant sur Ctrl-C, celle-ci s’arrête normalement car nous avons intercepté l’exception KeyboardInterrupt. Voici registry.py import sys from zope.component import getGlobalSiteManager from from from from interfaces interfaces interfaces interfaces import import import import IMember IBook ICirculation IDbOperation def initialize_rdb(): from interfaces import IRelationalDatabase from relationaldatabase import RelationalDatabase from member import MemberRDbOperation from catalog import BookRDbOperation from circulation import CirculationRDbOperation gsm = getGlobalSiteManager() db = RelationalDatabase() gsm.registerUtility(db, IRelationalDatabase) gsm.registerAdapter(MemberRDbOperation, (IMember,), IDbOperation) gsm.registerAdapter(BookRDbOperation, (IBook,), IDbOperation) gsm.registerAdapter(CirculationRDbOperation, 4.8. Étude de cas 117 Plone pour les développeurs, Version 1.0.0 (ICirculation,), IDbOperation) def initialize_odb(): from interfaces import IObjectDatabase from objectdatabase import ObjectDatabase from member import MemberODbOperation from catalog import BookODbOperation from circulation import CirculationODbOperation gsm = getGlobalSiteManager() db = ObjectDatabase() gsm.registerUtility(db, IObjectDatabase) gsm.registerAdapter(MemberODbOperation, (IMember,), IDbOperation) gsm.registerAdapter(BookODbOperation, (IBook,), IDbOperation) gsm.registerAdapter(CirculationODbOperation, (ICirculation,), IDbOperation) def check_use_relational_db(): use_rdb = False try: arg = sys.argv[1] if arg == ’-r’: return True except IndexError: pass return use_rdb def initialize(): use_rdb = check_use_relational_db() if use_rdb: initialize_rdb() else: initialize_odb() Observez la fonction initialize que nous appelons depuis le module principal zcalib.py. Cette fonction commence par vérifier quelle base de données utiliser : une base relationnelle (RDB) ou une base objet (ODB). Cette vérification est effectuée dans la fonction check_use_relational_db. Si l’option -r est passé dans la ligne de commandes, elle appelera la fonction initialize_rdb, sinon initialize_odb. Si la fonction RDB est appelée, elle configure tous les composants liés à la base relationnelle. Si la fonction ODB est appelée, elle configure tous les composants liés à la base objet. Voici mainwindow.py import os import gtk import gtk.glade from circulationwindow import circulationwindow from catalogwindow import catalogwindow from memberwindow import memberwindow class MainWindow(object): def __init__(self): 118 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 curdir = os.path.abspath(os.path.dirname(__file__)) xml = os.path.join(curdir, ’glade’, ’mainwindow.glade’) xmlobj = gtk.glade.XML(xml) self.mainwindow = xmlobj.get_widget(’mainwindow’) circulation = xmlobj.get_widget(’circulation’) member = xmlobj.get_widget(’member’) quit = xmlobj.get_widget(’quit’) catalog = xmlobj.get_widget(’catalog’) about = xmlobj.get_widget(’about’) self.mainwindow.connect(’delete_event’, self.delete_event) quit.connect(’activate’, self.delete_event) circulation.connect(’activate’, self.on_circulation_activate) member.connect(’activate’, self.on_member_activate) catalog.connect(’activate’, self.on_catalog_activate) about.connect(’activate’, self.on_about_activate) def delete_event(self, *args): gtk.main_quit() def on_circulation_activate(self, *args): circulationwindow.show_all() def on_member_activate(self, *args): memberwindow.show_all() def on_catalog_activate(self, *args): catalogwindow.show_all() def on_about_activate(self, *args): pass def run(self): self.mainwindow.show_all() def main(): mainwindow = MainWindow() mainwindow.run() gtk.main() La fonction main crée une instance de la classe MainWindow qui initialise tous les widgets. Voici memberwindow.py import os import gtk import gtk.glade from zope.component import getAdapter from components import Member from interfaces import IDbOperation class MemberWindow(object): def __init__(self): curdir = os.path.abspath(os.path.dirname(__file__)) xml = os.path.join(curdir, ’glade’, ’memberwindow.glade’) xmlobj = gtk.glade.XML(xml) 4.8. Étude de cas 119 Plone pour les développeurs, Version 1.0.0 self.memberwindow = xmlobj.get_widget(’memberwindow’) self.number = xmlobj.get_widget(’number’) self.name = xmlobj.get_widget(’name’) add = xmlobj.get_widget(’add’) update = xmlobj.get_widget(’update’) delete = xmlobj.get_widget(’delete’) close = xmlobj.get_widget(’close’) self.treeview = xmlobj.get_widget(’treeview’) self.memberwindow.connect(’delete_event’, self.on_delete_event) add.connect(’clicked’, self.on_add_clicked) update.connect(’clicked’, self.on_update_clicked) delete.connect(’clicked’, self.on_delete_clicked) close.connect(’clicked’, self.on_delete_event) self.initialize_list() def show_all(self): self.populate_list_store() self.memberwindow.show_all() def populate_list_store(self): self.list_store.clear() member = Member() memberdboperation = getAdapter(member, IDbOperation) members = memberdboperation.get() for member in members: number = member.number name = member.name self.list_store.append((member, number, name,)) def on_delete_event(self, *args): self.memberwindow.hide() return True def initialize_list(self): self.list_store = gtk.ListStore(object, str, str) self.treeview.set_model(self.list_store) tvcolumn = gtk.TreeViewColumn(’Member Number’) self.treeview.append_column(tvcolumn) cell = gtk.CellRendererText() tvcolumn.pack_start(cell, True) tvcolumn.add_attribute(cell, ’text’, 1) tvcolumn = gtk.TreeViewColumn(’Member Name’) self.treeview.append_column(tvcolumn) cell = gtk.CellRendererText() tvcolumn.pack_start(cell, True) tvcolumn.add_attribute(cell, ’text’, 2) def on_add_clicked(self, *args): number = self.number.get_text() name = self.name.get_text() member = Member() member.number = number member.name = name self.add(member) self.list_store.append((member, number, name,)) def add(self, member): memberdboperation = getAdapter(member, IDbOperation) 120 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 memberdboperation.add() def on_update_clicked(self, *args): number = self.number.get_text() name = self.name.get_text() treeselection = self.treeview.get_selection() model, iter = treeselection.get_selected() if not iter: return member = self.list_store.get_value(iter, 0) member.number = number member.name = name self.update(member) self.list_store.set(iter, 1, number, 2, name) def update(self, member): memberdboperation = getAdapter(member, IDbOperation) memberdboperation.update() def on_delete_clicked(self, *args): treeselection = self.treeview.get_selection() model, iter = treeselection.get_selected() if not iter: return member = self.list_store.get_value(iter, 0) self.delete(member) self.list_store.remove(iter) def delete(self, member): memberdboperation = getAdapter(member, IDbOperation) memberdboperation.delete() memberwindow = MemberWindow() Voici components.py from zope.interface import implements from interfaces import IBook from interfaces import IMember from interfaces import ICirculation class Book(object): implements(IBook) barcode = "" title = "" author = "" class Member(object): implements(IMember) number = "" name = "" class Circulation(object): implements(ICirculation) book = Book() member = Member() 4.8. Étude de cas 121 Plone pour les développeurs, Version 1.0.0 Voici interfaces.py from zope.interface import Interface from zope.interface import Attribute class IBook(Interface): barcode = Attribute("Barcode") author = Attribute("Author of book") title = Attribute("Title of book") class IMember(Interface): number = Attribute("ID number") name = Attribute("Name of member") class ICirculation(Interface): book = Attribute("A book") member = Attribute("A member") class IRelationalDatabase(Interface): def commit(): pass def rollback(): pass def cursor(): pass def get_next_id(): pass class IObjectDatabase(Interface): def commit(): pass def rollback(): pass def container(): pass def get_next_id(): pass class IDbOperation(Interface): def get(): pass def add(): pass 122 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 def update(): pass def delete(): pass Voici member.py from zope.interface import implements from zope.component import getUtility from zope.component import adapts from components import Member from from from from interfaces interfaces interfaces interfaces import import import import IRelationalDatabase IObjectDatabase IMember IDbOperation class MemberRDbOperation(object): implements(IDbOperation) adapts(IMember) def __init__(self, member): self.member = member def get(self): db = getUtility(IRelationalDatabase) cr = db.cursor() number = self.member.number if number: cr.execute("""SELECT id, number, name FROM members WHERE number = ?""", (number,)) else: cr.execute("""SELECT id, number, name FROM members""") rst = cr.fetchall() cr.close() members = [] for record in rst: id = record[’id’] number = record[’number’] name = record[’name’] member = Member() member.id = id member.number = number member.name = name members.append(member) return members def add(self): db = getUtility(IRelationalDatabase) cr = db.cursor() 4.8. Étude de cas 123 Plone pour les développeurs, Version 1.0.0 next_id = db.get_next_id("members") number = self.member.number name = self.member.name cr.execute("""INSERT INTO members (id, number, name) VALUES (?, ?, ?)""", (next_id, number, name)) cr.close() db.commit() self.member.id = next_id def update(self): db = getUtility(IRelationalDatabase) cr = db.cursor() number = self.member.number name = self.member.name id = self.member.id cr.execute("""UPDATE members SET number = ?, name = ? WHERE id = ?""", (number, name, id)) cr.close() db.commit() def delete(self): db = getUtility(IRelationalDatabase) cr = db.cursor() id = self.member.id cr.execute("""DELETE FROM members WHERE id = ?""", (id,)) cr.close() db.commit() class MemberODbOperation(object): implements(IDbOperation) adapts(IMember) def __init__(self, member): self.member = member def get(self): db = getUtility(IObjectDatabase) zcalibdb = db.container() members = zcalibdb[’members’] return members.values() def add(self): db = getUtility(IObjectDatabase) zcalibdb = db.container() members = zcalibdb[’members’] number = self.member.number if number in [x.number for x in members.values()]: db.rollback() raise Exception("Duplicate key") next_id = db.get_next_id(’members’) self.member.id = next_id members[next_id] = self.member db.commit() 124 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 def update(self): db = getUtility(IObjectDatabase) zcalibdb = db.container() members = zcalibdb[’members’] id = self.member.id members[id] = self.member db.commit() def delete(self): db = getUtility(IObjectDatabase) zcalibdb = db.container() members = zcalibdb[’members’] id = self.member.id del members[id] db.commit() 4.8.5 PySQLite 4.8.6 ZODB 4.8.7 Conclusions 4.9 Reference 4.9.1 adaptedBy Cette fonction permet de trouver les interfaces adaptées. – Emplacement : zope.component – Signature : adaptedBy(object) Exemple >>> from zope.interface import implements >>> from zope.component import adapts >>> from zope.component import adaptedBy >>> class FrontDeskNG(object): ... ... implements(IDesk) ... adapts(IGuest) ... ... def __init__(self, guest): ... self.guest = guest >>> adaptedBy(FrontDeskNG) (<InterfaceClass __builtin__.IGuest>,) 4.9.2 adapter Un adaptateur peut être n’importe quel objet appelable. Vous pouvez utiliser le décorateur adapter pour déclarer qu’un objet appelable s’adapte à des interfaces (ou des classes). – Emplacement : zope.component – Signature : adapter(*interfaces) Exemple 4.9. Reference 125 Plone pour les développeurs, Version 1.0.0 >>> >>> >>> >>> >>> from from from from from zope.interface zope.interface zope.interface zope.component zope.interface import import import import import Attribute Interface implementer adapter implements >>> class IJob(Interface): ... """A job""" >>> class Job(object): ... implements(IJob) >>> class IPerson(Interface): ... ... name = Attribute("Name") ... job = Attribute("Job") >>> class Person(object): ... implements(IPerson) ... ... name = None ... job = None >>> @implementer(IJob) ... @adapter(IPerson) ... def personJob(person): ... return person.job >>> jack = Person() >>> jack.name = "Jack" >>> jack.job = Job() >>> personJob(jack) <Job object at ...> 4.9.3 adapts Cette fonction permet de déclarer les classes adaptateurs. – Emplacement : zope.component – Signature : adapts(*interfaces) Exemple >>> from zope.interface import implements >>> from zope.component import adapts >>> class FrontDeskNG(object): ... ... implements(IDesk) ... adapts(IGuest) ... ... def __init__(self, guest): ... self.guest = guest ... ... def register(self): ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: guest.name, ... ’place’: guest.place, ... ’phone’: guest.phone ... } 126 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 4.9.4 alsoProvides Déclare des interfaces additionnelles fournies par un objet. Les arguments après l’objet sont une ou plusieurs interfaces. Les interfaces données sont ajoutées aux interfaces précédemment déclarées pour l’objet. – Emplacement : zope.interface – Signature : alsoProvides(object, *interfaces) Exemple >>> >>> >>> >>> from from from from zope.interface zope.interface zope.interface zope.interface import import import import Attribute Interface implements alsoProvides >>> class IPerson(Interface): ... ... name = Attribute("Name of person") >>> class IStudent(Interface): ... ... college = Attribute("Name of college") >>> class Person(object): ... ... implements(IDesk) ... name = u"" >>> >>> >>> >>> jack = Person() jack.name = "Jack" jack.college = "New College" alsoProvides(jack, IStudent) Vous pouvez tester de cette façon: >>> from zope.interface import providedBy >>> IStudent in providedBy(jack) True 4.9.5 Attribute En utilisant cette classe, vous pouvez définir des attributs dans une interface. – Emplacement : zope.interface – Signature : Attribute(name, doc=’‘) – Voir aussi : Interface Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IPerson(Interface): ... ... name = Attribute("Name of person") ... email = Attribute("Email Address") 4.9.6 classImplements Déclare des interfaces additionnelles qui doivent être fournies par les instances d’une classe. Les arguments après la classe sont une ou plusieurs interfaces. Les interfaces données sont ajoutées aux interfaces précédemment déclarées. – Emplacement : zope.interface 4.9. Reference 127 Plone pour les développeurs, Version 1.0.0 – Signature : classImplements(cls, *interfaces) Exemple >>> >>> >>> >>> from from from from zope.interface zope.interface zope.interface zope.interface import import import import Attribute Interface implements classImplements >>> class IPerson(Interface): ... ... name = Attribute("Name of person") >>> class IStudent(Interface): ... ... college = Attribute("Name of college") >>> class Person(object): ... ... implements(IDesk) ... name = u"" ... college = u"" >>> >>> >>> >>> classImplements(Person, IStudent) jack = Person() jack.name = "Jack" jack.college = "New College" Vous pouvez tester de cette façon : >>> from zope.interface import providedBy >>> IStudent in providedBy(jack) True 4.9.7 classImplementsOnly Déclare les seules interfaces qui devront être fournies par les instances d’une classe. Les arguments après la classe sont une ou plusieurs interfaces. Les interfaces fournies vont remplacer toutes les interfaces des déclarations précédentes. – Emplacement : zope.interface – Signature : classImplementsOnly(cls, *interfaces) Exemple >>> >>> >>> >>> from from from from zope.interface zope.interface zope.interface zope.interface import import import import Attribute Interface implements classImplementsOnly >>> class IPerson(Interface): ... ... name = Attribute("Name of person") >>> class IStudent(Interface): ... ... college = Attribute("Name of college") >>> class Person(object): ... ... implements(IPerson) ... college = u"" >>> classImplementsOnly(Person, IStudent) 128 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 >>> jack = Person() >>> jack.college = "New College" Vous pouvez tester de cette façon : >>> from zope.interface import providedBy >>> IPerson in providedBy(jack) False >>> IStudent in providedBy(jack) True 4.9.8 classProvides Normalement si une classe implémente une interface particulière, les instances de cette classe fourniront l’interface implémentée par la classe. Mais si vous voulez qu’une classe fournisse une interface, vous pouvez le déclarer grâce à la fonction classProvides. – Emplacement : zope.interface – Signature : classProvides(*interfaces) Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> from zope.interface import classProvides >>> class IPerson(Interface): ... ... name = Attribute("Name of person") >>> class Person(object): ... ... classProvides(IPerson) ... name = u"Jack" Vous pouvez tester de cette façon : >>> from zope.interface import providedBy >>> IPerson in providedBy(Person) True 4.9.9 ComponentLookupError C’est l’exception levée quand une recherche de composant échoue. Exemple >>> class IPerson(Interface): ... ... name = Attribute("Name of person") >>> person = object() >>> getAdapter(person, IPerson, ’not-exists’) Traceback (most recent call last): ... ComponentLookupError: ... 4.9.10 createObject Crée un objet en utilisant une fabrique. 4.9. Reference 129 Plone pour les développeurs, Version 1.0.0 Cette fonction trouve une fabrique nommée dans le site courant et l’appelle avec les arguments donnés. Si la bonne fabrique ne peut pas être trouvée, une erreur ComponentLookupError est déclenchée. Sinon l’objet créé est renvoyé. Un argument mot-clé contextuel peut être fourni pour rechercher une fabrique dans un emplacement différent du site courant. (Bien sûr, cela signifie qu’il est impossible de transmettre à la fabrique un argument mot-clé nommé « context ». – Emplacement : zope.component – Signature : createObject(factory_name, *args, **kwargs) Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IDatabase(Interface): ... ... def getConnection(): ... """Return connection object""" >>> class FakeDb(object): ... ... implements(IDatabase) ... ... def getConnection(self): ... return "connection" >>> from zope.component.factory import Factory >>> factory = Factory(FakeDb, ’FakeDb’) >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> from zope.component.interfaces import IFactory >>> gsm.registerUtility(factory, IFactory, ’fakedb’) >>> from zope.component import createObject >>> createObject(’fakedb’) <FakeDb object at ...> 4.9.11 Declaration N’a pas besoin d’être utilisé directement. 4.9.12 directlyProvidedBy Cette fonction renvoie les interfaces fournies directement par un objet donné. – Emplacement : zope.interface – Signature : directlyProvidedBy(object) Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IPerson(Interface): ... ... name = Attribute("Name of person") >>> class IStudent(Interface): 130 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 ... ... college = Attribute("Name of college") >>> class ISmartPerson(Interface): ... pass >>> class Person(object): ... ... implements(IPerson) ... name = u"" >>> >>> >>> >>> jack = Person() jack.name = u"Jack" jack.college = "New College" alsoProvides(jack, ISmartPerson, IStudent) >>> from zope.interface import directlyProvidedBy >>> jack_dp = directlyProvidedBy(jack) >>> IPerson in jack_dp.interfaces() False >>> IStudent in jack_dp.interfaces() True >>> ISmartPerson in jack_dp.interfaces() True 4.9.13 directlyProvides Déclare que des interfaces sont fournies directement par un objet. Les arguments après l’objet sont une ou plusieurs interfaces. Les interfaces données remplacent celles précédemment déclarées pour l’objet. – Emplacement : zope.interface – Signature : directlyProvides(object, *interfaces) Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IPerson(Interface): ... ... name = Attribute("Name of person") >>> class IStudent(Interface): ... ... college = Attribute("Name of college") >>> class ISmartPerson(Interface): ... pass >>> class Person(object): ... ... implements(IPerson) ... name = u"" >>> >>> >>> >>> jack = Person() jack.name = u"Jack" jack.college = "New College" alsoProvides(jack, ISmartPerson, IStudent) >>> from zope.interface import directlyProvidedBy >>> jack_dp = directlyProvidedBy(jack) 4.9. Reference 131 Plone pour les développeurs, Version 1.0.0 >>> ISmartPerson in jack_dp.interfaces() True >>> IPerson in jack_dp.interfaces() False >>> IStudent in jack_dp.interfaces() True >>> from zope.interface import providedBy >>> ISmartPerson in providedBy(jack) True >>> from zope.interface import directlyProvides >>> directlyProvides(jack, IStudent) >>> jack_dp = directlyProvidedBy(jack) >>> ISmartPerson in jack_dp.interfaces() False >>> IPerson in jack_dp.interfaces() False >>> IStudent in jack_dp.interfaces() True >>> ISmartPerson in providedBy(jack) False 4.9.14 getAdapter Cette fonction récupère un adaptateur (nommé) vers une interface, pour un objet donné. Elle renvoie un adaptateur qui peut adapter les objets à l’interface voulue. Si aucun adaptateur ne peut être trouvé, une erreur ComponentLookupError est émise. – Emplacement : zope.interface – Signature : getAdapter(object, interface=Interface, name=u’‘, context=None) Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IDesk(Interface): ... """A frontdesk will register object’s details""" ... ... def register(): ... """Register object’s details""" ... >>> from zope.interface import implements >>> from zope.component import adapts >>> class FrontDeskNG(object): ... ... implements(IDesk) ... adapts(IGuest) ... ... def __init__(self, guest): ... self.guest = guest ... ... def register(self): ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: guest.name, ... ’place’: guest.place, ... ’phone’: guest.phone 132 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 ... } >>> class Guest(object): ... ... implements(IGuest) ... ... def __init__(self, name, place): ... self.name = name ... self.place = place >>> jack = Guest("Jack", "Bangalore") >>> jack_frontdesk = FrontDeskNG(jack) >>> IDesk.providedBy(jack_frontdesk) True >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerAdapter(FrontDeskNG, ... (IGuest,), IDesk, ’ng’) >>> getAdapter(jack, IDesk, ’ng’) <FrontDeskNG object at ...> 4.9.15 getAdapterInContext Au lieu d’utiliser cette fonction, utilisez getAdapter avec un argument context. – Emplacement : zope.component – Signature : getAdapterInContext(object, interface, context) – Voir aussi : queryAdapterInContext Exemple >>> from zope.component.globalregistry import BaseGlobalComponents >>> from zope.component import IComponentLookup >>> sm = BaseGlobalComponents() >>> class Context(object): ... def __init__(self, sm): ... self.sm = sm ... def __conform__(self, interface): ... if interface.isOrExtends(IComponentLookup): ... return self.sm >>> context = Context(sm) >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IDesk(Interface): ... """A frontdesk will register object’s details""" ... ... def register(): ... """Register object’s details""" ... >>> from zope.interface import implements >>> from zope.component import adapts >>> class FrontDeskNG(object): ... ... implements(IDesk) 4.9. Reference 133 Plone pour les développeurs, Version 1.0.0 ... ... ... ... ... ... ... ... ... ... ... ... adapts(IGuest) def __init__(self, guest): self.guest = guest def register(self): next_id = get_next_id() bookings_db[next_id] = { ’name’: guest.name, ’place’: guest.place, ’phone’: guest.phone } >>> class Guest(object): ... ... implements(IGuest) ... ... def __init__(self, name, place): ... self.name = name ... self.place = place >>> jack = Guest("Jack", "Bangalore") >>> jack_frontdesk = FrontDeskNG(jack) >>> IDesk.providedBy(jack_frontdesk) True >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> sm.registerAdapter(FrontDeskNG, ... (IGuest,), IDesk) >>> from zope.component import getAdapterInContext >>> getAdapterInContext(jack, IDesk, sm) <FrontDeskNG object at ...> 4.9.16 getAdapters Recherche pour des objets tous les adaptateurs fournissant une interface donnée. Une liste d’adaptateurs est renvoyée. Si un adaptateur est nommé, seul l’adaptateur le plus spécifique pour un nom donné est renvoyé. – Emplacement : zope.component – Signature : getAdapters(objects, provided, context=None) Exemple >>> from zope.interface import implements >>> from zope.component import adapts >>> class FrontDeskNG(object): ... ... implements(IDesk) ... adapts(IGuest) ... ... def __init__(self, guest): ... self.guest = guest ... ... def register(self): ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: guest.name, ... ’place’: guest.place, 134 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 ... ... ’phone’: guest.phone } >>> jack = Guest("Jack", "Bangalore") >>> jack_frontdesk = FrontDeskNG(jack) >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerAdapter(FrontDeskNG, name=’ng’) >>> from zope.component import getAdapters >>> list(getAdapters((jack,), IDesk)) [(u’ng’, <FrontDeskNG object at ...>)] 4.9.17 getAllUtilitiesRegisteredFor Renvoie tous les utilitaires inscrits pour une interface. Ceci inclut les utilitaires surchargés. La valeur renvoyée est un objet iterable listant les instances d’utilitaires correspondants. – Emplacement : zope.component – Signature : getAllUtilitiesRegisteredFor(interface) Exemple >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IGreeter(Interface): ... def greet(name): ... "say hello" >>> class Greeter(object): ... ... implements(IGreeter) ... ... def greet(self, name): ... print "Hello", name >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> greet = Greeter() >>> gsm.registerUtility(greet, IGreeter) >>> from zope.component import getAllUtilitiesRegisteredFor >>> getAllUtilitiesRegisteredFor(IGreeter) [<Greeter object at ...>] 4.9.18 getFactoriesFor Renvoie un tuple (nom, fabrique) de fabriques inscrites capables de créer des objets qui fournissent une interface donnée. – Emplacement : zope.component – Signature : getFactoriesFor(interface, context=None) Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> from zope.interface import implements 4.9. Reference 135 Plone pour les développeurs, Version 1.0.0 >>> class IDatabase(Interface): ... ... def getConnection(): ... """Return connection object""" >>> class FakeDb(object): ... ... implements(IDatabase) ... ... def getConnection(self): ... return "connection" >>> from zope.component.factory import Factory >>> factory = Factory(FakeDb, ’FakeDb’) >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> from zope.component.interfaces import IFactory >>> gsm.registerUtility(factory, IFactory, ’fakedb’) >>> from zope.component import getFactoriesFor >>> list(getFactoriesFor(IDatabase)) [(u’fakedb’, <Factory for <class ’FakeDb’>>)] 4.9.19 getFactoryInterfaces Récupère les interfaces implémentées par une fabrique. Cette fonction trouve la fabrique possédant le nom donné la plus proche du contexte, puis renvoie l’interface ou le tuple d’interfaces que les instances créées par la fabrique vont fournir. – Emplacement : zope.component – Signature : getFactoryInterfaces(name, context=None) Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IDatabase(Interface): ... ... def getConnection(): ... """Return connection object""" >>> class FakeDb(object): ... ... implements(IDatabase) ... ... def getConnection(self): ... return "connection" >>> from zope.component.factory import Factory >>> factory = Factory(FakeDb, ’FakeDb’) >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> from zope.component.interfaces import IFactory 136 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 >>> gsm.registerUtility(factory, IFactory, ’fakedb’) >>> from zope.component import getFactoryInterfaces >>> getFactoryInterfaces(’fakedb’) <implementedBy __builtin__.FakeDb> 4.9.20 getGlobalSiteManager Renvoie le gestionnaire global de site. Cette fonction ne doit jamais échouer et doit toujours renvoyer un objet qui fournit IGlobalSiteManager. – Emplacement : zope.component – Signature : getGlobalSiteManager() Exemple >>> from zope.component import getGlobalSiteManager >>> from zope.component import globalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm is globalSiteManager True 4.9.21 getMultiAdapter Cette fonction recherche pour des objets un multi-adaptateur vers une interface donnée. Elle renvoie un multiadaptateur qui peut s’adapter aux objets donnés et fournir l’interface voulue. Si aucun adaptateur ne peut être trouvé, une erreur ComponentLookupError est émise. Le nom constitué de la chaîne vide est réservé aux adaptateur sans nom. Les méthodes des adaptateurs sans nom appellent souvent les méthodes des adaptateurs nommés en fournissant un nom vide (‘’). – Emplacement : zope.component – Signature : getMultiAdapter(objects, interface=Interface, name=’‘, context=None) – Voir aussi : queryMultiAdapter Exemple >>> from zope.interface import Interface >>> from zope.interface import implements >>> from zope.component import adapts >>> class IAdapteeOne(Interface): ... pass >>> class IAdapteeTwo(Interface): ... pass >>> class IFunctionality(Interface): ... pass >>> class MyFunctionality(object): ... implements(IFunctionality) ... adapts(IAdapteeOne, IAdapteeTwo) ... ... def __init__(self, one, two): ... self.one = one ... self.two = two >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerAdapter(MyFunctionality) 4.9. Reference 137 Plone pour les développeurs, Version 1.0.0 >>> class One(object): ... implements(IAdapteeOne) >>> class Two(object): ... implements(IAdapteeTwo) >>> one = One() >>> two = Two() >>> from zope.component import getMultiAdapter >>> getMultiAdapter((one,two), IFunctionality) <MyFunctionality object at ...> >>> myfunctionality = getMultiAdapter((one,two), IFunctionality) >>> myfunctionality.one <One object at ...> >>> myfunctionality.two <Two object at ...> 4.9.22 getSiteManager Récupère le gestionnaire de site le plus proche du contexte donné. Si context est None, cette fonction renvoie le gestionnaire global de site. Si le context n’est pas None, on s’attend à ce qu’un adaptateur sur le contexte vers IComponentLookup soit trouvé. Si aucun adaptateur n’est trouvé, une erreur ComponentLookupError est émise. – Emplacement : zope.component – Signature : getSiteManager(context=None) Exemple 1 >>> from zope.component.globalregistry import BaseGlobalComponents >>> from zope.component import IComponentLookup >>> sm = BaseGlobalComponents() >>> class Context(object): ... def __init__(self, sm): ... self.sm = sm ... def __conform__(self, interface): ... if interface.isOrExtends(IComponentLookup): ... return self.sm >>> context = Context(sm) >>> from zope.component import getSiteManager >>> lsm = getSiteManager(context) >>> lsm is sm True Exemple 2 >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> sm = getSiteManager() >>> gsm is sm True 138 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 4.9.23 getUtilitiesFor Récupère les utilitaires inscrits pour une interface particulière. Renvoie un objet iterable listant les paires nom/utilitaire. – Emplacement : zope.component – Signature : getUtilitiesFor(interface) Exemple >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IGreeter(Interface): ... def greet(name): ... "say hello" >>> class Greeter(object): ... ... implements(IGreeter) ... ... def greet(self, name): ... print "Hello", name >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> greet = Greeter() >>> gsm.registerUtility(greet, IGreeter) >>> from zope.component import getUtilitiesFor >>> list(getUtilitiesFor(IGreeter)) [(u’’, <Greeter object at ...>)] 4.9.24 getUtility Récupère l’utilitaire qui fournit une interface donnée. Cette fonction renvoie l’utilitaire le plus proche du contexte et qui fournit l’interface donnée. Si aucun n’est trouvé, une erreur ComponentLookupError est levée. – Emplacement : zope.component – Signature : getUtility(interface, name=’‘, context=None) Exemple >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IGreeter(Interface): ... def greet(name): ... "say hello" >>> class Greeter(object): ... ... implements(IGreeter) ... ... def greet(self, name): ... return "Hello " + name >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> greet = Greeter() >>> gsm.registerUtility(greet, IGreeter) 4.9. Reference 139 Plone pour les développeurs, Version 1.0.0 >>> from zope.component import getUtility >>> getUtility(IGreeter).greet(’Jack’) ’Hello Jack’ 4.9.25 handle Appelle tous les gestionnaires pour un objet donné. Les gestionnaires sont des fabriques d’abonnés qui ne produisent rien. Ils font tout leur travail lors de l’appel. Ils sont typiquement utilisés pour gérer des événements. – Emplacement : zope.component – Signature : handle(*objects) Exemple >>> import datetime >>> def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> from zope.interface import implements >>> class IDocumentCreated(Interface): ... doc = Attribute("The document that was created") >>> class DocumentCreated(object): ... implements(IDocumentCreated) ... ... def __init__(self, doc): ... self.doc = doc >>> def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() >>> from zope.component import adapter >>> @adapter(IDocumentCreated) ... def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerHandler(documentCreated) >>> from zope.component import handle >>> handle(DocumentCreated(doc)) >>> doc.created.__class__.__name__ ’datetime’ 4.9.26 implementedBy Renvoie les interfaces implémentées par une classe. – Emplacement : zope.interface – Signature : implementedBy(class_) Exemple 1 140 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IGreeter(Interface): ... def greet(name): ... "say hello" >>> class Greeter(object): ... ... implements(IGreeter) ... ... def greet(self, name): ... print "Hello", name >>> from zope.interface import implementedBy >>> implementedBy(Greeter) <implementedBy __builtin__.Greeter> Exemple 2 >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IPerson(Interface): ... name = Attribute("Name of person") >>> class ISpecial(Interface): ... pass >>> class Person(object): ... implements(IPerson) ... name = u"" >>> from zope.interface import classImplements >>> classImplements(Person, ISpecial) >>> from zope.interface import implementedBy Pour récupérer une liste de toutes les interfaces implémentées par cette classe :: >>> [x.__name__ for x in implementedBy(Person)] [’IPerson’, ’ISpecial’] 4.9.27 implementer Créer un décorateur permettant de déclarer des interfaces implémentées par une fabrique. Un objet appelable est renvoyé, qui fait une déclaration d’implémentation sur les objets qui lui sont transmis. – Emplacement : zope.interface – Signature : implementer(*interfaces) Exemple >>> from zope.interface import implementer >>> class IFoo(Interface): ... pass >>> class Foo(object): ... implements(IFoo) >>> @implementer(IFoo) ... def foocreator(): 4.9. Reference 141 Plone pour les développeurs, Version 1.0.0 ... foo = Foo() ... return foo >>> list(implementedBy(foocreator)) [<InterfaceClass __builtin__.IFoo>] 4.9.28 implements Déclare que des interfaces seront fournies par une classe. Cette fonction est appelée dans une définition de classe. Les arguments sont une ou plusieurs interfaces. Les interfaces données sont ajoutées aux interfaces précédemment déclarées. Les déclarations précédentes incluent les déclarations des classes debase, à moins que implementsOnly ne soit utilisé. – Emplacement : zope.interface – Signature : implements(*interfaces) Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IPerson(Interface): ... ... name = Attribute("Name of person") >>> class Person(object): ... ... implements(IPerson) ... name = u"" >>> jack = Person() >>> jack.name = "Jack" You can test it like this: >>> from zope.interface import providedBy >>> IPerson in providedBy(jack) True 4.9.29 implementsOnly Déclare les seules interfaces qui doivent être implémentées par une classe. Cette fonction est appelée dans la définition de classe. Les arguments sont une ou plusieurs interfaces. Les déclarations précédentes, y compris celles des classes de base, sont remplacées. – Emplacement : zope.interface – Signature : implementsOnly(*interfaces) Exemple >>> >>> >>> >>> from from from from zope.interface zope.interface zope.interface zope.interface import import import import Attribute Interface implements implementsOnly >>> class IPerson(Interface): ... ... name = Attribute("Name of person") >>> class IStudent(Interface): ... ... college = Attribute("Name of college") 142 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 >>> class Person(object): ... ... implements(IPerson) ... name = u"" >>> class NewPerson(Person): ... implementsOnly(IStudent) ... college = u"" >>> jack = NewPerson() >>> jack.college = "New College" You can test it like this: >>> from zope.interface import providedBy >>> IPerson in providedBy(jack) False >>> IStudent in providedBy(jack) True 4.9.30 Interface Grâce à cette classe, vous pouvez définir une interface. Pour définir une interface, contentez-vous d’hériter de cette classe. – Emplacement : zope.interface – Signature : Interface(name, doc=’‘) Exemple 1 >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IPerson(Interface): ... ... name = Attribute("Name of person") ... email = Attribute("Email Address") Exemple 2 >>> from zope.interface import Interface >>> class IHost(Interface): ... ... def goodmorning(guest): ... """Say good morning to guest""" 4.9.31 moduleProvides Déclare les interfaces fournies par un module. Cette fonction est utilisée dans une définition de module. Les arguments sont une ou plusieurs interfaces. Les interfaces fournies sont utilisées pour créer la spécification d’interface objet du module. Une erreur est levée si le module a déjà une spécification d’interface. Autrement dit, c’est une erreur d’appeler cette fonction plus d’une fois dans une définition de module. Cette fonction est fournie pour des raisons pratiques. Elle fournit une manière plus pratique d’appeler directlyProvides pour un module. – Emplacement : zope.interface – Signature : moduleProvides(*interfaces) – Voir aussi : directlyProvides Vous pouvez consulter un exemple d’utilisation dans le code source de zope.component lui-même. Le fichier __init__.py possède celle instruction 4.9. Reference 143 Plone pour les développeurs, Version 1.0.0 moduleProvides(IComponentArchitecture, IComponentRegistrationConvenience) Ainsi, le paquet zope.component fournit deux interfaces : IComponentArchitecture et IComponentRegistrationConvenience. 4.9.32 noLongerProvides Retire une interface de la liste des interfaces directement fournies par un objet. – Emplacement : zope.interface – Signature : noLongerProvides(object, interface) Exemple >>> >>> >>> >>> from from from from zope.interface zope.interface zope.interface zope.interface import import import import Attribute Interface implements classImplements >>> class IPerson(Interface): ... ... name = Attribute("Name of person") >>> class IStudent(Interface): ... ... college = Attribute("Name of college") >>> class Person(object): ... ... implements(IPerson) ... name = u"" >>> >>> >>> >>> jack = Person() jack.name = "Jack" jack.college = "New College" directlyProvides(jack, IStudent) You can test it like this: >>> from zope.interface import providedBy >>> IPerson in providedBy(jack) True >>> IStudent in providedBy(jack) True >>> from zope.interface import noLongerProvides >>> noLongerProvides(jack, IStudent) >>> IPerson in providedBy(jack) True >>> IStudent in providedBy(jack) False 4.9.33 provideAdapter Il est recommandé d’utilier registerAdapter . 4.9.34 provideHandler Il est recommandé d’utiliser registerHandler . 144 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 4.9.35 provideSubscriptionAdapter Il est recommandé d’utiliser registerSubscriptionAdapter . 4.9.36 provideUtility Il est recommandé d’utiliser registerUtility . 4.9.37 providedBy Teste si l’interface est implémentée par l’objet. Renvoie True si l’objet affirme qu’il implémente l’interface, y compris les interfaces étendues. – Emplacement : zope.interface – Signature : providedBy(object) Exemple 1 >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IPerson(Interface): ... ... name = Attribute("Name of person") >>> class Person(object): ... ... implements(IPerson) ... name = u"" >>> jack = Person() >>> jack.name = "Jack" You can test it like this: >>> from zope.interface import providedBy >>> IPerson in providedBy(jack) True Exemple 2 >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IPerson(Interface): ... name = Attribute("Name of person") >>> class ISpecial(Interface): ... pass >>> class Person(object): ... implements(IPerson) ... name = u"" >>> >>> >>> >>> >>> from zope.interface import classImplements classImplements(Person, ISpecial) from zope.interface import providedBy jack = Person() jack.name = "Jack" 4.9. Reference 145 Plone pour les développeurs, Version 1.0.0 Pour obtenir une liste de toutes les interfaces fournies par cet objet :: >>> [x.__name__ for x in providedBy(jack)] [’IPerson’, ’ISpecial’] 4.9.38 queryAdapter Recherche pour un objet un adaptateur nommé vers une interface. Renvoie un adaptateur qui peut s’adapter à un objet et fournir une interface. Si aucun adaptateur ne peut être trouvé, la valeur par défaut est renvoyée. – Emplacement : zope.component – Signature : queryAdapter(object, interface=Interface, name=u’‘, default=None, context=None) Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IDesk(Interface): ... """A frontdesk will register object’s details""" ... ... def register(): ... """Register object’s details""" ... >>> from zope.interface import implements >>> from zope.component import adapts >>> class FrontDeskNG(object): ... ... implements(IDesk) ... adapts(IGuest) ... ... def __init__(self, guest): ... self.guest = guest ... ... def register(self): ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: guest.name, ... ’place’: guest.place, ... ’phone’: guest.phone ... } >>> class Guest(object): ... ... implements(IGuest) ... ... def __init__(self, name, place): ... self.name = name ... self.place = place >>> jack = Guest("Jack", "Bangalore") >>> jack_frontdesk = FrontDeskNG(jack) >>> IDesk.providedBy(jack_frontdesk) True >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerAdapter(FrontDeskNG, ... (IGuest,), IDesk, ’ng’) 146 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 >>> queryAdapter(jack, IDesk, ’ng’) <FrontDeskNG object at ...> 4.9.39 queryAdapterInContext Recherche pour un objet un adaptateur spécial vers une interface. NOTE : cette méthode ne doit être utilisée que si un contexte personnalisé doit être fourni lors d’une recherche de composant personnalisée. Sinon, appelez l’interface comme ci-dessous interface(object, default) Renvoie un adaptateur qui peut s’adapter à l’objet et fournir l’interface. Si aucun adaptateur n’est fourni, la valeur par défaut est renvoyée. Le contexte est adapté à IServiceService, et ce service d’adaptateurs, fourni par l’adaptateur, est utilisé. Si l’objet possède une méthode __conform__, cette méthode est appelée avec l’interface demandée. Si la méthode renvoie une valeur différente de None, cette valeur est retournée. Sinon, si l’objet implémente déjà l’interface, cet objet est renvoyé. – Emplacement : zope.component – Signature : queryAdapterInContext(object, interface, context, default=None) – Voir aussi : getAdapterInContext Exemple >>> from zope.component.globalregistry import BaseGlobalComponents >>> from zope.component import IComponentLookup >>> sm = BaseGlobalComponents() >>> class Context(object): ... def __init__(self, sm): ... self.sm = sm ... def __conform__(self, interface): ... if interface.isOrExtends(IComponentLookup): ... return self.sm >>> context = Context(sm) >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IDesk(Interface): ... """A frontdesk will register object’s details""" ... ... def register(): ... """Register object’s details""" ... >>> from zope.interface import implements >>> from zope.component import adapts >>> class FrontDeskNG(object): ... ... implements(IDesk) ... adapts(IGuest) ... ... def __init__(self, guest): ... self.guest = guest ... ... def register(self): ... next_id = get_next_id() ... bookings_db[next_id] = { 4.9. Reference 147 Plone pour les développeurs, Version 1.0.0 ... ... ... ... ’name’: guest.name, ’place’: guest.place, ’phone’: guest.phone } >>> class Guest(object): ... ... implements(IGuest) ... ... def __init__(self, name, place): ... self.name = name ... self.place = place >>> jack = Guest("Jack", "Bangalore") >>> jack_frontdesk = FrontDeskNG(jack) >>> IDesk.providedBy(jack_frontdesk) True >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> sm.registerAdapter(FrontDeskNG, ... (IGuest,), IDesk) >>> from zope.component import queryAdapterInContext >>> queryAdapterInContext(jack, IDesk, sm) <FrontDeskNG object at ...> 4.9.40 queryMultiAdapter Recherche pour des objets un multi-adaptateur vers une interface. Cette fonction renvoie un multi-adaptateur qui peut adapter les objets à l’interface. Si aucun adaptateur ne peut être trouvé, la valeur par défaut est renvoyée. Le nom correspondant à la chaîne vide est réservé aux adaptateurs sans nom. Les methodes des adaptateurs sans nom appellent souvent les méthodes des adaptateurs nommés avec un nom vide (‘’). – Emplacement : zope.component – Signature : queryMultiAdapter(objects, interface=Interface, name=u’‘, default=None, context=None) – Voir aussi : getMultiAdapter Exemple >>> from zope.interface import Interface >>> from zope.interface import implements >>> from zope.component import adapts >>> class IAdapteeOne(Interface): ... pass >>> class IAdapteeTwo(Interface): ... pass >>> class IFunctionality(Interface): ... pass >>> class MyFunctionality(object): ... implements(IFunctionality) ... adapts(IAdapteeOne, IAdapteeTwo) ... ... def __init__(self, one, two): ... self.one = one ... self.two = two 148 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerAdapter(MyFunctionality) >>> class One(object): ... implements(IAdapteeOne) >>> class Two(object): ... implements(IAdapteeTwo) >>> one = One() >>> two = Two() >>> from zope.component import queryMultiAdapter >>> getMultiAdapter((one,two), IFunctionality) <MyFunctionality object at ...> >>> myfunctionality = queryMultiAdapter((one,two), IFunctionality) >>> myfunctionality.one <One object at ...> >>> myfunctionality.two <Two object at ...> 4.9.41 queryUtility Cette fonction est utilisée pour rechercher un utilitaire qui fournit une interface. Si aucun n’est trouvé, la valeur par défaut est renvoyée. – Emplacement : zope.component – Signature : queryUtility(interface, name=’‘, default=None) Exemple >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IGreeter(Interface): ... def greet(name): ... "say hello" >>> class Greeter(object): ... ... implements(IGreeter) ... ... def greet(self, name): ... return "Hello " + name >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> greet = Greeter() >>> gsm.registerUtility(greet, IGreeter) >>> from zope.component import queryUtility >>> queryUtility(IGreeter).greet(’Jack’) ’Hello Jack’ 4.9. Reference 149 Plone pour les développeurs, Version 1.0.0 4.9.42 registerAdapter Cette fonction est utilisée pour inscrire une fabrique d’adaptateur. – Emplacement : zope.component - IComponentRegistry – Signature : registerAdapter(factory, required=None, provided=None, name=u’‘, info=u’‘) – Voir aussi : unregisterAdapter Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IDesk(Interface): ... """A frontdesk will register object’s details""" ... ... def register(): ... """Register object’s details""" ... >>> from zope.interface import implements >>> from zope.component import adapts >>> class FrontDeskNG(object): ... ... implements(IDesk) ... adapts(IGuest) ... ... def __init__(self, guest): ... self.guest = guest ... ... def register(self): ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: guest.name, ... ’place’: guest.place, ... ’phone’: guest.phone ... } >>> class Guest(object): ... ... implements(IGuest) ... ... def __init__(self, name, place): ... self.name = name ... self.place = place >>> jack = Guest("Jack", "Bangalore") >>> jack_frontdesk = FrontDeskNG(jack) >>> IDesk.providedBy(jack_frontdesk) True >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerAdapter(FrontDeskNG, ... (IGuest,), IDesk, ’ng’) Vous pouvez tester de cette façon : >>> queryAdapter(jack, IDesk, ’ng’) <FrontDeskNG object at ...> 150 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 4.9.43 registeredAdapters Renvoie un objet iterable listant les IAdapterRegistrations. Ces inscriptions décrivent les inscriptions d’adaptateurs actuelles valables. – Emplacement : zope.component - IComponentRegistry – Signature : registeredAdapters() Exemple >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IDesk(Interface): ... """A frontdesk will register object’s details""" ... ... def register(): ... """Register object’s details""" ... >>> from zope.interface import implements >>> from zope.component import adapts >>> class FrontDeskNG(object): ... ... implements(IDesk) ... adapts(IGuest) ... ... def __init__(self, guest): ... self.guest = guest ... ... def register(self): ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: guest.name, ... ’place’: guest.place, ... ’phone’: guest.phone ... } >>> class Guest(object): ... ... implements(IGuest) ... ... def __init__(self, name, place): ... self.name = name ... self.place = place >>> jack = Guest("Jack", "Bangalore") >>> jack_frontdesk = FrontDeskNG(jack) >>> IDesk.providedBy(jack_frontdesk) True >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerAdapter(FrontDeskNG, ... (IGuest,), IDesk, ’ng2’) >>> reg_adapter = list(gsm.registeredAdapters()) >>> ’ng2’ in [x.name for x in reg_adapter] True 4.9. Reference 151 Plone pour les développeurs, Version 1.0.0 4.9.44 registeredHandlers Renvoie un objet iterable listant les IHandlerRegistrations. Ces inscriptions décrivent les inscriptions de gestionnaires actuelles. – Emplacement : zope.component - IComponentRegistry – Signature : registeredHandlers() Exemple >>> import datetime >>> def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> from zope.interface import implements >>> class IDocumentCreated(Interface): ... doc = Attribute("The document that was created") >>> class DocumentCreated(object): ... implements(IDocumentCreated) ... ... def __init__(self, doc): ... self.doc = doc >>> def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() >>> from zope.component import adapter >>> @adapter(IDocumentCreated) ... def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerHandler(documentCreated, info=’ng3’) >>> reg_adapter = list(gsm.registeredHandlers()) >>> ’ng3’ in [x.info for x in reg_adapter] True >>> gsm.registerHandler(documentCreated, name=’ng4’) Traceback (most recent call last): ... TypeError: Named handlers are not yet supported 4.9.45 registeredSubscriptionAdapters Renvoie un objet iterable listant les ISubscriptionAdapterRegistrations. Ces inscriptions décrivent les inscriptions d’adaptateurs d’abonnement actuelles. – Emplacement : zope.component - IComponentRegistry – Signature : registeredSubscriptionAdapters() Exemple 152 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> from zope.interface import implements >>> class IValidate(Interface): ... def validate(ob): ... """Determine whether the object is valid ... ... Return a string describing a validation problem. ... An empty string is returned to indicate that the ... object is valid. ... """ >>> class IDocument(Interface): ... summary = Attribute("Document summary") ... body = Attribute("Document text") >>> class Document(object): ... implements(IDocument) ... def __init__(self, summary, body): ... self.summary, self.body = summary, body >>> from zope.component import adapts >>> class AdequateLength(object): ... ... adapts(IDocument) ... implements(IValidate) ... ... def __init__(self, doc): ... self.doc = doc ... ... def validate(self): ... if len(self.doc.body) < 1000: ... return ’too short’ ... else: ... return ’’ >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerSubscriptionAdapter(AdequateLength, info=’ng4’) >>> reg_adapter = list(gsm.registeredSubscriptionAdapters()) >>> ’ng4’ in [x.info for x in reg_adapter] True 4.9.46 registeredUtilities Cette fonction renvoie un objet iterable listant les IUtilityRegistrations. Ces inscriptions décrivent les inscriptions d’utilitaires actuelles. – Emplacement : zope.component - IComponentRegistry – Signature : registeredUtilities() Exemple >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IGreeter(Interface): ... def greet(name): ... "say hello" 4.9. Reference 153 Plone pour les développeurs, Version 1.0.0 >>> class Greeter(object): ... ... implements(IGreeter) ... ... def greet(self, name): ... print "Hello", name >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> greet = Greeter() >>> gsm.registerUtility(greet, info=’ng5’) >>> reg_adapter = list(gsm.registeredUtilities()) >>> ’ng5’ in [x.info for x in reg_adapter] True 4.9.47 registerHandler Cette fonction est utilisée pour inscrire un gestionnaire. Un gestionnaire est un abonné qui ne crée pas d’adaptateur, mais effectue un traitement lorsqu’il est appelé. – Emplacement : zope.component - IComponentRegistry – Signature : registerHandler(handler, required=None, name=u’‘, info=’‘) – Voir aussi : unregisterHandler Note : l’implémentation actuelle de zope.component ne prend pas en charge l’attribut name. Exemple >>> import datetime >>> def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> from zope.interface import implements >>> class IDocumentCreated(Interface): ... doc = Attribute("The document that was created") >>> class DocumentCreated(object): ... implements(IDocumentCreated) ... ... def __init__(self, doc): ... self.doc = doc >>> def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() >>> from zope.component import adapter >>> @adapter(IDocumentCreated) ... def documentCreated(event): ... event.doc.created = datetime.datetime.utcnow() >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerHandler(documentCreated) 154 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 >>> from zope.component import handle >>> handle(DocumentCreated(doc)) >>> doc.created.__class__.__name__ ’datetime’ 4.9.48 registerSubscriptionAdapter Cette fonction est utilisée pour inscrire une fabrique d’abonnés. – Emplacement : zope.component - IComponentRegistry – Signature : registerSubscriptionAdapter(factory, required=None, provides=None, name=u’‘, info=’‘) – Voir aussi : unregisterSubscriptionAdapter Exemple >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> from zope.interface import implements >>> class IValidate(Interface): ... def validate(ob): ... """Determine whether the object is valid ... ... Return a string describing a validation problem. ... An empty string is returned to indicate that the ... object is valid. ... """ >>> class IDocument(Interface): ... summary = Attribute("Document summary") ... body = Attribute("Document text") >>> class Document(object): ... implements(IDocument) ... def __init__(self, summary, body): ... self.summary, self.body = summary, body >>> from zope.component import adapts >>> class AdequateLength(object): ... ... adapts(IDocument) ... implements(IValidate) ... ... def __init__(self, doc): ... self.doc = doc ... ... def validate(self): ... if len(self.doc.body) < 1000: ... return ’too short’ ... else: ... return ’’ >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerSubscriptionAdapter(AdequateLength) 4.9. Reference 155 Plone pour les développeurs, Version 1.0.0 4.9.49 registerUtility Cette fonction est utilisée pour inscrire un utilitaire. – Emplacement : zope.component - IComponentRegistry – Signature : registerUtility(component, provided=None, name=u’‘, info=u’‘) – Voir aussi : unregisterUtility Exemple >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IGreeter(Interface): ... def greet(name): ... "say hello" >>> class Greeter(object): ... ... implements(IGreeter) ... ... def greet(self, name): ... print "Hello", name >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> greet = Greeter() >>> gsm.registerUtility(greet) 4.9.50 subscribers Cette fonction est utilisée pour récupérer les abonnés. Les abonnés qui fournissent l’interface souhaités et qui dépendent de la séquence d’objets fournie sont renvoyés. – Emplacement : zope.component - IComponentRegistry – Signature : subscribers(required, provided, context=None) Exemple >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> from zope.interface import implements >>> class IValidate(Interface): ... def validate(ob): ... """Determine whether the object is valid ... ... Return a string describing a validation problem. ... An empty string is returned to indicate that the ... object is valid. ... """ >>> class IDocument(Interface): ... summary = Attribute("Document summary") ... body = Attribute("Document text") >>> class Document(object): ... implements(IDocument) ... def __init__(self, summary, body): ... self.summary, self.body = summary, body >>> from zope.component import adapts >>> class SingleLineSummary: 156 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 ... ... ... ... ... ... ... ... ... ... ... adapts(IDocument) implements(IValidate) def __init__(self, doc): self.doc = doc def validate(self): if ’\n’ in self.doc.summary: return ’Summary should only have one line’ else: return ’’ >>> class AdequateLength(object): ... adapts(IDocument) ... implements(IValidate) ... ... def __init__(self, doc): ... self.doc = doc ... ... def validate(self): ... if len(self.doc.body) < 1000: ... return ’too short’ ... else: ... return ’’ >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerSubscriptionAdapter(SingleLineSummary) >>> gsm.registerSubscriptionAdapter(AdequateLength) >>> from zope.component import subscribers >>> doc = Document("A\nDocument", "blah") >>> [adapter.validate() ... for adapter in subscribers([doc], IValidate) ... if adapter.validate()] [’Summary should only have one line’, ’too short’] >>> doc = Document("A\nDocument", "blah" * 1000) >>> [adapter.validate() ... for adapter in subscribers([doc], IValidate) ... if adapter.validate()] [’Summary should only have one line’] >>> doc = Document("A Document", "blah") >>> [adapter.validate() ... for adapter in subscribers([doc], IValidate) ... if adapter.validate()] [’too short’] 4.9.51 unregisterAdapter Cette fonction est utilisée pour désinscrire une fabrique d’adaptateur. Un booléen est renvoyé, indiquant si le registre a été modifié. Si le composant donné est None et qu’il n’y a aucun composant inscrit, ou si le composant donné n’est pas None mais n’est pas inscrit, alors la fonction renvoie False. Sinon True. – Emplacement : zope.component - IComponentRegistry – Signature : unregisterAdapter(factory=None, required=None, provided=None, name=u’‘) – Voir aussi : registerAdapter Exemple 4.9. Reference 157 Plone pour les développeurs, Version 1.0.0 >>> from zope.interface import Attribute >>> from zope.interface import Interface >>> class IDesk(Interface): ... """A frontdesk will register object’s details""" ... ... def register(): ... """Register object’s details""" ... >>> from zope.interface import implements >>> from zope.component import adapts >>> class FrontDeskNG(object): ... ... implements(IDesk) ... adapts(IGuest) ... ... def __init__(self, guest): ... self.guest = guest ... ... def register(self): ... next_id = get_next_id() ... bookings_db[next_id] = { ... ’name’: guest.name, ... ’place’: guest.place, ... ’phone’: guest.phone ... } >>> class Guest(object): ... ... implements(IGuest) ... ... def __init__(self, name, place): ... self.name = name ... self.place = place >>> jack = Guest("Jack", "Bangalore") >>> jack_frontdesk = FrontDeskNG(jack) >>> IDesk.providedBy(jack_frontdesk) True >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerAdapter(FrontDeskNG, ... (IGuest,), IDesk, ’ng6’) Vous pouvez tester de cette façon : >>> queryAdapter(jack, IDesk, ’ng6’) <FrontDeskNG object at ...> Maintenant on peut faire la désinscriptin : >>> gsm.unregisterAdapter(FrontDeskNG, name=’ng6’) True Après la désinscription : >>> print queryAdapter(jack, IDesk, ’ng6’) None 158 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 4.9.52 unregisterHandler Cette fonction est utilisée pour désinscrire un gestionnaire. Un gestionnaire est un abonné qui ne crée pas d’adaptateur, mais effectue un traitement lors de son appel. Un booléen et renvoyé, indiquant si le registre a été modifié. – Emplacement : zope.component - IComponentRegistry – Signature : unregisterHandler(handler=None, required=None, name=u’‘) – Voir aussi : registerHandler Exemple >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> from zope.interface import implements >>> class IDocument(Interface): ... ... summary = Attribute("Document summary") ... body = Attribute("Document text") >>> class Document(object): ... ... implements(IDocument) ... def __init__(self, summary, body): ... self.summary, self.body = summary, body >>> doc = Document("A\nDocument", "blah") >>> class IDocumentAccessed(Interface): ... doc = Attribute("The document that was accessed") >>> class DocumentAccessed(object): ... implements(IDocumentAccessed) ... ... def __init__(self, doc): ... self.doc = doc ... self.doc.count = 0 >>> from zope.component import adapter >>> @adapter(IDocumentAccessed) ... def documentAccessed(event): ... event.doc.count = event.doc.count + 1 >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerHandler(documentAccessed) >>> from zope.component import handle >>> handle(DocumentAccessed(doc)) >>> doc.count 1 Now unregister: >>> gsm.unregisterHandler(documentAccessed) True After unregistration: >>> handle(DocumentAccessed(doc)) >>> doc.count 0 4.9. Reference 159 Plone pour les développeurs, Version 1.0.0 4.9.53 unregisterSubscriptionAdapter Cette fonction est utilisée pour désinscrire une fabrique d’abonné. Un booléen est renvoyé, indiquant si le registre a été modifié. Si le composant donné est None et qu’il n’y a aucun composant inscrit, ou si le composant donné n’est pas None mais n’est pas inscrit, alors la fonction renvoie False. Sinon True. – Emplacement : zope.component - IComponentRegistry – Signature : unregisterSubscriptionAdapter(factory=None, required=None, provides=None, name=u’‘) – Voir aussi : registerSubscriptionAdapter Exemple >>> from zope.interface import Interface >>> from zope.interface import Attribute >>> from zope.interface import implements >>> class IValidate(Interface): ... def validate(ob): ... """Determine whether the object is valid ... ... Return a string describing a validation problem. ... An empty string is returned to indicate that the ... object is valid. ... """ >>> class IDocument(Interface): ... summary = Attribute("Document summary") ... body = Attribute("Document text") >>> class Document(object): ... implements(IDocument) ... def __init__(self, summary, body): ... self.summary, self.body = summary, body >>> from zope.component import adapts >>> class AdequateLength(object): ... ... adapts(IDocument) ... implements(IValidate) ... ... def __init__(self, doc): ... self.doc = doc ... ... def validate(self): ... if len(self.doc.body) < 1000: ... return ’too short’ ... else: ... return ’’ >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> gsm.registerSubscriptionAdapter(AdequateLength) >>> from zope.component import subscribers >>> doc = Document("A\nDocument", "blah") >>> [adapter.validate() ... for adapter in subscribers([doc], IValidate) ... if adapter.validate()] [’too short’] Mainenant on peut effectuer la désinscription : 160 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope Plone pour les développeurs, Version 1.0.0 >>> gsm.unregisterSubscriptionAdapter(AdequateLength) True Après la désinscription : >>> [adapter.validate() ... for adapter in subscribers([doc], IValidate) ... if adapter.validate()] [] 4.9.54 unregisterUtility Cette fonction est utilisée pour désinscrire un utilitaire. Un booléen est renvoyé, indiquant si le registre a été modifié. Si le composant donné est None et qu’il n’y a aucun composant inscrit, ou si le composant donné n’est pas None mais n’est pas inscrit, alors la fonction renvoie False. Sinon True. – Location : zope.component - IComponentRegistry – Signature : unregisterUtility(component=None, provided=None, name=u’‘) – See also : registerUtility Exemple >>> from zope.interface import Interface >>> from zope.interface import implements >>> class IGreeter(Interface): ... def greet(name): ... "say hello" >>> class Greeter(object): ... ... implements(IGreeter) ... ... def greet(self, name): ... return "Hello " + name >>> from zope.component import getGlobalSiteManager >>> gsm = getGlobalSiteManager() >>> greet = Greeter() >>> gsm.registerUtility(greet) >>> queryUtility(IGreeter).greet(’Jack’) ’Hello Jack’ Maintenant on peut effectuer la désinscription : >>> gsm.unregisterUtility(greet) True Après la désinscription : >>> print queryUtility(IGreeter) None 4.9. Reference 161 Plone pour les développeurs, Version 1.0.0 162 Chapitre 4. Le guide complet de l’Architecture de Composants de Zope CHAPITRE 5 Le langage ZCML Author Gilles Lenfant Version 1.0.0 En explorant le code de Plone, vous avez rencontré de nombreux fichiers *.zcml. En vous demandant à quoi celà peut bien servir. Le ZCML ou Zope Configuration Markup Language est un langage basé sur XML qui a été élaboré pour permettre de déclarer les liaisons entre les différents éléments logiciels utilisant la ZCA de votre application. Relisez-donc si nécessaire Le guide complet de l’Architecture de Composants de Zope pour revoir ce qu’est un adaptateur ou un utilitaire. Important : La lecture de ce chapitre nécessite la connaissance de base du langage XML, et notamment des notions d’élément, d’attribut, et de namespace. Si ces termes vous semblent abscons, je vous invite à passer une heure sur un tutoriel XML. 5.1 Introduction au ZCML 5.1.1 Pourquoi le ZCML ? L’utilisation du ZCML, comme indiqué plus haut, est une façon déclarative de configurer les services d’un composant que l’on pourrait écrire en Python. Par exemple, la déclaration suivante : <class class="foo.FooClass"> <implements interface="bar.interfaces.ISomething" /> </class> Revient à faire la déclaration suivante en Python : from zope.interface import classImplements import foo import bar.interfaces classImplements(foo.FooClass, bar.interfaces.ISomething) Quasiment toutes les déclarations ZCML ont leur équivalent en pur Python. Alors, me direz-vous, pourquoi donc inventer un langage supplémentaire pour faire ce qu’on peut faire en Python ? Plusieurs raisons à cela : 163 Plone pour les développeurs, Version 1.0.0 – La ZCA a été conçue pour faciliter la collaboration entre les composants qui n’ont pas forcément été conçus pour fonctionner de concert. – Il est fastidieux d’explorer le code Python d’une application complexe comme Plone pour prendre connaissance des interfaces d’adaptation disponibles, les vues de contenus. L’exploration des directives ZCML d’un composant de Plone ou de tierce partie vous permet d’identifier plus rapidement les aspects que vous pourrez personnaliser. – Il est plus facile à un intégrateur peu au fait des subtilités de la programmation en Python de personnaliser une application en ajoutant ses propres directives ZCML dans ses composants de type policy product. 5.1.2 Forme générale d’un fichier ZCML Comme dit plus haut, un fichier http://namespaces.zope.org/zope ZCML est un fichier XML du namespace Le plus petit fichier ZCML possible est : 1 2 3 4 <configure xmlns="http://namespaces.zope.org/zope" i18n_domain="mon.composant" /> 5 6 <!-- Vos directives --> 7 8 </configure> Quelques commentaires : – Ligne 1 : l’élément principal est toujours l’élément <configure> – Ligne 2 : représente le namespace par défaut s’un fichier ZCML. Nous verrons par la suite qu’un fichier ZCML peut nécessiter plusieurs namespaces, généralement autant de namespaces que de domaines de configuration. – Ligne 3 : indique le domaine de traduction utilisé. En effet, certains attributs d’une directive ZCML peuvent être traduits, tel parfois l’attribut title lorsqu’ils sont susceptibles de participer à l’interface utilisateur. Voir à ce sujet le chapitre Internationalisation d’un composant pour plus d’informations. – Au delà de la ligne 6, à vous de placer vos propres directives. Celles-ci seront enregistrées lors de leur traitement dans l’ordre de leur apparition. Important : L’ordre d’exécution effectif de l’effet produit par les directives ZCML n’est pas forcément l’ordre d’apparition de la directive lors du traitement de celles-ci. Certaines directives pour lesquelles l’ordre d’exécution peut revêtir une importance disposent généralement d’un attribut permettant au développeur de contrôler sa priorité ou ordre d’exécution. 5.2 Traitement des fichiers ZCML 5.2.1 Quand les fichiers ZCML sont-ils traîtés ? Les fichiers ZCML sont traîtés uniquement lors du démarrage de votre instance Zope, et le plus tard possible avant le traitement de la première requête par le publisher. Ce traitement commence par lire le fichier $INSTANCE_HOME/etc/site.zcml. Comme les commentaires de ce fichier nous l’indiquent, celui-ci commence par traiter le package Products.Five, puis traite dans cet ordre : – Les fichiers meta.zcml. Ces fichiers spéciaux, sur lesquels nous reviendrons par la suite, sont dédiés aux définitions des vocabulaires ZCML (namespaces, éléments et attributs) définis par certains composants de Zope et Plone, voire certains composants de tierce partie tels iw.fss, collective.monkeypatcher. – Les fichiers configure.zcml. Ces fichiers, les plus courants, utilisent les services et éléments définis dans les divers fichiers meta.zcml pour configurer et intégrer les éléments logiciels dans les composants dans lesquels ils figurent. 164 Chapitre 5. Le langage ZCML Plone pour les développeurs, Version 1.0.0 – Les fichier overrides.zcml. Ces fichiers permettent de remplacer une ou plusieurs directives figurant dans un fichier configure.zcml. Par exemple, si vous voulez dans votre policy product changer la vue par défaut d’un contenu définie dans un composant de tierce partie par une vie personnalisée, ou modifier une viewlet standard, la façon la plus simple de procéder est de fournir ceci dans un fichier overrides.zcml. Attention : Pour ceux qui connaissent déjà Zope 3 / Bluebream, les fichiers ftesting.zcml ne sont pas pris en compte par Zope 2. 5.2.2 Comment déclarer son ou ses fichiers ZCML ? Comme nous l’avons vu plus haut, Zope, lors du démarrage, ne s’intéresse qu’au seul fichier $INSTANCE_HOME/etc/site.zcml. Le langage ZCML inclut dans son namespace standard l’élément <include> qui permet d’inclure - comme son nom l’indique - les directives ZCML d’un fichier ou d’un package 1 . Pour que les directives des fichiers configure.zcml, et éventuellement overrides.zcml ou plus rarement meta.zcml soient traitées au démarrage de votre instance, encore faut-il que ces fichiers soient référencés dans ceux appellés depuis $INSTANCE_HOME/etc/site.zcml. C’est fastidieux. Heureusement, les développeurs du recipe plone.recipe.zope2instance que vous utilisez forcément pour configurer votre déploiement avec buildout vous permettent de créer automatiquement une jonction entre le fichier $INSTANCE_HOME/etc/site.zcml et vos configure.zcml, overrides.zcml et meta.zcml. Dans la mesure bien sûr et comme recommandé, où vous placez ces fichiers dans le répertoire principal de votre package. Exemple : ... [instance] recipe = plone.recipe.zope2instance ... zcml = mon.composant mon.composant-overrides nom.composant-meta Comme vous l’aurez certainement compris, la ligne mon.composant va permettre de traiter le fichier configure.zcml de ce package Python, mon.composant-overrides va permettre de traiter son fichier overrides.zcml et nom.composant-meta va permettre de traiter son fichier meta.zcml lors du prochain démarrage de votre instance. Attention : Dans l’exemple ci-avant, mon.composant est le nom du package Python qui inclut les fichiers *.zcml. Et non le nom de l’egg qui peut être différent dans certains cas, heureusement rares. Mieux, depuis Plone 3.2, plus aucune mentions aux éventuels fichiers *.zcml de vos composants depuis l’adoption en standard du composant z3C.autoinclude. Sans entrer dans les détails de fonctionnement de ce composant, il vous suffit de placer les lignes suivantes dans l’appel de la fonction setup, dans le fichier setup.py de votre composant : # ... def setup( # ... entry_points=""" # -*- Entry points: -*[z3c.autoinclude.plugin] target = plone """ # ... ) 1. Nous reviendrons plus en détail sur cette directive dans la référence ZCML en fin de ce chapitre. 5.2. Traitement des fichiers ZCML 165 Plone pour les développeurs, Version 1.0.0 À partir de ce moment, et dans la mesure où votre composant est une extension pour Plone, les intégrateurs de votre composant n’auront plus besoin de déclarer celui-ci dans la liste zcml du recipe plone.recipe.zope2instance. 5.3 Les directives ZCML Comme vu plus haut, les différentes directives ZCML sont rendues disponibles par divers composants de Zope et Plone qui exposent leurs propres namespaces XML propres aux fonctionnalités que les dits composants fournissent. 5.3.1 Les namespaces standard Les namespaces disponibles en standard avec Plone 4.1 sont les suivants : 166 Chapitre 5. Le langage ZCML Plone pour les développeurs, Version 1.0.0 TABLE 5.1 – Namespaces ZCML 2 Préfixe browser cache cmf five genericsetup i18n kss meta monkey plone pluggableauthservice 5.3. Les directives ZCML wicked URI Commentaire http ://naLes directives de ce namespace permettent mesde déclarer des pages, vues viewlets, et paces.zope.org/browser parfois des menus de vos composants. http ://naCe namespace fournit deux directives mespermettant d’associer des règles de cache paces.zope.org/cache avec des types de contenus. Ce namespace est fourni par plone.app.caching qui est fourni en standard avec Plone 4.1 http ://naUne directive permet de déclarer un layer de messkin CMF. Pour rappel, les layers de skins paces.zope.org/cmf CMF sont une survivance facilitant le support de composants anciens pour lesquels une adaptation minimaliste à Plone 3 a été effectuée. Les nouvelles extensions pour Plone devront utiliser de préférence les ressources du namespace browser pour ceci. http ://naLes directives spécifiques au composant mesProducts.Five qui permettent de paces.zope.org/five configurer des ressources spécifiques à Zope 2. http ://naLes directives fournies permettent de mesdéclarer les profils d’installation, paces.zope.org/genericsetup d’exportation de configuration et de migration fournies par Products.GenericSetup. http ://naL’unique directive de ce namespace permet mesde déclarer un répertoire de fichiers de paces.zope.org/i18n traductions (fichiers *.mo) disposés selon le standard “gettext”. Pour plus de précisions, voir le chapitre Internationalisation d’un composant. http ://naLes directives kss permettent d’enregistrer mesdes ressources Ajax. paces.zope.org/kss http ://naLes directives fournies par ce namespace mespermettent de définir de nouveaux éléments paces.zope.org/meta ZCML (s’ils figurent dans un fichier meta.zcml) ou - plus rarement - de remplacer et personnaliser des éléments existants (s’ils figurent dans un fichier overrides.zcml). http ://naCe namespace est fourni par le composant mescollective.monkeypatcher. Ce paces.plone.org/monkey composant permet de mettre en place des monkey patches de façon rationnelle et documentée. Les monkey patches actifs peuvent être vus dans le Control Panel Zope à l’aide du composant collective.monkeypatcherpanel. http ://naLes directives fournies permettent de mesdéclarer les portlets et les règles de contenus paces.plone.org/plone (content rules) http ://naL’unique directive mesregisterMultiPlugin permet de paces.zope.org/pluggableauthservice déclarer les plugins et multiplugins utilisables depuis PAS. http ://naLes directives de ce namespace permettent 167 mesd’identifier les champs d’un type de contenu paces.openplans.org/wicked Archetypes qui peuvent se comporter comme une page Wiki. Plone pour les développeurs, Version 1.0.0 Zope 3 ou BlueBream disposent d’autres namespaces qui ne sont pas rendus disponibles aux applications Zope 2 à travers Products.Five. Nous ne les aborderons pas ici, mais les curieux pourront toujours se reporter au chapitre ZCML de la documentation de BlueBream (en). Attention : Les namespaces provenant des composants de Zope 3 (plus précisément du “Zope toolkit”) sont exposés par le biais de Products.Five. Du fait des particularités de Zope 2, les directives ZCML exposées par les composants du Zope toolkit intégrés dans Zope 2.12 ne sont pas toutes disponibles. C’est particulièrement le cas de préfixes apidoc, mail et xmlrpc. 5.3.2 Explorer les namespaces et directives Comme l’auteur de ces lignes est soudain atteint d’une grosse fatigue au moment de détailler les diverses directives ZCML et leur utilisation, il a préféré réaliser un composant qui ajoute un tableau de bord Zope 2 qui affiche les détails des différents namespaces et directives mises à votre disposition. aws.zope2zcmldoc Je vous invite à installer sur vos instances de développement le composant aws.zope2zcmldoc et d’explorer depuis le tableau de bord la documentation au fur et à mesure de vos besoins. Autre avantage : comme aws.zope2zcmldoc utilise la “réflectivité” des directives ZCML, pour ses différentes vues, vous pourrez également l’utiliser pour lire la documentation de namespaces et directives fournis par des composants de tierce partie comme iw.fss ou d’autres. Quelques screenshots 5.4 Pour aller plus loin 5.4.1 Comprendre une directive ZCML L’une des explications sur une directive ci-dessus vous parait floue, ou bien une directive fournie par un composant de tierce partie n’est pas correctement documentée ? Voici la méthode pour pister la définition d’une directive ZCML quelconque. La directive est exposée à partir d’un namespace, il vous faudra donc trouver un fichier meta.zcml dans lequel est définie cette directive. En supposant que la directive s’appelle foo et qu’elle est exposée par le namespace http://namespace.mydomain.tld/bar, il vous faudra trouver, à l’aide des commandes shell find et grep un fichier meta.zcml dans lequel figure quelque-chose comme ceci : namespace="http://namespace.mydomain.tld/bar" ... name="foo" Si vous ne le trouvez pas, c’est que vous avez mal cherché, car il se trouve forcément quelque part dans l’un des packages de votre projet. Vous trouverez donc (si, si !) un fichier meta.zcml qui contiendra quelque chose comme ceci : <configure xmlns="http://namespaces.zope.org/zope" xmlns:meta="http://namespaces.zope.org/meta" > ... <meta:directives namespace="http://namespace.mydomain.tld/bar"> ... 168 Chapitre 5. Le langage ZCML Plone pour les développeurs, Version 1.0.0 F IGURE 5.1 – Le nouveau lien dans le Control Panel Zope 2 5.4. Pour aller plus loin 169 Plone pour les développeurs, Version 1.0.0 F IGURE 5.2 – Les namespaces disponibles dans votre instance 170 Chapitre 5. Le langage ZCML Plone pour les développeurs, Version 1.0.0 F IGURE 5.3 – Les différentes directives d’un namespace (ici “browser”) 5.4. Pour aller plus loin 171 Plone pour les développeurs, Version 1.0.0 F IGURE 5.4 – Le détail d’une directive ou sous-directive (ici “browser :menu) <meta:directive name="foo" schema="bar.zcml.IFooDirective" handler="bar.zcml.foo" /> ... </meta:directives> ... </configure> Ce qui signifie que les attributs de l’élément de configuration <foo ...> sont définis dans la classe bar.zcml.IFooDirective et que l’élément sera traité par la fonction bar.zcml.foo. La classe bar.zcml.IFooDirective est une interface (sous-classant zope.interface.Interface) et ses attributs sont définis par des objets des modules zope.configuration.fields ou de zope.schema et sont généralement abondament commentés. Le handler bar.zcml.foo prend pour paramètres un objet implémentant zope.configuration.interfaces.IConfigurationContext ainsi que l’ensemble des attributs de l’émément <foo ..>. Le reste, c’est la lecture du code Python, généralement assez facile, qui vous l’apprendra. 5.4.2 Autres ressources Pour aller plus loin, cet article de blog explique en détails comment faire sa propre directive ZCML. Quelques composants simples pour Zope et Plone qui exposent leur propre vocabulaire ZCML : – collective.monkeypatcher permet d’effectuer des monkey patches Python d’une façon documentée et rationnelle. – archetypes.configure évite la fastidieuse déclaration de types en Python dans les packages fournissant des types de contenus Archetypes 172 Chapitre 5. Le langage ZCML CHAPITRE 6 Utiliser la ZCA avec Grok 6.1 Introduction Grok est un composant qui permet de simplifier et d’accélérer considérablement l’écriture de code utilisant la Zope Component Architecture. C’est principalement un ensemble de directive permettant de diminuer l’utilisation du ZCML et d’écrire davantage de choses directement dans le code python. 6.2 Installation de five.grok Modifiez votre buildout.cfg comme ceci : [zope2] skip-fake-eggs = zope.app.publisher zope.component zope.i18n zope.interface zope.testing [instance] eggs = # ... five.grok zcml = # ... five.grok-meta five.grok Et ajoutez ces versions dans votre fichier versions.cfg : five.grok = 1.0 grokcore.annotation = 1.1 grokcore.component = 1.7 grokcore.formlib = 1.4 grokcore.security = 1.2 grokcore.site = 1.1 grokcore.view = 1.12.2 grokcore.viewlet = 1.3 five.localsitemanager = 1.1 martian = 0.11.1 zope.app.publisher = 3.5.1 173 Plone pour les développeurs, Version 1.0.0 zope.app.zcmlfiles = 3.4.3 zope.component = 3.4.0 zope.i18n = 3.4.0 zope.interface = 3.4.1 zope.schema = 3.4.0 zope.securitypolicy = 3.4.1 zope.testing = 3.7.6 Vous pouvez retrouver les dernières versions des packages qui fonctionnent bien ensemble dans le buildout de five.grok pour Zope 2.10/Plone 3, pour Zope 2.12/Plone 4. 6.3 Utiliser Grok Créez un package formation.transforms avec le template paster plone : $ cd /tmp $ paster create -t plone formation.transforms \ --svn-repository=http://devagile/Formation/packages $ cd formation.transforms $ svn rm --force formation.transforms/formation.transforms.egg-info $ svn ci -m"Added skel of formation.transforms" Dans votre buildout, éditez le fichier sources.cfg, ajoutez formation.transforms dans l’option auto-checkout et indiquez l’url suivante dans [sources] : formation.transforms = svn http://devagile/Formation/packages/formation.transforms/trunk Dans buildout.cfg, ajoutez formation.transforms dans l’option eggs. Relancez bin/buildout pour récupérer le product dans le dossier src. éditez src/formation.transforms/setup.py pour déclarer le produit comme plugin plone : entry_points=""" [z3c.autoinclude.plugin] target = plone """ Relancez bin/buildout pour regénérer les métadonnées de formation.transforms. Créez un fichier formation/transforms/interfaces.py : from zope.interface import Interface class IReplaceLetter(Interface): def getText(letter1, letter2): """Return a modified text, replace letter1 by letter2. """ Créez un fichier adapters.py : from five import grok from formation.transforms.interfaces import IReplaceLetter from Products.ATContentTypes.interface.document import IATDocument class ReplaceLetter(grok.Adapter): grok.implements(IReplaceLetter) grok.context(IATDocument) def getText(self, letter1, letter2): return self.context.getText().replace(letter1, letter2) Créez un module views.py : 174 Chapitre 6. Utiliser la ZCA avec Grok Plone pour les développeurs, Version 1.0.0 from five import grok from formation.transforms.interfaces import IReplaceLetter from Products.ATContentTypes.interface.document import IATDocument grok.templatedir(’templates’) class TransformedDocument(grok.View): grok.name("my-view") grok.context(IATDocument) def update(self, letter1=None, letter2=None): self.letter1 = letter1 self.letter2 = letter2 def getAuthenticatedUser(self): user = self.request.AUTHENTICATED_USER return user.getProperty(’fullname’, user.getId()) def getContent(self): if self.letter1 is None or self.letter2 is None: return self.context.getText() return IReplaceLetter(self.context).getText(self.letter1, self.letter2) Créez le répertoire templates/ et créez dedans un fichier transformeddocument.pt : <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal" xmlns:metal="http://xml.zope.org/namespaces/metal" xmlns:i18n="http://xml.zope.org/namespaces/i18n" lang="en" metal:use-macro="context/main_template/macros/master" i18n:domain="formation.transforms"> <div metal:fill-slot="content" tal:content="structure view/getContent"> </div> </html> Dans formation/transforms/configure.zcml, ajoutez l’espace de nom grok et la ligne : <grok:grok package=".adapters" /> <grok:grok package=".views" /> Vous pouvez également faire : <grok:grok package="." /> qui va rechercher l’ensemble des packages et modules qui comportent des classe héritant de grok. Dans Plone, créez un Document et accédez à la vue @@transformeddocument. 6.4 Ressources 6.4.1 Viewlets avec grok – http ://grok.zope.org/documentation/how-to/understanding-viewlets – Création d’une viewlet qui affiche un Avertissement lorsque http ://vincentfretin.ecreall.com/articles/creating-a-viewlet-with-grok 6.4. Ressources le document est expiré : 175 Plone pour les développeurs, Version 1.0.0 176 Chapitre 6. Utiliser la ZCA avec Grok CHAPITRE 7 Modélisation UML 7.1 Objectifs Connaitre les bases de la notation UML. 7.2 Savoir – Les différents diagrammes, – La représentation des classes, attributs, méthodes, associations, – La représentation des objets, des composants, des collaborations, des flux d’informations. Pour réaliser l’analyse et générer les composants nécessaires à Plone nous allons utiliser l’outil ArchGenXML en version 2. Nous réaliserons dans un premier temps un diagramme de classes, qui permettra de décrire le plan documentaire de notre application, puis nous associerons à nos documents des “workflows” sous forme de diagramme d’états transitions. Mais avant cela nous allons réaliser un bref rappel des notions UML utilisées dans la suite. 7.3 Qu’est ce qu’UML UML UML (Unified Modeling Language) est une notation graphique qui permet de traduire sous forme de vues l’architecture d’un logiciel ou d’une application orientée objet. UML est la fusion de différentes notations issues des méthodes de génie logiciel orienté objet antérieures aux années 1995. La version 1.0 a officiellement été publiée en 1997 par l’OMG (Object Management Group) qui est l’organisme responsable de sa standardisation. Depuis 2007, la version officielle est la 2.1.2. Historiquement, son usage intervenait dans la phase de conception pour permettre de “visualiser” ce que sera et fera le logiciel. Comme les feuillets des plans d’un architecte en bâtiment, différents diagrammes composent une vue spécifique de la solution apportée au problème. Le lecteur doit parcourir plusieurs vues et donc lire plusieurs diagrammes pour se créer son image mentale du logiciel. Cette abstraction n’est malheureusement pas évitable, puisque un logiciel est composé à la fois de parties statiques, telle que le code exécutable, de données dynamiques telles que les entrées sorties, et de comportements. Les vues sont des regroupements abstraits des diagrammes qui sont eux les “dessins” représentant les interactions entre les éléments constitutifs du logiciel. Ces diagrammes permettent de visualiser l’interaction entre les éléments 177 Plone pour les développeurs, Version 1.0.0 de l’architecture qui sont regroupés dans un “modèles d’éléments” et constitués des cas d’utilisations, classes, associations, attributs, etc. Actuellement, les courants de l’IDM (Ingénierie Dirigée par les Modèles), font que son usage est présent tout au long du cycle de vie du logiciel, et l’on met à jour le logiciel en modifiant le modèle puis en générant à nouveau l’application. Les outils de génération se chargent de fusionner l’existant avec les nouvelles fonctionnalités. Initialement, la conception orientée objet cherchait à résoudre un problème en le découpant non pas en un ensemble de fonctionnalités dont le tout forme un arbre ayant pour racine une fonction “main”, mais en boites travaillant ensemble par l’échange de messages et dont le comportement global réalise la tâche attendue. La description de ces boites forme un tout homogène de données et de fonctions appelée interface, c’est une abstraction. La définition d’un type de boite respectant une interface donnée est nommée “classe” si elle est simple, et composant si pour respecter le contrat de l’interface elle encapsule la collaboration de plusieurs classes et peut à elle seule être considérée comme une application. L’exécution d’une classe est appelée instance, elle possède alors ses propres valeurs d’attributs à commencer par son occupation mémoire ce qui la distingue des autres instances de la même classe. Mais revenons à UML. UML se base sur trois types de briques pour permettre la modélisation d’une architecture : – Les éléments, qui sont un peu le vocabulaire de UML ; – Les relations, qui représentent la syntaxe du projet ; – Les diagrammes, qui contiennent la sémantique du projet. 7.4 Les éléments Nous n’allons détailler ici que les éléments d’UML que nous utiliserons. 7.4.1 Les éléments structurels La classe C’est un types d’éléments qui partagent les mêmes propriétés (attributs, méthodes, relations, sémantique). Si les propriétés définies par la classe sont immuables et ne dépendent pas des éléments, on parle d’attributs ou de variables de classe et de méthodes de classes, inversement si leur contenu dépend de l’élément on parle d’attribut ou de variable d’instances et de méthodes. Les propriétés d’une classe ont une visibilité, c’est-à-dire que l’accès aux propriétés d’une classe par une autre est contrôlé. L’interface C’est un ensemble de méthodes qui définissent le comportement attendu d’une classe en laissant aux classes issues de cette interface de choisir l’implémentation. L’objet C’est un élément concret actif ou passif appartenant à une classe. Si la classe de l’objet est connue, on parle alors d’instance de cette classe. L’objet possède toutes les propriétés de sa classe et définie les valeurs des attributs d’instances. 178 Chapitre 7. Modélisation UML Plone pour les développeurs, Version 1.0.0 F IGURE 7.1 – Représentation des classes F IGURE 7.2 – Représentation des interfaces F IGURE 7.3 – Représentation des objets 7.4. Les éléments 179 Plone pour les développeurs, Version 1.0.0 L’acteur C’est une classe externe au système agissant ou réagissant au système. Ainsi tous les intervenants sur le système sont des acteurs. F IGURE 7.4 – Représentation des acteurs Le composant C’est une classe formant un tout cohérent et suffisant permettant de réaliser un ensemble de fonctionnalités, c’est la boite noire par excellence. F IGURE 7.5 – Représentation des composants La collaboration Lorsque plusieurs instances de classes réalisent un comportement global par échange de messages, on dit qu’ils forment une collaboration et que les objets y remplissent un rôle. La collaboration permet donc de visualiser la réalisation des exigences fonctionnelles. F IGURE 7.6 – Représentation des collaborations Le cas d’utilisation C’est un comportement du système dont est témoin un acteur, il est réalisé par des collaborations. 180 Chapitre 7. Modélisation UML Plone pour les développeurs, Version 1.0.0 F IGURE 7.7 – Représentation des cas d’utilisations 7.4.2 Les éléments comportementaux Les interactions Elles sont constituées par l’envoi de messages ou d’événements provoquant des actions chez le récepteur. L’appel d’une méthode de classe correspond à l’envoi d’un message dont le nom est suivi de parenthèses. F IGURE 7.8 – Représentation des messages Les états Ils permettent de constituer des automates décrivant le comportement d’une classe ou d’une méthode. F IGURE 7.9 – Représentation des états dans un diagramme d’états Les activités Les activités sont des comportements exécutables séquentiellement ou parallèlement. 7.4.3 Les éléments de regroupement Les paquetages Ils permettent de ranger les classes et diagrammes de façon à rendre plus clair le modèle. 7.4. Les éléments 181 Plone pour les développeurs, Version 1.0.0 F IGURE 7.10 – Représentation des activités dans un diagramme d’activités F IGURE 7.11 – Représentation des paquetages 182 Chapitre 7. Modélisation UML Plone pour les développeurs, Version 1.0.0 Les éléments d’annotation Les notes permettent de donner des informations. F IGURE 7.12 – Représentation des notes 7.5 Les relations 7.5.1 La dépendance C’est une relation sémantique indiquant que tout changement de l’élément indépendant peut affecter l’élément dépendant. F IGURE 7.13 – Représentation des dépendances 7.5.2 L’association Elle permet d’indiquer une relation entre deux éléments. La nature de la relation est donnée par le texte situé dessus. Un lien peut être précisé aux extrémités par le nombre et le rôle de chacun des éléments. Un lien de contenance est symbolisé par l’agrégation. F IGURE 7.14 – Représentation des associations 7.5.3 La généralisation Cette relation permet de définir des niveaux d’abstraction entre les classes, le but est de permettre de manipuler de façon homogène des ensembles d’objets qui partagent les mêmes propriétés. Cette factorisation des traitements s’appelle le polymorphisme. 7.5. Les relations 183 Plone pour les développeurs, Version 1.0.0 F IGURE 7.15 – Représentation des généralisations 7.5.4 La réalisation Cette relation indique que la classe implémente les comportements définis dans l’interface qu’elle réalise. F IGURE 7.16 – Représentation des réalisations 7.5.5 La transition Cette relation joint deux états et permet d’expliciter les conditions à respecter et les traitements à réaliser lors des changements d’états. 7.6 Les diagrammes que nous utiliserons Alors que UML permet de créer 13 types de diagrammes nous n’allons n’en utiliser que trois pour la génération de notre produit. Les autres diagrammes servirons à la documentation de notre architecture. Les diagrammes qui servirons à la génération sont : 184 Chapitre 7. Modélisation UML Plone pour les développeurs, Version 1.0.0 – Le diagramme de classes – Le diagramme d’états transitions – Le diagramme d’activités 7.6.1 Le diagramme de classes Ce diagramme permet de mettre en évidence la structure des classes c’est-à-dire les attributs et méthodes présents dans les classes, les relations entre classes. Ainsi, il fait apparaitre les hiérarchies de classes (héritage, abstraction, interface), les compositions et agrégations (un instance d’une classe contenant des instances d’autres classes), les dépendances, les “packages”. Par convention on limite le nombre de classes visibles dans un diagramme de classes. On va alors réaliser différents diagrammes de classes qui chacun doit montrer un aspect du problème en n’affichant que les attributs, méthodes et associations concernés par cet aspect. F IGURE 7.17 – Diagramme de classes 7.6.2 Le diagramme d’objets Il regroupe les objets et permet de les représenter avec leur liens à un moment donné. F IGURE 7.18 – Diagramme d’objets 7.6. Les diagrammes que nous utiliserons 185 Plone pour les développeurs, Version 1.0.0 7.6.3 Le diagramme de cas d’utilisation Il regroupe les cas d’utilisations et les acteurs concernés. F IGURE 7.19 – Diagramme de cas d’utilisations 7.6.4 Le diagramme de séquence Il permet de visualiser l’enchainement des messages entre objets. L’ordre de lecture est de haut en bas. 7.6.5 Le diagramme de collaboration C’est une représentation symétrique du diagramme de séquence. 7.6.6 Le diagramme d’états transitions Il est utilisé pour modéliser les changements d’états d’un élément. Nous l’utiliserons pour modéliser les workflows de nos types de contenu. 7.6.7 Le diagramme d’activités Il permet de voir l’enchainement des tâches du système ou des traitements déclenchés par les acteurs. 7.6.8 Le diagramme de composants Il montre les liens entre composants. 7.6.9 Le diagramme de déploiement Il permet de voir l’intégration. 186 Chapitre 7. Modélisation UML Plone pour les développeurs, Version 1.0.0 F IGURE 7.20 – Diagramme de séquences 7.6. Les diagrammes que nous utiliserons 187 Plone pour les développeurs, Version 1.0.0 188 Chapitre 7. Modélisation UML CHAPITRE 8 Génerer un composant Plone avec ArgoUML et ArchgenXML 8.1 Définition ArgoUML est un outil de modélisation d’application basé sur le formalisme UML. Dans le cas de création d’application Plone/Zope, il est utilisé pour avoir une vue synthétique de l’architecture d’un produit. 8.2 Savoir – – – – Création des diagrammes de classes correspondant à la structure du produit, Création des diagrammes d’états correspondant aux workflows, Création des étiquettes permettant de gérer les noms, les permissions et les worklists, Dérivation des classes du système. 8.3 Installation d’ArgoUML ArgoUML est un outil de modélisation UML open source qui gère la version 1.4 d’UML. Bien que sommaire, il suffit à modéliser notre besoin. Son format de sauvegarde sera interprété par l’outil de génération ArchGenXML pour produire le code du module de Plone qui satisfera le besoin de notre cahier des charges. ArgoUML est accessible à http ://argouml.tigris.org nous téléchargeons la version 0.30 et l’installons. Cette installation requière Java. Si aucune machine virtuelle java n’est présente sur votre poste, le logiciel d’installation correspondant à votre OS vous proposera d’installer celle de SUN. 8.4 Installation de ArchGenXML Nous utiliserons ArchGenXML pour générer le code Python à partir du modèle créé avec ArgoUML. De plus, ArchGenXML fournit un profil permettant d’ajouter à ArgoUML les types et les tags dont nous allons avoir besoin pour modéliser notre module. 189 Plone pour les développeurs, Version 1.0.0 L’installation peut être faite à partir de pypi ou depuis le dépôt subversion (subversion est accessible à http ://subversion.apache.org). L’intérêt du dépôt subversion est de disposer des dernières corrections : C:\Program Files> cd C:\ C:\> svn export http://svn.plone.org/svn/archetypes/ArchGenXML/buildout ArchGenXML C:\> cd ArchGenXML C:\ArchGenXML> C:\python26\python.exe bootstrap.py C:\ArchGenXML> bin\buildout.exe C:\ArchGenXML> bin\test-archgenxml.exe Running zope.testing.testrunner.layer.UnitTests tests: Set up zope.testing.testrunner.layer.UnitTests in 0.000 seconds. Ran 82 tests with 0 failures and 0 errors in 0.211 seconds. Tearing down left over layers: Tear down zope.testing.testrunner.layer.UnitTests in 0.000 seconds. La dernière commande permet d’exécuter les tests unitaires et donc de vérifier qu’ArchGenXML fonctionne, l’affichage montre qu’il a eu 82 tests exécutés sans erreurs. 8.5 Configuration d’ArgoUML pour utiliser le profil d’ArchGenXML Pour ajouter le profil, allez dans le menu Édition -> Préférences... -> Profiles -> Add et saisissez le chemin vers le dossier contenant le profil ArchGenXML C:\ArchGenXML\src\archgenxml\umltools\argouml. F IGURE 8.1 – Ajout du dossier contenant le profil ArchGenXML Vous devez voir normalement AGXProfile dans Available Profiles. Sur certaine version d’ArgoUML il fallait le relancer pour que soit pris en compte le nouveau répertoire des profils. On ouvre (à nouveau) la gestion des profils et l’on ajoute dans Default Profiles le profil ArchGenXML et on retire tous les autres excepté UML 1.4 qu’on ne peut pas retirer à partir d’ici. 190 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML Plone pour les développeurs, Version 1.0.0 F IGURE 8.2 – Ajout du profil ArchGenXML 8.5. Configuration d’ArgoUML pour utiliser le profil d’ArchGenXML 191 Plone pour les développeurs, Version 1.0.0 Maintenant dans le panneau qui s’ouvre lorsqu’on clique sur le lien Configure project specific settings... de la fenêtre précédente, retirez le profil UML 1.4. Vous ne devez avoir que le profil ArchGenXML d’activé. Voilà ArgoUML est prêt à être utilisé pour modéliser notre module. 8.6 Création du buildout de notre module La première étape est de créer et ajouter la structure de notre nouveau module (Product) à Plone. Pour cela plaçons nous dans le répertoire src de notre Plone (le créer s’il n’existe pas) et lançons la commande paster comme suit : c:\Plone4> cd src C:\Plone4\src> paster create -t basic_namespace Products.StandArt_AQ Selected and implied templates: zopeskel#basic_namespace A basic Python project with a namespace package Variables: egg: Products.StandArt_AQ package: productsstandart_aq project: Products.StandArt_AQ Expert Mode? (What question mode would you like? (easy/expert/all)?) [’easy’]: Version (Version number for project) [’1.0’]: 0.1 Description (One-line description of the project) [’’]: The Stand’Art company Qualite Assurance Creating template basic_namespace Creating directory .\Products.StandArt_AQ Recursing into +namespace_package+ Creating .\Products.StandArt_AQ\Products/ Recursing into +package+ L’arborescence créée est alors : C:\Program Files\Plone4\src> tree Products.StandArt_AQ Structure du dossier Le numéro de série du volume est XXXXXXXX XXXX:XXXX C:\PROGRAM FILES\PLONE4\SRC\PRODUCTS.STANDART_AQ |-- Products | ‘-- StandArt_AQ |-- Products.StandArt_AQ.egg-info ‘-- docs Nous allons ajouter un répertoire model/ dans Products.StandArt_AQ pour y stocker le modèle UML que nous allons réaliser. 8.7 Création du plan documentaire Notre modélisation reposera en partie sur le mécanisme des Archetypes proposé par Plone et sur celui des vues Zope 3. Il est donc possible de consulter la documentation dédiée à ces deux technologies pour compléter ce cours. Dans le cahier des charges nous avons vu que l’ensemble des documents sous assurance qualité allait être stocké dans un répertoire dédié et que les documents seront stockés dans un conteneur spécial possédant plusieurs champs. Modifier le nom du paquetage “untitledModel” en “Products.StandArt_AQ”. Pour cela mettez vous en vue “Orienté Paquetage”, cliquer sur “untitledModel” dans la navigation puis sur l’onglet “Propriété” et dans le champ “nom” saisissez “StandArt_AQ”. Puis enregistrez le modèle sous le nom StandArt_AQ dans le répertoire model 192 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML Plone pour les développeurs, Version 1.0.0 F IGURE 8.3 – Nommer le paquetage en Products.StandArt_AQ Cliquez sur “Diagramme de classe” dans la barre de navigation, vous pouvez alors ajouter les éléments symbolisés par les icônes de la zone de dessin. Ajoutez y un paquetage nommé “content”, par convention nous mettons tous les types de contenu dans ce répertoire, les classes utilitaires seront mises dans un paquetage nommé “tools”. De même, puisque généralement nos paquets ont vocation à être partagés avec la communauté Plone nous réalisons la modélisation en Anglais. Dans le paquetage “content”, créez un diagramme de classes “Content diagram” Éditez le diagramme pour ajouter les classes de bases ATFolder et ATDocument qui sont les types des répertoires et des documents de Plone. Nous les utiliserons comme classes de bases pour nos types de façon à réutiliser leur comportement. Ajoutez y le stéréotype stub qui permettra d’indiquer à ArchGenXML qu’il ne doit pas générer ces classes car elles existent déjà. Pour ATFolder allez dans l’onglet Étiquettes et ajoutez le marqueur (tag) import_from ayant pour valeur Products.ATContentTypes.content.folder. Pour ATDocument ajoutez le marqueur (tag) Products.ATContentTypes.content.document. import_from avec pour valeur On peut aussi utiliser le stéréotype <<ATFolder>> et <<ATDocument>> dans les classes dérivées au lieu de modéliser la généralisation, mais il arrive qu’ArchGenXML ne réussisse pas à générer le code. Nous créons le répertoire qui contiendra tous les documents sous assurance qualité en dérivant ATFolder et en nommant cette nouvelle classe StandArt_FolderAQ. Il est conseillé de préfixer ses types de contenu afin d’éviter toute collision avec d’autres modules Plone. De même pour les attributs, préfixez les d’un m pour “membre” par exemple pour éviter de surcharger par accident 8.7. Création du plan documentaire 193 Plone pour les développeurs, Version 1.0.0 tout attribut ou méthode existant dans la classe de base (et avec le mécanisme de wrapper c’est près d’une centaine de membres). Toutefois, si vous souhaitez redéfinir un attribut déjà existant gardez le nom d’origine (exemple : title, description). F IGURE 8.4 – Création du plan documentaire Pour les classes, la signification des marqueurs est la suivante : label Le nom du type tel qu’il apparaitra dans le menu d’ajout de contenu. creaPermission qu’il faut avoir pour créer une instance. tion_permission creation_roles Rôles qui ont la permission par défaut (à la racine). use_portal_factory1 signifie que l’on travaille dans une instance temporaire tant que l’enregistrement n’est pas fait. content_icon Permet d’associer un fichier d’icône aux instances. base_class 0 signifie qu’ArchGenXML ne doit pas associer par lui même de classe de base car elle est précisée dans le modèle par l’héritage. description Donne la description du type d’objet. Nous avons dérivé nos types de ceux contenus dans Plone, mais nous aurions très bien pu sur cet exemple partir d’un type défini dans un module. De cette façon il est possible d’étendre et particulariser des produits que l’on n’a pas fait soit même. Si l’on avait voulu qu’il y ait plusieurs roles pouvant créer des instances nous aurions alors donné au marqueur creation_roles la valeur python:("Manager", "Contributor"). Nous allons remplir les informations concernant le champ documentation : La signification des marqueurs du champs mDocument est : 194 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML Plone pour les développeurs, Version 1.0.0 F IGURE 8.5 – Information concernant le champ mDocument widget :label widget :label_msgid widget :description widget :description_msgid read_permission write_permission storage Le nom du champ. L’identifiant de traduction associé au nom. La description du champ. L’identifiant de traduction associé à la description. Le nom de la permission à avoir pour voir le champ. On peut ainsi en créer de nouvelle. Le nom de la permission à avoir pour modifier le champ. Comment le contenu du champ doit être stocké. Les formulaires d’édition ou de consultation sont générés automatiquement par Plone à partir du type des champs. Toutefois, un type de champ peut être affiché ou édité de façons différentes selon le widget qui lui est associé. Ces widgets sont configurés en étiquetant le champ avec des marqueurs préfixés par widget :. Il est également possible de créer ses propres widgets. Voici une liste des types des champs possibles. 8.7. Création du plan documentaire 195 Plone pour les développeurs, Version 1.0.0 Nom BackReferenceField BooleanField ColorField ComputedField Type backreference boolean color computed copy Widget par défaut BackReferenceWidget ComputedWidget ColorWiget ComputedWidget DataGridField DateTimeField FileField FixedPointField FloatField ImageField datagrid date file fixedpoint float image DataGridWidget CalendarWidget FileWidget FixedPointWidget FloatWidget ImageWidget IntegerField LinesField LinesField LinesField ReferenceField int keywords lines multiselection reference TextField StringField richtext selection IntegerWidget KeywordWidget LinesWidget MultiSelectionWidget ReferenceBrowserWidget RichWidget SelectionWidget StringField string StringWidget TextField string TextAreaWidget Description Référence navigable. Champ stockant vrai ou faux. Sélection d’une couleur. Champ calculé. Permet de surcharger un champ d’une des classes de base. Des lignes de tableau. Stocke des dates et heures. Stocke un fichier sans réaliser de traitement. Champ numérique à virgule fixe Champ numérique à virgule fixe Stocke une image et permet de la retailler dynamiquement. Stocke une donnée numérique entière. Une liste de données, par exemple des mots clés. Une liste de ligne. Une liste de données à sélections multiples. Permet de référencer un autre objet. Texte avec mise en forme. Permet de sélectionner une ligne de texte parmi plusieurs. Champ texte pour les chaines de moins de 100 caractères. Champ texte pour les grandes chaînes. Certains de ces champs reposent sur des produits. Ainsi DataGridField repose sur le produit Products.DataGridField qu’il faut alors ajouter au buildout. Le type copy est une astuce qui permet de surcharger les marqueurs d’un champ défini dans l’une des classes mères. Par exemple pour changer la description, le titre et la permission de lecture et d’écriture du champ title, il suffit de créer un champ portant comme Nom title et de lui associer les marqueurs suivants avec les valeurs voulues : Marqueur widget :label widget :label_msgid widget :description widget :description_msgid read_permission write_permission Valeur Le nom du champ IdentifiantDuNomPourLesTraductions La description du champ IdentifiantDeLaDescriptionPourLesTraductions View MonProduit : le nom de la permission Les identifiants doivent être en ASCII sans espaces. Le nom des permissions est soit celui d’une permission déjà existante soit une chaîne de caractères ASCII qui, par convention, doit être préfixée par le nom du produit, ce qui permettra de les regrouper dans l’onglet security de Zope. Parfois, ArchGenXML ne réalise pas automatiquement l’importation de la définition de certains types de champ ou de widget. Il faut alors l’ajouter soit comme marqueur de la classe en utilisant l’étiquette import soit dans la zone protégée du module python généré, c’est-à-dire entre les lignes : ##code-section module-header #fill in your manual code here from archetypes.referencebrowserwidget import ReferenceBrowserWidget ##/code-section module-header @TODO mettre à jour la liste suivante et expliquer l’usage 196 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML Plone pour les développeurs, Version 1.0.0 Chacun de ces types de champs possède un ou plusieurs attributs qui permettent de paramétrer leur comportement : – accessor – default – default_method – edit_accessor – enforceVocabulary – index – name – mode – multiValued – mutator – primary – required – schemata – searchable – validators – vocabulary – storage Nous allons générer une première version de notre module : C:\Program Files\Plone4\src\Products.StandArt_AQ\Products> c:\ArchGenXML\bin\ar chgenxml.exe ..\model\Products.StandArt_AQ.zargo StandArt_AQ INFO ArchGenXML Version 2.5.dev (c) 2003-2009 BlueDynamics Alliance, Austria, GPL 2.0 or later INFO Directories to search for profiles: [’C:\\ArchGenXML\\src\\archgenxml\\u mltools\\argouml’] INFO Parsing... INFO Profile files: ’{u’archgenxml_profile.xmi’: u’C:\\ArchGenXML\\src\\archg enxml\\umltools\\argouml\\archgenxml_profile.xmi’}’ INFO Directory in which we’re generating the files: ’StandArt_AQ’. INFO Generating... INFO Starting new Product: ’StandArt_AQ’. c:\archgenxml\eggs\i18ndude-3.1.2-py2.6.egg\i18ndude\odict.py:7: DeprecationWa rning: object.__init__() takes no parameters dict.__init__(self, dict) INFO Generating package ’content’. INFO Generating class ’StandArt_DocumentAQ’. INFO Generating class ’StandArt_FolderAQ’. INFO generator run took 0.96 sec. archgenxml.exe ..\model\Products.StandArt_AQ.zargo StandArt_AQ a lancé l’exécution de ArchGenXML en lui demandant de prendre comme fichier d’entrée notre modèle et de générer un répertoire StandArt_AQ contenant le code du module. Pour faire cela nous nous sommes d’abord placé dans le répertoire src\Products.StandArt_AQ\Products. Puis, nous pouvons ajouter ce nouveau module au buildout.cfg de notre Plone pour le faire apparaitre dans la liste des modules disponibles. Pour cela, il suffit d’y ajouter : eggs = Plone Products.StandArt_AQ develop = src/Products.StandArt_AQ D’enregistrer et de lancer la commande bin\buildout. Puis de démarrer l’instance et d’ajouter le produit. On peut alors, lorsqu’on est administrateur, ajouter au site Plone une instance de StandArt_FolderQA. 8.7. Création du plan documentaire 197 Plone pour les développeurs, Version 1.0.0 Vous constaterez également en regardant l’onglet Security de la ZMI que nos nouvelles permissions ont été ajoutées. Si l’on regarde le code du module StandArt_DocumentAQ.py, on constate qu’il contient la définition d’un schéma Archetypes : schema = Schema(( FileField( name=’mDocument’, widget=FileField._properties[’widget’]( label="Document", label_msgid="StandArt_DocumentAQ_Document_label", description="A document under Quality Assurance", description_msgid="StandArt_DocumentAQ_Document_description", i18n_domain=’StandArt_AQ’, ), storage=AttributeStorage(), read_permission="View", write_permission="StandArt: write AQ document", ), ), ) StandArt_DocumentAQ_schema = BaseSchema.copy() + \ getattr(ATDocument, ’schema’, Schema(())).copy() + \ schema.copy() La variable schema est une liste de champs, qui ici contient la déclaration du champ mDocument. On peut voir comment sont implémentés les différents marqueurs du champ et comment lui est associé son widget. Suit la définition de la classe : class StandArt_DocumentAQ(ATDocument, BrowserDefaultMixin): """ """ security = ClassSecurityInfo() implements(interfaces.IStandArt_DocumentAQ) meta_type = ’StandArt_DocumentAQ’ _at_rename_after_creation = True schema = StandArt_DocumentAQ_schema ##code-section class-header #fill in your manual code here ##/code-section class-header # Methods Qui utilise le schéma créé. Lors des futures générations du code, ArchGenXML conservera les modifications qui auront été apportées aux modules du produit, et complètera le schéma Archetypes avec les champs ajoutés à la classe dans le modèle UML. Le modèle et le code généré sont multi-plateforme. Ainsi vous pouvez éditer le modèle sous Windows ou GNU/Linux sans être lié à l’OS de développement ni à celui de déploiement. Dans la suite de ce cours nous continuerons notre exposé sous Ubuntu. En conséquence les chemins devront être adaptés, c’est-à-dire que les séparateurs de fichiers sont des slash au lieu d’être des anti-slash. 198 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML Plone pour les développeurs, Version 1.0.0 8.8 Création des workflows Nous allons associer un workflow documentaire à chaque classe créée. Pour cela sélectionnez StandArt_FolderAQ dans la barre de navigation, puis cliquez sur le bouton droit pour faire apparaître le menu contextuel. Sélectionnez le menu “Create Diagram” et le sous menu “Diagramme d’état”. F IGURE 8.6 – Ajout diagramme d’états-transition Une machine d’états-transitions avec le nom “(Unnamed StateMachine)” est créée dans “StandArt_FolderAQ”. Depuis la barre de navigation, descendez dans “StandArt_FolderAQ”, puis sélectionnez “(Unnamed StateMachine)”. Renommez le en “standart_folder_workflow”. Descendez d’un niveau dans “standart_folder_workflow”, s’y trouve le diagramme d’états-transitions “StandArt_FolderAQ 1”, renommez le “standart_folderaq_state_machine”. Lorsque vous sélectionnez le diagramme “standart_folderaq_state_machine” vous pouvez y créer des états et transitions. Ainsi vous définissez le workflow de votre objet. Pour ajouter des états il suffit de sélectionner le symbole correspondant dans la barre d’icônes, de positionner le curseur dans la zone de dessin et de cliquer. Pour ajouter des transitions, soit vous sélectionnez l’icône correspondante dans la barre et pouvez alors relier deux états déjà existants, soit vous sélectionnez un état et cliquez sur l’une des flèches apparaissant à la droite et à la gauche de l’état sélectionné. Nous allons créer le workflow de FolderAQ, qui contient un état private et un état published, reliés par une transition publish. Les noms des états et transitions doivent être saisi dans le champ Nom accessible par l’onglet Propriétés sur chaque état et transition. Les marqueurs des états vont nous permettre de définir qu’elles seront les permissions de l’objet lorsqu’il sera dans cet état. ArchGenXML permet de manipuler quatre meta permissions qui sont access, view, modify et list. Ainsi, à la génération du code, ArchGenXML remplace ces meta par le vrai nom des permissions. 8.8. Création des workflows 199 Plone pour les développeurs, Version 1.0.0 Si l’on veut directement travailler avec les permissions de Plone, il faut créer pour chaque permission une Tag Definition en cliquant sur l’icône TD de l’onglet Étiquettes et en lui donnant le nom de la permission voulue. On peut alors les ajouter comme marqueurs dans l’onglet Étiquettes de l’état. En valeur on précise alors les rôles qui auront cette permission. C’est de cette façon que l’on peut préciser qui a les permissions de lecture ou d’écriture que l’on a affecté spécifiquement aux champs de nos objets. Dans notre cas on crée un TD StandArt: write AQ document pour la permission d’écriture de notre champ mDocument et l’on donne cette permission au rôle Reviewer dans l’état submited pour que seuls les membres ayant le rôle Reviewer puissent changer le fichier associé au champ mDocument. F IGURE 8.7 – workflow de FolderAQ Il reste à créer de la même façon le workflow de DocumentAQ, qui contiendra trois états nommés private, submited, published. Puis éditez les transitions, nommez les submit, publish. Il est possible d’ajouter aux transitions un garde qui vérifiera que l’on peut ou non réaliser cette transition. Plone n’affichera la transition que si le garde est vérifié. Pour créer un garde il suffit de faire un clic droit dans la la zone d’édition du champ Garde dans l’onglet Propriété de la transition, puis de sélectionner l’item de menu Nouveau. On entre alors dans le panneau d’édition du garde. On peut alors expliciter comment on veut filtrer la transition en remplissant le champ Expression. Pour cela on dispose de trois possibilités : Prefixe du garde guard_roles guard_permissions guard_expr Valeur du garde Une liste de rôles Liste de permissions Expression Tales Exemple guard_roles : Manager ; Owner guard_permissions : View guard_expr :python :object.isOk() Les expressions peuvent être combinées, par exemple la transition avec le garde guard_roles:Reviewer|guard_permission:Modify Portal Content ne sera déclenchable 200 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML Plone pour les développeurs, Version 1.0.0 F IGURE 8.8 – workflow de DocumentAQ F IGURE 8.9 – Garde de la transition publish 8.8. Création des workflows 201 Plone pour les développeurs, Version 1.0.0 que par les modérateurs ayant les droits de modification. En plus des gardes, il est possible d’exécuter des scripts avant et après transition. Pour cela il faut ajouter une conséquence à la transition comme on l’a fait pour le garde. Le nom de la conséquence sera le nom de la fonction python appelée lors de la transition. F IGURE 8.10 – Garde et conséquence ArchGenXML va créer une arborescence Products.StandArt_AQ/Products/StandArt_AQ/profiles/default/wo contenant un répertoire contenant la définition XML du workflow pour FolderAQ et un autre pour DocumentAQ. Il génère aussi un fichier wfsubscribers.py contenant la définition des scripts appelés lors des transitions : # # # # # # # # # # -*- coding: utf-8 -*File: wfsubscribers.py Copyright (c) 2010 by Michael Launay <[email protected]> Generator: ArchGenXML Version 2.5.dev http://plone.org/products/archgenxml GNU General Public License (GPL) __author__ = """Michael Launay <[email protected]>""" __docformat__ = ’plaintext’ ##code-section module-header #fill in your manual code here ##/code-section module-header def onSubmitDocumentAQ(obj, event): """generated workflow subscriber.""" # do only change the code section inside this function. if not event.transition \ or event.transition.id not in [u’submit’] \ or obj != event.object: return ##code-section onSubmitDocumentAQ #fill in your manual code here ##/code-section onSubmitDocumentAQ ##code-section module-footer #fill in your manual code here ##/code-section module-footer 202 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML Plone pour les développeurs, Version 1.0.0 Pour ajouter du code spécifique tel que la notification par mail des principaux modérateurs, il suffit de le mettre entre la balise ##code-section onSubmitDocumentAQ et ##/code-section onSubmitDocumentAQ, mais si le code est volumineux ou doit être partagé nous pouvons le mettre dans un module qui sera importé dans la section ##code-section module-header et l’appel se fera dans la section ##code-section onSubmitDocumentAQ. L’événement event contient l’objet qui subit la transition, ainsi que la requête. Si vous voulez faire de l’introspection et voir ce qui se passe vous pouvez mettre un pdb.set_trace () dans la section, relancer Zope en mode foreground bin/instance fg, et déclencher la transition. Python s’arrêtera sur le point d’arrêt et en tapant la commande h vous aurez la liste des commandes possibles : ##code-section onSubmitDocumentAQ #fill in your manual code here import pdb pdb.set_trace () ##/code-section onSubmitDocumentAQ Il est possible d’associer un workflow créé pour un type de contenu donné à un autre type de contenu avec l’étiquette “use_workflow”, il suffit alors de donner le nom du workflow comme valeur à l’étiquette. 8.8.1 Ajout d’une worklist Il est possible pour un état donné d’un objet de le déclarer comme devant apparaître dans une liste de modération. Pour cela il suffit d’ajouter le marqueur worklist à la liste des étiquettes de l’état dans lequel l’objet doit être modéré. La valeur est alors le nom de la woklist dans laquelle devrait apparaître l’objet, mais la liste de modération de Plone regroupe tous les objets associés à une worklist. Le marqueur worklist:guard_roles permet de restreindre la notification de modération aux rôles donnés en valeur au marqueur. 8.8.2 Ajout d’un validateur sur un champ Les #validators# permettent de vérifier la cohérence des valeurs des champs saisis. Ils sont appelés à la saisie des champs en mode d’édition online ou à l’enregistrement des modifications réalisées sur le contenu en mode édition. Les champs de type Archetypes utilisent des objets implémentant l’interface IValidator. Lors de la saisie du champ, les validateurs associés à un champ sont appelés successivement. Dés que l’un deux retourne une valeur différente de True, le champ est en erreur et un message correspondant est positionné dans la variable error. Par défaut Plone propose plusieurs instances de validators en voici les plus utiles : 8.8. Création des workflows 203 Plone pour les développeurs, Version 1.0.0 Nom isDecimal Usage L’entrée est elle un un nombre décimal ? isInt isPrintable Est-ce un entier ? Ne doit pas contenir de caractères non imprimables Est un numéro de téléphone international ? isInternationalPhone Number isURL Est-ce une url ? isEmail Est-ce une adresse mél ? isMailTo isUnixLikeName isMaxSize Est-ce une adresse mél précédée par “mailto :” Est-ce un nom respectant le style Unix ? isValidDate isEmpty isEmptyNoError isValidId isTidyHtml isTidyHtmlWithCleanup isNonEmptyFile isTAL Vérifie si un fichier uploadé ou un objet supportant l’opération len() est plus petit qu’une valeur donnée. Est-ce que la chaîne de caractères peut être convertie en date ? La valeur doit être vide. La valeur doit être vide. La valeur doit correspondre à l’identifiant d’un objet. Utilise mx.Tidy pour valider l’entrée HTML. Utilise mx.Tidy pour valider l’entrée HTML. Détails Autorise la notation scientifique. r’[a-zA-Z0-9s]+$’ Vérifie le format et ignore les espaces, parenthèse et barres obliques. Reconnait la plus part des protocoles. Vérifiée par une expression régulière. r”^[A-Za-z][wd-_] {0,7}$” Vérifie à partir de la valeur maxsize définit sur le champ. L’erreur ne sera pas montrée. Échoue sur les erreurs et les alertes. Échoue sur les erreurs mais nettoie le code des alertes. Vérifie que le fichier uploader n’est pas vide. Est-ce une expression Template Attribute Language ? Pour les utiliser il suffit d’ajouter “validators” comme étiquette au champ avec pour valeur le nom du ou des validators à utiliser. Plone offre également la possibilité de paramétrer des validators en les instanciant avec des paramètres puis en utilisant comme validator ces instances. Par exemple ExpressionValidator qui permet de définir un validator utilisant une expression TALES pour dire si la valeur saisie est correcte ou non. L’instanciation de ce validator est supportée par ArchGenXML via les étiquettes validation_expression pour l’expression et validation_expression_errormsg pour le message d’erreur associé. RegexValidator du paquet Products.validation.validators.RegexValidator permet d’utiliser une expression régulière pour la validation, il faut l’instancier et l’enregistrer dans le code généré dans les parties réservées au code manuel. RangeValidator permet quant à lui de vérifier qu’une valeur est comprise entre deux bornes. Il est dans Products.validation.validators.RangeValidator. Nous allons voir comment créer un validator qui rappelle une méthode de notre objet. Pour cela nous commençons par créer un fichier objectcallbackvalidation.py qui contiendra un validator appelant une méthode dont le nom sera dynamiquement calculée à partir du nom du champ validé. Par convention nous imposons que la méthode ait un nom de la forme is_a_valid_nomduchamp sur la classe du contenu : from Products.validation.interfaces.IValidator import IValidator from zope.interface import implements 204 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML Plone pour les développeurs, Version 1.0.0 class ObjectCallbackValidation: implements(IValidator) def __init__(self,name): self.name = name def __call__(self, value, *args, **kwargs): fieldname = kwargs[’field’].getName() instance = kwargs[’instance’] method = "is_a_valid_"+fieldname # callback the object vaildation methode for the field return getattr(instance, method)(value) object_callback_validation = ObjectCallbackValidation("object_callback_validation") # validator registration from Products.validation import validation validation.register(object_callback_validation) Pour qu’au chargement de notre module ce code soit appelé il nous suffit d’en faire un import dans la fonction “initialize” du fichier __init__.py du produit. Ensuite il suffit de créer une méthode “is_a_valid_nomDeMonChamp” dans notre modèle sur notre classe pour chacun des champs à qui on a déclaré object_callback_validation pour validator. 8.8.3 Ajout d’une méthode L’ajout d’une méthode est simple puisqu’il suffit de l’ajouter à l’une des classes de nos contenus. Par exemple si nous voulons écrire une méthode que l’on utilisera comme validator à l’aide du code écrit précédemment pour le champ mDocument du type de contenu StandArtDocumentAQ pour vérifier que le fichier uplaoder est un pdf. F IGURE 8.11 – Ajout d’une méthode La génération du code produit la méthode suivante dans le code StandArt_DocumentAQ.py : 8.8. Création des workflows 205 Plone pour les développeurs, Version 1.0.0 security.declarePublic(’is_a_valid_mDocument’) def is_a_valid_mDocument(self): """ """ pass Important : En l’abscence de précision, la méthode générée est accessible par toute page template ou pythonscript quelque soit le rôle de l’utilisateur. Pour la protéger, il faut ajouter l’étiquette “permission” avec pour valeur le nom de la permission à avoir par exemple “Modify portal content”. Cette exposition est due à la présence de la documentation de la méthode qui est interprétée par Plone comme la volonté à exposer la méthode. Ajoutons au modèle le pramètre “value” à la méthode de type None (les informations de types ne sont pas utilisés par ArchGenXML) et générons le code à nouveau. Nous pouvons maintenant remplacer l’instruction pass par notre code : ##code-section module-header #fill in your manual code here import pyPdf from Products.StandArt_AQ import StandArt_AQFactory as _ ##/code-section module-header #... class StandArt_DocumentAQ(ATDocument, BrowserDefaultMixin): #... security.declareProtected("Modify portal content", ’is_a_valid_mDocument’) def is_a_valid_mDocument(self,value): """ Test if the file is a pdf ? """ try: tell = value.tell() value.seek(0) f = pyPdf.PdfFileReader(value) #imported at the top of the file f.documentInfo #nothing is done yet value.seek(tell) return 1 except: return _(u’file must be a pdf’) Pour la traduction il faut ajouter StandArt_AQFactory dans le __init__.py from zope.i18nmessageid import MessageFactory StandArt_AQFactory = MessageFactory(’StandArt’) Maintenant nous pouvons ajouter l’étiquette “validators” à mDocument avec pour valeur “object_callback_validation”. 8.8.4 Ajout d’une validation sur l’ensemble de l’édition Lorsque plusieurs champs sont liés entres eux il n’est possible de tester leur validité qu’à l’enregistrement de l’instance du contenu. Il suffit d’ajouter la méthode post_validate(self, REQUEST, errrors) au type de contenu. Si l’on constate des erreurs, on les remonte en remplissant le dictionnaire “errors” : 206 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML Plone pour les développeurs, Version 1.0.0 security.declarePublic(’post_validate’) def post_validate(self,REQUEST,errors): """ Check if the pdf info version match the DocumentNumber """ if not REQUEST : return pdf_file = REQUEST.has_key(’mDocument_file’) and \ REQUEST.form[’mDocument_file’] or None version_number = REQUEST.has_key(’mDocumentNumber’) and \ REQUEST.form[’mDocumentNumber’] or None m_document = self.getMDocument() m_document_number = self.getMDocumentNumber() if pdf_file is None and version_number is None or \ pdf_file is None and not m_document or \ version_number is None and m_document_number is None : return if pdf_file is None : pdf_file = StringIO.StringIO(m_document) if version_number is None : version_number = m_document_number tell = pdf_file.tell() pdf_file.seek(0) infos = pyPdf.PdfFileReader(pdf_file).documentInfo keywords = infos[’/Keywords’] pdf_file.seek(tell) str_version = u"Version : " posv = keywords.find(str_version) if posv != -1 : version = keywords[posv + len(str_version):].split()[0] if version != version_number : errors[’mDocumentNumber’] = _("Document numbers doesn’t match") Il est possible aussi de surcharger la méthode pre_validate qui est appellée avant l’appel individuel des “validators” des champs. Important : Dans le cas d’une édition en ligne c’est à dire sans passer par l’onglet Modifier la méthode post_validate n’est pas appelée. 8.8.5 Ajout d’une action En stéréotypant une méthode par “Action” nous allons ajouter un onglet à la barre d’édition de l’instance d’action. Cet onglet renvoie allors sur une page template que l’on peut définir. Par exemple nous allons ajouter un onglet “Pdf Informations” à notre DocumentAQ : Le nom de l’onglet, son identifiant, la permission d’accès, le nom de la template associée, et la condition d’affichage sont données comme valeurs des étiquettes correspondantes. L’action est alors ajoutée dans la déclaration xml du type StandArt_AQ/profiles/default/types/StandArt_DocumentAQ.xml : <action title="Pdf Informations" i18n:attributes="title" action_id="pdf_informations" category="object" condition_expr="python:object.getMDocument()" url_expr="string:${object_url}/document_aq_pdf_informations" visible="True"> <permission value="View"/> Nous pouvons alors créer la template StandArt_AQ/skins/standart_aq_templates/document_aq_pdf_informations.pt qui contient : 8.8. Création des workflows 207 Plone pour les développeurs, Version 1.0.0 F IGURE 8.12 – Ajout d’une action <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:tal="http://xml.zope.org/namespaces/tal" xmlns:metal="http://xml.zope.org/namespaces/metal" xmlns:i18n="http://xml.zope.org/namespaces/i18n" lang="en" metal:use-macro="here/main_template/macros/master" i18n:domain="plone"> <body> <div metal:fill-slot="main"> <table> <tbody tal:define="pdf_informations python:context.getPdfInformations()"> <tr tal:repeat="keyword pdf_informations"> <td tal:content="keyword">Title</td> <td tal:content="python:pdf_informations[keyword]">Value</td> </tr> </tbody> </table> </div> </body> </html> Cette template appèle une méthode du type de contenu appelée getPdfInformations qui retourne le dictionnaire des informations. Pour la définir il nous suffit d’ajouter cette méthode comme nous avons fait pour les méthodes précédentes. security.declareProtected("View", ’getPdfInformations’) def getPdfInformations(self): """ Return the pdf document informations """ document = self.getMDocument() if not document : return {} strfile = StringIO.StringIO(document) documentInfo = pyPdf.PdfFileReader(strfile).documentInfo return documentInfo.copy() #The copy is needed by security check 208 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML Plone pour les développeurs, Version 1.0.0 Il faut ajouter l’import 8.8.6 Ajout d’un portlet d’affichage des Documents soumis La portlet de modération est générique, il est possible de créer ses propres portlets et par exemple d’en créer des dédiées à la modération de nos types de données à l’exclusion des autres. Pour ajouter une portlet il suffit de créer une classe et de lui mettre le stéréotype “portlet”. F IGURE 8.13 – Ajout d’une portlet La génération du module avec ArchGenXML produit deux fichiers. Le fichier StandArt_AQ/profiles/default/portlets.xml contient la déclaration de la portlet : <portlets> <portlet addview="Products.StandArt_AQ.content.StandArt_WorkList.StandArt_WorkList" title="StandArt_WorkList" description="" /> <!-- ##code-section PORTLETS --> <!-- ##/code-section PORTLETS --> </portlets> On y voit le nom de la portlet et la vue associée. Si l’on ouvre le fichier de la vue générée StandArt_AQ/content/StandArt_WorkList.py on y voit principalement : class IStandArt_WorkList(IPortletDataProvider): """A portlet which renders the cart. """ class Assignment(base.Assignment): implements(IStandArt_WorkList) title = _(u’StandArt_WorkList’) ##code-section assignment-body #fill in your manual code here 8.8. Création des workflows 209 Plone pour les développeurs, Version 1.0.0 ##/code-section assignment-body class Renderer(base.Renderer): render = ViewPageTemplateFile(’templates/StandArt_WorkList.pt’) ##code-section renderer-body #fill in your manual code here ##/code-section renderer-body class AddForm(base.NullAddForm): def create(self): return Assignment() ##code-section addform-body #fill in your manual code here ##/code-section addform-body Pour remplir et rendre fonctionnel ce code lisez le chapitre Les portlets. Dans notre cas pour réaliser la worklist il faut faire : #... from Products.CMFCore.utils import getToolByName #... class Renderer(base.Renderer): render = ViewPageTemplateFile(’templates/StandArt_WorkList.pt’) ##code-section renderer-body #fill in your manual code here @memoize def _data(self, target): if self.anonymous: return [] context = aq_inner(self.context) ctool = getToolByName(context, ’portal_catalog’) wtool = getToolByName(context, ’portal_workflow’) ttool = getToolByName(context, ’portal_types’) ENM_AMC = ttool[’StandArt_DocumentAQ’] wfs = wtool.getWorkflowsFor(’StandArt_DocumentAQ’) review_states = [] for wf in wfs: wl = wf.worklists._objects wld = wf.worklists._mapping[wl[0][’id’]] review_states.extend(wld.var_matches[’review_state’]) return ctool.searchResults(meta_type = ’StandArt_DocumentAQ’, review_state = review_states) ##/code-section renderer-body Et la page template StandArt_AQ/content/templates/StandArt_WorkList.pt <dl class="portlet portletStandArt_WorkList" i18n:domain="StandArt_AQ"> <dt class="portletHeader"> <span class="portletTopLeft"></span> <h3> <span i18n:domain="plone" i18n:translate="box_review_list"> Review List </span> <span tal:content="view/data/search_filter"/> </h3> <span class="portletTopRight"></span> </dt> <tal:items tal:repeat="b view/review_items"> <dd class="portletItem" tal:define="oddrow repeat/b/odd; review_state b/review_state;" 210 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML Plone pour les développeurs, Version 1.0.0 tal:attributes="class python:oddrow and ’portletItem even’ or ’portletItem odd’"> <a href="#" tal:attributes="href string:${b/getPath}/view; title b/Description; "> <span tal:replace="b/Title"> Title </span> <span class="portletItemDetails"> <span tal:replace="b/Creator">Jim Smith</span> &mdash; <span tal:replace="b/modified"> May 5</span> </span> </a> </dd> </tal:items> <dd class="portletFooter"> <span class="portletBottomLeft"></span> Footer <span class="portletBottomRight"></span> </dd> </dl> 8.8.7 Modification de la vue d’édition Plone propose un mécanisme trés simple de modification de la vue d’édition d’un type donné. Il suffit de créer un fichier template dans le répertoire skins basé sur le nom du type mis en minucule et suivit du postfixe _edit par exemple pour modifier la vue d’édition de StandArt_DocumentAQ il suffit de créer le fichier StandArt_AQ/skins/standart_aq_templates/standart_documentaq_edit.pt qui contient alors les slots de la template de base que l’on veut surcharger. Par exemple si l’on souhaite “Faire un truc super cool” juste avant l’affichage du champ mDocumentNumero on peut faire des choses compliquées comme : <metal:macro define-macro="body"> <metal:use-macro use-macro="here/edit_macros/macros/body"> <metal:fill-slot fill-slot="widgets"> <tal:tabbed tal:condition="allow_tabbing | nothing"> <fieldset tal:define="sole_fieldset python:len(fieldsets) == 1" tal:repeat="fieldset fieldsets" tal:attributes="id string:fieldset-${fieldset}" tal:omit-tag="sole_fieldset"> <legend id="" tal:content="python: view.getTranslatedSchemaLabel(fieldset)" tal:attributes="id string:fieldsetlegend-${fieldset}" tal:condition="not:sole_fieldset" /> <tal:fields repeat="field python:schematas[fieldset].editableFields(here, visible_only=True)"> <div tal:condition="python:field.getName() == ’mDocumentNumero’"> Faire un truc super cool ! </div> <metal:fieldMacro use-macro="python:here.widget(field.getName(), mode=’edit’)" /> </tal:fields> </fieldset> </tal:tabbed> <tal:nottabbed tal:condition="not: allow_tabbing | nothing"> <tal:fields repeat="field python:schematas[fieldset].editableFields(here, visible_only=True)"> <metal:fieldMacro use-macro="python:here.widget(field.getName(), mode=’edit’)" /> </tal:fields> 8.8. Création des workflows 211 Plone pour les développeurs, Version 1.0.0 </tal:nottabbed> </metal:fill-slot> </metal:use-macro> </metal:macro> Pour savoir quoi surcharger, il faut aller voir les templates archetypes/edit_macro et archetypes/base_edit . 8.8.8 Modification de la vue de consultation La vue de consultation repose sur le même principe sauf que la template de base doit porter le nom du type en minuscule postfixé de _view et que la template de base est archetypes/base_view 8.9 exercice Création des fondements du modèle de l’application “StandArt_AQ”. 212 Chapitre 8. Génerer un composant Plone avec ArgoUML et ArchgenXML CHAPITRE 9 Nouvelles fonctionnalités de ArchGenXML 9.1 Définition La dernière version de ArchGenXML permet de modéliser plus de chose que dans les versions précédentes. 9.2 Savoir – modélisation des portlets – modélisation des vues zope 3 9.3 Modélisation d’un portlet Créez un package portlets et créez une classe avec le stéréotype portlet_class. 9.4 Modélisation de vue zope 3 Regardez le modèle de Products.rendezvous sur pypi : $ cd /tmp/ $ easy_install -b. -e Products.rendezvous Ouvrez le modèle /tmp/products.rendezvous/model/rendezvous.zargo avec ArgoUML. Procédure : – Créez un package browser, ajoutez y une classe MyView avec le stéréotype view_class. Vous pouvez préciser le stéréotypes name “myview” et permission “cmf.ModifyPortalContent” par exemple. – Créez un package content avec une classe MyContent avec le stéréotype archetypes. – Créez une dépendance de MyView vers MyContent. Vous aurez accès à la vue @@myview seulement si le context est un document MyContent et que vous avez la permission cmf.ModifyPortalContent. 213 Plone pour les développeurs, Version 1.0.0 9.5 Exercice Mise en pratique sur le composant “base de connaissances” 214 Chapitre 9. Nouvelles fonctionnalités de ArchGenXML CHAPITRE 10 Création d’un thème Plone Ajout de nouveaux composants visuels à Plone. 10.1 Installation de Gloworm Gloworm est un outil pour introspecter les viewlets d’une page Plone, très utile pour savoir comment customiser telle partie de la page. Gloworm est une interface alternative à portal_skins/custom et portal_view_customizations. On peut cliquer sur l’endroit que l’on veut customiser et cliquer sur customiser. Le produit ne fonctionne pas avec Plone 4. Un produit qui le remplace sur Plone 4 est probablement plone.app.themeeditor. Créez un fichier development.cfg : [buildout] extends = buildout.cfg [instance] eggs += Products.Gloworm Reexécutez bin/buildout -c development.cfg. 10.2 Création d’un theme Dans le dossier src/, créez un theme avec la commande : $ cd /tmp $ paster create -t plone3_theme formation.theme \ --svn-repository=http://devagile/Formation/packages Skin Name : Formation Theme Empty Styles? : False Include Documentation? : True $ svn rm --force formation.theme/formation.theme.egg-info \ formation.theme-configure.zcml formation/theme/skins/formation_theme_styles/base.css.dtml \ formation/theme/skins/formation_theme_styles/portlets.css.dtml \ formation/theme/skins/formation_theme_styles/public.css.dtml \ formation/theme/version.txt formation/theme/skins/formation_theme_styles/base_properties.props 215 Plone pour les développeurs, Version 1.0.0 base_properties.props n’est plus utilisé dans Plone 4. Éditez setup.py pour supprimer les deux lignes : setup_requires=["PasteScript"], paster_plugins=["ZopeSkel"] Éditez formation/theme/profiles/default/skins.xml pour remplacer based-on=”Plone Default” par based-on=”Sunburst Theme”. Et commitez : $ svn ci -m"Initial theme structure" Retournez dans votre buildout, ajoutez formation.theme dans auto-checkout du fichier sources.cfg, ainsi que dans eggs de la section [instance] de buildout.cfg, commitez et relancez le buildout. 10.3 Exercice 1 Changer la couleur des documents dans l’état “En attente de modération” (pending). Utilisez l’extension Firebug de firefox pour récupérer la règle css utilisé par défaut pour colorisé le lien en orange. Copiez ensuite cette règle dans browser/stylesheets/main.css et changez la couleur : .state-pending { color:#FFA5FF !important; } Passez portal_css en mode debug comme ça vous ne devrez pas redémarrer l’instance à chaque fois que vous modifiez les css. 10.4 Exercice 2 : Création d’une viewlet Créez une viewlet affichant une image dans le footer. Référez-vous au chapitre Les viewlets pour consulter la documentation complète sur cette notion. 10.4.1 Ajouter une image Ajoutez une image dans le dossier browser/images/. Pour accéder ensuite à l’image, vous devez utiliser le nom de la resourceDirectory : ++resource++formation.theme.images, suivit de / et du nom de votre image dans le dossier images. Exemple : ++resource++formation.theme.images/monimage.png Éditez le fichier browser/viewlets.py, décommentez la classe, nommez la LogoViewlet. Copiez le fichier viewlet.pt en logoviewlet.pt. Contenu du fichier logoviewlet.pt : <img tal:define="portal_state context/@@plone_portal_state; root_url portal_state/portal_url" tal:attributes="src string:$root_url/++resource++formation.theme.images/image.png" /> Éditez le fichier configure.zcml pour déclarer votre viewlet. Vous avez un exemple en commentaire. Il suffit de le décommenter et d’ajuster le nom de la viewlet et la classe. Cela donne : 216 Chapitre 10. Création d’un thème Plone Plone pour les développeurs, Version 1.0.0 <browser:viewlet name="formation.logoviewlet" manager="plone.app.layout.viewlets.interfaces.IPortalFooter" class=".viewlets.LogoViewlet" layer=".interfaces.IThemeSpecific" permission="zope2.View" /> Maintenant au lieu d’afficher votre LogoViewlet dans le PortalFooter, vous allez l’afficher dans portalheader en précisant que vous le voulez après le logo de plone. Dans configure.zcml, changez l’interface du viewletmanager en IPortalHeader : <browser:viewlet name="formation.logoviewlet" manager="plone.app.layout.viewlets.interfaces.IPortalHeader" class=".viewlets.LogoViewlet" layer=".interfaces.IThemeSpecific" permission="zope2.View" /> Éditez ensuite profiles/default/viewlets.xml : <?xml version="1.0"?> <object> <order manager="plone.portalheader" skinname="formation’s theme" based-on="Plone Default"> <viewlet name="zdevan.logoviewlet" insert-after="plone.logo" /> </order> </object> Relancez l’instance et réinstallez votre produit pour prendre en compte le changement. 10.5 Portlets – http ://plone.org/documentation/manual/portlets-developer-manual Créez le portlet : $ cd /tmp $ paster create -t plone3_portlet formation.portlet.docinfo \ --svn-repository=http://devagile/Formation/packages – – – – – – Supprimez le répertoire egg-info, le fichier version.txt Ajoutez ensuite l’entry point z3c.autoinclude.plugin dans setup.py Commitez Ajoutez ce nouveau egg dans votre buildout.cfg et sources.cfg Relancez le buildout Commitez les changements fait dans le buildout Démarrez l’instance, installez le produit et ajoutez le portlet. Éditez le fichier docinfoportlet.py et ajoutez un champ informations au schema IDocInfoPortlet : informations = schema.List(title=_(u"Information list"), description=_(u"A list of information about the document"), required=True, value_type=schema.Choice(values=(’Creator’,’effective’)) ) Modifiez également l’implémentation : 10.5. Portlets 217 Plone pour les développeurs, Version 1.0.0 class Assignment(base.Assignment): """Portlet assignment. This is what is actually managed through the portlets UI and associated with columns. """ implements(IDocInfoPortlet) informations = () def __init__(self, informations=()): self.informations = informations Dans la template, afficher les informations qui ont été choisies via le formulaire du portlet : <dl class="portlet portletDocInfoPortlet" i18n:domain="formation.portlet.docinfo"> <dt class="portletHeader"> <span class="portletTopLeft"></span> Information <span class="portletTopRight"></span> </dt> <dd class="portletItem odd" tal:define="informations view/data/informations; plone_view context/@@plone; mtool context/portal_membership" > <strong>Created by :</strong> <span tal:condition="python:’Creator’ in informations" tal:replace="python:mtool.getMemberById(context.Creator()).getProperty(’fullname’,co <strong>Date of creation :</strong> <span i18n:translate="text_effective_date"> <span tal:condition="python:’effective’ in informations" tal:content="python:plone_view.toLocalizedTime(context.effective())" /> </span> <tal:block condition="python:’ExpirationDate’ in informations"> <span i18n:translate="text_expiration_date" tal:condition="context/ExpirationDate"> <strong>Expiration :</strong> <span i18n:name="date" tal:content="python:plone_view.toLocalizedTime(context.ExpirationDate())" /> </span> </tal:block> </dd> </dl> Ajoutez une property available dans la classe Renderer pour afficher le portlet seulement pour les documents : class Renderer(base.Renderer): """Portlet renderer. This is registered in configure.zcml. The referenced page template is rendered, and the implicit variable ’view’ will refer to an instance of this class. Other methods can be added and referenced in the template. 218 Chapitre 10. Création d’un thème Plone Plone pour les développeurs, Version 1.0.0 """ @property def available(self): return self.context.meta_type == "ATDocument" render = ViewPageTemplateFile(’docinfoportlet.pt’) 10.6 GenericSetup – http ://plone.org/documentation/tutorial/genericsetup – http ://www.sixfeetup.com/swag/generic-setup-quick-reference-card 10.6. GenericSetup 219 Plone pour les développeurs, Version 1.0.0 220 Chapitre 10. Création d’un thème Plone CHAPITRE 11 Les viewlets “L’amour c’est un bouquet de viewlets” (adapté de “Violettes impériales” par Vincent Scotto) Author Gilles Lenfant Contributors Thomas Desvenain Version 1.0.0 Copyright (C) 2010 Gilles Lenfant. Chacun est autorisé à copier, distribuer et/ou modifier ce document suivant les termes de la licence Paternité-Pas d’Utilisation Commerciale-Partage des Conditions Initiales à l’Identique 2.0 France accessible à http ://creativecommons.org/licenses/by-nc-sa/2.0/fr Le code source présent dans ce document est soumis aux conditions de la “Zope Public License”, Version 2.1 (ZPL). THE SOURCE CODE IN THIS DOCUMENT AND THE DOCUMENT ITSELF IS PROVIDED “AS IS” AND ANY AND ALL EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE. La lecture de ceci nécessite la connaissance préalable du langage ZCML, ainsi que de l’outil GenericSetup. 11.1 Introduction Les différentes pages d’un site Plone sont publiées selon le modèle de la main_template depuis la version 1 de Plone. C’est toujours le cas, mais... Dans les versions antérieures de Plone, cette template faisait appel à de nombreuses macros pour déployer les différents blocs de code HTML nécessaire à l’affichage d’une page. Même si ceci est toujours - partiellement - vrai, pour aider le support des composants prévus pour les versions antérieures de Plone, l’utilisation des macros est remplacée par celle des content providers. L’inclusion du HTML produit par un content provider dans la main_template ressemble à ceci : <div id="portal-top" i18n:domain="plone"> <div tal:replace="structure provider:plone.portaltop" /> </div> Vous remarquez la présence de l’expression TAL provider. Dans le monde Zope 3, un provider est un multi adapter qui adapte le context, la request et la vue. Ici le provider est un viewlet manager. Les viewlets fournissent une portion de code HTML. Plone passe par des viewlet managers - une sorte d’aggrégateur de viewlets - qui permettent d’ordonnancer les viewlets avec un maximum de flexibilité. 221 Plone pour les développeurs, Version 1.0.0 Cette souplesse permise par les viewlet managers rend de moins en moins nécessaire la surcharge de la main_template lors de la réalisation de vos skins personnelles. Comme nous le verrons plus loin, vous pourrez également lors d’une personnalisation graphique “extrême”, ajouter vos propres viewlet managers. 11.2 Gestion interactive des viewlets Pour mieux illustrer les propos ci-avant, Plone fournit un outil visualisant l’articulation des viewlet managers et l’arrangement des viewlets dans ceux-ci. Ouvrez - étant authentifié comme administrateur - un navigateur sur la racine de vote site Plone et prolongez l’URL par /@@manage-viewlets. F IGURE 11.1 – Vue @@manage-viewlets : ordonnancement et masquage des viewlets Immédiatement, sous chaque cadre, vous trouvez le nom du viewlet manager, dont le plone.portaltop de l’extrait précédent. A l’intérieur de chaque viewlet manager, vous pouvez voir la liste des viewlets qu’il fournit. Chaque nom de viewlet est accompagné : – de son rang - commençant par 0 - dans le viewlet manager entre parenthèses, – de deux flèches permettant de monter ou descendre l’ordre d’apparition de la viewlet dans le viewlet manager, – du lien hide permettant de masquer la viewlet. Avec cet outil, en remontant la viewlet plone.global_sections en tête en quelques clics, on obtient le résultat suivant : Vous aurez également remarqué l’absence des colonnes de portlets dans le gestionnaire de viewlets. Celles-ci sont gérées dans un gestionnaire dédié, objet du chapitre sur Les portlets. 222 Chapitre 11. Les viewlets Plone pour les développeurs, Version 1.0.0 F IGURE 11.2 – La viewlet plone.global_sections remontée en tête 11.3 L’enregistrement en ZCML Les viewlet managers standard de Plone, ainsi que la plupart des viewlets standard sont enregistrés dans le configure.zcml du package plone.app.layout.viewlets dont voici un extrait : <configure xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser"> <!-- Register viewlet managers - used in plone’s main_template --> <browser:viewletManager name="plone.htmlhead" provides=".interfaces.IHtmlHead" permission="zope2.View" /> <browser:viewletManager name="plone.htmlhead.links" provides=".interfaces.IHtmlHeadLinks" permission="zope2.View" /> ... <!-- Define some viewlets --> ... <!-- Render the browser title --> <browser:viewlet name="plone.htmlhead.title" 11.3. L’enregistrement en ZCML 223 Plone pour les développeurs, Version 1.0.0 manager=".interfaces.IHtmlHead" class=".common.TitleViewlet" permission="zope2.View" /> <!-- Render the workflow history --> <browser:viewlet name="plone.belowcontentbody.workflowhistory" manager=".interfaces.IBelowContentBody" class=".content.WorkflowHistoryViewlet" permission="zope2.View" /> ... </configure> Vous remarquez les deux éléments viewletManager et viewlet qui enregistrent - respectivement : – les viewlet managers utilisables dans une template comme la main_template, – les viewlets pouvant être associées aux différents types de viewlet manager. Les attributs d’un élément “viewletManager” : 224 Chapitre 11. Les viewlets Plone pour les développeurs, Version 1.0.0 TABLE 11.1 – Attributs d’un élément viewletManager (* = obligatoire) Attribut name * permission * for Description Définition du nom du viewlet manager. Doit être unique dans l’instance Zope. Permission nécessaire pour voir le viewlet manager. Généralement zope2.View. Interface des contenus pour lesquels ce viewlet manager peut être utilisé. Défaut : zope.interface.Interface Exemple : Products.ATContentTypes.interface.IATDocument. Vous pouvez utiliser * pour signaler que le viewlet manager peut être utlisé dans n’importe quel contexte, ce qui est le cas la plupart du temps. layer Interface de skin spécifique pour laquelle le viewlet manager peut être utilisé. Ce paramètre n’a d’intérêt que lors de la réalisation d’une skin, comme dans le cas du composant NuPlone. Par défaut : zope.publisher.interfaces.browser.IDefaultBrowserLayer. view Interface de view pour laquelle le viewlet manager peut être utilisé. Défaut : zope.publisher.interfaces.browser.IBrowserView. Exemple : plone.app.content.browser.interfaces.IFolderContentsView. La valeur par défaut convient dans l’immense majorité des cas. provides interface marqueur spécifique à ce viewlet manager. Bien que cet attribut soit facultatif, il est nécessaire, comme nous le verrons plus loin, de l’associer à une interface marqueur spécifique afin de pouvoir y associer des viewlets. Par défaut : zope.viewlets.interfaces.IViewletManager. class * Éventuelle classe Python spécifique de ce viewlet manager. La classe par défaut est suffisante dans la majorité des cas. Si vous voulez donner à un administrateur la possibilité d’ordonner les viewlets - avec le panneau de contrôle vu plus haut - inscrites dans ce viewlet manager, la classe devra être : plone.app.viewletmanager.manager.OrderedViewletManager. Par défaut : zope.viewler.manager.ViewletManagerBase. template * Éventuelle template devant être utilisée pour ce viewlet manager. Ceci peut être utilisé dans certains cas en alternative à l’attribut class vu ci-avant. Pour illustrer ceci, je vous invite à lire le code de Products.ResourceRegistries. À noter que cette façon d’opérer est rarement utilisée. Par défaut : None. Attention, vous devez fournir l’attribut class ou l’attribut template mais en aucun cas les deux. allowed_interface Interface exposant les méthodes publiques de ce viewlet manager. Généralement inutile. Par défaut : None allowed_attributes Liste d’attributs et méthodes publics (séparés par des espaces) de ce viewlet manager. Par défaut : None Warning : Dans la définition ZCML d’un viewlet manager comme décrite ci-dessus, vous devez fournir l’attribut class ou l’attribut template mais en aucun cas les deux. De même, il est inutile de fournir allowed_interface et allowed_attributes. 11.3. L’enregistrement en ZCML 225 Plone pour les développeurs, Version 1.0.0 TABLE 11.2 – Attributs d’un élément viewlet (* = obligatoire) Attribut name * permission * for Description Définition du nom de la viewlet. Doit être unique dans l’instance Zope. Permission de vue de cette viewlet. Généralement zope2.View Interface de types de contenus pour lesquels cette viewlet sera affichée. Par exemple, pour n’afficher cette viewlet que dans les vues d’un ATDocument (ou type dérivé), vous placerez : Products.ATContentTypes.interface.IATDocument. Par défaut : zope.interface.Interface. Pour que la viewlet s’affiche dans tout contexte, vous placerez *. layer L’utilisation de cet attribut est identique à celle prévue pour le viewlet manager. view L’utilisation de cet attribut est identique à celle prévue pour le viewlet manager. manager L’interface de viewlet manager dans laquelle cette viewlet peut être inscrite. En d’autres termes, la valeur de cet attribut doit être identique à l’attribut “provides” de la déclaration ZCML du viewlet manager dans lequel vous voulez inscrire la présente viewlet. Par défaut : zope.viewlet.interfaces.IViewletManager, c’est-à-dire que par défaut, vous pourrez placer cette viewlet dans n’importe quel viewlet manager. class La classe qui réalise cette viewlet. Cette classe doit avoir une méthode render qui produit l’extrait de HTML. Par défaut : None. Exemple : browser.maviewlet.MaViewlet attribute La méthode de rendu de votre classe de viewlet, si celle-ci n’est pas render. Ceci n’a d’intérêt que pour avoir plusieurs méthodes de rendu pour une seule classe de viewlet. template Vous pouvez fournir une template pour fournir l’extrait HTML lorsque vous ne désirez pas, ou ne pouvez pas, faire réaliser ceci par la méthode render de la classe mentionnée plus haut. Dans ce cas, l’objet instancié depuis cette dite classe est accessible dans votre template personnelle à travers l’objet “view”. Cette approche est utilisée pour personnaliser des viewlets standard ou d’un composant tiers dans vos propres réalisations, sans avoir à faire une ligne de Python. allowed_interface L’utilisation de cet attribut est identique à celle prévue pour le viewlet manager allowed_attributes L’utilisation de cet attribut est identique à celle prévue pour le viewlet manager foo bar. Non, ce n’est pas un gag, vous pouvez fournir des attributs unicode supplémentaires de cette façon à la viewlet. Ceux-ci peuvent être exploités dans le code Python de la viewlet sous forme d’attributs simple self.foo, ou dans la template dans l’expression TALES view/foo. Si vous avez bien lu ce qui précède, vous savez comment invoquer un viewlet manager depuis une template quelconque, et déclarer la compatibilité d’une viewlet avec un viewlet manager, en faisant la liaison à l’aide de l’attribut “manager” d’une viewlet. 11.4 Le step GenericSetup Malheureusement ceci peut ne pas suffire ! Les déclarations zcml permettent d’associer les viewlets à des viewlet managers, mais pas de définir l’ordre des viewlets au sein du manager... Ceci est réalisé par un step “GenericSetup” dédié, le bien nommé viewlets.xml. 11.4.1 Ordre des viewlets La mise en place d’un profil complet GenericSetup fait l’objet d’un chapitre de la documentation intégrateur ; nous ne nous attarderons que sur ce step spécifique : <?xml version="1.0"?> <object> <order manager="monproduit.monmanager" skinname="*"> <viewlet name="monproduit.maviewlet" /> <viewlet name="monproduit.monautreviewlet" /> 226 Chapitre 11. Les viewlets Plone pour les développeurs, Version 1.0.0 </order> </object> L’élément <order ...> déclare le viewlet manager dans lequel la viewlet sera insérée. Notez que ceci ne crée pas le viewlet manager, la création de celui-ci étant faite dans le fichier ZCML vu dans le paragraphe précédent. La valeur de l’attribut manager doit correspondre à celle de l’attribut “name” du viewlet manager vu dans sa déclaration ZCML. De même, la valeur de l’attribut name des éléments <viewlet ...> doivent correspondre à l’attribut name des éléments <viewlet ...> du fichier configure.zcml. La valeur de l’attribut skinname permet de ne fournir le paramétrage des viewlets que pour une skin, en donnant le nom de cette skin. Par exemple Plone Default, ou My Beautiful Skin. Dans l’exemple ci-avant, la valeur * insère la viewlet pour toutes les skins. La déclaration minimale de step telle que fournie ci-avant remplace l’ensemble des viewlets éventuellement insérées par d’autres steps viewlets.xml dans le viewlet manager monproduit.monmanager. Ceci est sans doute l’effet voulu dans votre cas. Dans de nombreux autres cas, il est également possible d’ajouter des viewlets dans des viewlet managers existants fournis par Plone ou des produits tiers. Dans ce cas, on utilisera plutôt la notation suivante : <?xml version="1.0"?> <object> <order manager="plone.portaltop" skinname="*"> <viewlet name="monproduit.maviewlet" insert-after="plone.header"/> </order> </object> Facile à comprendre : on ajoute pour toutes les skins, dans le viewlet manager plone.portaltop, la viewlet monproduit.maviewlet après la viewlet de nom plone.header. L’attribut insert-after peut également prendre la valeur * pour signifier que la viewlet est insérée en dernière position. Il est également possible, dans le même ordre d’idées, d’insérer l’attribut insert-before. Je n’insulterai pas votre intelligence en décrivant les valeurs possibles de cet attribut. 11.4.2 Viewlets cachées De même, si vous désirez masquer certaines viewlets déclarées dans du code sur lequel vous n’avez pas la main, vous devez passer par le step : viewlets.xml. Il vous suffira d’ajouter un élément <hidden ...>, dont la syntaxe est similaire à celle de <order ...>. Ici, nous masquons le path_bar, appelé aussi breadcrumbs ou navigation horizontale : <hidden manager="plone.portaltop" skinname="Formation Theme" purge="true"> <viewlet name="plone.path_bar" /> </hidden> 11.5 Exemple Nous allons faire très simple, ici. Vous trouverez un exemple plus complet de création de viewlet dans le chapitre Création d’un thème Plone. Imaginons que vous vouliez que la date d’expiration d’un document apparaisse pour les modérateurs du site, sous la ligne d’information de modification (Par xxx - Dernière modification le...) : c’est un cas spécifique qui n’est pas fourni par défaut dans Plone, mais qui est peut-être indispensable à l’application de vos méthodes de travail. 11.5. Exemple 227 Plone pour les développeurs, Version 1.0.0 Nul besoin de modifier la template principale ou quoi que ce soit d’autre. Vous pouvez ajouter un élément en ajoutant une viewlet sans intervenir sur le reste du code. Ajoutez un module viewlets.py dans formation.theme, dans lequel vous ajouterez une classe ExpirationDateViewlet qui hérite de ViewletBase : class ExpirationDateViewlet(ViewletBase): def render(self): expires = self.context.getExpirationDate() return expires and ’<div class="documentByLine">Expire le : %s</div>’ % expires.Date() or Éditez ainsi le fichier configure.zcml de votre produit formation.policy pour déclarer votre viewlet (vous devrez peut être ajouter le namespace xmlns:browser="http://namespaces.zope.org/browser"). <browser:viewlet name="formation.expirationdate" for="Products.ATContentTypes.interface.IATContentType" manager="plone.app.layout.viewlets.interfaces.IBelowContentTitle" class=".viewlets.ExpirationDateViewlet" permission="cmf.ReviewPortalContent" /> Vous n’auriez probablement pas su quoi mettre dans manager. Pour le savoir, il faut consulter la vue @@manage-viewlets, où vous pourrez trouver l’interface du ViewletManager que vous voulez compléter. Redémarrez votre site, vous observerez le nouvel élément sur votre page d’accueil (si vous lui avez donné une date d’expiration...). 11.6 Pour aller plus loin Comme d’habitude, pour comprendre les détails de fonctionnement des mécanismes définis dans ce chapitre, il est nécessaire de lire le code source des modules suivants : TABLE 11.3 – Code à lire également Module Description plone.app.layout Les ressources responsables de la mise en page d’un site Plone plone.app.layout.viewlets Les ressources fournissant les viewlet managers et viewlets standard de Plone. zope.publisher.browser Les ressources Zope 3 de base des viewlets. 228 Chapitre 11. Les viewlets Plone pour les développeurs, Version 1.0.0 F IGURE 11.3 – Viewlet de la date d’expiration 11.6. Pour aller plus loin 229 Plone pour les développeurs, Version 1.0.0 230 Chapitre 11. Les viewlets CHAPITRE 12 Les portlets Author Gilles Lenfant Version 1.0.0 Copyright (C) 2010 Gilles Lenfant. Les portlets sont les boites thématiques disposées dans les colonnes droite et gauche des pages d’un site Plone. Plone fournit en standard quelques portlets, certaines quasi-indispensables telles que la portlet de navigation, d’autres fournissant des services dont l’utilité dépend de l’orientation donnée au site, telles que la portlet calendrier. 12.1 Personnalisation Les lecteurs connaissant les portlets des versions antérieures à Plone 3 seront déroutés lors de leur prise de connaissances de Plone 3 ou 4. En effet, les portlets de nouvelle génération s’apparentent à des contenus d’un nouveau genre qu’un administrateur peut disposer de façon distincte en association avec les divers types de contenus, ou en association avec les divers groupes d’utilisateur, ou encore de façon distincte en association avec chaque contenu individuel. 12.1.1 Portlets des types de contenus En tant qu’administrateur, vous pouvez associer tout type de contenu avec une ou plusieurs portlets qui seront affichées lors de chaque vue d’un contenu du type sélectionné. Rendez-vous d’abord dans le panneau de configuration des types : Choisissez le type de contenu auquel vous voulez associer une (ou plusieurs) portlets, et cliquez le lien “Gérer les portlets affectés à ce type de contenu”. Par exemple, pour les “documents”, allez à l’URL http://votre-site/@@manage-content-type-portlets?key=Do Ceci vous amène à un tableau de bord depuis lequel vous pouvez disposer les portlets de votre choix, portlets qui seront affichées sur chaque vue d’un contenu du type sélectionné, le Document dans notre exemple. Depuis ce tableau de bord, vous pouvez, comme dans cet exemple, ajouter la portlet des événements récents dans la colonne gauche à tous les contenus de type “événement”. 231 Plone pour les développeurs, Version 1.0.0 F IGURE 12.1 – Gestion des portlets assignées à un type de contenus 12.1.2 Portlets des groupes Les portlets d’un groupe seront affichées lors de la visite de l’espace du groupe, et uniquement aux visiteurs authentifiés membre du groupe. Pour gérer les portlets d’un groupe, rendez-vous dans le panneau de configuration des groupes. Sélectionnez l’un des groupes, puis cliquez l’onglet “Portlets de groupe”. De la même façon que pour les types de contenu, un tableau de bord analogue vous invite à ajouter, supprimer ou déplacer des portlets dans les colonnes gauche et droite. 12.1.3 Portlets des contenus ou portlets contextuelles De la même façon que pour les portlets des types de contenus et les portlets des groupes, la gestion des portlets contextuelles est réservée aux administrateurs. Vous noterez que, lorsque vous naviguez dans votre site, figurent sous les deux colonnes de portlets un lien “Gérer les portlets”. Ce lien permet d’agencer les portlets affichées dans le contexte du contenu courant. Vous trouvez dans chaque colonne, de haut en bas : – Un widget d’ajout de portlets dans le contenu courant. En effet, chaque contenu peut avoir sa propre collection de portlets. – La liste des portlets actuellement associées au contenu courant - la racine de Plone dans notre cas. Chaque portlet étant représentée dans un cadre bleu-ciel, dans la skin par défaut. Chaque représentation de portlet étant accompagnée des widgets de déplacement, et de suppression. Si la portlet est paramétrable, le titre de la portlet est un lien hypertexte au formulaire ad hoc. Nous y reviendrons plus loin. – Les règles de blocage/déblocage des portlets, pour les différentes catégories, portlets de groupe et portlets de types de contenu, vu précédemment, ainsi que les portlets contextuelles du dossier parent. Pour chaque catégorie et sur chaque colonne, vous pouvez : – Conserver les paramètres du dossier parent : les règles d’affichage des portlets de la catégorie considérée sont identiques à celles appliquées au dossier parent. – Bloquer : les éventuelles portlets de la catégorie considérée ne seront pas affichées dans ce contexte. – Toujours montrer : les éventuelles portlets de la catégorie considérée seront toujours affichées quelque soit le paramétrage concernant cette catégorie de portlets dans le dossier parent. 12.2 Portlets disponibles en standard Plone fournit en standard toute une liste de portlets. Essayez-les... 232 Chapitre 12. Les portlets Plone pour les développeurs, Version 1.0.0 F IGURE 12.2 – Gestion des portlets de contenus 12.2. Portlets disponibles en standard 233 Plone pour les développeurs, Version 1.0.0 TABLE 12.1 – Portlets standard Nom Login Liste de modération RSS feed Classic portlet Calendar portlet Rechercher Éléments récents Actualités Événements Description La classique portlet d’authentification, montrée uniquement aux visiteurs anonymes. Fournit à un modérateur la liste des contenus en attente d’approbation (voir le chapitre sur les workflows). Présentation des entrées d’un flux RSS. À noter que ceci permet de présenter facilement les premiers élément d’une collection dans une portlet. Une “classic portlet” permet de présenter une portlet conçue pour Plone 2.1 ou Plone 2.5 dans un site Plone 3 ou +. Attention, ce type de portlet ne peut pas bénéficier du paramétrage contextuel comme défini plus loin. Permet de présenter les événements dans une vue calendaire. Permet d’escamoter le widget de recherche du bandeau supérieur et de le placer en portlet Présente les publications les plus récente du site. Présente les articles d’actualités les plus récents. Présente les prochains événements publiés sur le site. 12.3 Retour sur le paramétrage d’une portlet Contrairement aux versions antérieures à Plone 3, le paramétrage d’une portlet est local, et s’applique à l’emplacement dans lequel la portlet est placée. On peut donc considérer ces portlets comme une sorte de paramètre attaché à un dossier ou à un document. Dans le cas des dossiers, ce paramètre est utilisé par défaut dans ses sous-contenus. Ce comportement peut être exploité de diverses manières. Par exemple pour proposer une autre portlet de navigation lors de la visite de certaines rubriques d’un site. 12.4 Habillage graphique Comme pour tout habillage graphique de Plone, l’habillage graphique des porlets doit – dans la mesure du possible – être effectué dans une feuille de styles CSS. Une visite des colonnes de portlets avec Firebug vous indiquera mieux que je ne pourrais le faire ici – et j’ai aussi un peu la flemme – les sélecteurs et classes CSS qui s’appliquent aux différentes colonnes et portlets. 12.5 Export/Import Generic setup du paramétrage des portlets Il est possible d’importer via Generic Setup la configuration complète des portlets, notamment les assignations locales et les paramètres de configuration. 12.6 Création d’une portlet personnelle En contrepartie des nouvelles possibilités permises par l’infrastructure des portlets de Plone, la réalisation d’une portlet personnelle nécessite dorénavant des compétences de développeur. La réalisation d’une portlet repose sur : – Un module Python fournissant les ressources d’édition des paramètres de la portlet – ou plutôt d’une instance de la portlet comme nous l’avons vu plus haut, ainsi que les données publiées par la portlet. – Une template représentant l’aspect graphique de la portlet. 234 Chapitre 12. Les portlets Plone pour les développeurs, Version 1.0.0 – Une déclaration ZCML pour inscrire la portlet dans l’outil de gestion et de publication des portlets de Plone. – Un profil GenericSetup pour installer la portlet dans un site Plone. Pour illustrer ceci, l’auteur de ces lignes préfère vous proposer l’exploration complète d’une portlet simple, mais utile, présentant l’auteur d’un contenu. 12.6.1 Structure du composant Conformément aux conventions d’organisation des composants pour Plone, notre composant est construit selon la structure classique suivante à partir du niveau formation.portlet.author. Je vous invite maintenant pour comprendre les explications qui suivent à installer le code de notre portlet de démo : $ svn co https://svn.plone.org/svn/collective/collective.trainingmanual/trunk.old/fr/examples/form TABLE 12.2 – Fichiers principaux du composant Classique création des ressources globales du module. Constantes propres au produit. Les déclarations ZCML classiques du composant, incluant la directive d’inscription de la portlet. profiles/default/Le profil GenericSetup d’installation de la portlet dan un site. authorportlet.py La définition Python de la portlet, c’est-à-dire l’essentiel de la substance de notre composant. authorportlet.pt La template de mise en page de la portlet. Celle-ci est intentionnellement simplifiée. __init__.py config.py configure.zcml Comme vous êtes un expert de Python et sans doute de Plone puisque vous avez parcouru le site jusqu’ici, vous aurez remarqué que l’auteur de la portlet n’a fait aucun effort pour optimiser le code. En effet, celui-ci est construit dans l’optique d’une lisibilité optimale. Nous y reviendrons plus loin à propos des décorateurs de cache. Jetons maintenant un oeil sur les fichiers essentiels de formation.portlet.author. 12.6.2 __init__.py Peu de choses à dire. Nous créons un “logger” utilisable globalement depuis l’ensemble du produit. Non indispensable, mais utile lors de la mise au point de produit. Un autre classique des composants pour Plone 3+ est la création d’un MessageFactory nécessaire : – à la traduction des différents textes dans les modules Python, – au marquage des textes ou “msgid” à traduire, exploitables depuis i18ndude. Voir Internationalisation d’un composant. Consultez le chapitre sur l’internationalisation et la localisation de ce site pour plus de détails à ce sujet. 12.6.3 config.py Encore un grand classique des produits pour Plone. Les constantes globales au produit sont définies dans ce module. 12.6.4 configure.zcml <configure xmlns="http://namespaces.zope.org/zope" xmlns:five="http://namespaces.zope.org/five" xmlns:genericsetup="http://namespaces.zope.org/genericsetup" xmlns:plone="http://namespaces.plone.org/plone" i18n_domain="formation.portlet.author"> <!-- ... --> 12.6. Création d’une portlet personnelle 235 Plone pour les développeurs, Version 1.0.0 <plone:portlet name="formation.portlet.author.AuthorPortlet" interface=".authorportlet.IAuthorPortlet" assignment=".authorportlet.Assignment" view_permission="zope2.View" edit_permission="cmf.ManagePortal" renderer=".authorportlet.Renderer" addview=".authorportlet.AddForm" editview=".authorportlet.EditForm" /> <!-- ... --> </configure> Également un grand classique des composants pour Plone 3+. Dans notre cas, seules deux directives sont inscrites : – L’inscription du profil “GenericSetup” (supprimée de l’extrait ci-dessus) sur laquelle nous ne nous attarderons pas sachant qu’un chapitre de ce site est consacré à GenericSetup. – L’inscription de la portlet dans l’infrastructure de Plone, qui est détaillée ci-après. La directive <plone:portlet... est une directive spécifique à Plone. Celle-ci doit incorporer les attributs suivants : TABLE 12.3 – Attributs de l’élément <plone :portlet ...>. Nom name Description Le nom unique de la portlet dans le registre global de Plone. Tant pis pour vous si vous reprenez un nom déja utilisé ;) title Le titre de la portlet tel qu’elle apparaitra – après éventuelle traduction – dans l’interface de gestion des portlets vue plus haut. interface La classe d’interface incorporant le schéma (Zope 3) des données d’option de la portlet. Il est rappellé que ces options s’appliquent au contexte dans lequel la portet est définie, contrairement aux portlets pour Plone 1 et Plone 2. assignment Classe utilisée pour stocker les options de la portlet. Le renderer - voir ci-après – de la portlet accède aux options de la portlet par son attribut data. renderer La classe utilisée pour générer le rendu HTML de la portlet. addview Une classe de vue (formulaire à la Zope 3) d’ajout de la portlet. editview Une classe de vue (formulaire à la Zope 3) d’édition des options de la portlet. view_permission La permission dont le visiteur doit bénéficier dans le contexte courant pour afficher la portlet. Dans le cadre de la notre, la permission zope2.View est suffisante. edit_permission La permission dont le visiteur doit bénéficier pour modifier les options de la portlet dans le contexte courant. Dans la plupart des cas – comme dans le notre – la permission cmf.ManagePortal convient parfaitement. Indice : editview Cet attribut doit être bien entendu omis si votre portlet n’a pas d’option de configuration. Comme nous le verrons plus loin, l’essentiel des ressources de cette portlet, référencées dans cette directive, sont définies dans le module “author.py”. 12.6.5 authorportlet.py Ce module fournit l’essentiel du corps de la portlet, et contient les éléments suivants : 236 Chapitre 12. Les portlets Plone pour les développeurs, Version 1.0.0 TABLE 12.4 – Éléments de authorportlet.py Classe Description IAuthorPortlet L’interface définissant le schéma (Zope 3) des options d’une portlet. AssignmentLa classe qui fournit les options de la portlet applicable dans le contexte courant. À noter que les objets de type Assignment sont construits par les objets AddForm que nous verrons plus loin. Dans notre cas, la classe est minimaliste, mais les objets Assignment à d’autres portlets peuvent comporter des méthodes plus complexes. Un objet Assignment est stocké de façon persistante dans le contexte dans lequel la portlet a été créée (objet de contenu, type de contenu, groupe ou dashboard) AddForm Un formulaire “Zope 3” permettant de créer un Assignment. Comme vous pouvez le lire dans le code, la réalisation d’un AddForm est très simple puisque s’appuyant sur le schéma de l’interface de la portlet (IAuthorPortlet dans notre cas). Seule la surcharge de la méthode create responsable de la construction de l’objet Assignment est requise. EditForm Un formulaire “Zope 3” permettant de modifier l’Assignment. Renderer Une classe responsable de la réalisation du rendu HTML de la portlet.Ce type de classe doit obligatoirement comporter une méthode render produisant le HTML de rendu. Comme dans notre exemple, le rendu est généralement délégué à un objet de type ViewPageTemplate. À noter qu’un objet de type Renderer doit propager le constructeur à sa classe mère (voir l’instruction “super(...)” au début du constructeur) et dispose de ce fait des attributs suivants : À noter que ces deux derniers attributs sont rarement utilisés. Vous noterez également que cette classe propose un attribut booléen available. Celui-ci est facultatif et “True” par défaut. Si cet attribut est False, la portlet n’est pas affichée. Pour plus de détails, nous invitons le lecteur à lire le code des classes de base de ces classes. Notez que les différentes classes énumérées ci-dessus sont référencées dans la déclaration de portlet du configure.zcml vu ci-avant. C’est cette déclaration qui fait la “glue” entre les différents composants de notre portlet. 12.6.6 authorportlet.pt Cette template met en page la portlet. Elle est volontairement minimaliste pour en faciliter la compréhension. Je vous laisse le soin de fignoler son habillage graphique si vous voulez l’incorporer dans vos propres réalisations. Vous noterez que : – Les portlets, depuis Plone 2.5, sont définies dans une structure de type <dl class="portlet">...</dl>. – L’objet de classe Renderer défini dans author.py est accessible dans les expressions TALES de cette portlet à travers la variable view. Les différentes données spécifiques à la portlet sont produites par des expressions TALES view/xxx où xxx est une méthode ou un attribut de la classe Renderer vue plus haut. 12.6.7 profiles/default/portlets.xml Fichier d’installation des portlets pour GenericSetup. Dans notre cas nous n’avons qu’une portlet à déclarer. Il n’y a rien de particulier à dire sur les attributs title et description. L’attribut “addview” doit ici avoir la même valeur que l’attribut name de la déclaration de portlet figurant dans notre configure.zcml. 12.7 Et le résultat... Le composant formation.portlet.author a été ajouté dans l’instance Zope, puis ajouté à un site Plone depuis le panneau de configuration d’installation des modules. Vous connaissez déjà ces étapes si vous êtes arrivé 12.7. Et le résultat... 237 Plone pour les développeurs, Version 1.0.0 à ce point du site. Le membre Albert Einstein a réalisé une étude scientifique très intéressante qu’il a publiée dans une page Plone... Comme tout membre d’un site Plone, Albert Einstein a rempli son formulaire de préférences personnelles, incluant son nom complet, sa ville, son mémo perso et sa photo au format requis. Le webmestre du site estime que les portlets classiques publiées par défaut à la droite ne rendent pas justice à l’auteur d’un contenu d’une telle valeur, et préfère remplacer celles-ci par notre portlet de présentation de l’auteur de façon spécifique à cette page. F IGURE 12.3 – L’article tel que publié par Albert Einstein Dans la gestion locale des portlets, on commence par bloquer toutes les portlets pouvant s’appliquer localement. F IGURE 12.4 – On ajoute une “Author portlet” détaillée Si show author details était décoché dans le formulaire d’ajout / modification, la portlet n’afficherait pas “Berlin” ni “Je suis un savant..”. 238 Chapitre 12. Les portlets Plone pour les développeurs, Version 1.0.0 F IGURE 12.5 – Et voilà le résultat... 12.8 Créer votre propre portlet ZopeSkel est votre ami. Puisque vous l’avez installé comme indiqué dans votre environnement de développement, il suffit de taper ceci pour avoir le squelette de votre portlet personnelle : $ paster create -t plone3_portlet ma.belle.portlet Suivez les instructions de l’assistant de création et à vous de jouer. 12.9 Pour aller plus loin Par souci de simplification, deux aspect du développement de portlets ont été passés sous silence dans l’exemple ci-avant : – Les ressources d’internationalisation et traduction de la portlet, les informations relatives à l’internationalisation fournies ailleurs dans ce site étant applicables ici. Voir à ce sujet Internationalisation d’un composant – L’utilisation des décorateurs de cache permettant d’optimiser les méthodes Python, ainsi que code HTML spécifique à la portlet. Le cache des portlets utilise les ressources du module plone.memoize, qui s’utilise sous la forme de décorateurs Python. Dans le cas de cette portlet, les décorateurs de vue (from plone.memoize import view...) auraient pu être utilisés. Pour réaliser des portlets plus complexes ou plus performantes, lisez le code des paquetages Python suivants : TABLE 12.5 – Code source relatif Module plone.app.portlets.portlets zope.formlib zope.schema plone.memoize Description Les portlets fournies en standard par Plone. Le gestionnaire de formulaires de Zope 3. L’alter-ego d’Archetypes pour Zope 3. Divers types de caches Python utilisables sous forme de décorateurs. Indice : Autres portlets Vous pouvez également vous inspirer des nombreuses portlets à votre disposition dans le référentiel Subversion “collective” https ://svn.plone.org/svn/collective, la grande majorité des portlets étant préfixées par collective.portlet.... 12.8. Créer votre propre portlet 239 Plone pour les développeurs, Version 1.0.0 240 Chapitre 12. Les portlets CHAPITRE 13 Les vues standard 13.1 Qu’est-ce que c’est ? Les vues standard, comme leur nom ne l’indiquent pas, ne sont pas des vues HTML sur les contenus d’un site Plone mais des multi-adapteurs (voir Le guide complet de l’Architecture de Composants de Zope) permettant d’accéder aux ressources les plus fréquemment utilisées lors de la programmation de vos applications pour Plone, pour réaliser vos templates ou dans les expressions utilisées dans les actions. 13.2 Pourquoi utiliser ces vues ? Il y a d’autres dispositifs pour obtenir les mêmes informations et services que ceux proposés par les vues standard. Ces API, que nous ne détailleront pas ici, sont maintenues dans Plone 3 et Plone 4 principalement pour faciliter l’adaptation des composants d’extension conçus pour fonctionner avec les versions antérieures à Plone 3. L’utilisation de ces vues en lieu et place des API antérieures ont deux avantages : – L’API est simplifiée et unifiée. L’adjonction de futures fonctionnalités seront reflétées dans ces vues ou d’autres vues pour les services futurs. – Si l’architecture logicielle sous-jacente est amenée à changer dans le futur, l’accès aux services et informations supportée par la dite architecture seront inchangés grâce particulièrement à ces vues. – Les éléments retournés par ces vues sont “cachés” dans la mesure du possible. C’est-à-dire que seul le premier accès à un objet retourné par ces vues est “coûteux” en terme de temps de réponse. Les accès successifs sont immédiats. 13.3 Quelles vues sont proposées ? Plone propose en standard les vues suivantes : 241 Plone pour les développeurs, Version 1.0.0 TABLE 13.1 – Liste des vues standard Nom Description plone_context_state Informations sur l’objet de contexte courant plone_portal_state Informations et services globaux du portail lui-même plone_tools Accès aux tools les plus fréquemment utilisés plone_interface_info Permet de tester les interfaces Zope 3 depuis du code “untrusted” (une template, une expression TALES d’action, un script de skin). Cette vue est également utile à du code “trusted” pour tester une interface connue par son dotted name. plone_layout Fournit l’accès à des méthodes relatives à la mise en page. plone Fournit l’accès aux objets qui étaient auparavant fournis en standard dans la main_template de Plone jusqu’à sa version 3 1 . Warning : Vous noterez que de nombreuses ressources de La vue “plone” sont en fait des reprises ou alias de méthodes figurant dans La vue “plone_portal_state”, La vue “plone_context_state” ou La vue “plone_layout”. Si le composant que vous développez n’est pas supposé être compatible avec une versions de Plone antérieure à Plone 3, vous préférerez l’utilisation des méthodes proposées par ces dernières vues. 13.4 Accéder aux ressources des vues On utilise pour illustrer les accès la vue fictive nommée une_vue, son attribut non moins fictif un_attrib et une méthode une_meth. 13.4.1 Dans une expression TALES d’une action python: object.restrictedTraverse(’@@une_vue’).un_attrib Ou, ce qui revient au même en plus court : object/@@une_vue/un_attrib 13.4.2 Dans une template ZPT <div tal:define="une_meth nocall:context/@@une_vue/une_meth; foo python:une_meth(bar)"> ... </div> 13.4.3 Dans du code Python “untrusted” une_meth = context.restrictedTraverse(’@@une_vue’).une_meth bar = une_meth(foo) 13.4.4 Dans du code Python “trusted” dans un fichier On peut utiliser la méthode standard restrictedTraverse(’@@une_vue’) comme dans du code “untrusted”. Cependant, et même si cette approche est plus verbeuse, elle est plus efficace en terme de performances. 242 Chapitre 13. Les vues standard Plone pour les développeurs, Version 1.0.0 from zope.component import getMultiAdapter # ... # Fournir l’objet de contexte par le moyen le plus approprié context = myOwnWayToGetIt() # Comme bon vous semble # Fournir l’objet request. Par défaut, vous pourrez utiliser l’acquisition # À noter que dans les classes de vues, ces deux objets sont généralement # self.context et self.request request = context.REQUEST # Obtenir la vue une_vue = getMultiAdapter((context, request), name=u’une_vue’) # Et l’utiliser foo = une_vue.un_attrib bar = une_vue.une_meth(dummy) Les différentes vues La vue “plone_context_state” Cette vue fournit de nombreuses méthodes et attributs fournissant des informations sur l’objet de contexte publié. À noter que dans vos templates utilisant la macro de gabarit de page par défaut, cette vue est accessible par l’objet context_state. <html use-macro="context/@@standard_macros/page"> ... <div>Le titre de cette page <span tal:content="context_state/object_title">Un titre</span> </div> ... </html> Détails dans plone.app.layout.globals.interfaces.IContextState 13.4. Accéder aux ressources des vues 243 Plone pour les développeurs, Version 1.0.0 TABLE 13.2 – plone_context_state Attribut Description Méthode current_page_url() L’URL de la page courante, incluant la template et la query string (paramètres CGI) : http://site/objet/template?foo=bar current_base_url() L’URL de la page courante, incluant la template mais sans la query string éventuelle (paramètres CGI) : http://site/objet/template canonical_object() L’objet canonique courant, c’est à dire l’objet publié. Si l’objet publié est la page par défaut d’un dossier, le dossier est retourné par cette méthode. canonical_object_url() L’URL du canonical_object vu ci-dessus. view_url() URL à utiliser pour voir la page présentant l’objet. Les images et fichiers sont directement téléchargés lorsque le navigateur évoque leur canonical_object_url mentionnée ci-avant. view_template_id() Le nom de la template actuellement utilisée pour cette page. is_view_template() True si l’URL actuelle (celle de la requête HTTP) utilise la vue standard de l’objet publié (celle de l’onglet “Voir”) object_url() L’URL de vue de l’objet publié. N’est pas forcément équivalent à canonical_object_url() vu ci-dessus si l’objet publié si l’on publie un dossier dont la page par défaut est l’un de ses contenus. object_title()Titre ou identifiant (en absence de titre) de l’objet publié. workflow_state() Nom de l’état de workflow de l’objet publié. parent() L’objet parent de l’objet publié. folder() Le dossier canonique courant, c’est-à-dire la même chose que parent() ci-dessus si l’objet courant n’est pas folderish (voir is_folderish() ci-dessous) is_folderish()True si l’objet publié peut contenir des sous-objets enfants. Type dossier ou apparenté. is_structural_folder() True si l’objet publié est un dossier permettant la navigation et la structuration interne libre. is_default_page() True si l’objet publié est la page par défaut de son dossier parent. is_portal_root() True si l’objet publié est le site plone ou sa page d’accueil. is_editable() True si l’objet publié est modifiable par l’utilisateur courant (Permet d’afficher ou non l’onglet Modifier). is_locked() True si un verrou WebDAV est posé sur l’objet publié. actions() Les actions filtrées sur le contexte, dépendant des droits de l’utilisateur courant. Toutes les catégories d’actions sont fournies sous la forme {catégorie: [{action}, {action}, ...], ...}. portlet_assignable() True si l’objet publié peut se voir assigner des portlets. Assigner des portlets signifie dans ce cas qu’une disposition de portlets spécifique à cet objet peut être envisagée (affichage du lien de gestion des portlets). Et non que des portlets peuvent être affichées, ce qui est toujours le cas. La vue “plone_portal_state” Les informations globales sur le portail sont accessibles à travers les méthodes et attributs de cette vue. À noter que dans vos templates utilisant la macro de gabarit de page par défaut, cette vue est accessible par l’objet portal_state. <html use-macro="context/@@standard_macros/page"> ... <div>Le titre du portail <span tal:content="portal_state/portal_title">Un titre</span> </div> ... </html> Détails dans plone.app.layout.globals.interfaces.IPortalState 244 Chapitre 13. Les vues standard Plone pour les développeurs, Version 1.0.0 TABLE 13.3 – plone_portal_state Attribut - Méthode Description portal() L’objet site Plone (voir également à la fin de ce tableau) portal_title() Le titre du portail. portal_url() L’URL de la racine du portail (voir navigation_root_url). navigation_root_path() Le path relatif de la racine de navigation du site (la page d’accueil). navigation_root_url() L’URL de la page d’accueil du site (celle du lien du logo du site ou du lien “Accueil” du breadcrumb / fil d’Ariane) default_language()L’abréviation IANA de la langue par défaut du site, comme fr pour le Français. language() L’abréviation IANA de la langue utilisée pour la publication de la page courante. locale() La locale utlisée pour la publication de la page courante (voir le module standard Python locale). is_rtl() La langue courante (voir language() plus haut) s’écrit de droite à gauche comme l’Arabe ou l’Hébreu. member() L’objet représentant le membre courant. None si anonyme. anonymous() True si l’utilisateur courant est anonyme. friendly_types() Liste de portal_types compatibles avec la recherche. Indice : Objet portail Grâce à la ZCA décrite dans Le guide complet de l’Architecture de Composants de Zope, une méthode plus rapide permet d’obtenir l’objet portail de façon plus efficace : from zope.component.hooks import getSite # ... portal = getSite() La vue “plone_tools” Cette vue permet d’accéder de façon efficace aux tools Plone les plus fréquemment utilisés. Détails dans plone.app.layout.globals.interfaces.ITools TABLE 13.4 – plone_tools Attribut - Méthode actions() catalog() membership() properties() syndication() types() url() workflow Description Le tool portal_actions. Le tool portal_catalog. Le tool portal_membership. Le tool portal_properties. Le tool portal_syndication. Le tool portal_types. Le tool portal_url Le tool portal_workflow La vue “plone_interface_info” Cette vue permet de vérifier la conformité d’un objet ou d’une classe avec une interface dont on ne connait que le dotted name. Détails dans plone.app.layout.globals.interfaces.IInterfaceInformation 13.4. Accéder aux ressources des vues 245 Plone pour les développeurs, Version 1.0.0 TABLE 13.5 – plone_interface_info Attribut - Méthode provides(dotted_name) Description True si l’objet de contexte fournit l’interface donnée par le dotted_name class_provides(dotted_name) True si la classe de l’objet de contexte fournit l’interface donnée par le dotted_name Attention : Une interface marqueur peut être assignée à un objet particulier sans que la classe du dit objet n’implémente cette interface. Donc on peut très bien avoir cette situation : >>> pii = context.restrictedTraverse(’@@plone_interface_info’) >>> pii.class_provides(’some.great.Interface’) True >>> # Si la classe implémente, alors l’objet aussi (forcément) >>> pii.provides(’some.great.Interface’) True >>> pii.class_provides(’another.great.Interface’) False >>> # Si la classe n’implémente pas l’objet lui le peut. >>> pii.provides(’another.great.Interface’) True Voir Le guide complet de l’Architecture de Composants de Zope pour de plus amples explications. La vue “plone_layout” Cette vue fournit des méthodes relatives à la mise en page. Détails dans plone.app.layout.globals.interfaces.ILayoutPolicy TABLE 13.6 – plone_layout Attribut - Méthode mark_view(view) Description Ajoute une interface marqueur à la vue si celle-ci est “la” vue pour l’objet de contexte. Cette conction ne devrait être appellée que depuis une template. hide_columns(column_left, Fournit la classe CSS pour masquer les colonnes de portlets vides. Vous aurez column_right) rarement à utiliser ce genre de méthode. have_portlets(manager_name, Détermine si une colonne de portlets est affichée. La colonne de gauche est view=None) appellée plone.leftcolumn, celle de droite plone.rightcolumn. icons_visible() True si les icônes doivent être affichées. Voir getIcon(item) ci-dessous. getIcon(item) Retourne un objet qui implémente l’interface IContentIcon qui fournit les informations et services nécessaires au rendu d’une icône. L’élément de contexte doit être adaptable à IContentIcon. Les icônes peuvent être désactivés globalement ou seulement aux anonymes avec la propriété icon_visibility de l’objet portal_properties/site_properties. renderBase() L’URL devant figurer dans la balise <base...> du <head> HTML. Vous aurez rarement l’occasion d’utiliser cette méthode. bodyClass(template, Retourne la classe CSS à utiliser sur le tag <body> lors de la publication de la view) page courante. Comme la plupart des templates de base sont déjà fournies par Plone, vous n’aurez dans les faits que très rarement l’occasion d’utiliser cette méthode. La vue “plone” Cette vue fournit nombre d’informations particulièrement utiles dans la construction de vos templates. 246 Chapitre 13. Les vues standard Plone pour les développeurs, Version 1.0.0 À noter que dans vos templates utilisant la macro de gabarit de page par défaut, cette vue est accessible par l’objet plone_view. <html use-macro="context/@@standard_macros/page"> ... <div>Cette URL est <span tal:content="plone_view/getCurrentUrl">http://foo</span> </div> ... </html> Détails dans Products.CMFPlone.browser.interfaces.IPlone Note : Vous noterez à la lecture des méthodes proposées que nombre d’entre elles sont reprises de La vue “plone_context_state”, La vue “plone_portal_state” ou La vue “plone_layout”. Celles-ci sont des raccourcis facilitant la compatibilité ascendante avec les versions antérieures de Plone. Pour programmer un composant qu’il est inutile de rendre compatible avec ces anciennes version, on préfèrera l’utilisations des méthodes équivalentes de La vue “plone_context_state”, La vue “plone_portal_state” ou La vue “plone_layout”. 13.4. Accéder aux ressources des vues 247 Plone pour les développeurs, Version 1.0.0 TABLE 13.7 – plone Attribut - Méthode getCurrentUrl() visibleIdsEnabled() uniqueItemIndex(pos=0) toLocalizedTime(time, long_format=None, time_only=None) normalizeString(text) isDefaultPageInFolder() isStructuralFolder() hide_columns(self, column_left, column_right) navigationRootPath Description URL courante incluant la query string (paramètres CGI) True si la modification des id des éléments de contenus sont permis au niveau du portail comme au niveau de l’utilisateur. Retourne un itérateur utilisable pour énumérer des éléments dans une template avec la méthode next. Exemple : tal:define="foo plone_view/uniqueItemIndex; un foo/next; deux foo/next" Convertit un objet DateTime ou une date sous forme de chaîne normalisée ISO 8601 en une date et heure localisées sous forme de chaîne de caractères. Transforme un texte quelconque éventuellement unicode en une chaîne ASCII compatible avec les identifiants Zope. Equivalent à la méthode is_default_page de La vue “plone_context_state”. Equivalent à la méthode is_structural_folder de La vue “plone_context_state”. Equivalent de la méthode hide_columns(self, column_left, column_right) de La vue “plone_layout”. Equivalent de la méthode navigation_root_path de La vue “plone_portal_state”. navigationRootUrl() Equivalent de la méthode navigation_root_url de La vue “plone_portal_state”. getParentObject() Equivalent de la méthode parent de La vue “plone_context_state”. getCurrentFolder() Equivalent de la méthode folder de La vue “plone_context_state”. getCurrentFolderUrl() L’URL du dossier retourné par getCurrentFolder ci-dessus. getCurrentObjectUrl() Equivalent de la méthode canonical_object_url de La vue “plone_context_state”. isFolderOrFolderDefaultPage() Equivalent de la méthode is_default_page de La vue “plone_context_state”. isPortalOrPortalDefaultPage() Equivalent de la méthode is_portal_root de La vue “plone_context_state”. getViewTemplateId() Equivalent de la méthode view_template_id de La vue “plone_context_state”. showEditableBorder() Détermine si le cadre d’édition (contenant les onglets “Contenu”, “Modifier”, ...) doit être affiché. displayContentsTab() Détermine si les onglets d’édition doivent être affichés. getIcon(item) Equivalent de la méthode getIcon(item) de La vue “plone_layout”. cropText(text, length, Tronque un texte brut jusqu’à length caractères tout en omettant ellipsis) l’éventuel mot coupé. have_portlets(manager_name, Equivalent de la méthode have_portlets(manager_name, view=None) view=None) de La vue “plone_layout”. mark_view(view) Equivalent de la méthode mark_view(view) de La vue “plone_layout”. site_encoding() L’encodage du texte de ce site. Typiquement utf-8. bodyClass(template, Equivalent de la méthode bodyClass(template, view) de La view) vue “plone_layout”. 248 Chapitre 13. Les vues standard CHAPITRE 14 Internationalisation d’un composant Author Vincent Fretin Version 1.0.0 Contributors Thomas Desvenain Copyright (C) 2010, Vincent Fretin <vincentfretin AT gmail.com>. Plone est multilingue. Cette partie explique comment rendre votre composant disponible en plusieurs langues. Internationalisation et localisation Nous distinguerons l’internationalisation (i18n), qui consiste à utiliser l’API de production de textes dans le code Python et les templates, de la localisation (l10n), qui consiste à fournir la traduction des labels produits lors de l’internationalisation. 14.1 Exemples de la formation Les exemples de cette formation sont dans le package collective.trainingmanual.i18nexamples. Pour l’installer, ajoutez à votre buildout : [buildout] eggs += # ... collective.trainingmanual.i18nexamples auto-checkout += # ... collective.trainingmanual.i18nexamples [instance] zcml += # ... collective.trainingmanual.i18nexamples [sources] collective.trainingmanual.i18nexamples = svn https://svn.plone.org/svn/collective/collective.train Vous pourrez observer les exemples de cette formation en allant à l’adresse /i18nexamples depuis la racine de votre site. 249 Plone pour les développeurs, Version 1.0.0 14.2 Internationaliser un composant 14.2.1 Connaissances requises Nous recommandons aux lecteurs de prendre préalablement connaissance des mécanismes standards d’internationalisation des applications, basé sur le standard “GNU-gettext” http ://fr.wikipedia.org/wiki/Gettext Les amateurs de détails pourront se reporter à la documentation complète en anglais ici : http ://www.gnu.org/software/gettext/manual/gettext.html 14.2.2 Notion de domaine Plone et plus généralement Zope (puisque nous parlons d’internationalisation Zope) sont des écosystèmes générant des centaines de composants d’extension sur l’utilité desquels nous ne reviendrons pas ici. Un intégrateur est susceptible de mixer nombre de ces composants. Ceci risque donc d’amener nombre de conflits de labels, le même label pouvant être traduit différemment selon l’emploi qui en est fait. Pour éviter ceci, le standard “gettext” définit la notion de “domaine” qui est un espace de nommage pour les traductions de labels. Ainsi le label “foo” peut être traduit différemment dans autant de domaines distincts. Dès lors qu’un composant propose une interface utilisateurs comportant du texte, il doit déclarer son domaine et le répertoire racine des traductions (traditionnellement, le répertoire locales/ de votre egg). Vous allez donc ajouter au fichier configure.zcml principal de votre composant les ligne suivantes : <configure xmlns="http://namespaces.zope.org/zope" xmlns:i18n="http://namespaces.zope.org/i18n" i18n_domain="i18nexamples"> ... <i18n:registerTranslations directory="locales" /> ... </configure> 14.2.3 Code Python Vous placez ensuite dans le __init__.py principal de votre composant les lignes suivantes : from zope.i18nmessageid import MessageFactory MyMessageFactory = MessageFactory(’i18nexamples’) Écrit en C Afin d’optimiser les performances, les primitives des MessageFactory sont écrites en C. Lorsque vous voulez placer un texte susceptible d’être traduit dans votre code, vous devez procéder ainsi. from collective.trainingmanual.i18nexamples import MyMessageFactory as _ # ... def message(bar): message = _(u’my_message’, default=u"Some words") # ... 250 Chapitre 14. Internationalisation d’un composant Plone pour les développeurs, Version 1.0.0 Utiliser les labels du domaine “plone” Pour des raisons de cohérence globale de votre site, il est recommandé, dans la mesure du possible de reprendre les traductions déjà fournies par Plone. Pour effectuer ceci dans du code Python : from Products.CMFPlone import PloneMessageFactory as PMF # ... def save(value): # ... message = PMF(u’Save’) # Traduit en français par "Enregistrer" Dans ce cas particulier, il n’est pas utile de fournir le paramètre nommé default à la messagefactory. Quelques explications : – Le symbole _ est un identificateur Python légal. Comme nous le verrons par la suite, i18ndude cherche dans tout le code Python de votre composant l’utilisation de cette fonction pour élaborer les modèles de dictionnaires (fichier *.pot). – my_message est le label de traduction qui figurera, après traitement par i18ndude, dans le modèle de dictionnaire *.pot de votre composant. – Some words est le texte par défaut, en anglais, du label my_message qui sera fourni aux visiteurs du site en l’absence de traduction spécifique pour son langage. Bien que, comme la syntaxe l’indique, le paramètre nommé default n’est pas obligatoire (votre code ne plantera pas) son utilisation est fortement recommandée. Messages avec variables Il est parfois nécessaire d’introduire des parties variables dans les messages à traduire. Par exemple : “Ce dossier contient 5 image(s)”. Utilisez pour ceci le paramètre nommé mapping ayant pour valeur un dictionnaire (ou objet quelconque ayant le même protocole) fournissant toutes les clés prévues par les emplacements réservés aux variables dans le message. Ces emplacements variables sont marqués dans le message sous la forme ${clé}. def map(value): # ... msg_map = {’count’: value} message = _(u’images_in_folder’, default=u"This folder has ${count} images", mapping=msg_map) 14.2.4 Templates ZPT De la même façon que le code Python, les templates ZPT peuvent être internationalisée. Le marquage d’internationalisation est effectué en utilisant l’espace de nommage XML i18n. <html xmlns="http://www.w3.org/1999/xhtml" xmlns:tal="http://xml.zope.org/namespaces/tal" xmlns:metal="http://xml.zope.org/namespaces/metal" xmlns:i18n="http://xml.zope.org/namespaces/i18n" > ... </html> i18n :domain : Déclaration du domaine Pour marquer le domaine de traduction d’une template (ou d’un élément quelconque figurant dans une template) la déclaration préalable de ce dernier est nécessaire dans un élément englobant. 14.2. Internationaliser un composant 251 Plone pour les développeurs, Version 1.0.0 ... <div i18n:domain="machin"> <!-- Les textes marqués pour traductions utiliseront les messages des dictionnaires du domaine "machin" --> ... <table i18n:domain="truc"> <!-- Les textes marqués de ce tableau utiliseront par contre les dictionnaires du domaine "truc" --> ... </table> </div> i18n :translate : Traduction du contenu d’un élément HTML Le contenu de cette <div> sera traduit selon la clé label_truc du domaine de traduction courant. ... <div i18n:translate="label_truc">Some stuff</div> ... Le contenu de cette <div> sera traduit selon la clé Some stuff du domaine de traduction courant. En effet, lorsque l’attribut i18n:translate est vide, le contenu de la balise est utilisé comme clé de traduction. ... <div i18n:translate="">Some stuff</div> ... Ceci permet de traduire des textes dont le contenu ne peut être connu qu’au moment de la publication de la template. ... <!-- On traduit le titre lorsque c’est possible --> <span i18n:translate="" tal:content="context/title_or_id">Ze title</span> i18n :attributes : Traduction des attributs d’un élément Les attributs title et alt sont traduits selon la clé fournie par leurs valeurs (statique ou calculée) ... <img title="img_title" alt="" tal:attributes="alt image/title" i18n:attributes="alt title" /> ... i18n :name placer des données variables dans une traduction. Il existe un système permettant de faire le mapping de messages ‘à trous’ dans les templates. Reprenons notre exemple précédent : This folder has ${count} images. Si on ne veut pas générer le message dans le code mais dans la template, on écrira : ... <p>Hi</p> Note Utilisez tout le temps i18n :translate=”“ dans vos templates, même si vous appelez une fonction qui retourne un objet Message (une chaine commencant par _( dans votre code Python). Bien que la traduction marcherait 252 Chapitre 14. Internationalisation d’un composant Plone pour les développeurs, Version 1.0.0 sans le préciser avec le moteur de template d’origine, cela ne marcherait pas avec le moteur de template alternatif Chameleon qui peut être utilisé dans Plone 4. 14.2.5 Traduire directement un message Vous pouvez avoir besoin de traduire un message directement dans le code python, si pour une raison ou une autre vous ne pouvez pas retourner un message à votre template. Dans ce cas, utilisez directement l’API de traduction de zope.i18n. Vous devez passer en paramètre la request, qui contient les informations permettant de retrouver la langue de l’utilisateur. def translated(self): return "Some words" #message = _(u’my_message’, default=u"Some words") #return translate(message, context=self.request) 14.3 Localiser un composant Créez un environnement avec i18ndude installé : $ mkvirtualenv i18n $ easy_install i18ndude Attention N’installez jamais i18ndude dans l’environnement Python standard ou l’environnement virtuel spécifique à un projet mais bien dans un environnement virtuel dédié. Dans collective/trainingmanual/i18nexamples/ créez un script update-l10n.sh : $ i18ndude rebuild-pot --pot i18nexamples.pot --create i18nexamples .. $ i18ndude sync --pot i18nexamples.pot */LC_MESSAGES/i18nexamples.po Le nom des fichiers po doit correspondre au domaine. Exécutez i18ndude -h pour savoir à quoi servent les différentes commandes. Installez le package Ubuntu gettext qui contient notamment la commande msginit. Installez également poedit (ou kbabel) pour traduire facilement : $ sudo apt-get install gettext poedit Exécutez le script update-l10n.sh, pour cela ajoutez le mode exécution : $ chmod u+x update-l10n.sh $ ./update-l10n.sh La première exécution du script échouera pour les commandes i18ndude sync car il n’y a encore aucune traduction. Pour le domaine i18nexamples, il vous faut tout d’abord créer le dossier dans locales : $ mkdir -p fr/LC_MESSAGES Générez le fichier i18nexamples.pot avec le script : $ ./update-l10n.sh Ensuite créez les fichiers de traduction : 14.3. Localiser un composant 253 Plone pour les développeurs, Version 1.0.0 $ msginit -i i18nexamples.pot -o fr/LC_MESSAGES/i18nexamples.po éditez les headers du fichier po comme ceci : "Language-Code: fr\n" "Language-Name: French\n" "Domain: i18nexamples\n" Traduisez ensuite avec poedit. Pour que vos traductions soient prises au démarrage de l’instance, vous devez également avoir la directive i18n :registerTranslations dans votre configure.zcml, par exemple : <configure xmlns="http://namespaces.zope.org/zope" xmlns:grok="http://namespaces.zope.org/grok" xmlns:i18n="http://namespaces.zope.org/i18n" i18n_domain="i18nexamples"> <i18n:registerTranslations directory="locales" /> <grok:grok package="." /> </configure> 14.4 La syntaxe des po en quelques mots "Header: valeur du header\n" #: src/name.c:36 msgid "identifiant_du_message" msgstr "La traduction du message" 14.5 Modifier les traductions de Plone 14.5.1 Créer un fichier de traduction du domaine plone Si vous voulez modifier des traductions de Plone pour les remplacer par vos propres messages créez à la main dans locales un fichier plone.pot. Ajoutez cette ligne à update-l10n.sh : i18ndude sync --pot plone.pot */LC_MESSAGES/plone.po Ajoutez le fichier de traduction française avec la commande : msginit -i plone.pot -o fr/LC_MESSAGES/plone.po Puis éditez ce fichier pour indiquer la langue, comme vu plus haut. 14.5.2 Modifier une traduction du domaine plone Si vous voulez modifier une traduction, recherchez d’abord le msgid, qui sera probablement dans plone.app.locales. Recopiez l’entrée concernée dans le fichier plone.pot de votre package. Le msgstr doit bien sûr être vide. 254 Chapitre 14. Internationalisation d’un composant Plone pour les développeurs, Version 1.0.0 #. Default: "Log out" #: CMFPlone/profiles/default/actions.xml msgid "Log out" msgstr "" Relancez le script de mise à jour. Vous pouvez maintenant éditer le .po de version française. Mettez “Me déconnecter” (au lieu de “Se...”), puis redémarrez le site. 14.6 Mise à jour des traductions Quand vous ajoutez des nouveaux messages dans votre composant, allez dans le répertoire locales et relancez le script ./update-l10n.sh. Les nouveaux messages du domaine i18nexamples seront ajoutés dans votre pot, puis synchronisés dans fr/LC_MESSAGES/i18nexamples.po. Vous n’aurez plus qu’à compléter les traductions. Les fichiers .po sont compilés en fichiers .mo. À chaque redémarrage du serveur, zope détecte les .po qui ont été modifiés et les recompile. 14.7 Ressources – – – – – – http ://plone.org/documentation/how-to/product-skin-localization http ://plone.org/documentation/how-to/i18n-for-developers http ://www.mattdorn.com/content/plone-i18n-a-brief-tutorial/ http ://maurits.vanrees.org/weblog/archive/2007/09/i18n-locales-and-plone-3.0 http ://dev.plone.org/plone/wiki/TranslationGuidelines http ://n2.nabble.com/Recipe-for-overriding-translations-td3045492ef221724.html Voir http ://dev.plone.org/plone/ticket/9090 pour l’état de la fusion des documents ci-dessus. 14.6. Mise à jour des traductions 255 Plone pour les développeurs, Version 1.0.0 256 Chapitre 14. Internationalisation d’un composant CHAPITRE 15 Gestion des utilisateurs avec PlonePAS 15.1 Présentation Comme vu dans le petit précis sur Zope, les utilisateurs, mots de passes et leurs rôles associés sont gérés dans des “User folders”. Ces objets ont l’identificateur réservé acl_users. Si l’outil fourni en standard avec Zope est amplement suffisant pour une authentification simple et l’octroi de droits à quelques utilisateurs, il est insuffisant au regard des besoins d’un CMS professionnel tel que Plone. Depuis sa version 2.5, la gestion des utilisateurs est motorisée par un service plus complet et flexible que le simple BasicuserFolder. Il s’agit de PlonePAS, basé sur le composant PluggableAuthService (PAS) de Zope corp. 1 A la gestion des login / mot de passe et des rôles des utilisateurs, PlonePAS ajoute : – La distinction entre le login et le user id – La gestion des propriétés des utilisateurs (adresse mail, nom complet, ...) – La gestion des groupes d’utilisateurs – L’attribution de rôles globaux et contextuels De plus, et comme son nom l’indique, les mécanismes de base de PlonePAS sont fournis par des composants dits plugin qui permettent une grande souplesse pour personnaliser ses propres règles : – D’extraction des données d’authentification – D’authentification – D’attribution de rôles – De classement des utilisateurs dans des groupes PlonePAS est muni d’origine d’un ensemble de plugins standard pré-paramétrés lors de la construction du site qui fournissent les services par défaut. De nombreux plugins de tierce partie permettent de personnaliser tout ceci pour intégrer au mieux votre site Plone dans votre système d’informations et selon votre politique de sécurité. Par exemple et avec une simple intégration de composants tiers et paramétrage ad hoc vous pouvez tout à la fois : – Intégrer Plone dans l’infrastructure SSO (NTLM, CAS) de votre entreprise – Assigner automatiquement l’adresse mail <[email protected]> à chaque utilisateur – Obtenir le nom complet et le département d’un utilisateur dans un serveur LDAP ou ActiveDirectory – Grouper les utilisateurs par fonction (direction, ingénierie, commercial, production, marketing par exemple) et par entités (fromages, apéritifs par exemple) à l’aide d’une requête dans un SGBDR externe classique (MySQL, Oracle...) Ceci n’est qu’un exemple parmi tant d’autres envisageables et plus ou moins complexes. 1. Notons que PAS n’est pas un composant strictement Plone et peut être utilisé dans n’importe quelle application Zope. 257 Plone pour les développeurs, Version 1.0.0 15.2 Architecture fonctionnelle Lorsque l’un des services de PlonePAS est sollicité par l’application - Plone ou l’un de ses composants d’extension – celui-ci sollicite l’un après l’autre chaque plugin compatible avec l’opération demandée. L’ordre de sollicitation des plugins pour chaque service est paramétrable en ZMI par l’intégrateur. Le schéma générique ci-dessous résume les opérations effectuées lors de l’utilisation par une application de l’un des services de PlonePAS. Quelques commentaires sur ce schéma : – L’utilisation du cache n’est pas systématique. Dans certains cas, les plugins eux-même peuvent être appellés à gérer leur propre cache. – Pour certains services de PAS, plusieurs catégories de plugins peuvent être utilisées. Le bloc “Sélection des plugins ...” et “Traitement des données ...” étant alors réitéré pour une autre catégorie de plugins. 258 Chapitre 15. Gestion des utilisateurs avec PlonePAS Plone pour les développeurs, Version 1.0.0 – Pour certains services, par exemple l’authentification d’un utilisateur, PlonePAS ne retient les résultats que d’un seul plugin, le premier qui a fourni un résultat positif. Pour d’autres services, tel que l’obtention des propriétés d’un utilisateur, les résultats fournis par l’ensemble des plugins sélectionnés sont agglomérés. 15.3 PlonePAS en ZMI Un petit tour en ZMI vous permettra de mieux comprendre tout ceci... La vue principale d’un PlonePAS expose l’ensemble des plugins - ici ne sont exposés que les plugins installés par défaut sur un site Plone 4 - ainsi que l’objet plugins, chef d’orchestre des règles de fonctionnement de PlonePAS. Intéressons-nous à ce dernier en premier. 2 15.3.1 Catégories de plugins Ouvrons maintenant l’objet acl_users/plugins qui est le “chef d’orchestre” de l’utilisation des plugin. 2. Ceci est un oxymore, je le sais. 15.3. PlonePAS en ZMI 259 Plone pour les développeurs, Version 1.0.0 Cette vue vous propose les différentes catégories de plugins de PlonePAS. Comme vous pouvez le constater, chaque catégorie de plugin est expliquée brièvement. Une petite traduction commentée : 260 Chapitre 15. Gestion des utilisateurs avec PlonePAS Plone pour les développeurs, Version 1.0.0 Anonymoususerfactory Plugins Les plugins de ce type sont des factories d’utilisateurs anonymes. Plone en standard n’en utilise pas, sachant qu’il ne propose pas de fonctionnalité nécessitant un objet évolué, contrairement aux utilisateurs authentifiés. Interface : Products.PluggableAuthService.interfaces.plugins.IAnonymousUserFactoryPlugin Authentication Plugins Les plugins de ce type sont responsable de vérifier la validité d’une accréditation, généralement un couple identité, mot de passe. Interface : Products.PluggableAuthService.interfaces.plugins.IAuthenticationPlugin Challenge Plugins Ces plugins sont chargés de retourner directement ou indirectement une ressource à partir de laquelle l’utilisateur pourra fournir ses accréditations - par exemple la page d’authentification. Ce genre de plugin est sollicité lorsqu’un utilisateur anonyme tente d’accéder directement ou indirectement à une ressource protégée. Interface : Products.PluggableAuthService.interfaces.plugins.IChallengePlugin Challenge Protocol Chooser Plugins Ces plugins sont chargés de fournir une liste de protocoles (HTTP, FTP, ...) pour lesquels un challenge d’authentification peut être lancé. Vous n’aurez généralement pas besoin de changer le paramétrage du chooser standard fourni avec Plone, encore moins de réaliser votre propre plugin de ce type. Interface : Products.PluggableAuthService.interfaces.plugins.IChallengeProtocolChooser Reset Credentials Plugins Les plugins de ce type sont chargés de supprimer le jeton d’authentification de l’utilisateur. Ils sont sollicités lorsque l’utilisateur de déconnecte du site. Comme le jeton d’authentification est récupéré par un plugin de type Extraction Plugins, et est placé par un plugin de type Update Credentials Plugins, ces trois rôles sont généralement assurés par le même plugin qui est donc un multiplugin. Interface : Products.PluggableAuthService.interfaces.plugins.ICredentialsResetPlugin Update Credentials Plugins Les plugins de ce type sont chargés de fournir au client le jeton d’authentification, après une authentification réussie à travers un Authentication Plugins ou un changement de mot de passe. Interface : Products.PluggableAuthService.interfaces.plugins.ICredentialsUpdatePlugin Extraction Plugins Les plugins de ce type ont pour rôle d’extraire l’accréditation du request. C’est-à-dire le jeton d’authentification de l’utilisateur authentifié. Dans le cas du protocole HTTP, celui-ci est généralement placé dans un header (typiquement WWW-Authenticate) ou dans un cookie. C’est typiquement ce type de plugin que vous devrez réaliser pour intégrer votre site Plone dans une infrastructure SSO. Interface : Products.PluggableAuthService.interfaces.plugins.IExtractionPlugin 15.3. PlonePAS en ZMI 261 Plone pour les développeurs, Version 1.0.0 Group_Enumeration Plugins Les plugins de ce type permettent de retrouver des informations sur des groupes en fonction de critères de recherche. Interface : Products.PluggableAuthService.interfaces.plugins.IGroupEnumerationPlugin Group_Introspection Plugins Les plugins de ce type sont sollicités pour fournir des informations avancées sur les groupes (informations étendues d’un groupe, membres d’un groupe, liste de groupes, ...). En général, ce genre de plugin est un multiplugin qui est également un Group_Enumeration Plugins Interface : Products.PlonePAS.interfaces.group.IGroupIntrospection Group_Management Plugins Les plugins de ce type gèrent des groupes dits “mutables”, c’est à dire dont les caractéristiques et les membres sont gérés à travers Zope et Plone comme c’est le cas du gestionnaire de groupes fourni en standard par Plone. Un multiplugin de ce type est généralement également de type Groups Plugins. Interface : Products.PlonePAS.interfaces.group.IGroupManagement Groups Plugins Les plugins de ce type fournissent les noms des groupes auxquels appartient un principal. Interface : Products.PluggableAuthService.interfaces.plugins.IGroupsPlugin Local_Roles Plugins Les plugins de ce type fournissent les rôles dont dispose un principal dans un contexte donné. Interface : Products.PlonePAS.interfaces.plugins.ILocalRolesPlugin Properties Plugins Les plugins de ce types sont chargés de fournir des propriétés à un principal (exemple : nom comlet, adresse mail, etc.). Ces propriétés sont fournies sous forme de. Lors de la collection des propriétés d’un principal, les propriétés fournies par l’ensemble des Properties Plugins activés sont cumulées. Interface : Products.PluggableAuthService.interfaces.plugins.IPropertiesPlugin À noter que les plugins de ce type peuvent également implémenter l’interface Products.PlonePAS.interfaces.plugins.IMutablePropertiesPlugin lorsqu’ils offrent à Plone la possibilité de modifier les propriétés de l’utilisateur. Request_Type_Sniffer Plugins Les plugins de ce type ont pour rôle de déterminer le type de requête de l’application cliente (WebDav, XML-RPC, FTP ou navigateur). Sauf en cas de besoins “spéciaux” pour un type de client particulier, vous n’aurez jamais à réaliser de plugin de ce type, ni même à modifier le paramétrage du plugin de ce type installé en standard. Interface : Products.PluggableAuthService.interfaces.plugins.IRequestTypeSniffer 262 Chapitre 15. Gestion des utilisateurs avec PlonePAS Plone pour les développeurs, Version 1.0.0 Role_Assigner Plugins Les plugins de ce type permettent d’ajouter ou de supprimer de façon persistente un rôle à un principal. Interface : Products.PluggableAuthService.interfaces.plugins.IRoleAssignerPlugin Role_Enumeration Plugins Les plugins de ce type permettent d’obtenir une liste de rôles (sous forme de mapping incluant des informations supplémentaires) répondant à des critères de recherche. Interface : Products.PluggableAuthService.interfaces.plugins.IRoleEnumerationPlugin Roles Plugins Les plugins de ce type permettent d’assigner des rôles à un principal. L’ensemble des plugins activés sont sollicités lors de chaque transaction pour déterminer les rôles dont dispose un utilisateur. Interface : Products.PluggableAuthService.interfaces.plugins.IRolesPlugin Update Plugins Cette interface n’est utilisée nulle part. Interface : Products.PluggableAuthService.interfaces.plugins.IUpdatePlugin User_Adder Plugins Les plugins implémentant cette interface ont l’aptitude d’ajouter des utilisateurs de façon persistente. Attention, on utilise plus souvent l’interface User_Management Plugins plus complète qui hérite de la présente interface. Interface : Products.PluggableAuthService.interfaces.plugins.IUserAdderPlugin User_Enumeration Plugins Les plugins implémentant cette interface sont utilisés pour énumérer les utilisateurs selon des critères particuliers. Par exemple pour rechercher les utilisateurs dont le nom contient “dup”. Interface : Products.PluggableAuthService.interfaces.plugins.IUserEnumerationPlugin Userfactory Plugins Les plugins implémentant cette interface sont susceptibles de construire des objets de type “utilisateur”. Ce type d’objet, conforme aux utilisateurs Zope standard (sécurité, etc.) fournit également les propriétés de l’utilisateur (adresse mail, nom complet, ...). Dans le cadre de l’utilisation dans Plone, un plugin de ce type est déjà fourni, et sauf exception spécifique, il sera très rarement nécessaire de fournir un plugin spécialisé. Interface : Products.PluggableAuthService.interfaces.plugins.IUserFactoryPlugin User_Introspection Plugins Les plugins implémentant cette interface permettent de lister sous différentes formes tous les utilisateurs qu’ils “contiennent”. Généralement, les plugins de ce type sont également des Authentication Plugins. Interface : Products.PlonePAS.interfaces.plugins.IUserIntrospection 15.3. PlonePAS en ZMI 263 Plone pour les développeurs, Version 1.0.0 User_Management Plugins Les plugins implémentant cette interface fournissent les outils pour changer le mot de passe d’un utilisateur et pour supprimer un utilisateur. Interface : Products.PlonePAS.interfaces.plugins.IUserManagement Validation Plugins Les plugins implémentant cette interface valident les modifications de propriétés d’un utilisateur. Interface : Products.PluggableAuthService.interfaces.plugins.IValidationPlugin 15.4 Les plugins standard PlonePAS est fourni en standard comme nous l’avons vu d’un certain nombre de plugins. En voici la description et les recommandations d’utilisation. 15.5 Les plugins de tierce partie Vous ne trouvez pas dans Les plugins standard celui ou ceux dont vous avez besoin pour votre politique sécurité et l’infrastructure des données d’entreprise existante. Vous trouverez certainement ce qu’il vous faut dans les plugins de tierce partie. Nous vous en présentons quelques-uns ici. 15.6 Votre plugin Hélas rien ne convient à votre cas particulier dans la collection de plugins PlonePAS que nous avons présentés dans Les plugins standard et Les plugins de tierce partie. Eh bien il va falloir vous cracher dans les mains et réaliser vous-même votre propre plugin. 264 Chapitre 15. Gestion des utilisateurs avec PlonePAS CHAPITRE 16 L’environnement de développement Le but est de présenter et permettre l’acquisition des outils utilisés pour le développement Plone 3. 16.1 Installation de Python 2.4 Sur Ubuntu : apt-get install python2.4 python2.4-dev build-essential libxml2-dev libxslt1-dev libjpeg62-dev zli build-essential permet d’installer le compilateur gcc et make entre autres. 16.2 Configuration de l’éditeur vim Installez gvim : apt-get install vim-gnome Créez un fichier ~/.vimrc avec la configuration suivante : " http://www.vex.net/~x/python_and_vim.html " for Python ab pdbs import pdb; pdb.set_trace () " (Ne mettez pas l’espace avant les parenthèses) set pastetoggle=<F8> syntax on autocmd BufRead *.py set smartindent cinwords=if,elif,else,for,while,try,except,finally,def,class autocmd BufRead *.xml,*pt set smartindent tabstop=2 shiftwidth=2 smarttab expandtab softtabstop=2 autocmd BufRead *.rst set smartindent tabstop=4 shiftwidth=4 smarttab expandtab softtabstop=4 Cette configuration permet de configurer l’indentation à 4 espaces pour les fichiers Python et reST, 2 espaces pour les fichiers XML et PT. Elle permet aussi d’activer la coloration syntaxique et passer en mode “collage” à l’aide de la touche F8. 16.3 Configuration de l’autocomplétion dans Python Créer un fichier ~/.pythonstartup avec le contenu suivant : 265 Plone pour les développeurs, Version 1.0.0 # python startup file import readline import rlcompleter import atexit import os # tab completion readline.parse_and_bind(’tab: complete’) # history file histfile = os.path.join(os.environ[’HOME’], ’.pythonhistory’) try: readline.read_history_file(histfile) except IOError: pass atexit.register(readline.write_history_file, histfile) del os, histfile, readline, rlcompleter Éditez ~/.bashrc et décommentez les 3 lignes qui incluent ~/.bash_aliases. Créez le fichier ~/.bash_aliases avec le contenu : export PYTHONSTARTUP=~/.pythonstartup alias archgenxml=~/workspace/archgenxml_buildout/bin/archgenxml alias argouml="cd ~/opt/argouml-0.28/ && ./argouml.sh" export EDITOR=vim # https://wiki.ubuntu.com/Spec/EnhancedBash shopt -s histappend PROMPT_COMMAND="history -a; $PROMPT_COMMAND" export HISTSIZE=1000 export HISTFILESIZE=1000 export GREP_OPTIONS=’--color=auto --exclude=*.svn-base --exclude=*.pyc’ Ici nous avons en plus les alias pour archgenxml et argouml que vous installerez par la suite. La dernière section est pour partager l’historique à travers tous les terminaux et mettre de la couleur pour la commande grep. 16.4 Activation de la recherche dans l’historique Dans le fichier /etc/inputrc décommenter ces lignes : "\e[5~": history-search-backward "\e[6~": history-search-forward Ces lignes permettent de rechercher dans l’historique de bash avec les touches Page Up/Page Down. Par exemple, tapez ssh<PageUp>. 16.5 Installation de ArgoUML et ArchGenXML Suivre la documentation de ArchGenXML. Pour vite installer ArgoUML : sudo apt-get install sun-java6-jdk mkdir ~/opt mkdir ~/downloads cd ~/downloads wget http://argouml-downloads.tigris.org/nonav/argouml-0.28.1/ArgoUML-0.28.1.tar.gz cd ~/opt tar xvf /home/vincentfretin/downloads/ArgoUML-0.28.1.tar.gzmkdir ~/opt 266 Chapitre 16. L’environnement de développement Plone pour les développeurs, Version 1.0.0 16.6 Checkout svn.zope.org Si vous avez un accès subversion au dépôt de Zope, créez votre fichier ~/.ssh/config comme ceci : Host svn.zope.org User votre_login Comme cela, si votre compte unix est différent de votre compte svn, vous n’aurez pas besoin de spécifier votre login lors d’un checkout. Pour réaliser un checkout : svn co svn+ssh://svn.zope.org/repos/main/grok 16.6. Checkout svn.zope.org 267 Plone pour les développeurs, Version 1.0.0 268 Chapitre 16. L’environnement de développement CHAPITRE 17 La gestion des sources avec subversion Le but est d’acquérir les connaissances de gestionnaire de code source subversion utilisé par la communauté Plone. 17.1 Savoir – – – – Création d’un dépôt Dump d’un dépôt Utilisation de subversion en ligne de commande Commandes : add cat changelist (cl) checkout (co) cleanup commit (ci) copy (cp) delete (del, remove, rm) diff (di) export help ( ?, h) import info list (ls) lock log merge mergeinfo mkdir move (mv, rename, ren) propdel (pdel, pd) propedit (pedit, pe) propget (pget, pg) proplist (plist, pl) propset (pset, ps) resolve resolved revert status (stat, st) switch (sw) unlock update (up) – Introduction à subversion – Un livre de référence : svnbook Red Bean 17.2 Création d’un dépôt via Apache http ://doc.ubuntu-fr.org/subversion Création et configuraton d’un dépôt à travers Apache : $ sudo apt-get install apache2 libapache2-svn subversion $ sudo vim /etc/apache2/mods-enabled/dav_svn.conf <Location /Formation> DAV svn SVNPath /home/svn/Formation AuthType Basic AuthName "Subversion Repository" AuthUserFile /etc/apache2/dav_svn.passwd Require valid-user </Location> $ $ $ $ /etc/init.d/apache2 restart htpasswd -cs /etc/apache2/dav_svn.passwd anthony htpasswd -s /etc/apache2/dav_svn.passwd vincent htpasswd -s /etc/apache2/dav_svn.passwd stephane 269 Plone pour les développeurs, Version 1.0.0 $ mkdir /home/svn $ svnadmin create /home/svn/Formation $ chown -R www-data:www-data /home/svn/Formation 17.3 Installation du client subversion Sous Linux : $ apt-get install subversion Sous Windows, installez SlickSVN à partir du site http ://subversion.tigris.org 17.4 Administration d’un dépôt subversion 17.4.1 Faire un dump d’un dépôt existant Exécutez : $ svnadmin dump /home/svn/Formation > /tmp/svn_agile.dump 17.4.2 Importer les révisions dans un autre dépôt Commandes : $ svnadmin create /home/svn/autre $ svnadmin load /home/svn/autre < /tmp/svn_agile.dump On vérifie : $ svn co file:///home/svn/autre 17.5 Mettre à jour la branche de production Vous avez commité un changement dans le trunk, il faut le backporter dans la branche production. Le commit sur le trunk est la révision 1023, pour merger ce commit sur la branche de production : $ cd path/to/trunk/workingcopy $ svn info URL : <url_to_repository>/trunk trunk$ cd ../branches/production branches/production$ svn merge -c 1023 <url_to_repository>/trunk . Puis il faut commiter le résultat en précisant dans le message les numéros de versions et leur origine : $ cd path/to/branches/production $ svn info ... Révision : 1025 ... branches/production$ svn ci -m"Merged -r1023:1025 from trunk" 270 Chapitre 17. La gestion des sources avec subversion Plone pour les développeurs, Version 1.0.0 17.5.1 Connaitre les révisions mergées Sur un serveur subversion >= 1.5 seulement : $ cd path/to/branches/production $ svn mergeinfo <url_to_repository>/trunk . 17.5.2 Connaitre les révisions à merger Sur un serveur subversion >= 1.5 seulement : $ cd path/to/branches/production $ svn mergeinfo --show-revs eligible <url_to_repository>/trunk . 17.5. Mettre à jour la branche de production 271 Plone pour les développeurs, Version 1.0.0 272 Chapitre 17. La gestion des sources avec subversion CHAPITRE 18 Migration des composants développés pour Plone 2.5 18.1 Définition La façon d’écrire un produit Plone a complètement changé entre la version 2.5 et 3. Les workflows ne sont plus écrit en Python, mais en XML. Plone 3 privilégie plus un développement à la Zope 3, amplifiant une tendance qui avait fait son apparition avec Plone 2.5 (utilisation de Five et de la notation ZCML) 18.2 Savoir – Migration des anciens modèles UML vers la dernière version de ArgoUML – Génération de code avec la dernière version de ArchGenXML – Migration du code de l’ancien composant vers le nouveau composant 18.3 Outils Sous Ubuntu pour comparer deux dossiers, on peut utiliser kompare (non éditable) ou meld (éditable) : $ sudo apt-get install kompare meld Pour utiliser meld par exemple : $ meld dossier1 dossier2 Sous MacOSX, les éditeurs BBEdit et TextWrangler proposent également une comparaison de deux dossiers. 18.4 Outils pour la preview des documents Pour le support de la prévisualisation des documentations avec ARFilePreview et AROfficeTransforms, avec les systèmes Ubuntu ou Debian : $ sudo apt-get install wv poppler-utils ppthtml xlhtml pstotext unrtf 273 Plone pour les développeurs, Version 1.0.0 18.5 Installation du produit AttachmentField Éditez le fichier buildout.cfg comme ceci : [buildout] parts = ... productdistros [instance] products = ... ${productdistros:location} [productdistros] # For more information on this step and configuration options see: # http://pypi.python.org/pypi/plone.recipe.distros recipe = plone.recipe.distros urls = http://plone.org/products/attachmentfield/releases/1.4.4/attachmentfield-1-4-4.tgz nested-packages = version-suffix-packages = 18.6 Génération du produit AgileKnowledgeBase Tout d’abord générez le produit AgileKnowledgeBase avec la dernière version de ArchGenXML. Vincent fait un checkout : $ svn co --username vincent http://devagile/Agile $ cd Agile Anthony fait un checkout, crée et commit un dossier packages : $ $ $ $ svn co --username anthony http://devagile/Agile cd Agile svn mkdir packages svn ci -m "Add new packages directory" Vincent fait un update : $ svn log -v -----------------------------------------------------------------------r1 | anthony | 2009-06-04 14:51:20 +0200 (jeu 04 jun 2009) | 1 line Chemins modifiés : A /packages add new packages directory ------------------------------------------------------------------------ Anthony fait : $ cd /tmp $ paster create -t basic_namespace Products.AgileKnowledgeBase \ --svn-repository=http://devagile/Agile/packages/ $ cd Products.AgileKnowledgeBase $ svn stat $ svn rm --force Products.AgileKnowledgeBase.egg-info $ svn ci -m "Add skeleton" $ cd projet/packages $ svn up $ cd Products.AgileKnowledgeBase/trunk $ svn mkdir model $ svn ci -m "Added a model directory" 274 Chapitre 18. Migration des composants développés pour Plone 2.5 Plone pour les développeurs, Version 1.0.0 Copiez-collez le fichier zargo dans ce dossier model et ajoutez le au référentiel svn : $ svn add model/AgileKB.zargo $ svn ci -m"Added zargo model" $ cd Products $ archgenxml ../model/AgileKB.zargo Note : sous Windows les images sont générées bizarrement. Éditez le fichier profiles/default/types.xml pour changer le meta_type du type d’agile_document : <object name="agile_document" meta_type="Factory-based Type Information for Plone Articles content types"/> Vous devrez faire attention à garder ce changement à chaque fois que vous regénèrerez le modèle. Mettez 1.0 pour la version dans profiles/default/metadata.xml : $ svn add AgileKnowledgeBase/* $ svn ci -m"Add generated code" Ajoutez Products.PloneArticle dans install_requires de setup.py. Dans profiles/default/metadata.xml : <?xml version="1.0"?> <metadata> <version>1</version> <dependencies> <dependency>profile-Products.PloneArticle:default</dependency> </dependencies> </metadata> Ajoutez Products.PloneArticle dans l’option eggs de la section [instance] sinon il ne sera pas dans le sys.path du script bin/instance 18.7 Configuration du calendrier Il faut ajouter le type agile_event dans le tool portal_calendar pour que les évènements s’affichent dans le calendrier. 18.8 Création d’un nouveau Plone Site Créez un nouveau Plone Site, installez les produits. 18.9 Migration du contenu Sur le serveur en production : – Passer le storage de FSS en AttributeStorage – Faire un export au format zexp de acl_users, portal_memberdata, portal_groupdata, aide, dossiers des espaces – Copier les logs Sur le nouveau serveur : – copier tous les zexp dans le dossier parts/instance/import/ – copier les logs en les renommant 18.7. Configuration du calendrier 275 Plone pour les développeurs, Version 1.0.0 – importer les utilisateurs : 1. Go to your new site and Delete the following objects (all relative to the Plone site root) : – acl_users -> Contents -> local_roles – acl_users -> Contents -> mutable_properties – acl_users -> Contents -> portal_role_manager – acl_users -> Contents -> source_groups – acl_users -> Contents -> source_users – portal_groupdata – portal_memberdata 2. Copy the corresponding objects from the old site and paste it into the new one at the same location. 3. For the 5 new objects in acl_users go to their Activate tab and activate them (check all checkboxes and click on Update). 4. Cut & paste Members’ content – importer les espaces – Aller dans plonearticle_tool et faire la migration en sélectionnant la version courante 18.10 Ajout nouvelles fonctionnalités 18.10.1 Prévisualisation des documents Word et PDF Hommage à Jean-Nicolas Jnut-Bès Jean-Nicolas, auteur principal de ARFilePreview et AROfficeTransforms nous a quitté à la suite d’une longue maladie qu’il a courageusement combattu. Nous rendons ici hommage à l’homme et au créateur qu’il fut. ARFilePreview est un produit qui ajoute la prévisualisation des documents Word et PDF. Vous pouvez installer le produit AROfficeTransforms pour installer des transformations pour Excel et OpenOffice.org. La dernière release de ARFilePreview sur plone.org est assez vieille, donc vous allez télécharger la dernière version de la branche v2. (La branche v2 fonctionne avec portal_transforms, le trunk (v3) fonctionne avec plone.transforms.) Pour cela, vous allez utiliser la recipe infrae.subversion pour faire un checkout du produit : [buildout] parts = ... development-products [development-products] recipe = infrae.subversion urls = http://svn.plone.org/svn/collective/ARFilePreview/branches/ARFilePreview-v2@88162 ARFilePrevie location = products Il faut que vous ayez la section productdistros activée pour que le produit soit pris en compte au démarrage. Pour installer AROfficeTransforms, ajoutez l’URL de l’archive dans la section productdistros : [productdistros] ... urls = ... http://plone.org/products/arofficetransforms/releases/0.9.2/arofficetransforms-0-9-2.tgz 276 Chapitre 18. Migration des composants développés pour Plone 2.5 Plone pour les développeurs, Version 1.0.0 18.10.2 Génération de pdf Installez Products.SmartPrintNG. Pour générer les PDFs, SmartPrintNG a besoin de fop : $ apt-get install fop Dans la section [instance], ajoutez : environment-vars = FOP_HOME /usr/bin fop cherche la librairie Java servlet-api.jar. Vous pouvez rechercher dans quel package Ubuntu il se trouve avec apt-file search. Tout d’abord installez la commande apt-file et mettez à jour la base de données : $ sudo apt-get install apt-file $ sudo apt-file update Cherchez où se trouve ce fichier : $ apt-file search servlet-api.jar eclipse-platform: /usr/lib/eclipse/plugins/org.eclipse.tomcat_5.5.17/lib/servlet-api.jar groovy: /usr/share/groovy/lib/servlet-api.jar libservlet2.4-java: /usr/share/java/servlet-api.jar libtomcat5.5-java: /usr/share/tomcat5.5/common/lib/servlet-api.jar tomcat6: /var/lib/tomcat6/lib/servlet-api.jar tomcat6-common: /usr/share/tomcat6/lib/servlet-api.jar Installez le package voulu : $ sudo apt-get install libservlet2.4-java 18.11 Exercice Migration du composant “base de connaissances” comme cas pratique. 18.11. Exercice 277 Plone pour les développeurs, Version 1.0.0 278 Chapitre 18. Migration des composants développés pour Plone 2.5 CHAPITRE 19 Programmation dirigée par les tests 19.1 Définition Apprentissage des techniques qui permettent d’assurer la qualité du logiciel. Apprendre à écrire des tests unitaires, d’intégration et fonctionnels. 19.2 Savoir – Tests unitaires avec le module unittest – Tests d’intégration – Tests fonctionnels avec selenium IDE 19.3 Test unitaire Les tests unitaires sont écrit à l’aide du module unittest. Dans un packages Python, la convention est de créer soit un module tests.py, soit un package tests avec plusieurs fichiers commencant par test_ dans ce packages. Il est en général conseillé de commencer tout de suite avec un package tests, plutôt qu’un module si l’on sait que le projet contiendra de nombreux modules. En effet la convention est de créer un fichier test_${module}.py pour chacun des modules de votre application. Donc dans votre package, créez un packages tests : mkdir tests touch tests/__init__.py Lancer les tests : bin/instance test -s collective.groupdelegation Vous pouvez utiliser la recipe zc.recipe.testrunner pour créer un script bin/test qui teste plusieurs eggs : [test] recipe = zc.recipe.testrunner eggs = collective.groupdelegation formation.portlet.docinfo 279 Plone pour les développeurs, Version 1.0.0 19.4 Exercice Écriture des différents types de tests sur le composant “base de connaissances” 280 Chapitre 19. Programmation dirigée par les tests CHAPITRE 20 Programmation dirigée par la documentation 20.1 Définition Faciliter la lecture de son code par une documentation exhaustive et exacte est une exigence du travail collaboratif des projets open source comme Plone. Nous verrons ici les techniques et normes à maîtriser et utiliser. 20.2 Savoir – Module doctest – Format reStructuredText 20.3 Exercice Création d’une documentation avec tests sur le composant “base de connaissance”. 281 Plone pour les développeurs, Version 1.0.0 282 Chapitre 20. Programmation dirigée par la documentation CHAPITRE 21 Sensibilisation aux méthodes agiles Les méthodes agiles telles que SCRUM sont parfaitement adaptées à la gestion des projets open source comme le développement de portails basés sur Plone. 21.1 Savoir – – – – – – – – – itérations/sprints, releases radiateur d’informations stand up meeting calcul de la vélocité planning poker eXtreme Programming code review refactoring pair programming 21.2 Ressources – – – – – – – L’organisation d’une équipe de développement logiciel, Emmanuel CHENU Article de Martin Aspeli sur scrum pour gérer un projet GSOC Scrum sur Wikipedia Blog de Claude Aubry sur scrum et l’agilité Blog Agilex : Agilité et Expertise Livre Scrum de Claude Aubry Livre Kanban et Scrum 21.3 Exercice Mise en pratique de certains concepts tout au long de la formation. 283 Plone pour les développeurs, Version 1.0.0 284 Chapitre 21. Sensibilisation aux méthodes agiles CHAPITRE 22 Glossaire Définition des termes et abréviations utilisés dans ce document. adapter Classe Python fournissant aux objets exposant une certaine interface des services supplémentaires, sans utiliser directement la technologie d’héritage Python. Les adapteurs font partie des concepts amenés par la ZCA (Zope Component Architecture). adaptateur Traduction libre de adapter. affectation On affecte une variable par une valeur en utilisant le signe = (qui n’a rien à voir avec l’égalité en math !). Dans une affectation, le membre de gauche reçoit le membre de droite ce qui nécessite d’évaluer la valeur correspondant au membre de droite avant de l’affecter au membre de gauche. a = 2 # le symbole "a" est une référence à un objet entier valant "2". b = 7.2 * math.log(math.e / 45.12) - 2*math.pi c = b ** a Les affectations relient les identificateurs aux données : si une donnée en mémoire n’est plus reliée, le ramasse-miettes (garbage collector) de Python la supprime automatiquement AT Abréviation courante d’Archetypes utilisée dans les documentations, blogs, mailing-lists, forums ayant trait à Plone. bool Classe de base Python pour les objets booléens. Ces derniers ne pouvant être que True ou False. brain Toute requête au catalogue fournit une itération de brains. Chaque brain étant une référence à un élément de contenu, et incorpore la copie de certains attributs de celui-ci (titre, description, ...). cheeseshop voir pypi. buildout Buildout – ou zc.buildout – est un outil multi-plateformes conçu pour installer et paramétrer les différents composants logiciels d’un système d’informations. Plone est installable par ce mode depuis sa version 2.5. zc.buildout permet en outre de personnaliser l’installation des composants qui concourent à l’installation de “votre” site Plone en modifiant son fichier de configuration. Le packaging sous forme d’eggs est favorisé. C Langage compilé de bas niveau utilisé pour coder - entre autres - Linux et Python. Un compilateur C est nécessaire pour installer Zope ainsi que de nombreuses extensions Python. component Package Python fournissant des fonctionnalités supplémentaires à Zope ou Plone. Les composants sont installés le plus souvent dans $INSTANCE_HOME/lib/python, et plus généralement dans les répertoires du système de fichiers désignés par la directive path du fichier de configuration de votre instance Zope. L’extension de Zope ou Plone par composants est la méthode recommandée pour faciliter la future migration vers le pur Zope 3. Attention, si un composant comporte des directives ZCML, celui-ci doit être déclaré dans la configuration de de votre instance Zope. composant Traduction libre de component. configlet Panneau de configuration de Plone permettant de paramétrer de façon globale certains aspects de Plone. Par exemple, le panneau de configuration des utilisateurs et des groupes ou le thème graphique. Certains composants d’extension de tierce partie ajoutent leur panneau de configuration. content type Un type de contenu est le moule avec lequel tous les élément d’un certain type sont gérés. Un type de contenu incorpore les dispositifs suivant : 285 Plone pour les développeurs, Version 1.0.0 – – – – factory de création de l’élément vues HTML de l’élément schéma d’informations portées par l’élément, incluant ses méta-données formulaires de création/modification de ce schéma d’informations services “métier” proposés par l’élément. distribute Distribute est une refonte et une alternative à setuptools élaborée à l’initiative de Tarek Ziadé. distribute fournit les outils de packaging, de distribution et d’installation de packages Python sous forme d’egg. Nous recommandons vivement l’utilisation de distribute dont les contributeurs sont bien plus réactifs que ceux de setuptools. Bien entendu, distribute est packagé sous forme d’egg. distutils distutils est un package fourni en standard avec Python fournissant les outils de packaging. distribute s’appuie sur distutils et on préfèrera l’utilisation de ce dernier qui fournit des outils plus avancés. dotted name Il n’y a pas de traduction en français facile et concise pour ce terme. Un dotted name permet - généralement dans un fichier ZCML - de désigner un symbole Python quelconque (objet, classe, fonction, ...) sous la forme d’une chaîne de caractères proche de celle utilisée par le mot clé Python import. Par exemple, le dotted name foo.bar.stuff représente l’équivalent en python from foo.bar import stuff. En outre, la notation dotted name admet la notation de package relatif par l’utilisation du préfixe .. Par exemple .foo.bar dans un fichier ZCML permet de référencer l’objet (classe, fonction, ...) bar du module foo.py lorsque ce module figure dans le même répertoire que le fichier ZCML. Un préfixe de deux points (..) permet d’accéder au répertoire parent de celui incluant le fichier ZCML et ainsi de suite. egg Packaging d’un module Python incluant ses méta-données telles que sa version, sa documentation et les éventuelles dépendances d’autres eggs. La grande majorité des eggs publics sont distribués sur le site pypi. Reportez-vous à la documentation ‘intégrateur’ pour plus de détails. eggs Voir egg. élément (de contenu) ou “content item”. Unité atomique de contenu fournie par un auteur, tel qu’un document, une image, un fichier. Certains types d’éléments, tel que le dossier ou la collection sont dits également conteneurs ou “folderish” de par leur aptitude à contenir d’autres éléments de la même façon qu’un répertoire peut inclure d’autres fichiers et répertoires dans un disque dur. expression Python : Une expression est une portion de code que l’interpréteur Python peut évaluer pour obtenir une valeur. Les expressions peuvent être simples ou complexes. Elles sont formées d’une combinaison de littéraux, d’identifiants et d’opérateurs. TALES : Expression utilisées dans les templates ZPT pour calculer les attributs et les contenus des éléments. groupe (d’utilisateurs). Un groupe d’utilisateurs permet d’associer un ensemble d’utilisateurs à une fonction dans un site Plone. Ceci permet - principalement - d’octroyer des droits particuliers, élément par élément, à tout un ensemble d’individus par le biais de l’onglet partage. Les groupes d’utilisateurs sont gérés par PlonePAS à travers le configlet Utilisateurs et groupes. Un utilisateur peut appartenir à autant de groupes que nécessaire, voire aucun groupe. Un groupe peut appartenir à un autre groupe (imbrication). i18n Abréviation commune du terme “internationalisation” (il y a 18 lettres entre le “i” et le “n” de “internatonalisation”). Opération consistant à marquer le code (Python, templates, ...) pour lui permettre de fournir les textes de l’interface utilisateur en différents langages. Voir l10n. identifiant Un identifiant Python valide est une suite non vide de caractères, de longueur quelconque, formée d’un caractère de début et de zéro ou plusieurs caractères de continuation. int Classe de base Python pour les objets représentant un nombre entier. interface (en Python) : Une interface est une classe spéciale décrivant l’API publique de toute autre classe l’implémentant. L’utilisation des interfaces est à la base de la ZCA. interface marqueur une interface vide (sans attribut ni méthode) permettant de placer un tag sur un objet. À noter qu’une interface est associée à un classe alors qu’une interface marqueur est attribuée objet par objet. invite de commandes Logiciel vous invitant à contrôler votre système en fournissant des commandes et en lisant le compte-rendu des dites commandes. Jusque dans les années 80, l’invite de commandes était l’unique moyen de contrôler un ordinateur. Sous Windows, l’invite de commandes est fournie par le logiciel cmd.exe‘ alors que sous Unix - le choix est plus vaste - le couple xterm et bash est généralement utilisé. l10n Abréviation commune de “localisation” (il y a 10 lettres entre le “l” et le “n” de “localisation”). Opération consistant à fournir les textes de l’interface utilisateur d’un logiciel (d’un composant Plone) dans un langage particulier. Voir i18n. 286 Chapitre 22. Glossaire Plone pour les développeurs, Version 1.0.0 msi installer Format de bundle d’installation du monde Windows. Les fichiers de ce type ont l’extension .msi. multiplugin Un plugin ayant l’aptitude de remplir plusieurs rôles, donc, généralement implémentant plus d’une interface. Un plugin pour PAS est d’ailleurs généralement un multiplugin. part Une part est une section d’un fichier buildout contenant une variable recipe définissant le rôle de la part dans le processus d’installation global défini par la configuration buildout. Une part n’est exécutée que si elle est référencée dans la liste parts = ... de la section [buildout]. parts voir part. permission Protection d’accès à un objet dont la sécurité est gérée par Zope. permissions voir permission. PAS Abréviation de PluggableAuthService, la base du service de gestion des utilisateurs, rôles et groupes de Plone. Voir la documentation intégrateur. paster Couteau suisse du développeur d’applications Python. Voir http ://pythonpaste.org/index.html PATH Variable d’environnement utilisée par votre système d’exploitation pour trouver les logiciels invoqués par l’invite de commandes. Cette variable d’environnement est constituée d’une liste de répertoires, séparés par : pour Unix et ; sous Windows. plugin Se dit d’un composant logiciel (fonction, classe, ou objet) s’intégrant dans une infrastructure existante et ayant une interface et un rôle pré-défini vis-à-vis de cette infrastructure (le rôle est généralement défini par une interface), mais implémentant le rôle d’une façon qui lui est propre. Les logiciels de type “plugin” sont légion dans le monde de Plone, et plus notamment dans le service de gestion des utilisateurs PAS. Voir la documentation intégrateur. PMI Plone Management Interface : Ensemble des formulaires fournis par Plone et ses éventuelles extensions permettant de gérer et paramétrer le contenu et les services. portlet Petite boite d’interface utilisateur située dans la colonne gauche d’un portail Plone. Les conditions d’apparition et les interfaces des portlets est automatiquement calculé en fonction du contexte (droits de l’utilisateur, et emplacement de navigation). L’outil de navigation et le calendrier sont des exemples de portlets. Dans la grande majorité des cas, les portlets ne font pas strictement partie du contenu. policy product Un policy product est un composant pour Plone défininissant la politique éditoriale du site, et - éventuellement - sa logique métier spécifique. un policy product a également pour rôle d’installer et paramétrer de façon spécifique Plone et les différents composants de tierce partie ajoutés. principal Agent du système Zope exécutant une action. Un agent dispose de permissions sur les objets Zope pour exécuter ces actions. Dans les applications Zope ou Plone, vous ne verrez que deux types de principals : les utilisateurs (ou membres) et les groupes. product Package Python fournissant des fonctionnalités supplémentaires à Zope ou Plone. Les produits sont installés le plus souvent dans $INSTANCE_HOME/Products, et plus généralement dans les répertoires du système de fichiers désignés par la directive “products” du fichier de configuration de votre instance Zope. L’extension de Zope et Plone par produits est un héritage des anciennes versions de Zope, facilitant ainsi l’adaptation d’extensions Zope ou Plone conçues pour des versions antérieures. Il est en conséquence recommandé de réaliser dorénavant des composants (voir plus haut) pour ajouter des fonctionnalités à Zope ou Plone. produit Traduction libre de product. profil Traduction libre de profile. profile (GenericSetup) : un profil désigne un lot de fichiers XML définissant le paramétrage appliqué à un site Plone permettant l’utilisation dans le dit site des ressources d’un composant ou d’un produit. Les fichiers d’un profil GenericSetup sont – généralement – placés dans le sous-répertoire profiles/default d’un composant ou produit. propertysheet En français : feuille de propriétés. Dans Plone, il y a deux types de feuilles de propriétés : – Les feuilles de propriétés servant à conserver des propriétés globales du site, celles-ci se trouvant dans le tool portal_properties – Les feuilles de propriétés dynamiques associées aux utilisateurs et aux groupes. pypi PYthon Packages Index, anciennement appellé cheeseshop, est le site dans lequel la grande majorité des packages open source Python sont mis à la disposition du public. Sa page d’accueil est http ://pypi.python.org/pypi 287 Plone pour les développeurs, Version 1.0.0 pywin32 Package Python pour Windows - disponible pour les architectures 32 et 64 bits - fournissant l’accès aux services système spécifiques à ce système d’exploitation : contrôle des services, de la base de registres, accès COM ou DCOM aux applications, accès à Exchange et Active Directory, extensions et filtres ISAPI, etc, etc. recipe Peut être traduit en français par “recette”. Une recipe définit le rôle d’une part d’une configuration buildout (exemples : installation/paramétrage d’Apache, instalation d’un client ZEO, fichier de comfiguration défini par une template, ...). La plupart des recipes disponibles sont disponible dans le site Pypi à cette adresse. rôle Un rôle regroupe un ensemble de permissions. Tout visiteur dispose d’un ou plusieurs rôles, soit de façon globale, soit de façon contextuelle. schéma zope.schema : Classe héritant de zope.interface.Interface, donc une interface dont les attributs sont définis par des objets exposés par le module zope.schema. Ces schémas sont généralement utilisés pour modéliser des formulaires (de type zope.formlib ou z3c.form) . Archetypes : Liste de champs représentant les données unitaires d’un type de contenu construit avec les ressources de Products.Archetypes. SSO Single Sign On ou Authentification unique. Agent permettant de n’effectuer qu’une seule authentification pour accéder à plusieurs sites. Comme par exemple pour Google apps (mail, agendas, documents). Voir http ://fr.wikipedia.org/wiki/SSO structural folder Type de contenu permettant à un contributeur d’y inclure les contenus de son choix. A contrario, le “non structural folder” permet pas à un contributeur de structurer librement son contenu. Les composants de tierce partie Products.PloneArticle et Products.Collage fournissent des exemples de “non structural folder”. TAL Template Attribute Language TALES Template Attribute Language Expression Syntax tool Dans un contexte CMF, donc Plone, un “tool” est un objet unique dans un site fournissant un ensemble de services et incorporant ses propres données persistantes de configuration. Les tools se trouvent à la racine de Plone et leurs noms sont généralement préfixés par portal_. portal_properties abrite les feuilles de propriétés de Plone et de la plupart des composants d’extension de tierce partie. portal_catalog indexe les contenus du site... TTW Ou “Through The Web”. Qualifie généralement le paramétrage ou la programmation d’une application Zope depuis des formulaires accessibles uniquemant à un administrateur ou un auteur authentifié. La ZMI est le principal outil Zope de programmation ou de paramétrage TTW. type de contenu Traduction libre de content type. unified installer Bundle d’installation complète de Plone, incluant Python, Zope, Plone ainsi que tous les modules requis (PIL, elementtree, ...). L’unified installer est disponible pour Windows, Mac OSX et Linux. utilitaire Traduction française de utility. utility Les utilitaires sont simplement des objets qui fournissent une interface et qui sont récupérés à partir de cette interface et d’un nom. Cette approche permet de créer un registre global dans lequel des instances peuvent être inscrites et récupérées à différents endroits de votre application, sans avoir besoin de transmettre les instances comme paramètres. Les utilitaires sont détaillées dans Le guide complet de l’Architecture de Composants de Zope. variable Une variable est un identifiant associé à une valeur. Informatiquement, c’est une référence d’objet situé à une adresse mémoire. variables Voir variable. viewlet Une viewlet est responsable du rendu d’un composant de page, donc de la réalisation d’un bloc - generalement - HTML. Les viewlets sont généralement assemblées par un viewlet manager. viewlet manager Un viewlet manager permet d’assembler les viewlets. L’utilisation de viewlets et de viewlet managers permet une disposition plus souple des composants d’une page que les classiques macros ZPT ainsi que des performances de publication améliorées. virtualenv virtualenv est un outil permettant de créer des installations virtuelles de Python (presque) indépendantes du Python installé de façon globale dans votre système, de sorte que tout egg ajouté dans une installation virtuelle ne vient pas “polluer” votre installation globale. Par extension, on appelle un “virtualenv” tout environnement virtuel créé avec cet outil. Reportez-vous à la documentation intégrateur pour plus de détails. Ou bien sur la page d’accueil de virtualenv 288 Chapitre 22. Glossaire Plone pour les développeurs, Version 1.0.0 widget Élément unitaire d’une page permettant au serveur de solliciter une information à l’utilisateur. Les widgets sont généralement assemblés dans un formulaire. Plone fournit bien évidemment tous les widgets standard du langage HTML (cases à cocher, boutons, listes déroulantes, champ de saisie de texte, ...). Ses divers composants, notamment la bibliothèque zope.formlib, KSS et Archetypes proposent des widgets plus évolués tel que des calendriers, éditeur WYSIWYG, arbres d’exploration, ... workflow Outil permettant de définir les différents états d’un élément, et les transitions permettant de passer d’un état à un autre. Pour chaque état, un sous-ensemble de permissions de cet élément fixe la limite les différents types d’accès à celui-ci. Par exemple pour interdire la vue à un utilisateur anonyme. L’exécution de chaque transitions est protégée de façon spécifique. Par exemple, seul un modérateur peut exécuter la transition “publier” Dans Pone, les workflows sont définis en ZMI à l’aide de l’outil DCWorkflow. Certains types d’éléments, comme l’image, ne sont pas associés à un workflow. ZCA Zope Component Architecture : Ensemble de services intégrés dans Zope 3, et adaptés à Zope 2 depuis sa version 2.9 par l’entremise de Five. La ZCA fournit les outils facilitant la collaboration entre objets et services de natures différentes concourant à la réalisation de fonctions applicatives, tout en évitant les pièges d’un héritage complexe de classes. Par exemple, vous disposez d’un composant (A) d’exploration AJAX d’arbre virtuel, d’un autre composant (B) gérant un contenu pouvant être représenté de façon arborescente, il vous suffira de fournir les adapteurs permettant d’explorer le contenu des objets fournis par B à l’aide des widgets fournis par A. Ceci ne peut bien entendu être possible que si ces deux composants exposent les ressources ZCA (généralement des interfaces) permettant ceci. L’utilisation de la ZCA, entamée par Plone depuis sa version 2.5, se généralise depuis sa version 3. Plus de détails dans Le guide complet de l’Architecture de Composants de Zope. zc.buildout voir buildout. ZCML Zope Configuration Markup Language : Langage basé sur XML permettant de configurer des composants ou produits pour Zope ou Plone dans des fichiers configure.zcml ou overrides.zcml. zexp Format binaire d’exportation et d’importation d’un objet (et éventuel sous-objets) de la ZODB. Pour exporter ou importer un fichier au format zexp, cliquez le bouton Import/Export et suivez les instructions. ZODB Zope Object Data Base : base de données objets native de Zope. ZMI Zope Management Interface : Interface Web de paramétrage et contrôle d’une instance Zope, permettant l’exploration de la ZODB. ZPT Zope Page Templates : Langage de templates pour produire - généralement - du texte XML ou HTML. Les templates ZPT utilisent les attributs spéciaux préfixés par tal, metal et i18n qui définissent les règles de transformation. 289 Plone pour les développeurs, Version 1.0.0 290 Chapitre 22. Glossaire CHAPITRE 23 Indexes et tables – genindex – search 291 Plone pour les développeurs, Version 1.0.0 292 Chapitre 23. Indexes et tables CHAPITRE 24 Contributeurs – – – – – – Vincent Fretin Robert Cordeau Gilles Lenfant Thomas Desvenain Michaël Launay Christophe Combelles 293 Plone pour les développeurs, Version 1.0.0 294 Chapitre 24. Contributeurs CHAPITRE 25 Evolutions planifiées de ce document 295 Plone pour les développeurs, Version 1.0.0 296 Chapitre 25. Evolutions planifiées de ce document Index Symbols G élément, 286 groupe, 286 A H acl_users, 257, 259 adaptateur, 100, 285 adapter, 285 affectation, 285 Algorithme, 14 AT, 285 Héritage, 64 I bool, 285 brain, 285 buildout, 285 i18n, 286 i18ndude, 253 identifiant, 286 Instruction composée, 31 int, 286 interface, 286 interface marqueur, 286 invite de commandes, 286 C L C, 285 Chaîne de caractères, 23 Collection, 36 component, 285 composant, 285 Composition, 66 configlet, 285 content type, 285 l10n, 286 Les types binaires, 27 Liste en intension, 38 D P Dérivation, 67 distribute, 286 distutils, 286 dotted name, 286 Package, 59 part, 287 parts, 287 PAS, 287 paster, 287 PATH, 287 permission, 287 permissions, 287 PlonePAS, 257, 259 architecture, 257 plugin, 287 PlonePAS, 257, 259 PMI, 287 policy product, 287 B E egg, 286 eggs, 286 expression, 286 Expression rationnelle, 69 F Fonction, 49 Fonction récursive, 69 M Module, 54 msi installer, 287 multiplugin, 287 297 Plone pour les développeurs, Version 1.0.0 Polymorphisme, 64 portlet, 287 principal, 287 product, 287 produit, 287 profil, 287 profile, 287 Programme, 14 propertysheet, 287 pypi, 287 pywin32, 288 R rôle, 288 recipe, 288 S schéma, 288 Sequence, 36 set, 43 SSO, 288 structural folder, 288 T Tableau associatif, 42 TAL, 288 TALES, 288 tool, 288 TTW, 288 Tuple, 39 type de contenu, 288 U UML, 177 unified installer, 288 utilitaire, 288 utility, 288 V variable, 288 variables, 288 viewlet, 288 viewlet manager, 288 virtualenv, 288 W widget, 289 workflow, 289 Z zc.buildout, 289 ZCA, 289 ZCML, 289 zexp, 289 ZMI, 289 ZODB, 289 ZPT, 289 298 Index