2008-01-11

Скелет контент класса в Zope.txt

  2008-01-11 19:02

Скелет контент класса в Zope: Для того, чтобы класс мог использоваться в Zope, должно быть сделано множество различных деклараций. Эти декларации настолько гибкие, что даже класс, который никогда не предполагалось размещать в Zope, можно легко затолкнуть туда, создав необходимые декларации. Все классы можно создать в рамках одного продукта, но чтобы лучше проявить сущность Zope3, создадим отдельный продукт для каждого класса, и посмотрим, как можно использовать их вместе и по-отдельности. В cамом деле, Zope3 - это среда, позволяющая писать множество маленьких, независимых продуктов-кубиков, а затем легко объединять их принеобходимости в одно целое. Скелет класса (теория) : Описание класса для Zope включает: 1. не был нулевой длины, например:: ## -*- coding: utf-8 -*- ####### # Make it a Python package Иначе некоторые утилиты по управлению исходным кодом могут быть сбиты с толку; Символическую ссылку на этот файл размещаем в катало ...

Скелет контент класса в Zope:

Для того, чтобы класс мог использоваться в Zope, должно быть сделано множество различных деклараций. Эти декларации настолько гибкие, что даже класс, который никогда не предполагалось размещать в Zope, можно легко затолкнуть туда, создав необходимые декларации.

Чтобы наиболее полно покрыть известные проблемы, мы будем рассматривать три независимых класса - класс-контейнер, вложенный в него терминальный класс и интерфейс-ограничение на вхождение терминального класса. Пусть это будут классы Notebook (блокнот), Note (заметка), а также Notestrict. Все классы можно создать в рамках одного продукта, но чтобы лучше проявить сущность Zope3, создадим отдельный продукт для каждого класса, и посмотрим, как можно использовать их вместе и по-отдельности. В cамом деле, Zope3 - это среда, позволяющая писать множество маленьких, независимых продуктов-кубиков, а затем легко объединять их принеобходимости в одно целое.

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

Скелет класса (теория) :

Описание класса для Zope включает:

  1. Описание строки регистрации, обычно хранится в директории с исходным кодом класса, в поддиректории etc. Для класса notebook называется notebook-configure.zcml и содержит строчку :
                        <include package="notebook" />
    
  2. Служебный файл __init__.py - без него директория не будет воспринята как содержащая продукт. Вообще говоря, его нужно класть везде, где лежат какие-либо коды.

    Желательно, чтобы файл __init__.py не был нулевой длины, например:

                        ## -*- coding: utf-8 -*- #######
                        # Make it a Python package
    

    Иначе некоторые утилиты по управлению исходным кодом могут быть сбиты с толку;

  3. Директория с продуктом должна быть расположена в PYTHONPATH;
  4. Должен быть файл interfaces.py, содержащий декларации интерфейсов (если продукт вводит какие-либо новые интерфейсы);
  5. Должен быть питоновский файл с декларациями классов продукта (напомним, что вообще говоря он генерируется автоматически по интерфейсу). Рекомендуется придерживаться правила: один файл на класс, хотя это и не всегда удается;
  6. Должна быть директория browser, содержащая темплейты и конфигурацию специальных видов, если таковые имеют место;
  7. В директории с продуктом должен лежать конфигурационный файл configure.zcml, регистрирующий определяемые продуктом классы и интерфейсы. Не забудьте указать в нем включение директори browser, а также других директорий продукта, кроме etc, если они есть:
                        <include package=".browser" />                    
    
  8. В директории browser должен лежать конфигурационный файл configure.zcml, содержащий декларацию видов (или, в более общем случае, административного веб-интерфейса класса);

Создание класса Note :

Класс Note - это класс, содержащий атрибуты заметки:

  1. Создаем директорию класса, в которой создаем директории browser, etc и файл __init__.py.
  2. В директории etc создаем файл note-configure.zcml со следующим содержимым :
                        <include package="note" />
    
  3. Символическую ссылку на этот файл размещаем в каталоге etc/package-includes сервера приложений Zope;
  4. Пишем файл interfaces.py.

    Подробно об интерфейсах можно прочитать в статье Использование схем интерфейсов.txt:

                        from zope.interface import Interface
                        from zope.schema import Text, TextLine, Datetime
                        from datetime import datetime
    
                        class INote(Interface):
                            """A note object."""
    
                            title = TextLine(
                                title=u"Title/Subject",
                                description=u"Title and/or subject of the message.",
                                default=u"",
                                required=True)
    
                            body = Text(
                                title=u"Note Body",
                                description=u"This is the actual note. Type whatever you wish.",
                                default=u"",
                                required=False)
    
                            datetime = Datetime(
                                title = u"Activation Time and Date",
                                required=False,
                                default=datetime.today())
    

    Чтобы узнать список возможных полей интерфейсов, можно воспользоваться поиском интерфейсов в экране помощи Zope (http://localhost:8080/++apidoc++/Interface/@@menu.html), введя поиск интерфейса IField и посмотреть список фабрик, создающих объекты реализующие данный интерфейс, или же можно заглянуть в исходные коды zope.schema;

  5. Корректируем PYTHONPATH так, чтобы он включал в себя директорию, в которой расположена директория с продуктом (например, /var/lib/zope/test/lib/python, если вы пользоветель Linux).

    Для этого нужно издать такую команду:

                        export PYTHONPATH=$PYTHONPATH:/var/lib/zope/test/lib/python
    

    На самом деле, если коды располагаются в специально отведенном месте (в папке lib/python инстанции zope), то в явном указании пути нет необходимости, поскольку Zope сам скорректирует путь при старте.

    В настоящее время хорошей практикой считается установка продуктов Zope в то место, которое в данном дистрибутиве было предусмотрено при сборке и настройке python для установки доплонительных модулей.

  6. Отдаем команду, которая автоматически сгенерирует скелет класса (пользователям Windows команду следует видоизменить):
                        /var/lib/zope/test/bin/pyskel note.interfaces.INote  >note.py
    
  7. Возможность генерировать скелеты классов замечательная, но к сожалению, тот код, который генерируется, не совсем соответствует современнности, поэтому на практике данная возможность используется редко.

    Но уж раз воспользовались, то скорректируйте полученный результат следующим образом:

    • заменим ссылку на интерфейс на относительную:
                              from interfaces import INote 
      
                              вместо 
      
                              from note.interfaces import INote
      
    • импортируем persistent.Persistent и zope.app.container.contained.Contained, после чего используем их как супер-класс.

    Полученный код должен выглядеть примено так:

                        from zope.interface import implements
                        from interfaces import INote
                        from persistent import Persistent
                        from zope.app.container.contained import Contained
    
                        class Note(Contained,Persistent):
                            __doc__ = INote.__doc__
    
                            implements(INote)
    
                            # See note.interfaces.INote
                            body = None
    
                            # See note.interfaces.INote
                            datetime = None
    
                            # See note.interfaces.INote
                            title = None
    

  8. Создаем в директории продукта файл configure.zcml.

    В нем указываем директивы interface (чтобы зарегистрировать интерфейс) и class (чтобы объявить существование класса):

                        <configure
                            xmlns="http://namespaces.zope.org/zope">
    
                          <interface 
                              interface=".interfaces.INote" 
                              type="zope.app.content.interfaces.IContentType"
                              /> 
    
                          <class class=".note.Note">
                            <require
                                permission="zope.View"
                                interface=".interfaces.INote"
                                /> 
    
                            <require
                                permission="zope.ManageContent"
                                set_schema=".interfaces.INote"
                                />
                          </class>
    
                          <include package=".browser" />
                        </configure>
    

    Обратите внимание на нотацию записи путей в configure:

    • если путь начинается не с точки, то это путь от директории, в которой лежит продукт (если быть точным - от одной из директорий в PYTHONPATH),
    • если путь начинается с одной точки, то это путь от текущей директории,
    • если путь начинается с двух точек, то это путь от директории уровнем выше,
    • путь представляет собой элементы, разделенные точками, где элемент - имя директории, имя файла (без расширения) или имя переменной (класса) внутри файла.

    В конце файла указана директива включения конфигурации из директории browser:

                          <include package=".browser" />
    

  9. Следующий шаг - создание административного веб-интерфейса.

    Создаем в директории browser файл configure.zcml и размещаем в нем директивы:

    addform
    чтобы создать форму добавления;
    addmenuitem
    чтобы добавить пункт в меню add, соответствующей форме добавления;
    editform
    чтобы создать вид редактирования заметки.

    Вот пример такого файла:

                        <configure
                            xmlns="http://namespaces.zope.org/browser">
    
                          <addform
                              label="Add Note"
                              name="AddNote.html"
                              schema="..interfaces.INote"
                              content_factory="..note.Note"
                              permission="zope.ManageContent"
                              set_before_add="datetime"
                              />
    
                          <addMenuItem
                              class="..note.Note"
                              title="Note"
                              description="Note Text"
                              permission="zope.ManageContent"
                              view="AddNote.html" 
                              />
    
                          <editform
                              schema="..interfaces.INote"
                              for="..interfaces.INote"
                              label="Edit"
                              name="edit.html"
                              permission="zope.ManageContent"
                              menu="zmi_views" title="Edit" 
                              />
    
                        </configure>
    

    В соответствии со спецификацией, addform как и editform позволяют указать меню для включения пункта формы, но даже если вы найдете какое меню указать, чтобы включить в именно в меню "добавление" (хотя это всего лишь zope.app.publisher.interfaces.browser.AddMenu), элемент меню добавления, включенный таким образом не будет правильно работать (по крайней мере в zope 3.3.1), Это вызвано, предположительно, особой организацией меню добавления. Впрочем, желающием могут поэкспериментировать самостоятельно.

  10. Перезапускаем zope;
  11. Как и было обещано, появились все необходимые формы для создания и редактирования класса.

Создание класса Notebook :

Класс Notebook создается при помощи тех же самых шагов. Заметим лишь, что автоматически сгенерированный класс мы сделали подклассом BTreeContainer и выкинули из него все методы, каждый из которых есть в BTreeContainer'e. Фактически, это повторное использование хорошо известного системе класса - BTreeContainer - в некотором новом качестве.

Итак, по очереди, interfaces.py:

            from zope.interface import Interface
            from zope.schema import Text, TextLine, Datetime, Field
            from datetime import datetime

            class INotebook(Interface):
                """A note object."""

                title = TextLine(
                    title=u"Title/Subject",
                    description=u"Title and/or subject of the message.",
                    default=u"",
                    required=True)

                body = Text(
                    title=u"Note Body",
                    description=u"This is the actual note. Type whatever you wish.",
                    default=u"",
                    required=False)

Файл notebook.py:

            from zope.interface import implements
            from interfaces import INotebook
            from zope.app.container.btree import BTreeContainer
            from zope.app.container.interfaces import IContainer

            class Notebook(BTreeContainer):
                __doc__ = INotebook.__doc__

                implements(INotebook,IContainer)

                # See notebook.interfaces.INotebook
                body = None

                # See notebook.interfaces.INotebook
                title = None

Файл configure.zcml:

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

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

              <class class=".notebook.Notebook">
                <factory
                    id="notebook.Notebook"
                    description="Notebook" 
                    />
                <require
                    permission="zope.ManageContent"
                    interface=".interfaces.INotebook"
                    /> 
                <require
                    permission="zope.ManageContent"
                    set_schema=".interfaces.INotebook"
                    />

                <require
                    permission="zope.View"
                    interface="zope.app.container.interfaces.IReadContainer"
                    />

                <require
                    permission="zope.ManageContent"
                    interface="zope.app.container.interfaces.IWriteContainer"
                    />
              </class>

              <include package=".browser" />
            </configure>

Обратите внимание на довольне нетривиальные декларации безопасности - IReadContainer, IWriteContainer - декларации на доступ к ним даются раздельно и с разными правами. Дело в том, что декларация "require ... interface" разрешает получать атрибуты интерфейса, декларация "require ... set_schema" разрешает их устанавливать. Но когда речь идет об интерфейсе, в схеме которого описан метод, то разрешение получения атрибута (метода в данном случае) автоматически означает разрешение вызвать этот метод. Поэтому методы позволяющие просматривать и редактировать контейнер пришлось разделить по разным интерефейсам и раздельно давать права на приведение к этим интерфейсам. Ислользование более простых решений (скажем "require ... set_schema ... IContainer") является распространенной ошибкой.

Файл browser/configure.zcml :

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

              <addform
                  label="Add Notebook"
                  name="AddNotebook.html"
                  schema="..interfaces.INotebook"
                  content_factory="..notebook.Notebook"
                  permission="zope.ManageContent"
                  set_before_add="title"
                  />

              <addMenuItem
                  class="..notebook.Notebook"
                  title="Notebook"
                  description="Notebook with notes"
                  permission="zope.ManageContent"
                  view="AddNotebook.html" 
                  />

              <editform
                  schema="..interfaces.INotebook"
                  for="..interfaces.INotebook"
                  label="Edit"
                  name="edit.html"
                  permission="zope.ManageContent"
                  menu="zmi_views" title="Edit" 
                  />

              <containerViews
                  for="..interfaces.INotebook"
                  index="zope.View"
                  contents="zope.View"
                  add="zope.ManageContent"
                  />
            </configure>

К случаю note/browser/configure добавилась директива containerViews, которая описывает вид и права на доступ к контейнеру. Помня про декларации доступа для интерфейсов класса, пожалуйста, постарайтесь понять: декларации доступа видов гарантируют только то, что злоумышленник не воспользуется этим видом для своих злых дел. Тем не менее, он может получить доступ к уязвимому интерфейсу класса каким-либо еще путем. Поэтому корректные декларации должны быть и там (что бы защитится от злоумылшенника) и здесь (что бы не пудрить мозг честным пользователям).

Строгое связывание Note и Notebook :

Классы Note и Notebook определены так, что класс Note может быть включен как в класс Notebook, так и в любой другой контейнер, да и в класс Notebook можно вставлять что угодно. Для борьбы с этим существуем специальная концепция "ограничений" в интерфейсах и можно было бы с легкостью модифицировать наши интерфейсы, чтобы достичь желанного эффекта. Но другой путь будет более общим и полезным.

Пусть Note & Notebook - это какие-то чужие классы, которые нет возможности исправлять, но хотелось бы использовать чуть иначе.

Создадим продукт notestrict, ограничившись в нем лишь файлами __init__.py, interfaces.py, configure.zcml и etc. В этом продукте определим интерфейсы:

ICommonContainer
некий общий контейнер, в который разрешается входить компонентам с интерфейсом INotebookContent;
INotebookContent
интерфейс, обозначающий содержимое ICommonContainer;
INotebookContainer
интерфейс, разрешающий вхождение только компонент с интерфейсом INotebookContent;

Если договорится, что для любого случая, когда компонент предоставляет интерфейс ICommonContainer, для него определен интерфейс с дополнительным ограничением, такой как INotebookContainer, то интерфейс INotebookContent обретает более новый смысл: это интерфейс, обладатель которого может быть содержимым INoteBook.

Это немного сложно понять и немного долго программировать. Зато потом появляется удивительная легкость настройки вхождения компонент.

Итак, начнем с определения всех этих волшебных интерфейсов:

            from zope.interface import Interface
            from zope.schema import Field
            from zope.app.container.constraints import ContainerTypesConstraint
            from zope.app.container.constraints import ItemTypePrecondition
            from zope.app.container.interfaces import IContained, IContainer

            class ICommonContainer(Interface) :
                """ Interface that specify then it can be container of some
                project component """

            class INotebookContent(Interface) :
                """ Interface that specify permission of object that can be content
                of notebook """
                __parent__ = Field(
                    constraint = ContainerTypesConstraint(ICommonContainer))

            class INotebookContainer(IContainer,ICommonContainer) :
                """ Article Container """

                def __setitem__(name, object) :
                    """ Add IArticle Content """

                __setitem__.precondition = ItemTypePrecondition(INotebookContent)

В configure.zcml указываем, что классы Note & Notebook предоставляют соответствующие интерфейсы:

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

                <interface interface=".interfaces.ICommonContainer" name="ICommonContainer"/>  
                <interface interface=".interfaces.INotebookContent" name="INotebookContent"/>  
                <interface interface=".interfaces.INotebookContainer" name="INotebookContainer"/>  

                <class class="note.note.Note">
                    <implements interface=".interfaces.INotebookContent"/>      
                </class>

                <class class="notebook.notebook.Notebook">
                    <implements interface=".interfaces.INotebookContainer"/>    
                </class>

            </configure>

Перезапустив Zope, убеждаемся, что это работает: можно легко добавлять объекты Note и Notebook, при этом объекты Note можно добавлять только внутри объектов Notebook.

Теперь, немного простоты. Если помните, в статье Использование сложных полей ввода.txt был разработан продукт bookmarknote, Если он у вас еще установлен, то болтается с возможностью добавить его куда попало. Легким движением руки, теперь его можно сделать содержимым исключительно Notebook, достаточно включить в вышеупомянутый configure.zcml следующие строки:

                <class class="bookmarknote.bookmarknote.BookmarkNote">
                        <implements interface=".interfaces.INotebookContent"/>  
                </class>

Все, вот она вожделенная простота.

Заключение :

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

  1. Приведенный здесь способ создания ограничения на вхождение как отдельного продукта notestrict является достаточно общей методикой, о которой специально будет рассказано в статье; Способ задания ограничений на вхождение.txt;
  2. Представьте себе, что у вас есть готовый продукт Note/Notebook, написанный кем-то для работы в какой-то совсем другой среде. Никто не мешает, декларировать в отдельном продукте интерфейсы для этих классов, зарегистрировать классы и интерфейсы и начать использовать внутри Zope. Единственно затруднение - наследование от persistent.Persistent - легко обходится введением дополнительного класса с множественным наследованием.

Таким образом, даже такой простой и маленький пример хорошо демонстрирует все преимущества и гибкость компонентной модели.

Официальный сайт Zope3 Московская группа изучения реактивного движения The Dream Bot Site noooxml