zope статьи

Работа Zope 3 шаг за шагом

Dmitry Vasiliev 23:27, 2012 2 6 17:13, 2008 6 7

Разработка тестового проекта

Практически не возможно держать в голове все детали работы сервера приложений Zope 3, поэтому в этом документе я рассмотрю по шагам, что происходит во время начальной загрузки и обработки HTTP запросов. На данный момент описание основывается на версии Zope 3.4 и я постараюсь обновлять его по мере необходимости. Для рассмотрения работы Zope 3 я буду использовать небольшой тестовый проект. Хотя проект небольшой и всего лишь выводит на странице время я постараюсь добавить в него как можно больше компонентов, чтобы в дальнейшем подробней рассмотреть их взаимодействие. Скачать архив проекта можно здесь:

Одно из основных новшеств в Zope 3 - это применение (хотя и немного упрощенной версии) шаблона проектирования MVC (Model-View-Controller, Модель-Представление-Контроллер). MVC достаточно давно применяется для построения графических интерфейсов и позволяет разделить работу по их созданию на логические части и соответственно упростить разработку и поддержку. В применении этого шаблона для Zope 3 нет четко выделенного Контроллера так как каждый запрос имеет синхронную природу и работает только с одним представлением в один момент времени, таким образом шаблон проектирования практически упрощается до MV (Model-View, Модель-Представление). При этом роль контроллера выполняет представление.

В Zope 3 проекты создаются в виде пакетов Python и для тестового проекта мы создадим следующую структуру:

clock/
    __init__.py
    interfaces.py
    tests.py
    README.txt
    model.py
    configure.zcml
    clock-configure.zcml

    browser/
        __init__.py
        tests.py
        views.txt
        views.py
        ftests.py
        README.txt
        index.pt
        configure.zcml

Кратко назначение каждого элемента (ниже я рассмотрю их подробней):

  • clock/ - директория тестового проекта;
  • __init__.py - модуль Python превращающий директорию clock/ в пакет Python (в нашем случае просто пустой файл);
  • interfaces.py - модуль Python содержащий публичные интерфейсы (которые могут быть использованы другими внешними компонентами) пакета;
  • tests.py - модуль Python с тестами классов модели;
  • README.txt - текстовый файл с тестами-документацией;
  • model.py - модуль Python содержащий реализацию модели часов;
  • configure.zcml - файл конфигурации Zope 3 связывающий компоненты проекта;
  • clock-configure.zcml - файл конфигурации для привязки проекта к Zope 3;
  • browser/ - директория с представлениями для Web браузера;
    • browser/__init__.py - модуль Python превращающий директорию browser/ в пакет Python;
    • browser/tests.py - модуль Python с тестами классов представлений;
    • browser/views.txt - текстовый файл с тестами для представлений;
    • browser/views.py - модуль Python с кодом представлений для часов;
    • browser/ftests.py - модуль Python с функциональными тестами классов представлений;
    • browser/README.txt - текстовый файл с функциональными тестами;
    • browser/index.pt - HTML шаблон представления проекта;
    • browser/confugure.zcml - файл конфигурации Zope 3 связывающий компоненты представлений;

Модель

В нашем случае модель должна предоставлять интерфейс для получения текущего времени в UTC (Coordinated Universal Time, Универсальное Координированное Время) для упрощения в будущем его преобразования в другие временные зоны. Файл interfaces.py содержит интерфейс нашей модели:

# -*- coding: utf-8 -*-
u"""Интерфейсы часов."""

from zope.interface import Interface


class IClock(Interface):
    u"""Интерфейс часов."""

    def getCurrentDateTime():
        u"""Вернуть datetime объект с текущими датой и временем в UTC."""

Наш интерфейс IClock имеет единственный метод getCurrentDateTime который должен возвращать текущую дату и время в UTC. Мы наследуем наш интерфейс от базового интерфейса Interface который превращает обычный класс Python в интерфейс описывающий внешнее поведение - например, нам не нужно указывать аргумент self у методов так как здесь мы смотрим на них со стороны клиента.

Прежде чем писать реализацию данного интерфейса создадим для нее юнит-тесты. Хотя для данного проекта это не так важно, но метод разработки через тестирование сразу позволяет взглянуть на работу будущего компонента с точки зрения пользователя: лучше проработать будущий интерфейс и плюс получить набор тестов которые могут в будущем сэкономить огромное количество времени. Я буду использовать тесты-документацию (doctests) которые позволяют сразу получить и набор тестов и документацию с работающими примерами. По соглашению для поиска тестов пакет zope.testing, используемый для тестов в Zope 3, использует модули (или пакеты) с именем tests (в случае с пакетом в нем должны быть модули с именами начинающимися на test_) и наш tests.py выглядит следующим образом:

# -*- coding: utf-8 -*-
u"""Тесты модели."""

import unittest
import doctest


def test_suite():
    suite = unittest.TestSuite()
    suite.addTest(doctest.DocFileSuite("README.txt"))
    return suite

В тестовых модулях пакет zope.testing ищет функцию с именем test_suite которая должна вернуть набор тестов модуля. В нашем случае это единственный тест-документация который расположен в файле README.txt:

=====
Часы.
=====

Часы должны реализовывать интерфейс ``IClock``::

    >>> from clock.model import Clock
    >>> from clock.interfaces import IClock
    >>> from zope.interface.verify import verifyObject

    >>> clock = Clock()
    >>> verifyObject(IClock, clock)
    True

Метод ``getCurrentDateTime()`` возвращает текущие дату и время в UTC::

    >>> clock.getCurrentDateTime() #doctest: +ELLIPSIS
    datetime.datetime(..., tzinfo=<UTC>)

Здесь проверяется, что объект класса Clock предоставляет интерфейс IClock и метод объекта getCurrentDateTime возвращает время в UTC.

Теперь мы можем реализовать нашу модель, которая будет расположена в файле model.py:

# -*- coding: utf-8 -*-
u"""Классы реализующие часы."""

from datetime import datetime
from pytz import timezone

from zope.interface import implements

from clock.interfaces import IClock


class Clock(object):
    u"""Часы."""

    implements(IClock)

    utc = timezone("UTC")

    def getCurrentDateTime(self):
        u"""Вернуть datetime объект с текущими датой и временем в UTC."""
        return datetime.now(self.utc)

Надо заметить, что на данном этапе класс Clock не имеет никакой связи с какими-либо Web-интерфейсами и может быть использован сам по себе в каком-либо проекте Python.

Для запуска тестов в архиве есть очень простой скрипт box.py. Перед использованием надо установить в переменную среды ZOPE_INSTANCE путь к каталогу с вашей инсталляцией Zope и после этого можно запустить тесты и посмотреть результат:

$ export ZOPE_INSTANCE=~/zope
$ ./box.py test
Running unit tests:
  Ran 1 test with 0 failures and 0 errors in 0.130 seconds.

Представление/Контроллер

Теперь, когда у нас есть модель часов надо подумать как представить их в браузере. Чтобы не усложнять, это будет просто HTML-страничка на которой будет выводиться текущее Московское время и для получения текущего времени страничку нужно обновлять вручную.

Наше представление фактически состоит из двух частей:

  • Кода Python в файле views.py преобразующего объект datetime, получаемый из модели, в строку с Московским временем для вывода на странице;
  • ZPT-шаблона index.pt выводящего строку со временем на HTML-страницу;

Начнем с views.py. Все представления в Zope 3 фактически являются мульти-адаптерами адаптирующими объект контекста и объект запроса к интерфейсу представления. Как и с моделью мы начинаем сначала с юнит-тестов. Файл запуска тестов browser/tests.py:

# -*- coding: utf-8 -*-
u"""Тесты представлений."""

import unittest
import doctest


def test_suite():
    suite = unittest.TestSuite()
    suite.addTest(doctest.DocFileSuite("views.txt"))
    return suite

И тест browser/views.txt:

=================================
Представление для даты и времени.
=================================

Для тестов создадим простую замену для часов которая возвращает постоянное
время::

    >>> from datetime import datetime
    >>> from clock.model import Clock

    >>> class TestClock(Clock):
    ...
    ...     def getCurrentDateTime(self):
    ...         return datetime(2007, 4, 1, 10, 20, 30, 60, self.utc)

Теперь создадим наше представление::

    >>> from zope.publisher.browser import TestRequest
    >>> from clock.browser.views import ClockView

    >>> clock = TestClock()
    >>> request = TestRequest(environ={"HTTP_ACCEPT_LANGUAGE": "ru"})
    >>> view = ClockView(clock, request)

Метод представления ``datetime()`` возвращает отформатированные дату и время по
Москве::

    >>> expected = unicode("1 Апрель 2007 г. 14:20:30 +400", "utf-8")
    >>> view.datetime() == expected
    True

Для представления мы наследуемся от класса BrowserView (хотя его реализация достаточно проста и может быть написана с нуля) и получаем следующее содержимое views.py:

# -*- coding: utf-8 -*-
u"""Представления часов."""

from pytz import timezone

from zope.publisher.browser import BrowserView


class ClockView(BrowserView):
    u"""Представление для даты и времени."""

    msk = timezone("Europe/Moscow")

    def datetime(self):
        utc_dt = self.context.getCurrentDateTime()
        formatter = self.request.locale.dates.getFormatter("dateTime", "long")
        msk_dt = self.msk.fromutc(utc_dt)
        return formatter.format(msk_dt)

Контекстный объект и объект запроса представлены здесь атрибутами класса self.context и self.request соответственно.

После этого можно запустить тесты и посмотреть результат:

$ ./box.py test
Running unit tests:
  Ran 2 tests with 0 failures and 0 errors in 0.270 seconds.

HTML-шаблон и связь модели с представлением

Пока мы имеем только два несвязанных класса в модулях model.py и browser/views.py и чтобы связать их между собой и подключить к серверу Zope 3 используется язык ZCML . Сначала мы создадим файл для подключения нашей ZCML конфигурации к серверу clock-configure.zcml и положим его в каталог etc/package-includes инсталляции Zope 3:

<include package="clock" />

Как видно в этом файле всего одна строка указывающая подключить конфигурацию из пакета clock. Теперь мы создадим ZCML описание нашей модели в файле configure.zcml:

<configure
  xmlns="http://namespaces.zope.org/zope"
  i18n_domain="clock">

  <interface
    interface=".interfaces.IClock"
    type="zope.app.content.interfaces.IContentType"
    />

  <class class=".model.Clock">
    <require
      permission="zope.View"
      interface=".interfaces.IClock"/>
  </class>

  <include package=".browser"/>

</configure>

Здесь директива interface устанавливает тип нашего интерфейса IClock в IContentType делая объекты которые его реализуют типами контента. Директива class описывает доступ к методам и атрибутам нашего класса модели Clock. Мы указываем, что для доступа к методам определенным в интерфейсе IClock нужно иметь права zope.View. И в итоге, директива include подключает ZCML конфигурацию из пакета browser.

В каталоге browser файл configure.zcml описывает конфигурацию связанную с представлением:

<configure
  xmlns="http://namespaces.zope.org/browser"
  i18n_domain="clock" >

  <addMenuItem
    class="..model.Clock"
    title="Часы"
    permission="zope.ManageContent"
    />

  <page
    for="..interfaces.IClock"
    name="index.html"
    class=".views.ClockView"
    template="index.pt"
    permission="zope.View"
    />

  <configure package="zope.app.preview">
    <page
      for="clock.interfaces.IClock"
      name="preview.html"
      template="preview.pt"
      permission="zope.ManageContent"
      menu="zmi_views" title="Preview"
      />
  </configure>

</configure>

Здесь директива addMenuItem добавляет пункт в меню для добавления нашей страницы с часами. Директива page определяет страницу в терминах клиента, связывая нашу модель и представления. Здесь атрибут for задает интерфейс модели, атрибут class - класс представления, template имя HTML-шаблона, permission - право для доступа к странице и name - итоговое имя страницы на сайте. И в итоге, для удобства, ниже задана еще одна страница для просмотра часов в административном интерфейсе.

Теперь мы связали модель, класс представления и HTML-шаблон browser/index.pt

<html i18n:domain="clock">

  <head>
    <title>Московское время</title>
  </head>

  <body>
    <h1>Московское время:
      <tal:block content="view/datetime">
        1 Апрель 2007 г. 14:20:30 +400
      </tal:block>
    </h1>
  </body>

</html>

Этот шаблон использует язык Zope Page Templates (Шаблон Страниц Zope) и просто выводит время в виде HTML страницы.

В итоге, чтобы протестировать работу часов в целом мы воспользуемся функциональными тестами, которые должны загружаться через файлы ftests.py (или ftests может быть именем пакета с файлами имена которых начинаются на test_). browser/ftests.py:

# -*- coding: utf-8 -*-
u"""Функциональные тесты."""

import unittest

from zope.app.testing.functional import FunctionalDocFileSuite


def test_suite():
    suite = unittest.TestSuite()
    suite.addTest(FunctionalDocFileSuite("README.txt"))
    return suite

Это файл загружает тест-документацию из файла README.txt:

===========================
Функциональные тесты часов.
===========================

Сначала создадим тестовый браузер::

    >>> from zope.testbrowser.testing import Browser
    >>> browser = Browser()

И добавим заголовки::

    >>> browser.addHeader("Authorization", "Basic mgr:mgrpw")
    >>> browser.addHeader("Accept-Language", "ru")

Теперь мы можем создать экземпляр часов::

    >>> browser.open("http://localhost/@@contents.html?type_name=BrowserAdd__clock.model.Clock")
    >>> form = browser.getForm(name="containerContentsForm")
    >>> form.getControl(name="new_value").value = u"clock"
    >>> form.submit()

Теперь можно посмотреть время::

    >>> browser.open("http://localhost/clock")
    >>> print browser.contents
    <html>
    <BLANKLINE>
      <head>
    <base href="http://localhost/clock/index.html" />
    <BLANKLINE>
        <title>Московское время</title>
      </head>
    <BLANKLINE>
      <body>
        <h1>Московское время:
          ... +400
        </h1>
      </body>
    <BLANKLINE>
    </html>
    <BLANKLINE>

Здесь мы создаем тестовый браузер browser и используем его, чтобы добавить страницу с часами и запросить время. Теперь мы можем запустить полные тесты нашего пакета:

$ ./box.py test
Running unit tests:
  Ran 2 tests with 0 failures and 0 errors in 0.242 seconds.
Running zope.app.testing.functional.Functional tests:
  Set up zope.app.testing.functional.Functional in 4.420 seconds.
  Ran 4 tests with 0 failures and 0 errors in 0.489 seconds.
Tearing down left over layers:
  Tear down zope.app.testing.functional.Functional ... not supported
Total: 6 tests, 0 failures, 0 errors

После этого можно запустить сервер с помощью команды ./box.py run, выбрать из меню добавления часы, добавить и посмотреть результаты работы в браузере.

Начальная загрузка

Теперь, когда у нас есть тестовый проект рассмотрим что происходит при начальной загрузке сервера Zope 3:

  1. Запуск сервера начинается со скрипта bin/runzope, который находится в каталоге установки Zope 3. Здесь проверяется соответствие версии Python и запускается функция main из модуля zope.app.twisted.main.
  2. Функция main загружает файл конфигурации etc/zope.conf с помощью функции load_options и схемы конфигурации zope/app/twisted/schema.xml. После загрузки опций загружается ZCML конфигурация с помощью функции setup.
  3. Функция setup включает логгинг и загружает ZCML конигурацию с помощью функции config из модуля zope.app.appsetup.appsetup.config.
  4. Функция config с помощью модулей zope.configuration.xmlconfig и zope.configuration.config загружает ZCML конфигурацию. Основной файл конфигурации (по умолчанию etc/site.zcml) загружается функцией file из модуля zope.configuration.xmlconfig.
  5. Файл etc/site.zcml загружает наш файл конфигурации clock-configure.zcml, который в свою очередь загружает наши файлы configure.zcml и browser/configure.zcml. Обработка всех запросов откладывается до момента обработки всей конфигурации.
  6. При обработке директивы interface из configure.zcml вызывается функция interface из модуля zope.component.zcml, которая оставляет запрос на предоставление интерфейса provideInterface.
  7. Директива class из configure.zcml обрабатывается с помощью класса ClassDirective из модуля zope.app.component.contentdirective. В результате работы этого обработчика будут установлены права пользования нашим классом модели Clock, создана, и зарегистрирована как утилита для интерфейса IFactory, фабрика для создания класса.
  8. В файле browser/configure.zcml директива addMenuItem будет обработана функцией addMenuItem из модуля zope.app.publisher.browser.menumeta. Она создает и добавляет элемент для наших часов в меню добавления.
  9. Директива page из configure.zcml обрабатывается функцией page из модуля zope.app.publisher.browser.viewmeta. Основная работа функции - зарегистрировать адаптер связывающий модель с классами и шаблонами представления. При этом связь шаблона и класса представления осуществляется автоматически создаваемым через функцию SimpleViewClass (модуль zope.app.pagetempalte.simpleviewclass) классом - оберткой. Создаваемый класс также наследуется от класс simple и таким образом реализует интерфейс zope.publisher.interfaces.IBrowserPublisher необходимый при публикации контента.
  10. После загрузки конфигурации все отложенные директивы выполняются и мы возвращаемся в функцию setup из модуля zope.app.twisted.main и открываем базу данных с помощью функции multi_database из модуля zope.app.appsetup.appsetup. Посылается событие zope.app.appsetup.interfaces.DatabaseOpenedEvent, создается и возвращается объект ZopeService с подключенными сервисами. Подключаемые сервисы создаются через фабрики определенные в модуле zope.app.twisted.server. Фабрики в свою очередь запрашивают сервисы как утилиты с интерфейсом zope.app.twisted.interfaces.IServerType и именами определенными в схеме конфигурации schema.xml. Конкретные сервисы определены в файле zope/app/twisted/configure.zcml.
  11. По событию DatabaseOpenedEvent при необходимости создается базовая структура приложения в базе функцией bootStrapSubscriber из модуля zope.app.appsetup.bootstrap.
  12. Мы возвращаемся в функцию main из модуля zope.app.twisted.main, где запускается набор сервисов через мульти-сервис ZopeService, посылается событие zope.app.appsetup.interfaces.ProcessStarting и запускается реактор Twisted. С этого момента сервер запущен и начинается обработка запросов.

Обработка HTTP запросов

После запуска сервера мы можем рассмотреть что же происходит при обработке запросов. Сначала создадим экземпляр нашего проекта и затем сделаем запрос к нашим часам.

  1. Связь сетевого уровня (Twisted) и Zope осуществляется через интерфейс WSGI и со стороны Zope точкой входа в случае HTTP протокола является класс zope.app.wsgi.WSGIPublisherApplication. При поступлении входящего запроса вызывается экземпляр WSGIPublisherApplication который с помощью вызова экземпляра фабрики zope.app.publication.httpfactory.HTTPPublicationRequestFactory создает объект запроса.

  2. Экземпляр HTTPPublicationRequestFactory запрашивает фабрику публикатора основываясь на HTTP методе, типе контента и переменных среды. Запрос фабрики происходит через реестр zope.app.publication.requestpublicationregistry.RequestPublicationRegistry в котором фабрики публикаторов предварительно зарегистрированы через конфигурацию zope/app/publication/configure.zcml.

  3. В нашем случае возвращается фабрика zope.app.publication.requestpublicationfactories.BrowserFactory которая запрашивает класс запроса как утилиту для интерфейса zope.app.publication.interfaces.IBrowserRequestFactory и возвращает класс запроса и публикатор zope.app.publication.browser.BrowserPublication.

  4. После получения класса запроса и публикатора экземпляр фабрики HTTPPublicationRequestFactory создает запрос, устанавливает для него публикатор и, в случае запроса из браузера, оформление (skin), после чего запрос возвращается.

  5. При получении запроса экземпляр WSGIPublisherApplication начинает публикацию запроса используя функцию publish из модуля zope.publisher.publish.

  6. Функция publish начинает обработку запроса с вызова его метода processInputs. В нашем случае это будет метод класса zope.publisher.browser.BrowserRequest. Метод processInputs обрабатывает все данные запроса и преобразует их к нужному виду и нужным типам.

  7. После обработки входных данных publish вызывает метод публикатора beforeTraversal. В нашем случае этот метод находится в классе zope.app.publication.zopepublication.ZopePublication. beforeTraversal пробует идентифицировать пользователя глобально и устанавливает его для запроса методом запроса setPrincipal, затем начинает новое взаимодействие (interaction) и начинает новую транзакцию.

  8. Метод publish запрашивает приложение (корневой объект сайта) методом публикатора getApplication из класса zope.app.publication.zopepublication.ZopePublication. В нашем случае это объект под корнем ZODB базы с именнем "Application".

  9. Теперь, когда корневой объект сайта получен начинается траверсинг - поиск объекта подлежащего публикации. Траверсинг производится методом traverse запроса BrowserRequest, но основная работа по траверсингу происходит в базовом классе запроса - zope.publisher.base.BaseRequest.

  10. На каждом шаге траверсинга определяется следующий объект в пути на основе текущего (корневого в начале траверсинга) объекта и имени следующего объекта (части пути). Для каждого объекта по одному разу вызывается метод публикатора callTraversalHooks. В нашем случае этот метод находится в классе zope.app.publication.zopepublication.ZopePublication. Метод посылает событие zope.app.publication.interfaces.BeforeTraverseEvent и пробует идентифицировать пользователя локально.

  11. Непосредственно операция определения следующего объекта в пути производится методом публикатора traverseName который находится в классе zope.app.publication.publicationtraverse.PublicationTraverse. Этот метод сначала проверяет не соответствует ли следующее имя в пути формату пространства имен и в этом случае обработчик пространства имен запрашивается функцией namespaceLookup из модуля zope.traversing.namespace. Или, в обычном случае, используется метод publishTraverse интерфейса zope.publisher.interfaces.IPublishTraverse для получения следующего объекта. Если текущий объект не реализует напрямую IPublishTraverse будет запрошен адаптер.

    С этого места нам надо рассмотреть несколько вариантов развития событий. Сначала зайдем на первую страницу административного интерфейса по адресу /@@contents.html:

    1. В функции traverseName мы подпадаем в ветку по запросу пространства имен и через namespaceLookup попадаем в метод traverse класса view (@@ - синоним для ++view++) из модуля zope.traversing.namespace.
    2. В методе traverse запрашивается представление-адаптер зарегистрированное во время старта системы. В данном случае представление для contents.html зарегистрировано в файле zope/app/component/browser/configure.zcml. И таким образом полное представление для первой страницы интерфейса создается шаблоном zope/app/container/browser/contents.pt с помощью класса-представления zope.app.container.browser.contents.Contents.

    Теперь мы добавим наши часы нажав в меню добавления ссылку "Часы". При этом мы попадаем по ссылке /@@contents.html?type_name=BrowserAdd__clock.model.Clock в представление описанное выше (Contents):

    1. В нашем случае у нас нет отдельной формы при добавлении и представление выводит форму ввода имени прямо в списке объектов. Мы вводим имя clock и нажимаем ввод. Непосредственно добавление нового объекта происходит в методе addObject класса zope.app.container.browser.contents.Contents. Метод запрашивает представление добавления с именем + и через него добавляет наш объект.
    2. Методу action класса zope.app.container.browser.adding.Adding (представление добавления) передается имя объекта и имя типа (в нашем случае BrowserAdd__clock.model.Clock). Метод запрашивает фабрику-утилиту для создания объекта через интерфейс zope.component.interfaces.IFactory (см. описание загрузки выше), создает новый объект (мы подпадаем в метод __init__ нашего класса clock.model.Clock, который в нашем случае является методом по умолчанию), посылает событие zope.lifecycleevent.ObjectCreatedEvent, добавляет объект в корневую папку методом add и перенаправляет нас опять на страницу /@@contents.html.

    После того как объект часов добавлен мы можем зайти на страницу /clock:

    1. Через метод traverseName класса PublicationTraverse мы попадаем сразу в метод publishTraverse класса zope.app.pagetemplate.simpleviewclass.simple (см. описание загрузки выше), через него в наш шаблон clock/browser/index.pt и в метод datetime нашего класса clock.browser.views.ClockView.

    Теперь мы рассмотрели как запрашивается объект для публикации и можно рассмотреть оставшуюся часть функции publish из модуля zope.publisher.publish.

  12. После возврата из метода запроса traverse вызывается метод публикатора afterTraversal (класс zope.publisher.publish) который опять пробует идентифицировать пользователя локально.

  13. После этого для получения результата публикации вызывается метод публикатора callObject который в свою очередь вызывает функцию mapply из модуля zope.publisher.publish. Функция mapply определяет количество аргументов которые нужно передать вызываемому объекту и вызывает его с нужными аргументами из запроса возвращая результат вызова. При обращении по адресу /clock возвращается результат визуализации шаблона clock/browser/index.pt.

  14. После того как результат получен вызывается метод setResult ответа (который в свою очередь является атрибутом response запроса) и результат сохраняется в объекте ответа.

  15. В итоге в функции publish вызывается метод afterCall публикатора который закрывает транзакцию и затем метод endRequest который закрывает взаимодействие и посылает событие zope.app.publication.interfaces.EndRequestEvent. После этого запрос закрывается методом close (который выполняет все задержанные вызовы, например, закрытие соединения с базой данных) и запрос возвращается из функции publish.

  16. Ответ как атрибут запроса response возвращается в метод __call__ класса zope.app.wsgi.WSGIPublisherApplication, где результат обработки запроса возвращается сетевому уровню с помощью методов ответа getStatusString, getHeaders и consumeBodyIter.