Django sob microscópio

Se, de acordo com o relatório de Artyom Malyshev ( proofit404 ), eles fizerem um filme, o diretor será Quentin Tarantino - ele já fez um filme sobre Django, e também o segundo. Todos os detalhes da vida útil dos mecanismos internos do Django, desde o primeiro byte da solicitação HTTP até o último byte da resposta. A extravagância de formas de analisador, compilação de SQL cheia de ação, efeitos especiais da implementação do mecanismo de modelo para HTML. Quem está gerenciando o pool de conexões e como? Tudo isso em ordem cronológica de processamento de objetos WSGI. Em todas as telas do país - a decodificação "Django sob microscópio".



Sobre o palestrante: Artyom Malyshev é o fundador do projeto Dry Python e o desenvolvedor principal do Django Channels versão 1.0. Ele escreve Python há 5 anos e ajudou a organizar reuniões do Python Rannts em Nizhny Novgorod. Artyom pode ser familiar para você sob o apelido PROOFIT404 . A apresentação do relatório é armazenada aqui .


Era uma vez, lançamos a versão antiga do Django. Então ela parecia assustadora e triste.



Eles viram que o self_check passou, instalamos tudo corretamente, tudo funcionou e agora você pode escrever código. Para conseguir tudo isso, tivemos que executar o 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. 

O processo inicia, processa solicitações HTTP e toda a mágica acontece dentro e todo o código que queremos mostrar aos usuários como um site é executado.

Instalação


django-admin aparece no sistema quando instalamos o Django usando, por exemplo, pip, o gerenciador de pacotes .

 $ 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 , que aponta para a função execute_from_command_line . Esta função é um ponto de entrada para qualquer operação com o Django, para qualquer processo atual.

Bootstrap


O que acontece dentro de uma função? Bootstrap , que é dividido em duas iterações.

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

Definir configurações


O primeiro é ler configurações :

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

As configurações padrão global_settings , a partir da variável de ambiente, tentamos encontrar o módulo com DJANGO_SETTINGS_MODULE , que o usuário escreveu. Essas configurações são combinadas em um espaço para nome.

Qualquer um que escreve no Django pelo menos “Olá, mundo” sabe que existe INSTALLED_APPS - onde escrevemos o código do usuário.

Preencher aplicativos


Na segunda parte, todos esses aplicativos, essencialmente pacotes, são iterados um por um. Criamos para cada configuração, importamos modelos para trabalhar com um banco de dados e verificamos a integridade dos modelos. Além disso, a estrutura executa Check , ou seja, verifica se cada modelo possui uma chave primária, todas as chaves estrangeiras apontam para os campos existentes e se o campo Null não está gravado no BooleanField, mas o NullBooleanField é usado.

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

Esta é a verificação mínima de sanidade para modelos, para o painel de administração, para qualquer coisa - sem conectar ao banco de dados, sem algo super complicado e específico. Nesta fase, o Django ainda não sabe qual comando você pediu para executar, ou seja, não distingue migrate do runserver ou shell .

Então nos encontramos em um módulo que tenta adivinhar, por argumentos da linha de comando, qual comando queremos executar e em qual aplicativo ele se encontra.

Comando de gerenciamento


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

Nesse caso, o módulo runserver terá um módulo django.core.management.commands.runserver interno. Depois de importar o módulo, por convenção, a classe global Command é chamada dentro, é instanciada e dizemos: " Encontrei você, aqui você tem os argumentos da linha de comando que o usuário passou, faça algo com eles ".

Em seguida, vamos ao módulo runserver e vemos que o Django é feito de "regexp and sticks" , sobre o qual falarei em detalhes hoje:

 # 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


Role uma tela e meia - finalmente chegamos à definição de nossa equipe que inicia o 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 executa um conjunto mínimo de operações para que os argumentos da linha de comando resultem em argumentos para chamar as funções de **options *args e **options . Vemos que a instância do servidor WSGI está sendo criada aqui, o WSGIHandler global está instalado neste servidor WSGI - este é exatamente o God Object Django . Podemos dizer que esta é a única instância do framework. A instância é instalada no servidor globalmente - através do set application e diz: "Gire no loop de eventos, execute solicitações".

Sempre há um loop de eventos em algum lugar e um programador que lhe dá tarefas.

Servidor WSGI


O que é o WSGIHandler ? O WSGI é uma interface que permite processar solicitações HTTP com um nível mínimo de abstração e se parece com algo na forma de uma função.

Manipulador 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 exemplo, aqui está uma instância de uma classe que possui uma call definida. Ele espera pela entrada do dicionário, na qual os cabeçalhos serão apresentados como bytes e como manipulador de arquivos. O manipulador é necessário para ler o <body> solicitação. O próprio servidor também fornece retorno de chamada start_response para que possamos enviar response.headers e seu cabeçalho, por exemplo, status, em um pacote.

Além disso, podemos passar o corpo da resposta para o servidor através do objeto de resposta. A resposta é um gerador sobre o qual você pode iterar.

Todos os servidores escritos para WSGI - Gunicorn, uWSGI, Waitress, trabalham nessa interface e são intercambiáveis. Agora estamos considerando um servidor para desenvolvimento, mas qualquer servidor chega ao ponto em que no Django ele atravessa o ambiente e o retorno de chamada.

O que há dentro de Deus Object?


O que acontece dentro dessa função global de Objeto Deus dentro do Django?

  • PEDIDO.
  • MIDDLEWARES.
  • ROUTING para visualizar.
  • VIEW - processamento de código do usuário dentro da view.
  • FORMULÁRIO - trabalhe com formulários.
  • ORM.
  • MODELO
  • RESPOSTA.

Todo o maquinário que queremos do Django ocorre em uma única função, que está espalhada por toda a estrutura.

Pedido


Nós agrupamos o ambiente WSGI, que é um dicionário simples, em algum objeto especial, para a conveniência de trabalhar com o ambiente. Por exemplo, é mais conveniente descobrir a duração de uma solicitação do usuário trabalhando com algo semelhante a um dicionário do que com uma sequência de bytes que precisa ser analisada e procurar por entradas de valor-chave. Ao trabalhar com cookies, também não quero calcular manualmente se o período de armazenamento expirou ou não e de alguma forma interpretá-lo.

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

A solicitação contém analisadores, bem como um conjunto de manipuladores para controlar o processamento do corpo da solicitação POST: se é um arquivo na memória ou temporário no armazenamento em disco. Tudo é decidido dentro da solicitação. A requisição no Django também é um objeto agregador no qual todos os middlewares podem colocar as informações necessárias sobre a sessão, autenticação e autorização do usuário. Podemos dizer que este também é um Objeto Divino, mas menor.

Solicitação adicional chega ao middleware.

Middlewares


Middleware é um invólucro que envolve outras funções como um decorador. Antes de abrir mão do controle do middleware, no método de chamada, respondemos ou chamamos um middleware já empacotado.

É assim que o middleware se parece do ponto de vista de um programador.

Configurações


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

Do ponto de vista do Django, os middlewares parecem uma espécie de pilha:

 # 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 

Pegamos a função get_response inicial, envolvemos-a em um manipulador, que traduzirá, por exemplo, permission error e permission error not found error no código HTTP correto. Envolvemos tudo no middleware em si da lista. A pilha de middlewares cresce e cada próxima envolve a anterior. Isso é muito semelhante à aplicação da mesma pilha de decoradores em todas as visualizações de um projeto, apenas centralmente. Não há necessidade de sair e organizar os invólucros com as mãos de acordo com o projeto, tudo é conveniente e lógico.

Passamos por 7 círculos de middlewares, nossa solicitação sobreviveu e decidimos processá-la à vista. Além disso, chegamos ao módulo de roteamento.

Encaminhamento


É aqui que decidimos qual manipulador solicitar uma solicitação específica. E isso está resolvido:

  • com base no URL;
  • na especificação WSGI, em que request.path_info é chamado.

 # 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


Pegamos o resolvedor, alimentamos o URL de solicitação atual e esperamos que ele retorne a função view, e a partir do mesmo URL obteremos os argumentos com os quais chamar o view. Então, get_response chama a visualização, lida com exceções e faz algo com ela.

 # 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


É assim que o resolvedor se parece:

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

Isso também é regexp, mas recursivo. Ele entra em partes do URL, procura o que o usuário deseja: outros usuários, postagens, blogs ou é algum tipo de conversor, por exemplo, um ano específico que precisa ser resolvido, colocado em argumentos, convertido em int.

É característico que a profundidade da recursão do método de resolução seja sempre igual ao número de argumentos com os quais a exibição é chamada. Se algo deu errado e não encontramos um URL específico, ocorre um erro não encontrado.

Então finalmente chegamos à vista - o código que o programador escreveu.

Ver


Em sua representação mais simples, é uma função que retorna solicitação de resposta, mas dentro dela realizamos tarefas lógicas: “para, se algum dia” - muitas tarefas repetitivas. O Django nos fornece uma visão baseada em classe, onde você pode especificar detalhes específicos, e todo o comportamento será interpretado no formato correto pela própria classe.

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

Fluxograma do método


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

O método de dispatch desta instância já está no mapeamento de URL em vez de em uma função. A expedição baseada no verbo HTTP entende qual método chamar: O POST chegou até nós e provavelmente queremos instanciar o objeto do formulário, se o formulário for válido, salve-o no banco de dados e mostre o modelo. Tudo isso é feito através do grande número de mixins que compõem essa classe.

Formulário


O formulário deve ser lido no soquete antes de entrar na visualização do Django - através do mesmo manipulador de arquivos que se encontra no ambiente WSGI. form-data é um fluxo de bytes, no qual os separadores são descritos - podemos ler esses blocos e fazer algo deles. Pode ser uma correspondência de valor-chave, se for um campo, parte de um arquivo e, novamente, algum campo - tudo está misturado.

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

Analisador


O analisador consiste em 3 partes.

O iterador de partes que cria as leituras esperadas do fluxo de bytes se transforma em um iterador que pode produzir boundaries . Garante que, se algo retornar, será um limite. Isso é necessário para que, dentro do analisador, não seja necessário armazenar o estado da conexão, ler do soquete ou não ler para minimizar a lógica do processamento de dados.

Em seguida, o gerador envolve o LazyStream , que novamente cria um arquivo de objeto, mas com a leitura esperada. Portanto, o analisador já pode percorrer pedaços de bytes e criar um valor-chave a partir deles.

campo e dados aqui sempre serão strings . Se recebermos um horário de dados no formato ISO, o formulário do Django (que foi escrito pelo programador) receberá, usando certos campos, por exemplo, carimbo de data / hora.

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

Além disso, o formulário, provavelmente, quer se salvar em um banco de dados, e aqui o Django ORM começa.

ORM


Aproximadamente através desses pedidos DSL para ORM são executados:

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

Usando chaves, você pode coletar expressões SQL semelhantes:

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

Como está indo isso?

Queryset


O método de exclude tem um objeto de Query sob o capô. O objeto passa argumentos para a função e cria uma hierarquia de objetos, cada um dos quais pode se transformar em uma parte separada da consulta SQL como uma sequência.

Ao percorrer a árvore, cada uma das seções pesquisa seus nós filhos, recebe consultas SQL aninhadas e, como resultado, podemos construir o SQL como uma string. Por exemplo, o valor-chave não será um campo SQL separado, mas será comparado com o valor-valor. A concatenação e a negação de consultas funcionam da mesma maneira que uma passagem de árvore recursiva, para cada nó do qual uma conversão ao SQL é chamada.

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

Saída


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

Um pequeno compilador auxiliar é passado para esse método, que pode distinguir o dialeto do MySQL do PostgreSQL e organizar corretamente o açúcar sintático usado no dialeto de um banco de dados específico.

Roteamento de banco de dados


Quando recebemos a consulta SQL, o modelo bate no roteamento de banco de dados e pergunta em qual banco de dados ele está. Em 99% dos casos, será o banco de dados padrão, nos 1% restantes - algum tipo próprio.

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

O agrupamento de um driver de banco de dados de uma interface específica da biblioteca, como Python MySQL ou Psycopg2, cria um objeto universal com o qual o Django pode trabalhar. Há um invólucro para cursores, um invólucro para transações.

Piscina de conexão


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

Nesta conexão em particular, enviamos solicitações para o soquete que está batendo no banco de dados e aguardamos a execução. O wrapper da biblioteca lerá a resposta humana do banco de dados na forma de um registro, e o Django coleta a instância do modelo desses dados nos tipos Python. Esta não é uma iteração complicada.

Escrevemos algo no banco de dados, lemos algo e decidimos contar ao usuário sobre ele usando a página HTML. Para fazer isso, o Django tem uma linguagem de modelo não-apreciada pela comunidade que se parece com uma linguagem de programação, apenas em um arquivo HTML.

Template


 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> 

Analisador


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

Surpresa - regexp novamente. Somente no final deve haver uma vírgula, e a lista será muito abaixo. Este é provavelmente o regexp mais difícil que eu já vi neste projeto.

Lexer


O manipulador e o intérprete de modelos são bastante simples. Existe um lexer que usa o regexp para traduzir o texto em uma lista de pequenos tokens.

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

Repetimos a lista de fichas, veja: “Quem é você? Envolva você em um nó de tag. ” Por exemplo, se este é o início de alguns if ou for ou for , o manipulador de tags usará o manipulador apropriado. O manipulador for novamente diz ao analisador: "Leia-me uma lista de tokens até a tag de fechamento".

A operação vai para o analisador novamente.

Um nó, tag e analisador são coisas mutuamente recursivas, e a profundidade da recursão geralmente é igual ao aninhamento do próprio modelo por tags.

Analisador


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

O manipulador de tags nos fornece um nó específico, por exemplo, com um loop for, para o qual o método render aparece.

For loop


 # 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 o nó


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

O método de render é uma árvore de renderização. Cada nó superior pode ir para um nó filha, pedir para ela renderizar. Os programadores estão acostumados a mostrar algumas variáveis ​​neste modelo. Isso é feito através do context - é apresentado na forma de um dicionário regular. Esta é uma pilha de dicionários para emular um escopo quando inserimos uma tag. Por exemplo, se o próprio context alterar alguma outra tag dentro do loop for , quando sairmos do loop, as alterações serão revertidas. Isso é conveniente porque, quando tudo é global, é difícil trabalhar.

Resposta


Finalmente conseguimos nossa linha com a resposta HTTP:

Olá Mundo!

Podemos dar a linha ao usuário.

  • Retorne esta resposta da vista.
  • Exibir listas de middlewares.
  • Middlewares essa resposta modifica, complementa e aprimora.
  • A resposta começa a iterar dentro do WSGIHandler, é parcialmente gravada no soquete e o navegador recebe uma resposta do nosso servidor.

Todas as startups famosas que foram escritas no Django, como Bitbucket ou Instagram, começaram com um ciclo tão pequeno que todo programador passou.

Tudo isso, e uma apresentação no Moscow Python Conf ++, é necessário para você entender melhor o que está em suas mãos e como usá-lo. Em qualquer mágica, há uma grande parte do regexp que você deve poder cozinhar.

Artyom Malyshev e outros 23 grandes palestrantes em 5 de abril nos darão novamente muita reflexão e discussão sobre o tópico Python na conferência Moscow Python Conf ++ . Estude o cronograma e participe da troca de experiências na solução de vários problemas usando o Python.

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


All Articles