Planète AFPy
[afpyro] AFPyro à Lyon - le 22 mai 2013
Un Afpyro aura lieu le mercredi 22 mai à partir de 20h à l’Antre Autre - 11 rue Terme - 69001 Lyon.
Arthur Vuillard fera une présentation éclair sur Django, le fameux framework web pour les perfectionnistes. Cette présentation donnera suite à une discussion sur les diffèrents frameworks webs disponibles en Python.
L’Antre Autre est un lieu où nous pouvons discuter autour d’un verre, et, pour ceux qui le souhaitent, prendre un repas.
- Pour se rendre à l’Antre Autre :
- en métro : arrêt Hôtel de Ville
- en bus : lignes C13 et C18 arrêt Mairie du 1er ou lignes 19, C14 et C3 à l’arrêt Terreaux
- en vélo’v : stations Place Sathonay, Carmélites Burdeau, Place de la paix
[afpy.org] Solution linux 2013 à Paris
L'AFPy sera présente au salon Solution Linux le 28 et 29 mai.
[carlchenet] Vrac de mini-messages n°2
Suivez-moi aussi sur Identi.ca ou sur Twitter. Pour cette catégorie d’article, je passe à une publication hebdomadaire Les liens d’origine sont enrichis des approfondissements que j’ai pu effectuer entre la publication du dent/tweet et la publication de cet article. #debian #wheezy 7.1 devrait être publiée samedi 15 juin http://ur1.ca/dvraj => Information très intéressante qui n’a pas été beaucoup relayée. La […]
[tarek] A step-by-step introduction to Circus
Note
Circus is a process & socket manager. See https://circus.readthedocs.org
During Django Con, I was asked how to use Circus to run & monitor a Python web application. The documentation has no single page step-by-step tutorial yet, so here goes... this blog post will be integrated into the documentation for the next release.
Installation
Circus is tested under Mac OS X and Linux, on the latest Python 2.6 and 2.7. To run a full Circus, you will also need libzmq, libevent & virtualenv.
Under Debuntu:
$ sudo apt-get install libzmq-dev libevent python-virtualenvCreate a virtualenv and install circus, circus-web and chaussette in it
$ virtualenv /tmp/circus $ cd /tmp/circus $ bin/pip install circus $ bin/pip install circus-web $ bin/pip install chaussetteOnce this is done, you'll find a plethora of commands in the local bin dir.
Usage
Chaussette comes with a default Hello world app, try to run it:
$ bin/chaussetteYou should be able to visit http://localhost:8080 and see hello world.
Stop Chaussette and add a circus.ini file in the directory containing:
[circus] stats_endpoint = tcp://127.0.0.1:5557 httpd = 1 [watcher:webapp] cmd = bin/chaussette --fd $(circus.sockets.web) numprocesses = 3 use_sockets = True [socket:web] host = 127.0.0.1 port = 9999This config file tells Circus to bind a socket on port 9999 and run 3 chaussettes workers against it. It also activates the Circus web dashboard and the statistics module.
Save it & run it using circusd:
$ bin/circusd --daemon circus.iniNow visit http://127.0.0.1:9999, you should see the hello world app.
You can also visit http://localhost:8080/ and enjoy the Circus web dashboard.
Interaction
Let's use the circusctl shell while the system is running:
$ bin/circusctl circusctl 0.7.1 circusd-stats: active circushttpd: active webapp: active (circusctl)You get into an interactive shell. Type help to get all commands:
(circusctl) help Documented commands (type help <topic>): ======================================== add get list numprocesses quit rm start stop decr globaloptions listen numwatchers reload set stats dstats incr listsockets options restart signal status Undocumented commands: ====================== EOF helpLet's try basic things. Let's list the web workers processes and add a new one:
(circusctl) list webapp 13712,13713,13714 (circusctl) incr webapp 4 (circusctl) list webapp 13712,13713,13714,13973Congrats, you've interacted with your Circus! Get off the shell with Ctrl+D and now run circus-top:
$ bin/circus-topThis is a top-like command to watch all your processes' memory and CPU usage in real time.
Hit Ctrl+C and now let's quit Circus completely via circus-ctl:
$ bin/circusctl quit okNext steps
You can plug your own WSGI application instead of Chaussette's hello world simply by pointing the application callable.
Chaussette also comes with many backends like Gevent or Meinheld.
Read https://chaussette.readthedocs.org/ for all options.
[anybox] Python : compréhension des intensions en 1 minute
[Biologeek] Une quête de sens
Je suis (non-)intervenu à SudWeb 2013 pour animer un débat sur le sens de notre implication dans notre métier suite à une informelle donnée à ParisWeb 2012. J'avais choisi des thématiques très larges telles que l'argent, l'utilité, la reconnaissance, l'adrénaline, le partage, la santé, l'écologie ou le fun. L'objectif était d'avoir un débat non technique et de faire se poser quelques questions à l'auditoire sur les choix qu'ils font professionnellement (et finalement personnellement aussi) au cours de leur vie. Petite rétrospective sur cette heure d'échanges à plus de 100.
Ce qui a bien marché
- le fait d'énoncer des règles claires en début de session a permis de ne pas avoir de monopolisation de la parole par un seul petit groupe de personnes (après discussion avec le staff de SudWeb, ils faisaient attention dans la distribution des micros à répartir équitablement la parole donc cela ne se faisait pas tout seul non plus ;-)) ;
- la disposition de la salle a beaucoup fait pour que le débat soit possible et qu'une dynamique de communication en face-à-face se mette en place ;
- la participation a été assez exceptionnelle avec un démarrage immédiat du débat (je stressais un peu de me retrouver avec une salle muette) et un engagement qui n'a pas faibli au cours de la session ;
- la variabilité dans les points de vues et les niveaux de recul des participants, c'était à titre personnel assez rafraichissant d'avoir des remarques qui partaient un peu dans tous les sens sans forcément suivre les thématiques proposées ;
- mon silence, j'ai réussi à me retenir plusieurs fois d'intervenir pour laisser la parole à la salle, c'est extrêmement frustrant mais je pense que cela a été bénéfique au débat ;
- les discussions que cela a produit au cours des jours suivants, je n'avais jamais eu autant de retours suite à une intervention et au-delà des retours personnels j'ai pu observer de nombreux échanges — en périphérie des sessions — relatifs au débat ce qui ne peut que me ravir.
Ce qui pourrait être amélioré
- beaucoup de consensualité dans les échanges et j'ai du mal à trouver comment est-ce que cela pourrait évoluer, ni même si ça doit l'être. Ma crainte est plus dans le mode « bisounours » activé par la prise de parole en public ;
- l'introduction des thèmes à partir de questions focalisait la salle sur les questions et peu sur la thématique au sens large, c'est dommage mais je n'ai pas trouvé mieux pour lancer les sujets ;
- le sujet sur l'adrénaline n'a pas été compris par tout le monde et j'ai eu des retours très contrastés par la suite (certains ne s'y retrouvant pas du tout et d'autres à fond), l'objectif était surtout de faire une pause dans les sujets plutôt lourds qui étaient discutés avant et après ;
- le sentiment de frustration vu le nombre de personnes qui souhaitent s'exprimer mais c'est le jeu, on avait 60 minutes pour une centaine de personnes ça fait quelques secondes seulement par participant…
Et la suite ?
Une question ouverte en guise de conclusion sans avoir vraiment de proposition technique concrète pour continuer le débat. Après réflexion (et de nombreuses discussions), je ne pense pas qu'il soit pertinent de continuer en ligne par contre je serais ravi que les discussions continuent ici ou ailleurs en espérant avoir semé quelques graines qui pourront germer de proche en proche.
[carlchenet] Les nouveautés de Python 3.3 (GLMF)
Suivez-moi aussi sur Identi.ca ou sur Twitter. Mon article sur les nouveautés de Python 3.3 est paru dans le Gnu/Linux Magazine France d’avril 2013. Voici le sommaire de ce long article détaillant les principales nouveautés de la dernière mouture du langage Python, exemples à l’appui le plus souvent. Avec l’accord de mon éditeur, je vous propose […]
[anybox] Python : comprendre les docstrings en 1 minute
[tarek] FaitMain magazine #2
We've launched the second issue of Fait Main, a French magazine about the DIY movement. There are a lot of rough edges remaining since the magazine is built on our week-ends, but it's starting to look good. I love the content of the second issue because it talks about very different topics and not only the usual suspects (Python, Raspberry-PI, Arduino.)
One thing I want to mention to all my English speaking friends: we also accept articles in English - we take care of the translation.
This issue has two articles that were originally in English:
The first one appeared on my radar in Hacker News, and I asked the author the permission to translate it and publish it in the magazine. The second one was written by a member of the Python community, Fritz van Deventer I met during the FOSDEM in a corridor session. I was impressed by his work and asked him to write a little article for our magazine.
So, if you want to write something for the third issue, or know a great article we should translate - let me know in the comments.
Issue #3 will be out in August.
[sciunto] Et si github fermait ?
Que feriez-vous ? Le titre est provocateur. Pour ceux qui n’utilisent pas github, je me doute que vous ne ferez rien. Quoique… Comme vous le savez, github est un service de gestion de projet utilisant git. Comme tout service administré par un tiers (qu’il soit libre ou non, ça ne change rien), sa pérennité n’est […]
[afpyro] AFPyro à Bruxelles (BE) - vendredi 10 Mai
Un AFPyro, Apéro Python, aura lieu ce 10 mai à l’Université de Bruxelles. Il se composera d’une série de Lightning Talks au Batiment K de l’ULB suivi d’un passage au Restaurant « La Bécasse ».
Le thème est « les projets Python libres que vous faites » pour avoir une idée de ce qui se fait en Belgique. Bien entendu, ce thème n’est pas limitant et si vous souhaitez parler d’autre chose, faites vous plaisir ! La deadline pour les propositions de Lightning Talk est le dimanche 28 au soir, vous pouvez me les envoyer directement à l’adresse : cortex@worlddomination.be
Si vous souhaitez venir au restaurant après, inscrivez-vous sur le Framadate pour que l’on puisse réserver le bon nombre de place: http://framadate.org/jze5k7xeqgo3dlh5
Pour récapituler :
- Début des lightning talks à 19h au bâtiment K de l’ULB (Campus Solbosh), salle K.4.601
- restaurant à « La Bécasse » à partir de 21h00
Au plaisir de vous y voir.
[ascendances] Évolution du nombre de messages sur debian-l10n-fr et debian-users-fr avec pychart
Pychart est une bibliothèque Python permettant des graphiques directement en python. Un paquet Debian est disponible (nommé python-pychart) avec la dernière version 1.39 (qui date de 2006). Debian utilise massivement des listes de diffusion pour la collaboration de ses membres. Parmi les nombreuses listes existantes, trois sont francophones : debian-users-fr, dédiée aux questions des utilisateurs ; debian-l10n-fr, [...]
[sciunto] Des logiciels pour être efficace
Depuis que j’utilise des logiciels libres, je me suis tourné vers la programmation mais c’est aussi parce que je voulais faire cette apprentissage que je suis passé à GNU/Linux. De cette expérience, je tire différents savoirs nouveaux et au final, je me demande si je n’ai pas appris d’avantage concernant les éléments périphériques que sur [...]
[afpyro] AFPyro à Toulouse le 30 avril 2013
Un Afpyro aura lieu le mardi 30 avril à partir de 20h à La Tireuse 24 rue Pargaminières
Pas de présentation prévue, car organisé au pied levé à la pause café ! Ce sera juste une rencontre autour d’une bière (ou d’un jus de fruit) pour parler de python, avec des pythonistas toulousains.
Pour se rendre à la tireuse, métro ligne A : arrêt Capitole, puis aller dans la rue qui rejoint la place St Pierre.
[tarek] Thoughts on Load Testing
We are Funkload fans at Mozilla Services. Writing a load test against one of our web service is dead easy using that tool.
It boils down to writing a functional test in Python, that calls the service, then ask Funkload to run it in parallel, accross many threads and boxes.
We've written a web app on the top of Funkload called Marteau - initial blog post about it that now allows us to provision slaves on demand on Amazon WS and run distributed load tests.
You can watch this hangout to better understand what Marteau & Funkload are https://plus.google.com/106436370949746015255/posts/Lq7t4jkiwNR
Marteau is already useful to us but I am now facing some difficulties to improve the tool and add some features we want, like:
- realtime JS charts of the ongoing load test
- web socket load testing
- the ability to run load tests written in other languages or frameworks, like node.js
Funkload is a project that was started 10 years ago, and is very robust. But it has evolved from a tool that runs on a single box to a distributed load testing tool by using SSH. In other words, a distributed load test in Funkload is just several load tests running in parallel, then a merge of all the results files that are copied back to a single box through SSH.
It means that there is no specific client/server protocol and no way to interact live with the slaves that are running in the distributed mode. You can kill them of course, or just wait for them to return.
I started to add some communication channel in the tool but the core itself was not built with this in mind.
I have found the locust.io project, which has this approach and which looks very promising, but does not exactly provide what I am looking for.
For example, I don't really want developers to have to write load tests using yet another set of APIs. The concept of writing Python unit tests is fabulous and I don't want to lose it.
Starting Loads
I am experimenting on something new, based on what we've learned from our experience with Funkload and what we need in Marteau.
It's called Loads and it's a client/server architecture based on ZMQ that will use a very simple protocol based on Message Pack or maybe BJson - we will see.
It's quite similar to locust.io in the principles, but instead of introducing new APIS, it's going to rely on a set of API people know & like : Requests.
So, how will a load test with Loads look like ?
import unittest from loads import Session class TestWebSite(unittest.TestCase): def setUp(self): self.session = Session() def test_something(self): res = self.session.get('http://blog.ziade.org') self.assertTrue('Tarek' in res.content)That's it. A unittest class that uses a Session class. The Session object is the same one you find in Requests.
I am not sure yet how I will extend the API so it can work with web sockets. That'll come later.
This test can then be executed using the loads command. Example for 10 concurrent users and 10 runs each:
$ bin/loads test_blog.TestWebSite.test_something -c 10 -n 10 [====================================================================================================] 100%Like locust.io, Loads uses greenlets here, so you can already push quite some load from a single box.
Everytime a request is done, the status code returned by the server and the time it took are pushed in a stream. The stream can be the standard output or a ZMQ stream.
And I will be using the ZMQ stream to actually drive distributed tests.
Each agent will connect to a master through ZMQ. The master will be able to drive them through a dedicated ROUTE socket and will ask agents to run some load tests.
Results will be sent back via ZMQ in real-time using a dedicated channel.
The master will then publish all results in a merged stream - a ZMQ pub socket.
From there, the Marteau web app will be able to register to that stream of result to display some real time charts and allow any kind of interaction. Or any app that whishes to do something with the results.
And since there are some ZMQ bindings in most languages, it's possible to implement a node.js client for example, so the system can have agents able to run Javascript-based tests and report back results to the master like the built-in Python agent.
That's the plan. I have started the prototype here: https://github.com/tarekziade/loads and I am very excited about this project.
[Biologeek] Des API et des hommes
Les API actuelles — s'auto-proclamant RESTful — nécessitent bien souvent de développer un client qui leur est propre pour accéder aux données en raison de leurs spécificités. Au mieux, ces API utilisent HTTP à bon escient et font transiter du JSON à partir d'URL « propres ».
Cela semble bien éloigné de la vision de Roy T. Fielding (qui a défini REST en 2000 dans sa thèse) et qui a écrit un billet on ne peux plus clair en 2008 :
A truly RESTful API looks like hypertext. Every addressable unit of information carries an address.
Puis surenchérit en commentaire :
Think of it in terms of the Web. How many Web browsers are aware of the distinction between an online-banking resource and a wiki resource? None of them.
5 ans plus tard, on en est encore à réécrire un client pour chaque API ce qui équivaudrait à écrire un navigateur propre à chaque site web visité ! Comment y remédier facilement ? Revenir à la partie oubliée de REST : les liens.
Si votre API devient navigable, en liant chaque ressource présentée depuis sa racine, un client générique va pouvoir la parcourir de proche en proche en suivant les liens comme un utilisateur le fait sur le Web.
Cela résout énormément de problématiques à la fois lorsque l'on prend cette approche :
- versionnement : est-ce qu'un utilisateur se soucie de la version du site qu'il consulte ? Non. Il suit les liens et si la migration a bien été effectuée il y a des redirections et les codes HTTP appropriées pour gérer ses anciens favoris. Les formulaires ont été mis à jour avec le site et il suffit qu'il remplisse correctement ceux qui lui sont dorénavant présentés.
- URL propres : est-ce qu'un utilisateur se soucie de la beauté des URL qu'il parcoure ? Quand je vois la tête de celles produites par Google ou Amazon j'en doute. Un développeur ne devrait pas avoir à se soucier de cela si le client suit les liens qui lui sont proposés.
- documentation : est-ce qu'un utilisateur a besoin d'une documentation pour naviguer sur votre site ? De toute façon, il y a peu de chance qu'il la lise, en revanche il est utile de lui formuler des messages d'erreurs intelligibles lorsqu'il se trompe de chemin. Il peut être intéressant de faire un rappel sur le métier et les concepts abordés car le développeur — à la différence du visiteur — n'est peut-être pas concerné par le sujet de l'API en question.
- pagination : en utilisant les attributs permettant de typer la relation entre les liens, il est possible de fournir les liens vers la page suivante et précédente explicitement.
Ces questions se sont posées pour les sites Web également il y a des années : souvenez-vous des sites avec un /v4/ dans l'URL ou d'une page d'accueil expliquant comment accéder aux différentes parties du site en « cliquant ici ».
Bien sûr tout cela implique d'avoir un format qui soit hypertexte (pas JSON donc) comme XHTML ou Atom. Si vous voulez vraiment adapter votre JSON actuel il existe 4 implémentations tentant d'introduire des liens typés :
- JSON-LD (LD pour Linked Data), proche des concepts du Web Sémantique ;
- HAL JSON le plus simple, peut-être un peu trop ;
- JSON Collections que je n'ai pas essayé ;
- Siren le plus récent qui est en train de monter rapidement.
Lorsque vous voulez fournir un moyen d'accéder à vos données via une API hypermedia, mettez vous à la place du développeur et demandez vous si votre API est navigable, fait partie intégrante du Web et nécessite une documentation.
Ce billet fait suite à mon intervention à Mix-IT lors d'un lightning talk dont vous pourrez retrouver le support sur la partie dédiée.
[afpyro] AFPyro à Lyon - le 24 avril 2013
Un Afpyro aura lieu le mercredi 24 avril à partir de 20h à l’Antre Autre - 11 rue Terme - 69001 Lyon.
Blandine Bourgois (du groupe Nao sur Seine) et Xavier Basset (du groupe Nao is a gone) nous feront une présentation sur et avec un robot Nao, un petit robot humanoïde programmé en Python.
L’Antre Autre est un lieu où nous pouvons discuter autour d’un verre, et, pour ceux qui le souhaitent, prendre un repas.
- Pour se rendre à l’Antre Autre :
- en métro : arrêt Hôtel de Ville
- en bus : lignes C13 et C18 arrêt Mairie du 1er ou lignes 19, C14 et C3 à l’arrêt Terreaux
- en vélo’v : stations Place Sathonay, Carmélites Burdeau, Place de la paix
[afpyro] AFPyro à Paris - le 19 avril 2013
Le premier Afpyro du printemps aura lieu le vendredi 19 avril, à partir de 19h30, à JMSI / HackSpark, 64 Rue Alexandre Dumas, Paris.
Nous pourrons prendre des bières au bar à côté, et profiter des locaux de JMSI pour discuter.
- Pour se rendre à JMDS / HackSpark :
- en métro : arrêt Alexandre Dumas
- en bus : bus 76, arrêt Alexandre Dumas
[Biologeek] Passage à l'échelle
Je m'interroge de plus en plus sur cette notion de passage à l'échelle que l'on nous encourage à anticiper avec le Cloud. J'ai de plus en plus l'impression qu'elle est liée à des business models déficients qui misent sur la masse d'utilisateurs pour avoir une chance à terme de monétiser le service. Je comprends que l'on puisse avoir des investissements à rentabiliser et qu'en visant haut, malléable, on pense pouvoir retrouver un équilibre financier plus rapidement. Pourquoi placer des paliers financiers aussi élevés ?
Avoir l'ambition d'un service mondial avec des millions d'utilisateurs est finalement aller à l'encontre du Web en centralisant des données et des usages. Il y a de la place pour plusieurs services, pour de la diversité, pour des motivations et des valeurs différentes, pour des communautés complémentaires. Pourquoi vouloir devenir le TF1 du Web ?
Quelles sont les relations que vous pouvez entretenir avec des millions d'usagers ? Sont-elles sincères ? Automatisées ? Avez-vous délégué ces relations ? La reconnaissance de l'utilité d'un service passe par ces retours, c'est une forme de motivation qui s'inscrit dans la durée. On ne vend pas un service au plus offrant lorsque l'on a établi ces relations. Pourquoi chercher à ne plus être en mesure de gérer ces liens ?
Le passage à l'échelle est emblématique d'une croissance effrénée et malsaine.
[logilab] Pylint 10th years anniversary from June 17 to 19 in Toulouse
After a quick survey, we're officially scheduling Pylint 10th years anniversary sprint from monday, June 17 to wednesday, June 19 in Logilab's Toulouse office.
There is still some room available if more people want to come, drop me a note (sylvain dot thenault at logilab dot fr).
[logilab] Pylint development moving to BitBucket
Hi everyone,
After 10 years of hosting Pylint on our own forge at logilab.org, we've decided to publish version 1.0 and move Pylint and astng development to BitBucket. There has been repository mirrors there for some time, but we intend now to use all BitBucket features, notably Pull Request, to handle various development tasks.
There are several reasons behind this. First, using both BitBucket and our own forge is rather cumbersome, for integrators at least. This is mainly because BitBucket doesn't provide support for Mercurial's changeset evolution feature while our forge relies on it. Second, our forge has several usability drawbacks that make it hard to use for newcomers, and we lack the time to be responsive on this. Finally, we think that our quality-control process, as exposed by our forge, is a bit heavy for such community projects and may keep potential contributors away.
All in all, we hope this will help to have a wider contributor audience as well as more regular maintainers / integrators which are not Logilab employees. And so, bring the best Pylint possible to the Python community!
Logilab.org web pages will be updated to mention this, but kept as there is still valuable information there (eg tickets). We may also keep automatic tests and package building services there.
So, please use https://bitbucket.org/logilab/pylint as main web site regarding pylint development. Bug reports, feature requests as well as contributions should be done there. The same move will be done for Pylint's underlying library, logilab-astng (https://bitbucket.org/logilab/astng). We also wish in this process to move it out of the 'logilab' python package. It may be a good time to give it another name, if you have any idea don't hesitate to express yourself.
Last but not least, remember that Pylint home page may be edited using Mercurial, and that the new http://docs.pylint.org is generated using the content found in Pylint source doc subdirectory.
Pylint turning 10 and moving out of its parents is probably a good time to thank Logilab for paying me and some colleagues to create and maintain this project!
[novapost] Django1.5 : passer au Configurable User Model
Depuis la version 1.5 de Django, il est possible d'utiliser un Configurable User Model en lieu et place de django.contrib.auth.User.
Cela permet, par exemple, de se passer de proxy model ou encore de fusionner le profil avec l'utilisateur, pour éviter des join dans les requêtes SQL.
Très pratique, et facile à mettre en place sur un projet qui commence juste, mais comment gérer ça en utilisant South sur un projet déjà bien en place ?
Le but est donc de fusionner l'utilisateur et le profil, avec pour aide/contrainte d'utiliser South, autant sur des plateformes existantes (serveur de production, de pré-production) que sur les plateformes de développement : donc les migrations doivent fonctionner sur une création de base, tout autant que sur une migration simple.
Nous allons détailler plusieurs stratégies.
Contexte
Notre projet utilise depuis longtemps un proxy model sur l'utilisateur, ne rajoutant que quelques méthodes. Toutes les données liées à l'utilisateur sont par ailleurs stockées dans un profil, qui est utilisé par le biais de get_profile() (et le setting AUTH_PROFILE_MODULE).
from django.db import models from django.contrib.auth.models import User class RH2User(User): class Meta: proxy = True ... class RH2UserProfile(models.Model): some_field = models.CharField(max_length=50) some_other_field = models.BooleanField() def some_method(self): ...Le résultat, une fois le profil fusionné avec l'utilisateur :
from django.db import models from django.contrib.auth.models import AbstractUser class RH2User(AbstractUser): some_field = models.CharField(max_length=50) some_other_field = models.BooleanField() def some_method(self): ...Ne pas oublier de fusionner les managers, les méthodes save(), et de dédoublonner les champs ayant le même nom (dans notre cas, RH2UserProfile.last_login a été renommé en RH2User.previous_last_login, étant donné que le modèle auth.User d'origine avait déjà un champ last_login).
Il faut par ailleurs rechercher et remplacer le cas échéant toutes les occurrences de :
- RH2UserProfile
- get_profile()
- rh2userprofile__
- .user
La problématique
À partir du moment où le paramètre AUTH_USER_MODEL est renseigné :
- les tables auth_user, auth_user_user_permissions, auth_user_groups ne sont plus automatiquement crées par un python manage.py syncdb
- toutes les migrations South existantes sur des modèles ayant une ForeignKey ou Many to Many ne passerons plus tel quel
Il y a donc principalement deux stratégies pour les migrations South, une fois qu'on a notre modèle RH2User complet (et non plus proxy) ainsi que AUTH_USER_MODEL = 'account.RH2User' dans les paramètres :
- Modifier la migration initiale de l'app account, puis toutes les migrations suivantes ainsi que les migrations des app ayant une relation avec l'utilisateur pour qu'elles se basent sur account_rh2user au lieu de auth_user
- Rajouter la création de la table auth_user, auth_user_user_permissions et auth_user_groups dans la migration initiale de l'app contenant le modèle complet, puis rajouter une migration qui va renommer la table auth_user en account_rh2user
Dans les deux cas, il faudra être attentif à l'ordre d'exécution des migrations : toutes les applications ayant une relation avec l'utilisateur devront dépendre de la migration initiale qui crée la table auth_user ou account_rh2user.
Dans le deuxième cas, il faudra de plus que la première des migration suivant le renommage, pour chaque application, dépende de cette migration.
Création de account_rh2user et modification des migrations
Le plus simple est de créer une migration de schéma pour avoir le code nécessaire à la migration 0001_initial de l'application account :
$ python manage.py schemamigration accountIl suffit alors de recopier le code de la migration créée, de le rajouter au fichier account/migrations/0001_initial.py, puis de supprimer cette nouvelle migration qui ne sera pas utilisée.
Il faut ensuite modifier chacune des migration, en prenant exemple sur ce qui a été fait sur django-oauth2-provider.
Il reste la problématique de la migration des serveurs déjà en production (qui ont déjà un certain nombre de migrations effectuées, et une base de donnée à conserver). Une solution serait de créer une migration de données et de tester l'existence de la table auth_user, et le cas échéant de dupliquer les données dans la table account_rh2user.
N'ayant pas testé cette solution, je ne peux la garantir.
Création de auth_user puis renommage
C'est la solution que nous avons choisi, étant donné le nombre de migrations que nous avons (près d'une centaine), qu'il aurait fallu modifier une à une, ainsi que le soucis de migration des serveurs déjà en production.
Il faut dans l'ordre :
- créer les tables auth_user, auth_user_user_permissions et auth_user_groups dans la migration 0001_initial de account
- créer une migration dans account qui renomme la table auth_user en account_rh2user
- créer une migration dans account qui rajoute les champs du modèle profil à l'utilisateur
- créer une migration de données pour dupliquer toutes les données de profil dans la table account_rh2user
- pour chaque application ayant une relation vers l'utilisateur, la prochaine migration créée devra dépendre de la migration qui renomme la table
Conclusion
Le plus compliqué dans toute cette histoire est la gestion de dépendances entre les migrations.
Une autre solution non évoquée aurait été de repartir de 0 pour les migrations : supprimer toutes les migrations existantes, ainsi que la table south_migrationhistory, puis reconvertir toutes les applications à South :
$ python manage.py convert_to_south ....L'avantage est qu'il n'y a alors aucun soucis de dépendances entre les migrations, et qu'on repars de quelque chose de propre.
Les inconvénients sont multiples : gérer une migration (à la main?) pour les plateformes en cours d'utilisation, impossibilité de retourner en arrière automatiquement, perte de l'historique...
Il y a une autre possibilité (à tester !) qui consiste à spécifier l'attribut db_table = 'auth_user' dans la Meta de notre nouveau modèle RH2User, pour qu'il utilise exactement la même table. En théorie, il n'y a alors pas besoin de migration, mais il reste à gérer la fusion du profil dans l'utilisateur.
[ascendances] Cohabitation Jinja et AngularJS
Jinja2, un moteur de templates en Python, et AngularJS, un framework web côté client en Javascript, utilisent tous les deux les accolades pour indiquer une variable ou une structure à interpréter. Par exemple {{ choucroute }} pour une variable qui a du goût. D’où conflit. Pour résoudre ce problème, trois possibilités : Diviser les fichiers pour régner […]
[tshirtman] Kivy hackathon in paris
Last week saw the first 24h hackathon event entirely dedicated to Kivy happening, in Paris. It was initiated by a hightly motivated student of the Cifacom school in paris, David Scheck, and attended by 4 groups of students, each with 3 to 5 members. I was pleased to share the hard task to help students discover the technology with my colleague Julien Miotte, from Majerti during the event.
The student weren’t very familiar with Python, and totally new to kivy, so it was a really challenging situation for both the student and the framework, would they be able to achieve anything in the hard limit of 24 hours?
First, after the students brainstormed on their project ideas, we got them through the major concepts and important classes of Kivy, using the Getting started page, it’s hard to get the point of properties accross in 10 minutes to student that didn’t have experienced the need of them, but I tried to at least make them aware it would be useful. The kv language presentation at this point, even if rudimentary, was probably useful, as we will see later.
Then students went on to try to build their project, except for a few quickly solved issues with windows installation, the starting was smooth. Julien and I gave some more explanations about how to start building a basic App (the doc is there, but who reads the doc? ;)) and people started designing their interface, and build the core of their application.
One of the most required feature was certainly the use of multiple screens in an application, to build menu, so the use of ScreenManager was explained to at least two groups. Most of the end results consisted of interfaces done mainly in Kv, and some internal logic to display data, most group had a quite ambitious target, using geolocalisation, databases, complex interactions, and I wasn’t very optimistic on the odds of seeing them completed. Although I was right in that, I was still happy of the good designs they came up with, applications were incomplete (to a notable exception, will come back to that soon), but some interfaces were beautiful, reactive and engaging, so it was nice to see the magic happen in such a short time.
One group had a very different objective of the others, and although they had a difficult start, it turns out they were probably the best at teamwork, and had chosen a target they could actually achieve, a game! These motivated coders/artists came up with a classic 2D sidescroller, when the main character has to avoid elements on her way. Simple, to the point, and a quite a good realisation in such a short time, the fact that they created original artworks and integrated them in their game, and that the general theme had a nice touch of humour in the current political events in France certainly hearned them points, on top of having a completed project. They even could test it on Android during the competition time, and it was running perfectly. Their hard work earned them the right to run in a bigger international competition in the near future the angel hack in paris, next month, congratulation! I can only hope they’ll chose kivy as their secret weapon in this competion too.
I would love to have better pictures, but i didn’t think much of taking them during the event, I should have better ones soon.
[Biologeek] Être geek
Si j'en crois les récents et moins récents billets, le geek serait un paralytique. Doublé d'un perfectionniste à la limite de l'autisme. Dur.
Difficile pour moi car c'est justement ce que je considère être la définition du geek : non pas celle de la paralysie mais celle de la curiosité (maladive parfois, soit). Pour moi le terme « geek » n'est plus du tout connoté informatique et encore moins lié à une culture, c'est le fait de pouvoir se renseigner dans un domaine particulier de façon efficace et rationnelle.
Cette faculté se développe souvent au service de la consommation car cela permet de croiser facilement des données tangibles mais pas uniquement. Il y a des geeks dans tous les domaines qui vont être à la recherche de la rareté, de la qualité, de l'esthétique dans leurs professions et/ou dans leurs loisirs. Qui vont se réaliser dans l'apprentissage de nouveaux métiers qui ne leur serviront parfois qu'une fois dans leur vie mais ils auront au moins eu le sentiment d'avoir essayé de bien le faire (de façon insatisfaisante d'après eux mais c'est un autre débat).
Revenons à l'informatique, connaissez-vous un autre métier qui génère autant de diversité ? On pourrait y voir un manque de maturité du domaine tout jeune, j'y vois plutôt cette approche geek poussée à l'extrême : creuser tout ce qui peut l'être, faire pousser l'arbre des possibles autant que cela est faisable, rejoindre un horizon qui en ouvre tant d'autres. La chance de l'informatique ce n'est pas d'être un secteur encore à défricher mais un catalyseur à personnes curieuses. Et c'est la raison pour laquelle j'aime mon métier, en mouvement :-).
[tarek] Declaring dependencies in Python
In Python Packaging, when you are giving for your project a list of dependencies, the right approach is to do whatever works for the majority of your users because there's a plethora of techniques used by people out there to deploy Python software.
That's the case for 2 main reasons:
- Most Python projects can be deployed in different operating systems - and unless you're doing a specific packaging work for each one of them, you are doomed to provide a good enough generic package.
- There are different installers with different approaches and your Python projects should try to be compatible with all of them.
Some of users will have issues with your projects, you have to accept this fact and just make sure you provide enough documentation and hints for them to work around those issues.
This blog entry tries to summarize my current knowledge on what's the best way to defining dependencies - I hope I'll have some feedback so I can update it with better techniques. Also, note that I have not applied this to all my projects. I should.
So if you disagree on my approach please comment !
Note
I am not talking about Virtualenv on purpose here, to avoid extra complexity.
Nature of your project
The first thing to think about is the nature of your project. They are two kind of projects in the Python world that can be installed by users:
- library & tools that will be used in conjunction with other Python projects.
- End-user applications that are using a plethora of other Python projects themselves.
The first category is what we create most of the time: utility modules, extensions for some frameworks, library to connect to a database, etc.
The second is a bit specific. It can be a framework, a website or a desktop application - Most of the time it's driving the whole Python environment it's running in and dictates what should be installed.
Library & tools
For library & tools, my advice is to do the following:
- provide a setup.py file that uses distribute/setuptools install_requires option, and when appliable provide a pure distutils fallback.
- do not pin any dependencies in your setup.py - it turns out it's making people's life a pain when they want to tweak the versions of the libraries themselves in tools like zc.buildout
- provide a pip requirements file where everything is pinned. That's your dependencies documentation. It says what versions of each dependencies your project depends on. When possible, add indirect dependencies as well in it.
- In your installation instructions, explain that using the pip requirements file is the recommended way -- I usually even provide a Makefile that does it - but that running pip install directly should work fine.
- In your continuous integration tool - you are using one, right? ;) use tox to run your tests in all Python versions you are supporting, and also by deploying your code with pinned dependencies and unpinned dependencies.
Here's a setup.py example:
try: from setuptools import setup install_requires = ['gevent', 'requests'] try: import argparse except ImportError: install_requires.append('argparse') kws = {'install_requires': install_requires} except ImportError: from distutils.core import setup kws = {} setup(name='yourproject', version='1.1', etc.., **kws)And the Pip requirements file for Python 2.6
gevent==0.13.8 requests==1.2.0 argparse==1.2.1End-user application
Not maintaining one myself, I have no clue what's the best way to do this but I suspect you really want to maintain a list of projects versions that are working with a given version of your project.
I recall Zope has this pretty neat thing called the Known Good Set (KGS) where they maintain a list of versions that are known to work well together: https://pypi.python.org/pypi/zope.kgs
In any case, deploying a whole Python stack in real life cannot be done with a simple pip install PROJECT call, unless it's a small thing. So maintaining a pip requirements file sounds like a good approach here.
So all-in-all I guess every advice I gave in the first section can be applied for end-user applications as well - as long as you make it clear that running pip install PROJECT won't be enough.
[sciunto] Mat : supprimer les métadonnées de fichiers
mat est un logiciel libre (GPLv2) utilisable en console et en interface graphique. Il permet de supprimer les métadonnées présents dans des fichiers (images, documents, multimédia). Ces métadonnées peuvent contenir des noms d’auteur, des numéros de série d’appareil ou encore des données de localisation. Je suis le mainteneur du logiciel pour archlinux. Mat est écrit […]
[Biologeek] Manuel vs. jardin
Gawel me faisait remarquer que mes réflexions sur ce site laissaient un goût d'inachevé. Cela m'a fait pas mal réfléchir après 3 mois à publier un jour sur deux et je pense avoir trouvé la raison de cette insatisfaction. J'écrivais auparavant surtout des articles techniques qui expliquent une bonne pratique ou une façon de faire quelque chose de précis à la manière d'un manuel. Ce n'est plus le cas, je sème aujourd'hui des graines de réflexions pour qu'elles puissent germer chez d'autres. Je ne cherche plus à apporter des réponses toutes faites, je souhaite qu'elles se développent indépendamment du semeur. À vous de faire fleurir votre jardin !
PS : j'accepte aussi les graines étrangères dans mon jardin.
[afpy.org] Le nouveau site de l'AFPy
Le week-end du 30-31 Mars 2013, nous avons terminé et mis en prod le nouveau site de l'AFPy...
[Biologeek] Propriéterre
Le premier qui, ayant enclos un terrain, s'avisa de dire « Ceci est à moi », et trouva des gens assez simples pour le croire, fut le vrai fondateur de la société civile. Que de crimes, de guerres, de meurtres, que de misères et d'horreurs n'eût point épargnés au genre humain celui qui, arrachant les pieux ou comblant le fossé, eût crié à ses semblables : « Gardez-vous d'écouter cet imposteur ; vous êtes perdus, si vous oubliez que les fruits sont à tous, et que la terre n'est à personne. »
Jean-Jacques Rousseau, L'origine de l'inégalité parmi les hommes (2ème Discours seconde partie)
Thomas me demandait hier mon avis sur la propriété (vs. location) et je dois dire que mon avis n'a pas changé au cours de ces dernières années : j'ai beaucoup de mal à concevoir que l'on puisse être propriétaire d'un morceau de planète. Je ne peux cautionner les dérives que cela a engendré.
Ce qui m'importe par contre c'est le suivi volontaire de mon adresse postale, de mon numéro de téléphone ou de mon nom de domaine. Les affaires peuvent changer de lieux, les amis de répertoire, les données de serveurs mais le plus important est de pouvoir y accéder de manière pérenne. Quel que soit le nombre de redirections nécessaires. Malheureusement, si cela est facile et encouragé sur le web, il est beaucoup plus difficile de le mettre en pratique pour un numéro de téléphone ou une adresse physique, les redirections étant temporaires (et coûteuses) a fortiori lorsque l'on change de pays !
La propriété de la terre est souvent mise en avant comme un héritage — on se dédouane de cet acte en proposant ses enfants comme alibi, excuse culpabilisante classique. Or il se trouve que je suis également contre le mécanisme d'héritage ;-). L'autre argument courant est « si les choses tournent mal » et là je suis assez circonspect. Si ça tourne vraiment mal, il vaudra mieux être prêt à survivre de manière itinérante qu'à avoir un lieu fixe à défendre. S'il s'agit juste de la peur de la vieillesse et de la rupture du lien social avec sa famille et ses enfants il y a d'autres questions à se poser en amont pour éviter cette situation.
J'aime la liberté et la légèreté que me procure la location, l'idée d'être « de passage » dans cet endroit et d'en apprécier pleinement chaque instant, l'idée de se limiter dans ses appartenances pour pouvoir être mobile. De plus, la propriété d'un bien m'est beaucoup plus stressante que son éphémérité.
[blog.afpy.org] Comment écrire un blog post ?
Tous les membres de l'AFPy peuvent proposer un article si ils le souhaitent. La procédure est simple:
récupérez les sources du blog:
hg clone http://hg.afpy.org/blogvérifiez que le blog se compile bien correctement: après avoir installer pelican en local, tapez:
make installpushez votre code sur le hg de l'afpy:
hg add votre_fichier.py hg commit -m "votre message" -u username hg pushet voilà !
Si vous souhaitez uniquement proposer un article, vous pouvez utiliser la fonctionalité de "draft" de pelican, il suffit de mettre ":status: draft" comme métadonnée pour que l'article ne soit pas publié.
Tout cela est rappelé dans le fichier README du dépôt !
[logilab] PyLint 10th years anniversary, 1.0 sprint
In a few week, pylint will be 10 years old (0.1 released on may 19 2003!). At this occasion, I would like to release a 1.0. Well, not exactly at that date, but not too long after would be great. Also, I think it would be a good time to have a few days sprint to work a bit on this 1.0 but also to meet all together and talk about pylint status and future, as more and more contributions come from outside Logilab (actually mostly Google, which employs Torsten and Martin, the most active contributors recently).
The first thing to do is to decide a date and place. Having discussed a bit with Torsten about that, it seems reasonable to target a sprint during june or july. Due to personal constraints, I would like to host this sprint in Logilab's Toulouse office.
So, who would like to jump in and sprint to make pylint even better? I've created a doodle so every one interested may tell his preferences: http://doodle.com/4uhk26zryis5x7as
Regarding the location, is everybody ok with Toulouse? Other ideas are Paris, or Florence around EuroPython, or... <add your proposition here>.
We'll talk about the sprint topics later, but there are plenty of exciting ideas around there.
Please, answer quickly so we can move on. And I hope to see you all there!
[QuiSaura] Python et les boucles imbriquées via itertools.product
Je ne connaissais pas non plus.
[Biologeek] Fondée sur des valeurs
J'étais à Devoxx hier soir comme annoncé pour présenter une approche différente de la SSII et je devais décrire l'expérience scopyleft :
scopyleft est une SCOP (Société Coopérative et Participative) toute jeune — seulement 3 mois — ce qui ne permet pas d'avoir le recul nécessaire pour valider ou invalider une approche. Ni même pour juger de sa viabilité. C'est pourquoi j'ai choisi de vous parler de sa genèse : autrement dit, de l'avant scopyleft.
Notre approche a été de ne pas commencer par le traditionnel business plan mais de nous aligner entre nous 4 sur les valeurs fondatrices que nous voulions comme cap au cours de la vie de l'entreprise. Si l'approche économique nous donnait une vision pour environ 1 an avec son lot d'incertitudes et autres pivotages, passer par des valeurs nous amenait à considérer une durée beaucoup plus longue… de l'ordre de la décennie. Ambitieux projet.
Cela commence par mieux se connaître, discuter de valeurs permet d'aller beaucoup plus en profondeur qu'une discussion sur le retour sur investissement, les salaires ou le titre que l'on souhaite avoir dans l'entreprise. Une fois d'accord sur le fond — honnêteté intellectuelle, courage, bien-être, respect et partage —, nous sommes arrivés à formuler une phrase (à défaut d'un manifeste) résumant notre objectif commun :
Travailler entre humains, sur des projets éthiques et intéressants, tout en privilégiant le bien-être et le plaisir de chacun.
Ces valeurs et cette maxime nous guident dans nos choix stratégiques au quotidien pour accepter ou non un client, pour concrétiser une initiative ou pour accompagner un projet.
C'est notamment ce qui nous a menés à choisir le statut de SCOP, un statut basé sur une gouvernance démocratique (1 homme = 1 voix) et favorisant la pérennité des emplois et du projet d'entreprise (co-entrepreunariat et réserve importante imposés). Ce statut met l'humain au cœur de l'entreprise, ce qui diffère d'une entreprise traditionnelle qui se concentre sur son capital. C'est un changement de paradigme majeur dans une société capitaliste. Nous avons enrichi ces statuts d'une co-gérance tournante (faute de pouvoir gérer à 4) et d'une égalité salariale.
Un autre aspect de la SCOP qui a attisé notre curiosité est la notion de solidarité inter-entreprises au sein de la confédération des SCOP. C'est notamment ce que l'on a pu constater avec les entreprises qui nous ressemblent, il existe relativement peu de SCOP (dans l'informatique) mais elles se serrent les coudes !
Ces 3 derniers mois nous ont permis de reconsidérer notre approche économique, de mieux nous connaître, d'avoir énormément de retours (à la fois de nos clients et de nos pairs), de coder ensemble, d'accompagner ensemble, d'assister à des conférences ensemble, de stresser ensemble, de faire de l'administratif ensemble, autant de tâches qui sont loin d'être insurmontables et qui sont le lot quotidien du créateur d'entreprise mais qui s'avèrent être moins pénibles lorsque l'on poursuit un objectif un peu plus « élevé » (sain ?) que le simple aspect financier. Augmenté par le fait de le réaliser à plusieurs.
Cette aventure aurait difficilement pu être envisageable sans avoir confiance dans notre savoir-faire acquis lors de nos expériences respectives à nos comptes. Malgré notre expérience dans le domaine, l'un de nos objectifs à terme est de nous libérer de la prestation pour co-produire des produits utiles, éthiques et open-source.
Pour résumer, scopyleft est une entreprise fondée sur des valeurs pour créer de la valeur. En coopérant.
Je n'ai pas dû dire la moitié de tout ça au final car l'approche monologue était un peu ennuyante et je préférais avoir plus de temps pour discuter. Et là je n'ai pas été déçu car les réactions ont été nombreuses, un peu décousues et agressives mais cela montrait un intérêt certain.
Décousues car on était nombreux à pouvoir répondre (une dizaine) et qu'il y avait beaucoup de questions qui passaient brutalement du fond à la forme selon le niveau de réflexion de chacun. Peut-être qu'un autre format (groupes de discussions par exemple) se prêtait mieux à la discussion ouverte mais la salle n'était pas adaptée.
Agressives principalement car le titre NoSSII a été interprété comme un affrontement alors que l'on avait bien mis en avant le côté Not only. Dommage, l'idée n'était pas du tout d'aller dans ce sens mais il est peut-être normal que certains se sentent déstabilisés lorsqu'on présente quelque chose de différent.
Quelques questions dont je me souviens :
À quoi cela sert-il de créer une SCOP vs. un GIE (Groupement d'intérêt économique) d'indépendants ?
L'objectif n'est pas du tout le même, principalement car on ne se regroupe pas en SCOP pour un intérêt économique mais pour partager et échanger à un autre niveau.
Qu'est-ce qui vous différencie d'une SSII classique finalement ?
D'une part le fait d'avoir le contrôle sur les objectifs de la société, ils peuvent être lucratifs ou pas, ils peuvent être citoyens ou pas, ils peuvent être éthiques ou pas, ils peuvent s'émanciper de la prestation ou pas. D'autre part, le fait de mettre l'humain au cœur du cadre de travail est un changement radical, ce qui change aussi les relations avec les clients.
Comment gérez-vous les problèmes d'éthiques ?
Nous n'avons pas de règle pré-définie, chaque cas aux limites est discuté longuement et un vote suivant les pratiques de l'Open-Source (et de Django) permet de trancher.
Est-ce que ça peut fonctionner à plus grande échelle ?
L'exemple du handbook de Valve a été mis en avant avec quelques autres. La question c'est plutôt de savoir quel est l'intérêt de passer à une autre échelle ? Dans notre cas par exemple, c'est une volonté forte de rester à taille humaine.
Ça rejoint d'ailleurs une question relative au référencement dans les services achats des grosses entreprises. C'est peut-être plus difficile en étant petit mais en fait ça nous intéresse peu de travailler avec ce type de structures donc ça limite le problème. Beaucoup de questions n'allaient pas assez loin dans le pourquoi et se limitaient au comment.
Comment trouvez-vous des clients ?
On ne sait pas démarcher. Partant de ce constat, ça passe plutôt par de la recommandation ou des connaissances qui nous suivent depuis longtemps et le partage de nos expériences. On a également la chance d'avoir des entreprises partageant nos valeurs qui nous transmettent des demandes.
Les réactions à chaud sont plutôt positives, j'espère que l'on aura réussi avec Ninja Squad et Lateral Thoughts à au moins attiser la curiosité de certains et pourquoi pas à en motiver pour monter leur propre structure !
[Mise à jour] : Retour sur Devoxx France - BOF NoSSII
[tarek] News from the Quality front
Flake8 plugins
Some time ago, we've unified our efforts to build & maintain code quality checkers. For instance, Flake8 is now entirely based on plugins: there's a pep8, pyflakes & mccabe plugin and you can add more.
The family of Flake8 plugin is growing & Florent has started listing them at http://flake8.readthedocs.org/en/latest/extensions.html#existing-extensions
Mailing list
Another good news is the creation of a mailing list dedicated to all the code quality tools : http://mail.python.org/mailman/listinfo/code-quality
If you're interested in those tools, or you are maintaing one yourself, consider joining this mailing list.
[afpyro] AFPyro à Lyon - le 27 mars 2013
Le premier Afpyro du printemps aura lieu le mercredi 27 mars à partir de 20h à l’Antre Autre - 11 rue Terme - 69001 Lyon.
Simon Sapin nous fera une présentation sur Sphinx. Sphinx est un outil qui permet de créer de la documentation pour vos projets ou vos produits.
L’Antre Autre est un lieu où nous pouvons discuter autour d’un verre, et, pour ceux qui le souhaitent, prendre un repas.
- Pour se rendre à l’Antre Autre :
- en métro : arrêt Hôtel de Ville
- en bus : lignes C13 et C18 arrêt Mairie du 1er ou lignes 19, C14 et C3 à l’arrêt Terreaux
- en vélo’v : stations Place Sathonay, Carmélites Burdeau, Place de la paix
[noirbizarre] Huit recettes pour Pelican
Après avoir passé beaucoup de temps sur mon blog depuis le passage à Pelican, j’ai décidé de publier quelques recettes que j’utilise.
Libre à vous de les utiliser et de les améliorer. Je suis évidement intéréssé par votre retour.
Arborescence plate
Cette recette n’en est pas vraiment une, mais elle servira de base pour les chemins des autres recettes.
Je l’utilise pour avoir l’arborescence suivante dans mon dépôt git:
├── articles │ ├── categorie1 │ │ └─ article.rst │ └── categorie2 ├── extras │ ├─ 404.html │ └─ robots.txt ├── images │ ├── theme1 │ │ └─ image.png │ ├── theme2 │ │ └─ image.png │ └─ image.png ├── local_plugins │ └─ plugin.py ├── pages │ └─ page.rst ├── theme │ ├── static │ │ ├── css │ │ └── images │ └── templates │ ├─ template1.html │ └─ template2.html ├─ .gitignore ├─ Makefile ├─ nginx.conf ├─ pelicanconf.py ├─ publish.sh ├─ publishconf.py └─ requirements.pipPour cela j’ai modifié mon fichier pelicanconf.py comme suit:
PATH = dirname(__file__) OUTPUT_PATH = join(PATH, 'output') ARTICLE_DIR = 'articles' THEME = 'theme' STATIC_PATHS = ("images", ) FILES_TO_COPY = ( ('extras/robots.txt', 'robots.txt'), ) PLUGINS = ( 'pelican.plugins.gzip_cache', 'pelican.plugins.sitemap', 'local_plugins.plugin' )Délégation d’authentification OpenID
Mon blog Wordpress me servait aussi à m’authentifier grâce à OpenID. Cette modification a donc été la première que j’ai réalisée. Je l’ai réalisée avec MyOpenId comme fournisseur puisque c’est celui que j’utilise, libre à vous de l’adapter à votre fournisseur.
Créez un template theme/templates/myopenid.html:
{% if MYOPENID_USERNAME %} <link rel="openid.server" href="http://www.myopenid.com/server" /> <link rel="openid.delegate" href="http://{{MYOPENID_USERNAME}}.myopenid.com/" /> <link rel="openid2.local_id" href="http://{{MYOPENID_USERNAME}}.myopenid.com" /> <link rel="openid2.provider" href="http://www.myopenid.com/server" /> <meta http-equiv="X-XRDS-Location" content="http://www.myopenid.com/xrds?username={{MYOPENID_USERNAME}}.myopenid.com" /> {% endif %}Dans le bloc <head> du template theme/templates/base.html ajoutez:
{% include 'myopenid.html' %}Dans le fichier de configuration de publication publishconf.py, ajoutez:
MYOPENID_USERNAME = 'me'Boutons Google +1, Twitter et Flattr
Cette modification de thème permet d’insérer des boutons de partage chargés de façon asynchrone.
Ces boutons ne seront visibles qu’en mode publié puisqu’ils requierent que SITEURL soit définie.
Créez les templates des boutons
theme/templates/plusone.html:
{% if PLUS_ONE and share_url %} <span class="g-plusone" data-href="{{ share_url }}" data-size="medium"></span> {% endif %}theme/templates/twitter.html:
{% if TWITTER_USERNAME and share_url and share_title %} <a href="http://twitter.com/share" class="twitter-share-button" data-count="horizontal" data-via="{{TWITTER_USERNAME}}" data-related="{{TWITTER_USERNAME}}" data-url="{{share_url}}" data-text="{{share_title|striptags}}">Tweet</a> {% endif %}theme/templates/flattr.html:
{% if FLATTR_USERNAME and share_url and share_title %} <a class="FlattrButton" style="display:none;" title="{{share_title}}" href="{{share_url}}" data-flattr-uid="{{FLATTR_USERNAME}}" {% if article and article.tags %}data-flattr-tags="{{article.tags|join(',')}}"{% endif %} {% if FLATTR_LANG %}data-flattr-language="{{FLATTR_LANG}}"{% endif %} data-flattr-button="compact" data-flattr-category="text"> {{share_title}} </a> {% endif %}Créez les templates des scripts de chargement asynchrone:
theme/templates/plusone_script.html:
{% if PLUS_ONE and SITEURL %} <script type="text/javascript"> window.___gcfg = {lang: '{{PLUS_ONE_LANG}}'}; (function() { var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true; po.src = 'https://apis.google.com/js/plusone.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s); })(); </script> {% endif %}theme/templates/twitter_script.html:
{% if TWITTER_USERNAME and SITEURL %} <script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="https://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script> {% endif %}theme/templates/flattr_script.html:
{% if FLATTR_USERNAME and SITEURL %} <script type="text/javascript"> (function() { var s = document.createElement('script'); var t = document.getElementsByTagName('script')[0]; s.type = 'text/javascript'; s.async = true; s.src = '//api.flattr.com/js/0.6/load.js?mode=auto'; t.parentNode.insertBefore(s, t); })(); </script> {% endif %}A chaque emplacement où vous désirez voir apparaitre ces boutons, insérez le bloc suivant:
{% if SITEURL %} {%- set share_url = [SITEURL, article.url]|join('/') -%} {%- set share_title = article.title -%} {% include 'twitter.html' %} {% include 'plusone.html' %} {% include 'flattr.html' %} {% endif %}Modifiez biensur les variable share_url et share_title en fonction du contexte.
Modifiez votre template theme/templates/base.html pour y insérer à la fin du bloc <body>:
{% if SITEURL %} {% include 'twitter_script.html' %} {% include 'plusone_script.html' %} {% include 'flattr_script.html' %} {% endif %}Renseignez vos identifiant et paramètres dans votre configuration de publication pelicanconf.py:
TWITTER_USERNAME = 'me' FLATTR_USERNAME = "me" FLATTR_LANG = "fr_FR" PLUS_ONE = True PLUS_ONE_LANG = 'fr'Pour adapter les paramètres à vos besoins, consultez les documentations officielles:
Filtre “urlencode” pour Jinja
Jinja 2 ne propose plus de filtre urlencode par défaut (Issue #17 et Pull Request #62).
Si pour une raison ou une autre vous en avez besoin, vous pouvez le rajouter vous même dans votre fichier pelicanconf.py:
import urllib from jinja2.utils import Markup def urlencode_filter(s): if type(s) == 'Markup': s = s.unescape() s = s.encode('utf8') s = urllib.quote_plus(s) return Markup(s) JINJA_FILTERS = { 'urlencode': urlencode_filter, }Balises META
Cette recette permet déclarer les balises <meta> dans le fichier de configuration.
Déclarez un dictionnaire META dans votre fichier pelicanconf.py:
from pelican import __version__ as PELICAN_VERSION METAS = { 'author': u'Me', 'description': u"My blog description", 'keywords': u'some, keywords, for, seo', 'generator': u'Pelican %s' % PELICAN_VERSION, }Ajoutez dans le bloc <head> du template theme/templates/base.html:
{% for name, content in METAS.iteritems() %} <meta name="{{name}}" content="{{content}}" /> {% endfor %}Page d’erreur 404
Cette recette fourni une page d’erreur 404 personnalisée avec le thème de votre blog. Elle fonctionne avec NGinx mais est adaptable à tout autre serveur.
Créez le template de votre page d’erreur 404, dans mon cas extras/404.html:
{% extends "base.html" %} {% block content_title %}Erreur 404{% endblock %} {% block content %} <section class="body page"> <h1 class="page-title">Erreur 404</h1> La page que vous cherchez n'existe pas. </section> {% endblock content %}Déclarez cette page dans votre configuration pelicanconf.py:
TEMPLATE_PAGES = {'extras/404.html': '404.html'}Indiquez à NGinx où trouver cette page:
server { # ... error_page 404 /404.html; # ... }Publication par git post-receive hook
Cette recette me permet de déployer mon blog dès que je push sur mon serveur.
A la fin de chaque déploiement, je ping les moteurs de recherche pour les notifier des modification du fichier sitemap.xml généré par le plugin Sitemap.
La configuration de NGinx et les dépendances Python sont elles aussi stoquées dans mon dépôt git, respectivement dans les fichiers nginx.conf et requirements.pip. Dans mon cas, j’utilise virtualenv mais vous pouvez l’adapter à votre configuration.
Sur votre serveur, modifiez ou créez le fichier blog.git/hooks/post-receive:
#!/bin/sh STAGING="/home/me/staging/blog" GIT_WORK_TREE=$STAGING git checkout -f cd $STAGING ./publish.shCe fichier doit être exécutable.
Dans votre dépôt git, ajoutez le fichier publish.sh à la racine:
#!/bin/sh VENV="venv" PUBLIC="/path/to/your/public/blog" SITEMAP=http://your.blog/sitemap.xml.gz # Setup virtualenv if [ ! -d "$VENV" ]; then virtualenv --distribute $VENV fi . $VENV/bin/activate pip install -r requirements.pip --use-mirrors # Deploy blog pelican -v -o $PUBLIC -s publishconf.py cp -f nginx.conf /etc/nginx/sites-available/your.blog sudo service nginx reload # Ping sitemap GOOGLE=http://www.google.com/webmasters/tools/ping?sitemap=$SITEMAP BING=http://www.bing.com/webmaster/ping.aspx?siteMap=$SITEMAP for url in $GOOGLE $BING; do curl -s -w "%{http_code} %{url_effective}\\n" "$url" -o /dev/null donePour ne pas perdre de temps à chaque push, je commente la ligne qui installe les dépendances python et je la décommente uniquement lorsque je les modifie.
Donnez les droits pour recharger NGinx via sudo à votre utilisateur:
# Reload nginx command Cmnd_Alias NGINX_RELOAD = /usr/sbin/service nginx reload # User privilege specification me ALL=NGINX_RELOAD, NOPASSWD: NGINX_RELOAD
[Biologeek] Éduquer à la joie
Ce livre est un appel aux parents, aux enseignants, aux maîtres et maîtresses d'école, aux adultes qui ne savent plus rêver, pour qu'ils mettent de côté, le temps de cette lecture, la masse des connaissances intellectuelles, de savoirs et de théories éducatives qu'ils ont appris sur leurs enfants, ce qu'ils croient « être bon » pour eux et pour leur développement. C'est une proposition impertinente de ne pas « faire les devoirs » que l'éducation nous a imposés tout au long de notre vie (réussir, aller vite, être les premiers…), mais de se laisser plutôt embarquer par le vent du changement qui parcourt notre monde aujourd'hui, y compris celui de l'école. Pour en finir avec le mépris systémique de ce qui, avec la pensée, nous distingue des animaux — à savoir notre capacité de rêver —, nous nous devons de redonner à l'éducation son rôle d'éveil. Chacun de nous recèle un don, un talent inné qui, s'il est respecté et honoré, nous transformera en être humain heureux, en contact avec sa joie de vivre, en accord avec soi-même et les autres. L'éducation a le devoir de reconnaître ce trésor, de le révéler et d'aider tout individu à le développer : l'enjeu n'est rien d'autre que l'accomplissement d'un monde en paix.
Antonella Verdiani, Ces écoles qui rendent les enfants heureux
Je suis en train de pas mal me renseigner sur les pédagogies et méthodes d'éducation dites alternatives suite à mes recherches sur l'Anthroposophie et sur ce qui distingue une secte d'une approche non traditionnelle (Montessori, Steiner, Freinet, modèle scandinave, éducation démocratique, éducation lente, école à la maison, etc).
Le point commun de ces écoles est qu'elles sont animées par des valeurs et des notions communes avant tout intérêt financier :
- Reliance
- Ouverture au questionnement existentiel
- Transdisciplinarité
- Complexité
- (In)novation
- Autorisation
- Incertitude
- Planter là où c'est fertile
Aussi lorsque j'apprends l'ouverture d'une nouvelle école dédiée à l'excellence de la France en matière de numérique (avec tout un vocabulaire anglophone mais passons), je ne peux que me réjouir et chercher les valeurs associées à cette méthode d'éducation qui se veut atypique et innovante. Malheureusement, la recherche est vaine. Le seul fil rouge du site est financier :
- comment vous rendre « rapidement opérationnel » pour l'entreprise ?
- comment devenir un « informaticien aux compétences recherchées » ?
- comment « coller à des réalités économiques » ?
J'ose espérer que mon métier ne se résume pas à une telle approche. Que mes futurs collaborateurs n'auront pas été formés comme des « commandos de Marines » pour sélectionner les meilleurs d'entre eux. Le rêve de ce regroupement d'entrepreneurs est finalement de créer une armée de mercenaires pour concrétiser leurs idées à bas prix. Et ce sans poser de questions car si ce n'est pas toi, ressource, ce sera ton voisin qui a reçu la même formation industrialisée basée sur la performance.
Né pour coder ? Non. Né pour prendre du plaisir à coder. Nuance.
[Mise à jour] : Éric D. trouve que 42 pour une seule école ? ça fait 41 de trop
[Biologeek] Accompagnement EMI
Accompagner, c'est célébrer, c'est manifester l'incroyable splendeur qu'est chacun(e).
Marie Milis
J'intervenais cette semaine à l'EMI CFD comme précédemment mentionné. L'occasion de rencontrer une promotion orientée graphisme et rédaction bien éloignée de la sphère des développeurs.
L'objet était de produire une petite application en une semaine qui utilise des données sur la thématique du Festival de Cannes, ce qui s'est avéré être un challenge intéressant autant du point de vue de l'accompagnement en lui-même que de celui du sociologue qui sommeille en moi.
Il y a eu 2 problèmes transverses aux équipes accompagnées sur lesquels je voudrais revenir :
- le manque de démarche expérimentale : lorsque l'on commence à « s'imbiber de données » (sic), il est important de formuler des hypothèses qui doivent être vérifiée par les données et non énoncer des points de vues que l'on cherche absolument à prouver avec des données. Il ne faut pas avoir peur de produire des esquisses de données — à travers des graphes tout simples — avant de penser en termes d'infographies. Pressés par le temps, nous n'avons pas pu développer la discussion sur la rigueur scientifique des journalistes mais cela aurait pu être intéressant, il est trop tentant de vouloir raconter une histoire pour sa simple audience.
- le manque de coopération : il y avait 3 équipes qui sont restées dans leur bulle tout le long de la semaine, quel dommage ! Chaque équipe — même dans le cadre d'une coopétition — aurait gagnée à mutualiser des données ou à se challenger sur les résultats obtenus. Cela aurait encouragé également une démarche itérative pour que ces échanges soient rendus possibles. Note : on nous a récemment qualifiés d'« intégristes de la coopération » avec scopyleft et j'assume pleinement ce rôle.
En recherche d'efficacité optimale, j'ai introduit la méthode MoSCoW la dernière journée en improvisant un ersatz de backlog priorisé. Le résultat s'est révélé être au-delà de mes espérances en terme de fluidité des cartes au sein de l'équipe pour effectuer les dernières tâches rapidement.
Il était assez frustrant de ne pas avoir le temps d'expliquer la vision que j'ai de mon métier et de transmettre une façon de travailler permettant de garder un rythme soutenable et procurant du plaisir. Frustration également (salutaire cette fois) de se retenir de trop encadrer, certaines expériences devant se faire par soi-même. Cela étant dit, mon seul vrai regret sur la semaine aura été de ne pas avoir pu rester à la fin lors de la rétro accompagnateurs et du pot qui aurait pu permettre de parler d'autre chose que de données :-).
Quelques idées pour améliorer la formation :
- donner quelques rudiments méthodologiques pour faire fonctionner les groupes, au moins en leur expliquant les différents axes possibles d'organisation (centralisé, démocratique, sociocratique, etc), la méthode à la RACHE ayant montré ses limites ;
- lisser la pression, elle ne devrait pas être croissante comme j'ai pu l'observer quel que soit le groupe, l'approche itérative permet d'y arriver efficacement ;
- donner davantage de temps pour affiner la vision initiale en leur proposant le thème plus tôt, beaucoup de temps a été perdu par simple manque de réflexion en amont.
L'éducation authentique ne se fait pas de A vers B, ni de A sur B, mais par A avec B, par l'intermédiaire du monde.
Paulo Freire
J'ai au moins autant appris que les « étudiants » cette semaine en ayant eu la possibilité d'observer des groupes de travail à l'ouvrage et en constatant une fois de plus la difficulté à travailler ensemble même en poursuivant un but commun. La concrétisation d'un projet ne doit jamais se faire au détriment de la communication dans l'équipe. Auquel cas on perd non seulement le projet, mais également l'équipe…
Pour finir, je voudrais remercier Yohan qui m'a permis de participer à cette formation (et de dormir sur son bateau pendant une semaine !).
[novapost] Django : Comment booster ses tests en 1 minute
Introduction
Hier, Boris, en stage ici pour 6 mois, me parle d'une astuce pour Django ultra simple, mais qui a tout bonnement divisé par deux notre temps de tests :O
Connexion, views et tests
Lors des tests, on doit souvent se connecter pour tester les droits de l'utilisateur ou encore accéder à des certaines pages de l'application Django décorée par un user_passes_test ou un login_required.
Si vous n'utilisez pas encore les techniques de tests unitaires des views proposée la semaine dernière par Benoît Bryon, cette solution simple va déjà vous faire gagner un temps fou.
À un problème, une solution
Grâce à l'article de Igor Sobreira [1], on s'est rendu compte que le temps passé dans le hachage des mots de passe était significatif sur la durée totale des tests.
Nous avons donc essayé la solution proposée par l'article (utiliser un UnsaltedMD5PasswordHasher pour les tests) et avons effectivement constaté une amélioration du temps total des tests.
En regardant le code de UnsaltedMD5PasswordHasher on s'est dit qu'on pouvait gagner encore plus de temps à la fois sur le hachage du mot de passe et sur la vérification de celui-ci.
En effet, tous les hasher Django utilisent une méthode de comparaison des mots de passe en temps constant afin d'éviter les problèmes de timing attaques [2].
Et en pratique ?
Il suffit de définir un PASSWORD_HASHER simplifié pour les tests pour gagner un temps fou lors de l'exécution des tests.
C'est aussi simple que ça.
La première étape consiste à définir un PASSWORD_HASHER minimaliste
# -*- coding: utf-8 -*- from django.contrib.auth.hashers import BasePasswordHasher, mask_hash from django.utils.datastructures import SortedDict class PlainPasswordHasher(BasePasswordHasher): """ The plain password hashing algorithm for test (DO NOT USE in production) """ algorithm = "plain" def salt(self): return '' def encode(self, password, salt): return '%s$$%s' % (self.algorithm, password) def verify(self, password, encoded): algorithm, hash = encoded.split('$$', 1) assert algorithm == self.algorithm return password == hash def safe_summary(self, encoded): return SortedDict([ ('algorithm', self.algorithm), ('hash', mask_hash(encoded, show=3)), ])Ensuite dans le fichier de settings propre aux tests, on modifie la liste des PASSWORD_HASHERS.
settings_test.py
from myapp.settings import * PASSWORD_HASHERS = ( 'myapp.hashers.PlainPasswordHasher', ),Attention à ne pas utiliser cela en production, car tous les mots de passes seraient stockés en clair dans la base de données.
Je vous invite à tester cette astuce sur votre base de tests pour voir vous-même la différence de temps et nous faire un retour en commentaire du post.
Si vous utilisez des fixtures de user, vous pouvez également utiliser la solution ci-dessous dans votre settings de test afin que les tests puissent décoder les mots de passe déjà stockés dans la base avec un autre algorithme de mot de passe.
settings_test.py
from myapp.settings import * PASSWORD_HASHERS = ['myapp.hashers.PlainPasswordHasher'] + list(PASSWORD_HASHERS)Conclusion
Cette astuce commence à être connue dans le monde Django, il y a même un ticket de Django qui en parle [3] et elle est maintenant décrite dans la documentation officielle de Django [4].
Notre hasher personnalisé permet juste de gagner encore un peu plus de temps. Sur notre base de tests nous avons gagné 10 minutes sur 50 minutes soit 20% de temps de tests en passant du UnsaltedMD5PasswordHasher à notre PlainTextPasswordHasher
Nous avions initialement divisé par deux notre temps de tests en passant à UnsaltedMD5PasswordHasher
[Inspyration] Python.org : Visualisez le nouveau site
Python.org fait peau neuve, le nouveau site est en prévisualisation pour test, avant le PyCON.
[novapost] Python decorators made easy
A Python decorator is, basically, a function that take a function as argument and return a function. This is a powerful feature. But it has some drawbacks:
- decorators are quite tricky to develop. Of course they are for Python newbies. But, as an experienced Python developer, I must admit I also have to think every time I code a decorator. I often feel the decorator pattern is a bit complex.
- decorators that take arguments are even more tricky to develop. They are decorator factories, aka "functions that return a function that take a function and return a function". Inception WTF?
- decorators without arguments are used without parentheses, whereas decorators with arguments require parentheses, even if you pass empty arguments. So when using a decorator, you have to wonder whether it takes arguments or not. A bit more to think about everytime you use a decorator.
- last but not least, function-based decorators are hard to test, because they return functions and you can't easily check internals. How can you check the state of the decorator after it decorated the function, but before you actually run it? Classes are really helpful for that.
This article introduces class-based decorators for Python, as a convenient way to develop and use Python decorators.
The examples described below are available as a Python file at https://gist.github.com/benoitbryon/5168914
Decorators are tricky to develop and use
As a reminder of drawbacks of decorators, here are some examples. If you are aware of those facts, feel free to jump to the next section.
Here is a simple decorator which prints "moo" then executes input function:
def moo(func): def decorated(*args, **kwargs): print 'moo' return func(*args, **kwargs) # Run decorated function. return decoratorYou use it like this:
>>> @moo ... def i_am_a(kind): ... print "I am a {kind}".format(kind=kind) >>> i_am_a("duck") 'moo' 'I am a duck'Here is the same decorator, which allows you configure the value to print:
def speak(word='moo'): def decorator(func): def decorated(*args, **kwargs): print word return func(*args, **kwargs) return decorated return decoratorYou use it like that:
>>> @speak('quack') ... def i_am_a(kind): ... print "I am a {kind}".format(kind=kind) >>> i_am_a("duck") 'quack' 'I am a duck'If you want to use default arguments for "speak", you have to use empty parentheses, i.e. you can't write @speak like you used @moo, you have to write @speak() instead.
I won't tell more about decorators here, there are plenty of articles about them on the web. I just wanted to highlight the fact that even simplest decorators are not as simple as they pretend.
But they could be! Let's introduce class-based decorators...
Hello world example
Here is a sample usage of the Decorator class:
class Greeter(Decorator): """Greet return value of decorated function.""" def setup(self, greeting='hello'): self.greeting = greeting def run(self, *args, **kwargs): name = super(Greeter, self).run(*args, **kwargs) return '{greeting} {name}!'.format(greeting=self.greeting, name=name)The implementation is pretty simple, isn't it? So is the usage!
As a Decorator, you can use it without options.
>>> @Greeter ... def world(): ... return 'world' >>> world() 'hello world!'The example above is the same as providing empty options.
>>> @Greeter() ... def world(): ... return 'world' >>> world() 'hello world!'It accepts one greeting option:
>>> @Greeter(greeting='goodbye') ... def world(): ... return 'world' >>> world() 'goodbye world!'greeting option defaults to 'hello':
>>> my_greeter = Greeter() >>> my_greeter.greeting 'hello'You can create a Greeter instance for later use:
>>> my_greeter = Greeter(greeting='hi') >>> @my_greeter ... def world(): ... return 'world' >>> world() 'hi world!'Which gives you an opportunity to setup the greeter yourself:
>>> my_greeter = Greeter() >>> my_greeter.greeting = 'bonjour' >>> @my_greeter ... def world(): ... return 'world' >>> world() 'bonjour world!'In this example, all arguments are proxied to the decorated function:
>>> @Greeter ... def name(value): ... return value >>> name('world') 'hello world!' >>> @Greeter(greeting='goodbye') ... def names(*args): ... return ' and '.join(args) >>> names('Laurel', 'Hardy') 'goodbye Laurel and Hardy!'Wrapping functions with functools
functools [1] provides utilities to "wrap" a function, i.e. make the decorator return value look like the original function.
Here is another class-based decorator sample. It adds "functools.update_wrapper" features to Decorator:
import functools class Chameleon(Decorator): """A Decorator that looks like decorated function. It uses ``functools.update_wrapper``. This is a base class which acts as a transparent proxy for the decorated function. Consider overriding the ``run()`` method. .. warning:: Take care of what you pass in ``assigned`` or ``updated``: you could break the Chameleon itself. As an example, you should not pass "assigned", "run" or "__call__" in ``assigned``, except you know what you are doing. """ def setup(self, assigned=functools.WRAPPER_ASSIGNMENTS, updated=functools.WRAPPER_UPDATES): self.assigned = assigned self.updated = updated def decorate(self, func): """Make self wrap the decorated function.""" super(Chameleon, self).decorate(func) functools.update_wrapper(self, func, assigned=self.assigned, updated=self.updated)Again, the implementation is pretty simple.
Let's look at the result...
>>> @Chameleon ... def documented(): ... '''Fake function with a docstring.''' >>> documented.__doc__ 'Fake function with a docstring.'It accepts options assigned and updated, that are proxied to functools.update_wrapper.
Default values are functools.WRAPPER_ASSIGNMENTS for assigned and empty tuple for updated.
>>> def hello(): ... '''Hello world!''' >>> wrapped = Chameleon(hello) >>> wrapped.assigned ('__module__', '__name__', '__doc__') >>> wrapped.updated ('__dict__',) >>> wrapped.__doc__ == hello.__doc__ True >>> wrapped.__name__ == hello.__name__ True >>> only_doc_wrapped = Chameleon(hello, assigned=['__doc__']) >>> only_doc_wrapped.__doc__ == hello.__doc__ True >>> only_doc_wrapped.__name__ == hello.__name__ # Doctest: +ELLIPSIS Traceback (most recent call last): ... AttributeError: 'Chameleon' object has no attribute '__name__' >>> hello.__dict__ = {'some_attribute': 'some value'} # Best on an object. >>> attr_wrapped = Chameleon(hello, updated=['__dict__']) >>> attr_wrapped.updated ['__dict__'] >>> attr_wrapped.some_attribute 'some value'Here we have a good replacement for decorators using functools.wraps.
As an example, the django-traditional-style decorator shown in Testing Django view decorators article...
from functools import wraps from django.utils.decorators import available_attrs def authenticated_user_passes_test(test_func, unauthorized=UnauthorizedView.as_view(), forbidden=ForbiddenView.as_view()): """Make sure user is authenticated and passes test.""" def decorator(view_func): @wraps(view_func, assigned=available_attrs(view_func)) def _wrapped_view(request, *args, **kwargs): if not request.user.is_authenticated(): return unauthorized(request) if not test_func(request.user): return forbidden(request) return view_func(request, *args, **kwargs)... would be written like this with class-based-style:
class authenticated_user_passes_test(Chameleon): """Make sure user is authenticated and passes test.""" def setup(self, **kwargs): try: self.test_func = kwargs.pop('test_func') except KeyError: raise TypeError('decorator requires "test_func" keyword argument') self.unauthorized = kwargs.pop('unauthorized', UnauthorizedView.as_view()) self.forbidden = kwargs.pop('forbidden', ForbiddenView.as_view()) super(authenticated_user_passes_test, self).setup(**kwargs) def run(self, request, *args, **kwargs): if not request.user.is_authenticated(): return self.unauthorized(request) if not self.test_func(request.user): return self.forbidden(request) return = super(authenticated_user_passes_test, self).run(request, *args, **kwargs)The class-based way is a bit longer because we have to handle the required test_func manually. But it clearly separates setup from run. It makes the code readable, easy to test, and easy to extend.
The Decorator class
Here is the base class. Little magic inside.
class Decorator(object): """Base class to easily create convenient decorators. Override :py:meth:`setup`, :py:meth:`run` or :py:meth:`decorate` to create custom decorators. Decorator instances are callables. The :py:meth:`__call__` method has a special implementation in Decorator. Generally, consider overriding :py:meth:`run` instead of :py:meth:`__call__`. """ def __init__(self, func=None, **kwargs): """Constructor. Accepts one optional positional argument: the function to decorate. Other arguments **must** be keyword arguments. And beware passing ``func`` as keyword argument: it would be used as the function to decorate. """ self.setup(**kwargs) if func is not None: self.decorate(func) def decorate(self, func): """Remember the function to decorate.""" self.decorated = func def setup(self, **kwargs): """Store decorator's options.""" self.options = kwargs def __call__(self, *args, **kwargs): """Run decorated function if available, else decorate first arg.""" try: self.decorated except AttributeError: func = args[0] self.decorate(func) return self else: return self.run(*args, **kwargs) def run(self, *args, **kwargs): """Actually run the decorator. This base implementation is a transparent proxy to the decorated function: it passes positional and keyword arguments as is, and returns result. """ return self.decorated(*args, **kwargs)This base class transparently proxies to decorated function:
>>> @Decorator ... def return_args(*args, **kwargs): ... return (args, kwargs) >>> return_args() ((), {}) >>> return_args(1, 2, three=3) ((1, 2), {'three': 3})This base class stores decorator's options in options dictionary. But it doesn't use it... it's just a convenient mechanism for subclasses.
>>> @Decorator ... def nothing(): ... pass >>> nothing.options {} >>> @Decorator() ... def nothing(): ... pass >>> nothing.options {} >>> @Decorator(one=1) ... def nothing(): ... pass >>> nothing.options {'one': 1}Limitations
This Decorated implementation has some limitations:
- you can't use positional arguments to configure the decorator itself.
- required decorator arguments must be handled explicitely, because you can't use positional arguments.
- you have to remember the Decorator API, mainly setup() and run() methods.
Are there other limitations? Right now, I don't know...
Benefits
- As a decorator author, you focus on setup() and run(). It is easy to remember. It produces readable code.
- As a decorator user, you don't bother with parentheses. You just use the decorator depending on your needs, and it works.
- As a test writer, you can write tests for decorators internals: what is the state of the decorator after its own initialization, what is its state after a run...
Would you use it?
See also
- "Python et les décorateurs", by Gilles Fabio is a good article (in french). It ends with an list of useful links (most in english). It also provides a function-based implementation of decorators that work with or without arguments.
- Testing Django view decorators
[1] http://docs.python.org/2.7/library/functools.html
[novapost] Testing Django decorators
How to test view decorators of Django applications? Here are some tips.
In a post before, I recommended to avoid decorating views in place (i.e. not in views.py). Once decorators and views are separated, we can unit test the views. That was the topic of the post before. This article focuses on testing decorators.
The examples described below are available as a Python file at https://gist.github.com/benoitbryon/5156512
Use unittest.mock
Learn about unittest.mock [2] (or backward-compatible mock [1]) library! This article makes heavy use of those wonderful features.
In tests.py:
try: from unittest import mock except ImportError: import mockAnd in project's or app's setup.py:
# ... requirements = [] try: from unittest import mock except ImportError: requirements.append('mock') # ... setup( # ... install_requires(requirements), # ... )Fake the request
We want to focus on the decorator. Does it rely on the request to perform some actions? Fake/stub/mock all and only what you need.
django.test.RequestFactory [3] can be useful. But sometimes it is overkill and django.http.HttpRequest [4] is enough.
In the hello world example below, we'll use a completely fake request:
request = 'fake request'In the authenticated_user_passes_test example below, we use mocks to support request.user.is_authenticated():
request = mock.MagicMock() request.user.is_authenticated = mock.MagicMock(return_value=True)Stub the decorated view
We want to focus on the decorator. We don't care about decorated view implementation. But we care about how the decorator handles the view. Let's use unittest.mock [2].
Decorated views are functions (or callables). We can instantiate and check a mocked-view like this:
import unittest class MockViewTestCase(unittest.TestCase): def test_stub(self): # Setup. request = 'fake request' view = mock.MagicMock(return_value='fake response') # Run. response = view(request) # Check. view.assert_called_once_with(request) self.assertEqual(response, view.return_value)hello_world decorator
Before we dive into a real-life example, let's consider a really simple one.
Here is the decorator:
from django.http import HttpResponse def hello_world(view_func): """Run the decorated view, but return "Hello world!".""" def decorated_view(request, *args, **kwargs): view_func(request, *args, **kwargs) return HttpResponse(u'Hello world!') return decorated_viewHere is the test case:
import unittest class HelloWorldTestCase(unittest.TestCase): def test_hello_decorator(self): """hello_world decorator runs view and returns greetings.""" # Setup. request = 'fake request' request_args = ('foo', ) request_kwargs = {'bar': 'baz'} view = mock.MagicMock(return_value='fake response') # Run. decorated = hello_world(view) response = decorated(request, *request_args, **request_kwargs) # Check. # View was called. view.assert_called_once_with(request, *request_args, **request_kwargs) # But response is "Hello world!". self.assertEqual(response.status_code, 200) self.assertEqual(response.content, u"Hello world!")The test looks like a documentation for the decorator :)
Some noticeable points:
- we haven't used Django's builtin test client. We haven't needed to setup URLconfs, settings, "real" views...
- there is no database transaction involved, so we used unittest.TestCase.
authenticated_user_passes_test decorator
Now let's consider a real-life example, with a custom decorator:
from functools import wraps from django.utils.decorators import available_attrs def authenticated_user_passes_test(test_func, unauthorized=UnauthorizedView.as_view(), forbidden=ForbiddenView.as_view()): """Make sure user is authenticated and passes test. This is an adaptation of ``django.contrib.auth.decorators.user_passes_test`` where: * if user is anonymous, the request is routed to ``unauthorized`` view. No additional tests are performed in that case. * if user is authenticated and doesn't pass ``test_func ``test, the request is routed to ``forbidden`` view. * else, request and arguments are passed to decorated view. Typical ``unauthorized`` view returns HTTP 401 status code and gives the user an opportunity to log in: access may be granted after authentication. Typical ``forbidden`` view returns HTTP 403 status code: with active user account, access is refused. As explained in rfc2616, 401 and 403 status codes could be suitable. .. seealso:: * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4 * https://en.wikipedia.org/wiki/List_of_HTTP_status_codes """ def decorator(view_func): @wraps(view_func, assigned=available_attrs(view_func)) def _wrapped_view(request, *args, **kwargs): if not request.user.is_authenticated(): return unauthorized(request) if not test_func(request.user): return forbidden(request) return view_func(request, *args, **kwargs) return _wrapped_view return decoratorThis decorator depends on some additional stuff:
from django.http import HttpResponse, HttpResponseForbidden from django.views.generic import TemplateView class HttpResponseUnauthorized(HttpResponse): status_code = 401 class UnauthorizedView(TemplateView): response_class = HttpResponseUnauthorized template_name = '401.html' class ForbiddenView(TemplateView): response_class = HttpResponseForbidden template_name = '403.html'And, here is the test case! It seems quite long, but isn't it readable? The whole test lives in the TestCase: no external URLconf, no external views...
- First we setup fakes or mocks for all dependencies: request.user, test_func, unauthorized view, forbidden view, and the view to be decorated.
- Then we declare a run_decorated_view function to avoid repeating code.
- Finally we test the 3 main situations: unauthorized, forbidden, authorized.
import unittest class AuthenticatedUserPassesTestTestCase(unittest.TestCase): def setUp(self): """Common setup: fake request, stub views, stub user test function.""" super(AuthenticatedUserPassesTestTestCase, self).setUp() # Fake request and its positional and keywords arguments. self.request = mock.MagicMock() self.request.user.is_authenticated = mock.MagicMock() self.request_args = ['fake_arg'] self.request_kwargs = {'fake': 'kwarg'} # Mock user test function. self.test_func = mock.MagicMock() # Mock unauthorized and forbidden views. self.unauthorized_view = mock.MagicMock( return_value=u"401 - You may log in.") self.forbidden_view = mock.MagicMock( return_value=u"403 - Insufficient privileges.") # Mock the view to decorate. self.authorized_view = mock.MagicMock( return_value=u"200 - Greetings, Professor Falken.") def run_decorated_view(self, is_authenticated=True, user_passes_test=True): """Setup, decorate and call view, then return response.""" # Custom setup. self.request.user.is_authenticated.return_value = is_authenticated self.test_func.return_value = user_passes_test # Get decorator. decorator = authenticated_user_passes_test( self.test_func, unauthorized=self.unauthorized_view, forbidden=self.forbidden_view) # Decorate view. decorated_view = decorator(self.authorized_view) # Return response. return decorated_view(self.request, *self.request_args, **self.request_kwargs) def test_unauthorized(self): """authenticated_user_passes_test first tests user authentication.""" response = self.run_decorated_view(is_authenticated=False) # Check: unauthorized view was called with request as unique positional # argument. self.unauthorized_view.assert_called_once_with(self.request) self.assertEqual(response, self.unauthorized_view.return_value) # Test func was not called. self.assertFalse(self.test_func.called) # Of course, authorized and forbidden views were not called. self.assertFalse(self.authorized_view.called) self.assertFalse(self.forbidden_view.called) def test_test_func_args(self): """authenticated_user_passes_test passes user instance to test func.""" self.run_decorated_view(is_authenticated=True) # Check: test_func was called with one argument: user instance. self.test_func.assert_called_once_with(self.request.user) def test_forbidden(self): """authenticated_user_passes_test runs forbidden view if user fails.""" response = self.run_decorated_view(is_authenticated=True, user_passes_test=False) # Check: forbidden view was called with request as unique positional # argument. self.forbidden_view.assert_called_once_with(self.request) self.assertEqual(response, self.forbidden_view.return_value) # Of course, authorized and unauthorized views were not triggered. self.assertFalse(self.authorized_view.called) self.assertFalse(self.unauthorized_view.called) def test_authorized(self): """authenticated_user_passes_test runs view if user passes test.""" response = self.run_decorated_view(is_authenticated=True, user_passes_test=True) # Check: decorated view has been called, request and other arguments # were proxied as is, response was not altered. self.authorized_view.assert_called_once_with(self.request, *self.request_args, **self.request_kwargs) self.assertEqual(response, self.authorized_view.return_value) # Of course, forbidden and unauthorized views were not triggered. self.assertFalse(self.forbidden_view.called) self.assertFalse(self.unauthorized_view.called)Would you trust the authenticated_user_passes_test decorator?
[novapost] A Django testing challenge
Here at Novapost, we have quite large Django projects. Big projects mean big maintenance. Hopefully, those projects are covered by tests. Thousands of tests. We are quite proud of this fact. Tests save our lives. But we also have some worries and need to improve... Here is our testing challenge!
Some issues...
Here are some problems we encounter...
Performance
In the biggest project, our continuous integration service can't run a full test suite in less than one hour. That's embarrassing, because we can't wait for global test results before we commit.
We'd like a collection of tests that run fast and give us the status of most of the project. So that we can run it before we commit. Then the continuous integration service would cover the rest.
Maintenance
When we change a feature, it sometimes has side effects on some tests which seem unrelated. It looks like our tests involve much more than necessary. Not only it affects performance, but it also affects maintenance: sometimes we know something is wrong because tests fail, but we cannot tell exactly what's wrong. And finally, we have to adapt several tests for only one feature.
We'd like tests to focus on small units, so that, when something goes wrong, tests help us figure out what's exactly wrong. And we'd like to change only what's really related.
Tests pass, deployment fail
Sometimes tests are not enough: we fix things, but we can't tell if the fix works after an upgrade. As an example, changes that involve settings are hard to cover with tests: once the fix is committed and pushed, the story cannot be closed before we make sure the configuration has been updated and everything is ok on servers. We are doing some manual checks...
We'd like to involve more into deployment, supervision and monitoring, so that we can automate some checks related to the issues or features we develop.
Some clues...
We already have some ideas about what's wrong with our testing strategy. Here are some hypotheses:
- one cause of those long test suites is the heavy use of self.client.get() aka functional/system test for the unaware ;
- we could mock/fake more things ;
- we could separate tests that run fast from tests that run slow ;
- we could split our big all-in-one test suite into smaller parts: unit tests, functional tests, integration tests, health checks...
A challenge
We are aware of some issues with our testing strategy and have some ideas... Let's improve!
We started teamwork as a challenge:
- try various improvements when we implement features or fix bugs ;
- adopt improvements that work, measure how it improves things ;
- give feedback on the blog!
We are to share experience on this blog, through several articles with the testing tag.
Here are articles we already posted:
- unit test your Django views
- testing Django view decorators
- ... to be continued...
See also
- The "Fast test, slow test" presentation by Gary Bernhardt is a must see!
[Inspyration] Les décorateurs
Lorsqu'on les croise pour la première fois, les décorateurs revêtent cet aspect magique. On ne sait pas trop comment çà fonctionne, mais on trouve çà classe. C'est facile à utiliser, çà demande peut d'efforts et c'est très puissant. Que demande le peuple ? Par contre, lorsque l'on souhaite en créer, c'est une autre paire de manches. Comme dirait un coach de rugby : "On va tout mettre sur la table et repartir des fondamentaux".
[novapost] Unit test your Django views
How to test views of a Django application?
Django's builtin test client is not suitable for unit testing! It performs system tests: it handles your views as a black box in a project's environment.
This article provides a recipe for developers to replace Django's builtin test client by smaller, fine-grained, view-centric tests.
self.client.get(): system tests for the unaware
Here are some reasons why Django's builtin client performs system tests:
- it resolves URLs,
- it traverses middlewares,
- it traverses decorators,
- it uses template context processors,
- it relies on settings,
- ... and perhaps more. Who knows? Do you really want to know?
All the stuff above is not the view, it is the environment surrounding the view.
It means that, by using the test client, you don't test the view itself, but the system the view is part of. And the environment is quite hard (and boring) to control.
Here, we want to focus on the view, so let's emancipate from all those third party mechanisms.
Testing view functions
Let's consider this simple view:
from django.http import Http404, HttpResponse def hello(request, name): if name == u'Waldo' raise Http404("Where's Waldo?") return HttpResponse(u'Hello {name}!'.format(name=name))Then test it:
import unittest class HelloTestCase(unittest.TestCase): def test_get(self): """hello view actually tells 'Hello'.""" # Setup. request = 'fake request' name = 'world' # Run. response = hello(request, name) # Check. self.assertEqual(response.status_code, 200) self.assertEqual(response.content, u'Hello world!') def test_waldo(self): """Cannot find Waldo to tell him 'Hello'.""" # Setup. request = 'fake request' name = 'Waldo' # Run and check. self.assertRaises(Http404, hello, request, name)Pretty simple isn't it?
Really, you don't need Django's builtin test client to write such tests!
Use unittest or SimpleTestCase wherever you can
In the example above, we didn't hit the database, so there were no reasons to use django.test.TransactionTestCase [1] or derivatives.
With such a configuration, tests run really fast!
Note
Performance is another reason you should avoid Django's builtin test client. But that's another story.
Wherever you can, use unittest, or django.test.SimpleTestCase [2].
Don't decorate views in place
The "hello" example above would have been broken if the view were decorated in place. As an example:
from django.http import Http404, HttpResponse from django.contrib.auth.decorators import login_required @login_required def hello(request, name): if name == u'Waldo' raise Http404("Where's Waldo?") return HttpResponse(u'Hello {name}!'.format(name=name))How can we test hello() view now?
We would have to perform (mock) a login, and we would have to check the response with or without authentication. As a consequence, our tests would become longer, less readable, less efficient... Moreover, what if the login decorator has bugs or changes? It would break hello's tests even if hello itself doesn't change. How bad!
So, don't decorate views in place.
Instead:
- decorate views somewhere related to URLconfs (urls.py), not related to views.
- have specific tests for decorators, i.e. validate login_required works.
- have specific tests for URLconfs, i.e. validate login_required is applied to hello in project's configuration (this is a system test).
Use request factories
Django's builtin test client is a special kind of request factory [3], which uses URL resolution to trigger the views (deep inside the system). Now we have isolated views from system. But a view still takes a request as argument. How to get a request?
In the function-based example above, we used a completely fake request. But sometimes you can't do that and need a HttpRequest.
Django provides django.test.RequestFactory [4] to mock requests.
With a request factory, you get a request instance you can pass as argument to views' methods such as dispatch().
from django.test import RequestFactory request_factory = RequestFactory() request = request_factory.post('/fake-path', data={'name': u'Waldo'})Note
Some notes about request factories, which could make a full article...
- Django's builtin RequestFactory requires one positional argument: path. But, in the scope of tests of this article, we really don't care about the path. The path is mandatory for the test client to resolve URLs... So, unless your view actually uses the path argument, you can safely use a fake value.
- If your view uses the messages framework, you'll need to setup (or mock) request._messages. Notice that's a feature, since messages should be tested too ;)
- Idem about session: you may need to mock request.session if your view depends on the session.
- Yes, you are getting aware of your view's dependencies :)
Testing class-based views
Once we got rid of Django's builtin test client, we can consider views themselves. How do they look like?
Function-based views look like black boxes: things that take a request and return a response. No way to test internals.
With class-based views, we have various methods and attributes. So we can write fine-grained tests!
The idea here is to test every custom method or attribute of the class-based views you write.
Let's consider the following view:
class HelloView(TemplateView): def get_context_data(self, **kwargs): kwargs = super(HelloView, self).get_context_data(**kwargs) kwargs.update('name', self.kwargs.get('name')) return kwargsAnd let's consider we'd like to reproduce this URLconf scenario:
- view: hello = HelloView.as_view(template_name='hello.html')
- URL: url(r'(?P<name>\w+)', hello)
as_view() is not enough
Testing class-based views using as_view() and RequestFactory is now described in Django's documentation along with django.test.RequestFactory [4]:
import unittest from django.test import RequestFactory class HelloViewTestCase(unittest.TestCase): def test_get(self): """HelloView.get() sets 'name' in response context.""" # Setup name. name = 'peter' # Setup request and view. request = RequestFactory().get('/fake-path') view = HelloView.as_view(template_name='hello.html') # Run. response = view(request, name=name) # Check. self.assertEqual(response.status_code, 200) self.assertEqual(response.template_name[0], 'home.html') self.assertEqual(response.context_data['name'], name)Ok, it works. But, in the HelloView above, I just overrid the get_context_data() method. So I'd like to test only that. I mean, status code and template name are features inherited from TemplateView, and they are covered by TemplateView's test suite.
We can't use as_view() to perform fine-grained testing.
One issue with as_view() is that it returns a function, not an instance of the view class. And this callable is a proxy to view's dispatch(), which involves almost all view's methods, depending on the arguments.
Using as_view() in tests is the same as having a function-based view. You don't really take advantage of the class-based view.
Alright, let's get rid of as_view() and focus on get_context_data()...
Mimic as_view()
Here is a simple replacement for as_view():
def setup_view(view, request, *args, **kwargs): """Mimic as_view() returned callable, but returns view instance. args and kwargs are the same you would pass to ``reverse()`` """ view.request = request view.args = args view.kwargs = kwargs return viewHere is how to use it in a test:
import unittest from django.test import RequestFactory class HelloViewTestCase(unittest.TestCase): def test_context_data(self): """HelloView.get_context_data() sets 'name' in context.""" # Setup name. name = 'django' # Setup request and view. request = RequestFactory().get('/fake-path') view = HelloView(template_name='hello.html') view = setup_view(view, request, name=name) # Run. context = view.get_context_data() # Check. self.assertEqual(context['name'], name)That's all. What happened?
- Just tested the get_context_data method which we overrid. Other methods inherited from TemplateView are covered by TemplateView test suite.
- We used unittest since there is no transaction involved.
The fairy as_view() and the ugly dispatch()
Let's end with a story about as_view() magic.
Using as_view() is quite elegant:
request = RequestFactory().get('/fake-path') view = HelloView.as_view(template_name='hello.html') response = view(request, name='bob')Using dispatch() is ugly:
request = RequestFactory().get('/fake-path') view = HelloView(template_name='hello.html') view = setup_view(view, request, name='bob') response = view.dispatch(view.request, *view.args, **view.kwargs)Got it? dispatch() receives arguments the instance already knows...
Diving into fine-grained tests on Django-style class-based views may awake trolls. Billy-Thread-Safe, Kate-Instance and Frank-Class-Attribute may join the party soon ;)
In fact, it looks like Django's class-based views haven't been designed to be fine-grained tested.
If your are curious, have a look on Django's tests...
At https://github.com/django/django/blob/1.5/tests/regressiontests/generic_views/base.py#L278 nothing proves we are testing a TemplateView. It relies on the URLconf. Calling view's get_context_data() may have been more efficient and readable.
At https://github.com/django/django/blob/1.5/tests/regressiontests/generic_views/base.py#L67 are we really testing the queryset? It seems we are testing the queryset and the context data and the status code and the template name and the URL configuration and... all in a row.
There could be one test around default get_queryset() to check that it returns Author.objects.all(). Then another test around get_context_data() to check that the queryset (a fake queryset that doesn't hit the database) is registered in context.
and soooooo many tests that handle views (or system) as black boxes...
What's next
Since you test your views as isolated items, you have to test everything else:
- middlewares,
- decorators,
- context processors,
- models...
And you can fake/mock many things inside tests of views, so that you don't rely on database, settings, ...
References
[1] https://docs.djangoproject.com/en/1.5/topics/testing/overview/#transactiontestcase
[2] https://docs.djangoproject.com/en/1.5/topics/testing/overview/#simpletestcase
[3] https://github.com/django/django/blob/56e54727661bc34bd2b6f9fa6a75f5370149256e/django/test/client.py#L345
[4] (1, 2) https://docs.djangoproject.com/en/1.5/topics/testing/advanced/#django.test.client.RequestFactory
[cubicweb] data.bnf.fr gets the Stanford Prize for Innovation in Research Libraries
data.bnf.fr and Gallica just got awarded the Stanford Prize for Innovation in Research Libraries 2013. The CubicWeb community is very pleased to see that data.bnf.fr, which is built with CubicWeb, is being recognized at the top international level as leading innovation its domain! Read the comments of the judges for more details.
[sciunto] Détection de cercles par une transformation de Hough dans scikit-image
Au détour de quelques questions que je me posais que la détection de forme, j’ai découvert les transformations de Hough. Ces transformations permettent, en jouant avec la géométrie de détecter par exemple des droites ou des cercles dans des images. Le principe est basé sur un accumulateur construit dans un espace réciproque à l’image dont [...]
[Inspyration] Dans vos kiosques : Du Python, du Python et ... encore du Python.
Python est à l'honneur en mars : 3 magazines pour 3 articles sur Python plus un hors-série consacré entièrement à notre langage préféré
[logilab] Release of PyLint 0.27 / logilab-astng 0.24.2
Hi there,
I'm very pleased to announce the release of pylint 0.27 and logilab-astng 0.24.2. There has been a lot of enhancements and bug fixes since the latest release, so you're strongly encouraged to upgrade. Here is a detailed list of changes:
- #20693: replace pylint.el by Ian Eure version (patch by J.Kotta)
- #105327: add support for --disable=all option and deprecate the 'disable-all' inline directive in favour of 'skip-file' (patch by A.Fayolle)
- #110840: add messages I0020 and I0021 for reporting of suppressed messages and useless suppression pragmas. (patch by Torsten Marek)
- #112728: add warning E0604 for non-string objects in __all__ (patch by Torsten Marek)
- #120657: add warning W0110/deprecated-lambda when a map/filter of a lambda could be a comprehension (patch by Martin Pool)
- #113231: logging checker now looks at instances of Logger classes in addition to the base logging module. (patch by Mike Bryant)
- #111799: don't warn about octal escape sequence, but warn about o which is not octal in Python (patch by Martin Pool)
- #110839: bind <F5> to Run button in pylint-gui
- #115580: fix erroneous W0212 (access to protected member) on super call (patch by Martin Pool)
- #110853: fix a crash when an __init__ method in a base class has been created by assignment rather than direct function definition (patch by Torsten Marek)
- #110838: fix pylint-gui crash when include-ids is activated (patch by Omega Weapon)
- #112667: fix emission of reimport warnings for mixed imports and extend the testcase (patch by Torsten Marek)
- #112698: fix crash related to non-inferable __all__ attributes and invalid __all__ contents (patch by Torsten Marek)
- Python 3 related fixes:
- Include full warning id for I0020 and I0021 and make sure to flush warnings after each module, not at the end of the pylint run. (patch by Torsten Marek)
- Changed the regular expression for inline options so that it must be preceeded by a # (patch by Torsten Marek)
- Make dot output for import graph predictable and not depend on ordering of strings in hashes. (patch by Torsten Marek)
- Add hooks for import path setup and move pylint's sys.path modifications into them. (patch by Torsten Marek)
- pylint-brain: more subprocess.Popen faking (see #46273)
- #109562 [jython]: java modules have no __doc__, causing crash
- #120646 [py3]: fix for python3.3 _ast changes which may cause crash
- #109988 [py3]: test fixes
Many thanks to all the people who contributed to this release!
Enjoy!






