Django bajo microscopio

Si, según el informe de Artyom Malyshev ( prueba 404 ), harán una película, entonces el director será Quentin Tarantino; ya ha hecho una película sobre Django, también filmará la segunda. Todos los detalles de la vida de los mecanismos internos de Django desde el primer byte de la solicitud HTTP hasta el último byte de la respuesta. Extravaganza de formas de análisis, compilación de SQL llena de acción, efectos especiales de la implementación del motor de plantillas para HTML. ¿Quién administra el grupo de conexiones y cómo? Todo esto en orden cronológico de procesamiento de objetos WSGI. En todas las pantallas del país - la decodificación "Django bajo microscopio".



Sobre el orador: Artyom Malyshev es el fundador del proyecto Dry Python y el desarrollador principal de Django Channels versión 1.0. Ha estado escribiendo Python durante 5 años y ha ayudado a organizar reuniones de Python Rannts en Nizhny Novgorod. Artyom puede estar familiarizado con el apodo PROOFIT404 . La presentación del informe se almacena aquí .


Érase una vez, lanzamos la versión anterior de Django. Luego se veía aterradora y triste.



Vieron que self_check pasó, instalamos todo correctamente, todo funcionó y ahora puedes escribir código. Para lograr todo esto, tuvimos que ejecutar el 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. 

El proceso comienza, procesa las solicitudes HTTP y toda la magia ocurre dentro y se ejecuta todo el código que queremos mostrar a los usuarios como sitio.

Instalación


django-admin aparece en el sistema cuando instalamos Django usando, por ejemplo, pip, el administrador de paquetes .

 $ 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' ] }, ) 

Aparece entry_points setuptools , que apunta a la función execute_from_command_line . Esta función es un punto de entrada para cualquier operación con Django, para cualquier proceso actual.

Bootstrap


¿Qué pasa dentro de una función? Bootstrap , que se divide en dos iteraciones.

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

Configurar ajustes


El primero es leer configuraciones :

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

La configuración predeterminada global_settings , luego, desde la variable de entorno, intentamos encontrar el módulo con DJANGO_SETTINGS_MODULE , que escribió el usuario. Estas configuraciones se combinan en un espacio de nombre.

Cualquiera que escriba en Django al menos "Hola, mundo" sabe que hay INSTALLED_APPS , donde escribimos el código de usuario.

Completar aplicaciones


En la segunda parte, todas estas aplicaciones, esencialmente paquetes, se repiten una por una. Creamos para cada configuración, importamos modelos para trabajar con una base de datos y verificamos la integridad de los modelos. Además, el marco ejecuta Check , es decir, comprueba que cada modelo tiene una clave primaria, todas las claves externas apuntan a campos existentes y que el campo Nulo no está escrito en BooleanField, sino que se utiliza NullBooleanField.

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

Esta es la verificación de sanidad mínima para modelos, para el panel de administración, para cualquier cosa, sin conectarse a la base de datos, sin algo súper complicado y específico. En esta etapa, Django aún no sabe qué comando solicitó ejecutar, es decir, no distingue la migrate de runserver o shell .

Luego nos encontramos en un módulo que intenta adivinar mediante los argumentos de la línea de comandos qué comando queremos ejecutar y en qué aplicación se encuentra.

Comando de gestión


 # 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) 

En este caso, el módulo runserver tendrá un módulo django.core.management.commands.runserver . Después de importar el módulo, por convención, la clase global Command se llama dentro, se instancia, y decimos: " Te encontré, aquí tienes los argumentos de la línea de comando que el usuario pasó, haz algo con ellos ".

A continuación, vamos al módulo runserver y vemos que Django está hecho de " expresiones regulares y palos" , sobre lo que hablaré en detalle hoy:

 # 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) 

Comandos


Desplácese hacia abajo una pantalla y media; finalmente llegamos a la definición de nuestro equipo que inicia el servidor.

 # 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 realiza un conjunto mínimo de operaciones para que los argumentos de la línea de comandos BaseCommand como resultado argumentos para llamar a las funciones de **options *args y **options . Vemos que la instancia del servidor WSGI se está creando aquí, el WSGIHandler global está instalado en este servidor WSGI, esto es exactamente God Object Django . Podemos decir que esta es la única instancia del marco. La instancia se instala en el servidor globalmente, a través de la set application y dice: "Girar en el bucle de eventos, ejecutar solicitudes".

Siempre hay un Event Loop en alguna parte y un programador que le asigna tareas.

Servidor WSGI


¿Qué es WSGIHandler ? WSGI es una interfaz que le permite procesar solicitudes HTTP con un nivel mínimo de abstracción, y se ve como algo en forma de una función.

Controlador 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 

Por ejemplo, aquí es una instancia de una clase que tiene una call definida. Espera su entrada de diccionario, en la que los encabezados se presentarán como bytes y un controlador de archivos. Se necesita un controlador para leer el <body> la solicitud. El servidor en sí también proporciona callback start_response para que podamos enviar response.headers y su encabezado, por ejemplo, estado, en un paquete.

Además, podemos pasar el cuerpo de respuesta al servidor a través del objeto de respuesta. La respuesta es un generador sobre el que puedes iterar.

Todos los servidores que están escritos para WSGI: Gunicorn, uWSGI, Waitress, funcionan en esta interfaz y son intercambiables. Ahora estamos considerando un servidor para el desarrollo, pero cualquier servidor llega al punto de que en Django golpea el entorno y la devolución de llamada.

¿Qué hay dentro de Dios Objeto?


¿Qué sucede dentro de esta función global de Dios Objeto dentro de Django?

  • SOLICITUD
  • MIDDLEWARES.
  • Enrutamiento solicitud para ver.
  • VISTA - procesamiento de código de usuario dentro de la vista.
  • FORMULARIO - trabajar con formularios.
  • ORM
  • PLANTILLA
  • RESPUESTA

Toda la maquinaria que queremos de Django se lleva a cabo dentro de una sola función, que se extiende por todo el marco.

Solicitud


Envolvemos el entorno WSGI, que es un diccionario simple, en algún objeto especial, para la conveniencia de trabajar con el entorno. Por ejemplo, es más conveniente averiguar la longitud de una solicitud de usuario trabajando con algo similar a un diccionario que con una cadena de bytes que debe analizarse y buscar entradas de valor clave en ella. Cuando trabajo con cookies, tampoco quiero calcular manualmente si el período de almacenamiento ha expirado o no, y de alguna manera interpretarlo.

 # 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 solicitud contiene analizadores, así como un conjunto de controladores para controlar el procesamiento del cuerpo de la solicitud POST: ya sea un archivo en la memoria o temporal en el almacenamiento en el disco. Todo se decide dentro de la Solicitud. Request en Django también es un objeto agregador en el que todos los middlewares pueden poner la información que necesitamos sobre la sesión, la autenticación y la autorización del usuario. Podemos decir que este también es un Objeto de Dios, pero más pequeño.

Solicitud adicional llega al middleware.

Middlewares


El middleware es un contenedor que envuelve otras funciones como un decorador. Antes de renunciar al control del middleware, en el método de llamada damos una respuesta o llamamos a un middleware ya envuelto.

Así es como se ve el middleware desde el punto de vista de un programador.

Configuraciones


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

Definir


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

Desde el punto de vista de Django, los middlewares parecen una especie de pila:

 # 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 

Aplicar


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

Tomamos la función get_response inicial, la get_response en un controlador, que traducirá, por ejemplo, permission error y permission error not found error al código HTTP correcto. Envolvemos todo en el middleware de la lista. La pila de middlewares crece, y cada siguiente envuelve la anterior. Esto es muy similar a aplicar la misma pila de decoradores a todas las vistas en un proyecto, solo de manera centralizada. No es necesario ir y organizar los envoltorios con las manos de acuerdo con el proyecto, todo es conveniente y lógico.

Revisamos 7 círculos de middlewares, nuestra solicitud sobrevivió y decidimos procesarla a la vista. Luego llegamos al módulo de enrutamiento.

Enrutamiento


Aquí es donde decidimos a qué controlador llamar para una solicitud en particular. Y esto está resuelto:

  • basado en url;
  • en la especificación WSGI, donde se llama a request.path_info.

 # 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 

Urls


Tomamos el resolutor, lo alimentamos con la url de solicitud actual y esperamos que devuelva la función de vista en sí, y de la misma url obtenemos los argumentos con los que llamar a view. Luego, get_response llama a la vista, maneja las excepciones y hace algo con ella.

 # 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


Así es como se ve el resolutor:

 # 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}) 

Esto también es regexp, pero recursivo. Va en partes de la url, busca lo que el usuario quiere: otros usuarios, publicaciones, blogs, o es algún tipo de convertidor, por ejemplo, un año específico que necesita ser resuelto, puesto en argumentos, enviado a int.

Es característico que la profundidad de recursión del método de resolución sea siempre igual al número de argumentos con los que se llama la vista. Si algo salió mal y no encontramos una URL específica, se produce un error no encontrado.

Entonces finalmente llegamos a la vista: el código que escribió el programador.

Vista


En su representación más simple, esta es una función que devuelve la solicitud de la respuesta, pero dentro de ella realizamos tareas lógicas: "para, si, algún día" - muchas tareas repetitivas. Django nos proporciona una vista basada en la clase en la que puede especificar detalles específicos, y todo el comportamiento será interpretado en el formato correcto por la clase misma.

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

Diagrama de flujo del método


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

El método de dispatch de esta instancia ya está en la asignación de URL en lugar de una función. El envío basado en el verbo HTTP comprende qué método llamar: POST vino a nosotros y lo más probable es que queramos crear una instancia del objeto de formulario, si el formulario es válido, guárdelo en la base de datos y muestre la plantilla. Todo esto se hace a través de la gran cantidad de mixins que componen esta clase.

Forma


El formulario debe leerse desde el socket antes de que entre en la vista de Django, a través del mismo controlador de archivos que se encuentra en el entorno WSGI. form-data es un flujo de bytes, en el que se describen los separadores: podemos leer estos bloques y hacer algo con ellos. Puede ser una correspondencia clave-valor, si es un campo, parte de un archivo, luego nuevamente algún campo: todo está mezclado.

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

Analizador


El analizador consta de 3 partes.

El iterador de fragmentos que crea las lecturas esperadas de la secuencia de bytes se convierte en un iterador que puede producir boundaries . Garantiza que si algo vuelve, será un límite. Esto es necesario para que dentro del analizador no sea necesario almacenar el estado de la conexión, leer desde el socket o no leer para minimizar la lógica del procesamiento de datos.

A continuación, el generador se ajusta en LazyStream , que nuevamente crea un archivo objeto a partir de él, pero con la lectura esperada. Por lo tanto, el analizador ya puede recorrer pedazos de bytes y construir un valor-clave a partir de ellos.

El campo y los datos aquí siempre serán cadenas . Si recibimos un tiempo de datos en formato ISO, el formulario Django (que fue escrito por el programador) recibirá, utilizando ciertos campos, por ejemplo, marca de tiempo.

 # 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)) 

Además, el formulario, muy probablemente, quiere guardarse en una base de datos, y aquí comienza Django ORM.

ORM


Aproximadamente a través de tales solicitudes DSL para ORM se ejecutan:

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

Usando claves, puede recopilar expresiones SQL similares:

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

¿Cómo va esto?

Conjunto de consultas


El método de exclude tiene un objeto Query debajo del capó. El objeto se pasa argumentos a la función, y crea una jerarquía de objetos, cada uno de los cuales puede convertirse en una parte separada de la consulta SQL como una cadena.

Al atravesar el árbol, cada una de las secciones sondea sus nodos secundarios, recibe consultas SQL anidadas y, como resultado, podemos construir SQL como una cadena. Por ejemplo, el valor-clave no será un campo SQL separado, sino que se comparará con el valor-valor. La concatenación y la denegación de consultas funcionan de la misma manera que un recorrido de árbol recursivo, para cada nodo del que se llama una conversión a SQL.

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

Compilador


 # 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') 

Salida


 >>> 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 … 

Se pasa un pequeño compilador auxiliar a este método, que puede distinguir el dialecto de MySQL de PostgreSQL y organizar correctamente el azúcar sintáctico que se usa en el dialecto de una base de datos particular.

Enrutamiento de base de datos


Cuando recibimos la consulta SQL, el modelo toca el enrutamiento de la base de datos y pregunta en qué base de datos se encuentra. En el 99% de los casos, será la base de datos predeterminada, en el 1% restante, algo propio.

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

Al envolver un controlador de base de datos desde una interfaz de biblioteca específica, como Python MySQL o Psycopg2, se crea un objeto universal con el que Django puede trabajar. Hay un contenedor para cursores, un contenedor para transacciones.

Piscina de conexión


 # 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() 

En esta conexión en particular, enviamos solicitudes al socket que está golpeando la base de datos y esperamos su ejecución. El contenedor sobre la biblioteca leerá la respuesta humana de la base de datos en forma de registro, y Django recopila la instancia del modelo de estos datos en tipos de Python. Esta no es una iteración complicada.

Escribimos algo en la base de datos, leímos algo y decidimos informarle al usuario al respecto mediante la página HTML. Para hacer esto, Django tiene un lenguaje de plantilla que no le gusta a la comunidad que se parece a un lenguaje de programación, solo en un archivo HTML.

Plantilla


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

Código


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

Analizador


 # 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)))) 

Sorpresa - regexp de nuevo. Solo al final debe haber una coma, y ​​la lista irá muy abajo. Esta es probablemente la expresión regular más difícil que he visto en este proyecto.

Lexer


El manejador de plantillas y el intérprete son bastante simples. Hay un lexer que usa regexp para traducir texto en una lista de pequeños tokens.

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

Repetimos la lista de tokens, mira: “¿Quién eres? Te envuelven en un nodo de etiqueta. Por ejemplo, si este es el comienzo de algunos if o for or for , el manejador de etiquetas tomará el manejador apropiado. El manejador for nuevamente le dice al analizador: "Léame una lista de tokens hasta la etiqueta de cierre".

La operación vuelve al analizador.

Un nodo, una etiqueta y un analizador son elementos recursivos entre sí, y la profundidad de la recursión suele ser igual a la anidación de la plantilla en sí por las etiquetas.

Analizador


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

El manejador de etiquetas nos proporciona un nodo específico, por ejemplo, con un bucle for, para el cual aparece el método de render .

Para bucle


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

Para el nodo


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

El método de render es un árbol de renderizado. Cada nodo superior puede ir a un nodo hijo, pedirle que procese. Los programadores están acostumbrados a mostrar algunas variables en esta plantilla. Esto se hace a través del context : se presenta en forma de diccionario regular. Esta es una pila de diccionarios para emular un ámbito cuando ingresamos una etiqueta. Por ejemplo, si el context mismo cambia alguna otra etiqueta dentro del ciclo for , entonces cuando salgamos del ciclo los cambios se revertirán. Esto es conveniente porque cuando todo es global, es difícil trabajar.

Respuesta


Finalmente obtuvimos nuestra línea con la respuesta HTTP:

Hola mundo

Podemos darle la línea al usuario.

  • Devuelve esta respuesta de la vista.
  • Ver listas de middlewares.
  • Middlewares esta respuesta modifica, complementa y mejora.
  • La respuesta comienza a repetirse dentro de WSGIHandler, se escribe parcialmente en el socket y el navegador recibe una respuesta de nuestro servidor.

Todas las startups famosas que se escribieron en Django, como Bitbucket o Instagram, comenzaron con un ciclo tan pequeño que todos los programadores pasaron.

Todo esto, y una presentación en Moscow Python Conf ++, es necesario para que comprenda mejor lo que está en sus manos y cómo usarlo. En cualquier magia, hay una gran parte de expresiones regulares que debes poder cocinar.

Artyom Malyshev y otros 23 grandes oradores el 5 de abril nuevamente nos darán mucha reflexión y discusión sobre el tema de Python en la conferencia Conf ++ de Python en Moscú . Estudie el horario y únase al intercambio de experiencias para resolver una variedad de problemas usando Python.

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


All Articles