Django sous microscope

Si, selon le rapport d'Artyom Malyshev ( proofit404 ), ils feront un film, alors le réalisateur sera Quentin Tarantino - il a déjà fait un film sur Django, il tournera également le second. Tous les détails de la vie des mécanismes internes de Django du premier octet de la requête HTTP au dernier octet de la réponse. L'extravagance des formulaires d'analyse, la compilation de SQL pleine d'action, les effets spéciaux de la mise en œuvre du moteur de modèle pour HTML. Qui gère le pool de connexions et comment? Tout cela par ordre chronologique de traitement des objets WSGI. Sur tous les écrans du pays - le décodage "Django sous microscope".



À propos de l'orateur: Artyom Malyshev est le fondateur du projet Dry Python et le développeur principal de Django Channels version 1.0. Il écrit du Python depuis 5 ans et a aidé à organiser des réunions Python Rannts à Nizhny Novgorod. Artyom vous est peut-être familier sous le surnom PROOFIT404 . La présentation du rapport est stockée ici .


Il était une fois, nous avons lancé l'ancienne version de Django. Puis elle avait l'air effrayante et triste.



Ils ont vu que self_check passé, nous avons tout installé correctement, tout fonctionnait et maintenant vous pouvez écrire du code. Pour y parvenir, nous avons dû exécuter la commande django-admin runserver .

 $ django-admin runserver Performing system checks… System check identified no issues (0 silenced). You have unapplied migrations; your app may not work properly until they are applied. Run 'python manage.py migrate1 to apply them. August 21, 2018 - 15:50:53 Django version 2.1, using settings 'mysite.settings' Starting development server at http://127.0.0.1:8000/Quit the server with CONTROL-C. 

Le processus démarre, traite les requêtes HTTP et toute la magie se produit à l'intérieur et tout le code que nous voulons montrer aux utilisateurs en tant que site est exécuté.

L'installation


django-admin apparaît sur le système lorsque nous installons Django en utilisant, par exemple, pip, le gestionnaire de paquets .

 $ pip install Django # setup.py from setuptools import find_packages, setup setup( name='Django', entry_points={ 'console_scripts': [ 'django-admin = django.core.management:execute_from_command_line' ] }, ) 

entry_points setuptools apparaît, qui pointe vers la fonction execute_from_command_line . Cette fonction est un point d'entrée pour toute opération avec Django, pour tout processus en cours.

Bootstrap


Que se passe-t-il à l'intérieur d'une fonction? Bootstrap , qui est divisé en deux itérations.

 # django.core.management django.setup(). 

Configurer les paramètres


Le premier est la lecture des configs :

 import django.conf.global_settings import_module(os.environ["DJANGO_SETTINGS_MODULE"]) 

Les paramètres global_settings par défaut global_settings , puis à partir de la variable d'environnement, nous essayons de trouver le module avec DJANGO_SETTINGS_MODULE , que l'utilisateur a écrit. Ces paramètres sont combinés dans un espace de nom.

Quiconque écrit dans Django au moins «Hello, world» sait qu'il y a INSTALLED_APPS - où nous écrivons le code utilisateur.

Remplissez les applications


Dans la deuxième partie, toutes ces applications, essentiellement des packages, sont itérées une par une. Nous créons pour chaque config, nous importons des modèles pour travailler avec une base de données et nous vérifions l'intégrité des modèles. En outre, le framework exécute Check , c'est-à-dire qu'il vérifie que chaque modèle a une clé primaire, que toutes les clés étrangères pointent vers des champs existants et que le champ Null n'est pas écrit dans le BooleanField, mais le NullBooleanField est utilisé.

 for entry in settings.INSTALLED_APPS: cfg = AppConfig.create(entry) cfg.import_models() 

C'est le contrôle de santé mentale minimum pour les modèles, pour le panneau d'administration, pour n'importe quoi - sans se connecter à la base de données, sans quelque chose de super compliqué et spécifique. À ce stade, Django ne sait pas encore quelle commande vous avez demandé d'exécuter, c'est-à-dire qu'il ne distingue pas migrate de runserver ou shell .

Ensuite, nous nous retrouvons dans un module qui essaie de deviner par des arguments de ligne de commande quelle commande nous voulons exécuter et dans quelle application elle se trouve.

Commande de gestion


 # django.core.management subcommand = sys.argv[1] app_name = find(pkgutils.iter_modules(settings.INSTALLED_APPS)) module = import_module( '%s.management.commands.%s' % (app_name, subcommand) ) cmd = module.Command() cmd.run_from_argv(self.argv) 

Dans ce cas, le module runserver aura un module django.core.management.commands.runserver intégré. Après avoir importé le module, par convention, la classe globale Command est appelée à l'intérieur, est instanciée, et nous disons: " Je vous ai trouvé, ici vous avez les arguments de ligne de commande que l'utilisateur a passés, faites quelque chose avec eux ."

Ensuite, nous allons au module runserver et voyons que Django est fait de "regexp et sticks" , dont je parlerai en détail aujourd'hui:

 # django.core.management.commands.runserver naiveip_re = re.compile(r"""^(?: (?P<addr> (?P<ipv4>\d{1,3}(?:\.\d{1,3}){3}) | # IPv4 address (?P<ipv6>\[[a-fA-F0-9:]+\]) | # IPv6 address (?P<fqdn>[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*) # FQDN ):)?(?P<port>\d+)$""", re.X) 

Commandes


Faites défiler un écran et demi - enfin, nous entrons dans la définition de notre équipe qui démarre le serveur.

 # django.core.management.commands.runserver class Command(BaseCommand): def handle(self, *args, **options): httpd = WSGIServer(*args, **options) handler = WSGIHandler() httpd.set_app(handler) httpd.serve_forever() 

BaseCommand effectue un ensemble minimal d'opérations afin que les arguments de la ligne de commande aboutissent à des arguments pour appeler les fonctions d' **options *args et **options . Nous voyons que l'instance du serveur WSGI est en cours de création ici, le WSGIHandler global est installé sur ce serveur WSGI - c'est exactement God Object Django . Nous pouvons dire que c'est la seule instance du cadre. L'instance est installée sur le serveur globalement - via l' set application et dit: "Tournez dans la boucle d'événement, exécutez les requêtes."

Il y a toujours une boucle d'événement quelque part et un programmeur qui lui confie des tâches.

Serveur WSGI


Qu'est-ce que WSGIHandler ? WSGI est une interface qui vous permet de traiter les requêtes HTTP avec un niveau d'abstraction minimum, et ressemble à quelque chose sous la forme d'une fonction.

Gestionnaire WSGI


 # django.core.handlers.wsgi class WSGIHandler: def __call__(self, environ, start_response): signals.request_started.send() request = WSGIRequest(environ) response = self.get_response(request) start_response(response.status, response.headers) return response 

Par exemple, ici, c'est une instance d'une classe qui a un call défini. Il attend son entrée de dictionnaire, dans laquelle les en-têtes seront présentés comme des octets et un gestionnaire de fichiers. Le gestionnaire est nécessaire pour lire le <body> la demande. Le serveur lui-même donne également un rappel start_response afin que nous puissions envoyer response.headers et son en-tête, par exemple, status, dans un seul paquet.

De plus, nous pouvons transmettre le corps de la réponse au serveur via l'objet de réponse. Response est un générateur que vous pouvez parcourir.

Tous les serveurs écrits pour WSGI - Gunicorn, uWSGI, Waitress, fonctionnent sur cette interface et sont interchangeables. Nous envisageons maintenant un serveur pour le développement, mais tout serveur arrive au point que dans Django, il passe à travers environ et le rappel.

Qu'y a-t-il à l'intérieur de Dieu Object?


Que se passe-t-il à l'intérieur de cette fonction globale d'objet divin à l'intérieur de Django?

  • DEMANDE.
  • MIDDLEWARES.
  • ROUTAGE demande de visualisation.
  • VIEW - traitement du code utilisateur dans la vue.
  • FORM - travailler avec des formulaires.
  • ORM.
  • MODÈLE
  • RÉPONSE.

Toutes les machines que nous voulons de Django se déroulent dans une seule fonction, qui est répartie sur l'ensemble du cadre.

Demande


Nous enveloppons l'environnement WSGI, qui est un dictionnaire simple, dans un objet spécial, pour la commodité de travailler avec l'environnement. Par exemple, il est plus pratique de déterminer la longueur d'une demande utilisateur en travaillant avec quelque chose de similaire à un dictionnaire qu'avec une chaîne d'octets qui doit être analysée et y rechercher des entrées de valeur-clé. Lorsque je travaille avec des cookies, je ne veux pas non plus calculer manuellement si la période de stockage a expiré ou non, et en quelque sorte l'interpréter.

 # django.core.handlers.wsgi class WSGIRequest(HttpRequest): @cached_property def GET(self): return QueryDict(self.environ['QUERY_STRING']) @property def POST(self): self._load_post_and_files() return self._post @cached_property def COOKIES(self): return parse_cookie(self.environ['HTTP_COOKIE']) 

La requête contient des analyseurs, ainsi qu'un ensemble de gestionnaires pour contrôler le traitement du corps de la requête POST: qu'il s'agisse d'un fichier en mémoire ou temporaire en stockage sur disque. Tout est décidé dans la demande. La requête dans Django est également un objet agrégateur dans lequel tous les middlewares peuvent mettre les informations dont nous avons besoin sur la session, l'authentification et l'autorisation utilisateur. Nous pouvons dire que c'est aussi un objet divin, mais plus petit.

La demande supplémentaire parvient au middleware.

Middlewares


Le middleware est un wrapper qui enveloppe d'autres fonctions comme un décorateur. Avant de renoncer au contrôle du middleware, dans la méthode d'appel, nous donnons une réponse ou appelons un middleware déjà encapsulé.

Voici à quoi ressemble le middleware du point de vue du programmeur.

Paramètres


 # settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ] 

Définir


 class Middleware: def __init__(self, get_response=None): self.get_response = get_response def __call__(self, request): return self.get_response(request) 

Du point de vue de Django, les middlewares ressemblent à une sorte de pile:

 # django.core.handlers.base def load_middleware(self): handler = convert_exception_to_response(self._get_response) for middleware_path in reversed(settings.MIDDLEWARE): middleware = import_string(middleware_path) instance = middleware(handler) handler = convert_exception_to_response(instance) self._middleware_chain = handler 

Postuler


 def get_response(self, request): set_urlconf(settings.ROOT_URLCONF) response = self._middleware_chain(request) return response 

Nous prenons la fonction get_response initiale, l'enveloppons dans un gestionnaire, qui traduira, par exemple, une permission error et une not found error dans le code HTTP correct. Nous emballons tout dans le middleware lui-même de la liste. La pile des middlewares s'agrandit et chaque suivant encapsule le précédent. Cela est très similaire à l'application de la même pile de décorateurs à toutes les vues d'un projet, uniquement de manière centrale. Pas besoin de faire le tour et d'organiser les emballages avec vos mains selon le projet, tout est pratique et logique.

Nous sommes passés par 7 cercles de middlewares, notre demande a survécu et avons décidé de la traiter en vue. Nous arrivons ensuite au module de routage.

Acheminement


C'est là que nous décidons quel gestionnaire appeler pour une demande particulière. Et cela est résolu:

  • basé sur l'url;
  • dans la spécification WSGI, où request.path_info est appelé.

 # django.core.handlers.base def _get_response(self, request): resolver = get_resolver() view, args, kwargs = resolver.resolve(request.path_info) response = view(request, *args, **kwargs) return response 

URL


Nous prenons le résolveur, lui fournissons l'url de requête actuelle et attendons qu'il renvoie la fonction de vue elle-même, et à partir de la même URL, nous obtenons les arguments avec lesquels appeler view. Ensuite, get_response appelle view, gère les exceptions et fait quelque chose avec.

 # urls.py urlpatterns = [ path('articles/2003/', views.special_case_2003), path('articles/<int:year>/', views.year_archive), path('articles/<int:year>/<int:month>/', views.month_archive) ] 

Resolver


Voici à quoi ressemble le résolveur:

 # django.urls.resolvers _PATH_RE = re.compile( r'<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>' ) def resolve(self, path): for pattern in self.url_patterns: match = pattern.search(path) if match: return ResolverMatch( self.resolve(match[0]) ) raise Resolver404({'path': path}) 

C'est aussi regexp, mais récursif. Il va dans certaines parties de l'URL, cherche ce que l'utilisateur veut: d'autres utilisateurs, des publications, des blogs, ou est-ce une sorte de convertisseur, par exemple, une année spécifique qui doit être résolue, mise en arguments, castée en int.

Il est caractéristique que la profondeur de récursivité de la méthode de résolution soit toujours égale au nombre d'arguments avec lesquels la vue est appelée. Si quelque chose s'est mal passé et que nous n'avons pas trouvé d'URL spécifique, une erreur non trouvée se produit.

Ensuite, nous voyons enfin - le code que le programmeur a écrit.

Afficher


Dans sa représentation la plus simple, il s'agit d'une fonction qui renvoie une demande de réponse, mais à l'intérieur, nous effectuons des tâches logiques: «pour, si, un jour» - de nombreuses tâches répétitives. Django nous fournit une vue basée sur la classe où vous pouvez spécifier des détails spécifiques, et tout le comportement sera interprété au format correct par la classe elle-même.

 # django.views.generic.edit class ContactView(FormView): template_name = 'contact.html' form_class = ContactForm success_url = '/thanks/' 

Organigramme de la méthode


 self.dispatch() self.post() self.get_form() self.form_valid() self.render_to_response() 

La méthode de dispatch de cette instance est déjà dans le mappage d'URL au lieu d'une fonction. La répartition basée sur le verbe HTTP comprend la méthode à appeler: POST nous est parvenue et nous souhaitons très probablement instancier l'objet formulaire, si le formulaire est valide, l'enregistrer dans la base de données et afficher le modèle. Tout cela se fait grâce au grand nombre de mixins qui composent cette classe.

Formulaire


Le formulaire doit être lu à partir du socket avant d'entrer dans la vue Django - via le même gestionnaire de fichiers qui se trouve dans l'environnement WSGI. form-data est un flux d'octets, dans lequel les séparateurs sont décrits - nous pouvons lire ces blocs et en faire quelque chose. Il peut s'agir d'une correspondance clé-valeur, s'il s'agit d'un champ, d'une partie d'un fichier, puis à nouveau d'un champ - tout est mélangé.

 Content-Type: multipart/form-data;boundary="boundary" --boundary name="field1" value1 --boundary name="field2"; value2 

Analyseur


L'analyseur se compose de 3 parties.

L'itérateur de bloc qui crée les lectures attendues à partir du flux d'octets se transforme en un itérateur qui peut produire des boundaries . Il garantit que si quelque chose revient, ce sera la frontière. Ceci est nécessaire pour qu'à l'intérieur de l'analyseur, il ne soit pas nécessaire de stocker l'état de la connexion, lu depuis le socket ou non lu pour minimiser la logique du traitement des données.

Ensuite, le générateur encapsule dans LazyStream , qui crée à nouveau un fichier objet à partir de celui-ci, mais avec la lecture attendue. Ainsi, l'analyseur peut déjà parcourir des morceaux d'octets et créer une valeur-clé à partir d'eux.

champ et données ici seront toujours des chaînes . Si nous avons reçu une heure au format ISO, le formulaire Django (qui a été écrit par le programmeur) recevra, en utilisant certains champs, par exemple, l'horodatage.

 # django.http.multipartparser self._post = QueryDict(mutable=True) stream = LazyStream(ChunkIter(self._input_data)) for field, data in Parser(stream): self._post.append(field, force_text(data)) 

De plus, le formulaire, très probablement, veut se sauvegarder dans une base de données, et ici Django ORM commence.

ORM


Environ à travers de telles requêtes DSL pour ORM sont exécutées:

 # models.py Entry.objects.exclude( pub_date__gt=date(2005, 1, 3), headline='Hello', ) 

À l'aide de clés, vous pouvez collecter des expressions SQL similaires:

 SELECT * WHERE NOT (pub_date > '2005-1-3' AND headline = 'Hello') 

Comment ça se passe?

Queryset


La méthode exclude a un objet Query sous le capot. L'objet reçoit des arguments de la fonction et crée une hiérarchie d'objets, chacun pouvant se transformer en une partie distincte de la requête SQL sous forme de chaîne.

Lors de la traversée de l'arborescence, chacune des sections interroge ses nœuds enfants, reçoit des requêtes SQL imbriquées et, par conséquent, nous pouvons construire SQL en tant que chaîne. Par exemple, la valeur-clé ne sera pas un champ SQL distinct, mais sera comparée à la valeur-valeur. La concaténation et le refus des requêtes fonctionnent de la même manière qu'une traversée d'arborescence récursive, pour chaque nœud dont une conversion en SQL est appelée.

 # django.db.models.query sql.Query(Entry).where.add( ~Q( Q(F('pub_date') > date(2005, 1, 3)) & Q(headline='Hello') ) ) 

Compilateur


 # django.db.models.expressions class Q(tree.Node): AND = 'AND' OR = 'OR' def as_sql(self, compiler, connection): return self.template % self.field.get_lookup('gt') 

Sortie


 >>> Q(headline='Hello') # headline = 'Hello' >>> F('pub_date') # pub_date >>> F('pub_date') > date(2005, 1, 3) # pub_date > '2005-1-3' >>> Q(...) & Q(...) # ... AND ... >>> ~Q(...) # NOT … 

Un petit assistant-compilateur est passé à cette méthode, qui peut distinguer le dialecte MySQL de PostgreSQL et organiser correctement le sucre syntaxique utilisé dans le dialecte d'une base de données particulière.

Routage DB


Lorsque nous avons reçu la requête SQL, le modèle frappe sur le routage DB et demande dans quelle base de données il se trouve. Dans 99% des cas, ce sera la base de données par défaut, dans le 1% restant - une sorte de sienne.

 # django.db.utils class ConnectionRouter: def db_for_read(self, model, **hints): if model._meta.app_label == 'auth': return 'auth_db' 

Envelopper un pilote de base de données à partir d'une interface de bibliothèque spécifique, telle que Python MySQL ou Psycopg2, crée un objet universel avec lequel Django peut travailler. Il existe un wrapper pour les curseurs, un wrapper pour les transactions.

Piscine communicante


 # django.db.backends.base.base class BaseDatabaseWrapper: def commit(self): self.validate_thread_sharing() self.validate_no_atomic_block() with self.wrap_database_errors: return self.connection.commit() 

Dans cette connexion particulière, nous envoyons des requêtes au socket qui frappe sur la base de données et attendons l'exécution. L'encapsuleur sur la bibliothèque lira la réponse humaine de la base de données sous la forme d'un enregistrement, et Django collecte l'instance de modèle à partir de ces données dans des types Python. Ce n'est pas une itération compliquée.

Nous avons écrit quelque chose dans la base de données, lu quelque chose et décidé d'en informer l'utilisateur à l'aide de la page HTML. Pour ce faire, Django dispose d'un langage de modèle détesté par la communauté qui ressemble à quelque chose comme un langage de programmation, uniquement dans un fichier HTML.

Gabarit


 from django.template.loader import render_to_string render_to_string('my_template.html', {'entries': ...}) 

Code


 <ul> {% for entry in entries %} <li>{{ entry.name }}</li> {% endfor %} </ul> 

Analyseur


 # django.template.base BLOCK_TAG_START = '{%' BLOCK_TAG_END = '%}' VARIABLE_TAG_START = '{{' VARIABLE_TAG_END = '}}' COMMENT_TAG_START = '{#' COMMENT_TAG_END = '#}' tag_re = (re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END), re.escape(COMMENT_TAG_START), re.escape(COMMENT_TAG_END)))) 

Surprise - regexp à nouveau. Ce n'est qu'à la fin qu'il devrait y avoir une virgule, et la liste ira très loin. C'est probablement l'expression rationnelle la plus difficile que j'ai vue dans ce projet.

Lexer


Le gestionnaire de modèles et l'interpréteur sont assez simples. Il existe un lexer qui utilise regexp pour traduire le texte en une liste de petits jetons.

 # django.template.base def tokenize(self): for bit in tag_re.split(template_string): lineno += bit.count('\n') yield bit 

Nous parcourons la liste des jetons, regardons: «Qui êtes-vous? Enveloppez-vous dans un nœud de tag. " Par exemple, s'il s'agit du début de certains if for ou for , le gestionnaire de balises prendra le gestionnaire approprié. Le gestionnaire for lui-même dit à nouveau à l'analyseur: "Lisez-moi une liste de jetons jusqu'à la balise de fermeture."

L'opération revient à l'analyseur.

Un nœud, une balise et un analyseur sont des choses récurrentes mutuellement, et la profondeur de récursivité est généralement égale à l'imbrication du modèle lui-même par des balises.

Analyseur


 def parse(): while tokens: token = tokens.pop() if token.startswith(BLOCK_TAG_START): yield TagNode(token) elif token.startswith(VARIABLE_TAG_START): ... 

Le gestionnaire de balises nous donne un nœud spécifique, par exemple, avec une boucle for, pour lequel la méthode de render apparaît.

Pour boucle


 # django.template.defaulttags @register.tag('for') def do_for(parser, token): args = token.split_contents() body = parser.parse(until=['endfor']) return ForNode(args, body) 

Pour le nœud


 class ForNode(Node): def render(self, context): with context.push(): for i in self.args: yield self.body.render(context) 

La méthode de render est un arbre de rendu. Chaque nœud supérieur peut accéder à un nœud fille, lui demander de rendre. Les programmeurs ont l'habitude d'afficher certaines variables dans ce modèle. Cela se fait par le context - il est présenté sous la forme d'un dictionnaire régulier. Il s'agit d'une pile de dictionnaires pour émuler une portée lorsque nous entrons une balise. Par exemple, si le context lui-même change une autre balise à l'intérieur de la boucle for , alors lorsque nous quitterons la boucle, les modifications seront annulées. C'est pratique parce que quand tout est global, c'est difficile de travailler.

Réponse


Enfin, nous avons obtenu notre ligne avec la réponse HTTP:

Bonjour tout le monde!

Nous pouvons donner la ligne à l'utilisateur.

  • Renvoyez cette réponse de la vue.
  • Afficher les middlewares de listes.
  • Les middlewares modifient, complètent et améliorent cette réponse.
  • La réponse commence à itérer dans WSGIHandler, est partiellement écrite dans le socket et le navigateur reçoit une réponse de notre serveur.

Toutes les startups célèbres qui ont été écrites dans Django, comme Bitbucket ou Instagram, ont commencé avec un cycle si petit que chaque programmeur a traversé.

Tout cela, et une présentation à Moscou Python Conf ++, sont nécessaires pour mieux comprendre ce qui est entre vos mains et comment l'utiliser. Dans toute magie, il y a une grande partie de l'expression rationnelle que vous devez pouvoir cuisiner.

Artyom Malyshev et 23 autres grands orateurs le 5 avril nous donneront à nouveau beaucoup de matière à réflexion et à discussion sur le thème du Python lors de la conférence Python Conf ++ de Moscou . Étudiez le calendrier et participez à l'échange d'expériences dans la résolution de divers problèmes à l'aide de Python.

Source: https://habr.com/ru/post/fr433482/


All Articles