Full-stack Web Developer
0,0
рейтинг
28 мая 2012 в 19:48

Разработка → Django своими руками часть 1: Собираем шаблоны для jinja2 из песочницы

Введение


В этом посте хотелось бы описать создание небольшого фреймворка с системой плагинов как django. Только с использованием внешних компонентов. Jinja2 для шаблонов, bottle для получения переменых среды, вместо ORM будет использоваться pymongo, а сессиями будет заниматься beaker.
В первой части хочу рассказать как удобно подсоединить Jinja2 чтоб шаблоны можно было собирать из разных папок (читай плагинов) и кешировать их.
Также в следующей части хотелось бы рассказать как подключить к шаблонам gettext и автоматизировать их перевод.

Структура фреймворка


Предполагается что наш фреймворк как библиотека может лежать в любом каталоге скорее всего для ubuntu '/usr/local/lib/python2.6/dist-packages/', а проект где ни будь, ну скажем, например, '/home/user/worckspace/'. В проекте имеется index.wsgi для mod_wsgi или для uwsgi index_wsgi.py в котором указан путь к нашей библиотеке, если она вручную куда то копировалась.
Проект имеет примерно такую структуру:
/project/
	/__init__.py
	/app/
		/__init__.py
		/app1/
			/__init__.py
			/templ/
			/static/
			/routes.py
			/models.py
			/views.py
	/teml/
	/static/
	/index.py
	/routes.py
	/views.py
	/models.py
	/settings.py

Соответственно в подкаталогах /templ будут лежать шаблоны, в /static статика, в /app любое количество приложений (или компонентов, кому как больше нравится).
Соответствено предполагается что в нашей библиотеке также есть папка app аналог джанговского contrib в которой тоже будут лежать компоненты со своими шаблонами.
Также в проекте по молчанию еще будет создаваться папка например caсhe в нее jinja2 будет сохранять кеш шаблонов.

Подключение шаблонов


Итак все наше подключение будет лежать в файлике core.py в пакете core который в свою очередь лежит в корне библиотеки. Импортируем необходимые классы.
from jinja2 import Environment, PackageLoader, FunctionLoader, FileSystemBytecodeCache

Далее определяем путь для шаблонов, эту функцию мы будем вызывать при загрузке шаблонов.
Шаблоны на выбор могут иметь несколько расширений.

def get_app_root(app):
	"""Returns path to app or app name."""
	if isinstance(app, (str, )):
		__import__(app)
		app = sys.modules[app]
	return os.path.dirname(os.path.abspath(app.__file__))

templ_exts = ['', '.tpl', '.html', '.htm'] 
def split_templ_name (t):
	""" определение пути для загрузки шаблонов. Двоеточие выступает символом разделителя для указания в каком модуле будет лежать шаблон. Чтоб не было путаницы с одинаковыми именами. """
	if ':' in t:
		# тут составляем путь до шаблона конкретного модуля
		module, file = t.split(":", 1)
		module_path = os.path.join(get_app_root( module), "templ", "")
	else:
		# путь к шаблонам которые лежат в папке проекта.
		module = ' '
		module_path = os.sep + 'templ' + os.sep
		file = t
	return (module, module_path, file)

Собственно сама загрузка шаблонов.
Здесь можно реализовать альтернативные места хранения шаблонов, например реализовав загрузку из базы
def union_templ(t, **p):

	(module, module_path, file) = split_templ_name (t)
	def load_template (base, module_path, file):

		path = base + module_path + file
		template = ' '
		for ext in templ_exts:
			filename = path+ext
			if os.path.exists(filename):
				with open(filename, "rb") as f:
					template = f.read()
				break;
		return template
	template = load_template (os.getcwd(), module_path, file);
	if not template:
		template = load_template( settings.lib_path, module_path, file);
	if not template:
		return 'Template not found %s' % t
	return template.decode('UTF-8')


Автоматическое создание папки для кеширования шаблонов.
jcp = os.path.join(os.getcwd(), 'jinja_cache')
if not os.path.isdir(jcp): os.mkdir(jcp)


Создание объекта, управляющего кешем.
bcc = FileSystemBytecodeCache(jcp, '%s.cache')


Вешаем хук на jinja2, где указываем на необходимость кеширования и передаем функцию, которую нужно вызывать для загрузки шаблонов.

jinja = Environment(auto_reload=True, loader=FunctionLoader(union_templ), bytecode_cache  = bcc ) 


Функция которая непосредственно выполняет отрисовку шаблона. Сейчас она ничего особенного не делает, напрямую передавая управление jinja2, но мы сюда еще вернемся.
def render_templ(t, **p):
	template = jinja.get_template(t)
	return template.render(**p)


Импорт этой функции на глобальный уровень.
__builtin__.templ = render_templ


Теперь благодаря этой последней строчке будет достаточно в любом файле вызвать функцию templ() и передать ей в аргументах название шаблона и что в нем вывести. Например:
return templ('app.base:base', param = param) или return templ('base', param = param), ':' значит что шаблон лежит не в проекте, а в соответствующем компоненте в данном случае 'app.base'.

Резюме


Итак для начала у нас есть небольшой фреймворк с модульной структурой. На базе которого мы будем двигаться дальше.
Это пока первая статья и многое, наверно, можно было бы написать лучше, поэтому буду рад всем замечаниям и вопросам.

Продолжение
Александр @Alex10
карма
73,0
рейтинг 0,0
Full-stack Web Developer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (28)

  • 0
    Ну что же — пока всё хорошо, но маловато. Пишите еще :)
  • +6
    Больше велосипедов, и чем квадратнее колёса, тем лучше!
    • +2
      Пожалуй, слишком саркастично получилось. Лучше спрошу безо всякого сарказма: а почему не Flask, например?
      • 0
        Если продолжать логическую цепочку, то видимо можно дойти до вопросов, почему не PHP, почему не Perl, почему не… впрочем, не будем пока об извращениях. Очень много хороших вещей не появилось бы на свет, если бы их авторы изначально были демотивированы мыслью о велосипедах (причём несовершенных в первых реализациях — первый блин, как известно).
        Разумеется, есть именитые профессионалы, которые с первой попытки создают эпичные вещи, о которых позже слагают легенды, но есть и новички, в которых изначально мало кто верит, но потом вырастают вместе со своими проектами.
        Мне кажется, было бы конструктивней, если бы Вы немного расшифровали свою оценку «квадратности колёс» — кто знает, возможно, дадите новому проекту более подходящее направление и поможете избежать неочевидных автору граблей.
        • +2
          По-моему, ошибка (очевидная) тут прямо в заголовке. Ну или в постановке задачи, если хотите. Как так — Django своими руками, для чего? Flask (джангоподобный фреймворк, который не накладывает ограничений на применяемые батарейки) существует давно и успешно.

          Ни одного камня в его сторону не полетело, а ведь в рамках статьи явственно видна попытка сделать то же самое. Вот и получается, что это всего лишь попытка исправить пресловутый фатальный недостаток.

  • 0
    В веб-разработке на Python есть два основных лагеря. В первом любят монолитный код и Django, а вторые стараются переехать с Pylons и Repoze/BFG на Pyramid. Есть ещё разные кучки анархистов с микрофреймворками, но бурного развития там никогда не было.

    Так вот. Фреймворк из статьи целиком и полностью покрывается функциональностью Pyramid, в котором модульность компонентов носит характер сектантского культа. Благодаря этому, даже в рамках одного проекта, разработчику дозволяется пользоваться сразу несколькими шаблонными движками. И Jinja2 в этом списке, естественно, присутствует.

    Возможно в следующих статьях вы разовьёте идею своего фреймворка в какую-то уникальную и практичную идею, но пока это просто разминочные упражнения для отдельно взятого программиста.
    • 0
      Pyramid очень крутая штука, но концептуально очень сложная. Но очень крутая.

      Я по ней даже планировал написать пару статей, но в итоге отказался, поскольку слишком много возможностей получается, по сути не статьи получаются, а реклама конкретного подхода к реализации проекта на pyramid.
      • 0
        В нём не так много изменений по сравнению с Pylons. Там уже были заложены принципы построения «фреймворка в виде клея» между сторонними пакетами. Это самая сложная концептуальная часть. Если до этого вы работали с Pylons, то разобраться во внутренностях Pyramid — дело одной недели. Рефакторинг-миграция с Pylons 1.0 на Pyramid 1.3 у меня заняла примерно такое количество времени.

        Основательно, в Pyramid решили всего три главные проблемы:
        1. Избавились от paste.registry.StackedObjectProxy в пользовательском коде, который эмулировал потокобезопасные локальные глобальные переменные. Пользовательские контроллеры теперь стали больше походить на джанговские, когда весь контекст запроса хранится в виде аттрибутов объекта request. Это существенно упростило процесс написания функциональных тестов.

        2. Избавились от необходимости наследовать BaseController для всех пользовательских контроллеров, что позволило проще комбинировать модули внутри отдельно взятого проекта.

        3. Полностью перешли на pkg_resources и систему сигналов для внедрения сторонних плагинов. Раньше (в Pylons), для подключения плагинов использовались WSGI Middleware, которые просто добавляли новые слои логики поверх вашего веб-приложения. Это было не очень гибко и подходило не для всех плагинов. Теперь подобных проблем практически нет.

        Этого оказалось достаточно, чтобы решить почти все трудности, возникавшие при разработке на Pylons.

        Все остальные части фреймворка — это кодовая база Repoze/BFG и их небольшое наследие от Zope (ZCML и Traverse), которые совершенно не обязательно использовать в своих проектах.
        • 0
          От pylons там ОЧЕНЬ много отличий, вот от repoze.bfg мало. По сути разработка на pylons сводилась к использованию «глобальных» переменных. А тут резко всё обрубили, причём настолько, что прямая миграция сложного проекта становится очень сложной — мне не очень нравится официально предложенные подход миграции с pylons в виде реализации слоя эмуляции пилоновских граблей. Опять же совершенно адовая аутентификация с авторизацией. Pylons был существенно проще. Но архитектурно pyramid гораздо стройнее, хотя и сложнее.

          • 0
            Миграция довольно простая.
            1. Сначала, оставаясь на Pylons, проведите рефакторинг своего кода, где глобальные переменные config, url, session, response, tmpl_context биндятся к объекту request в самом начале запроса (это можно сделать в BaseController) и используются в коде через request.config, request.url и т.д. Другими словами — локализуйте контекст в одном объекте.
            2. Переопределите семантику вызова методов из pylons.controllers.core.WSGIControllerBaseController таким образом, чтобы каждый пользовательский контроллер получал объект request в виде обязательного первого аргумента. Просто унаследуйте этот класс и переопределите метод _perform_call:
                def _perform_call(self, func, args):
                    """Hide the traceback for everything above this method"""
                    # Unused variable
                    #__traceback_hide__ = 'before_and_this'
                    # Inject request object here in order to emulate Pyramid view
                    # callable in controllers
                    req = self._py_object.request
                    return func(req, **args)
            

            Теперь вы практически полностью эмулируете API простого view callable из Pyramid.
            3. Замените все вызовы request.url(...) на request.route_path(...), а request.url(..., qualified=True) на request.route_url(...).
            4. Замените request.config на request.registry.
            5. После этого, создайте репозиторий с pyramid_sqla приложением, и влейте в него весь код от проекта на pylons.
            6. Настройте простой контроллер в Pyramid, убедитесь что он работает, после чего начинайте итеративно рефакторить каждый контроллер из pylons. Рефакторинг заключается в переносе кода контроллера и кода из Routes в систему роутинга Pyramid.

            Таким способом, я итеративно, без особых проблем перевёл проект с 20К+ строк кода на Pylons в Pyramid за одну неделю (календарную).
            • 0
              *pylons.controllers.core.WSGIController
    • НЛО прилетело и опубликовало эту надпись здесь
  • +4
    Так не надо:

    '/app/' + module +'/templ/'
    


    Лучше так:

    module.join(("/app/", "/templ/"))
    


    А лучше всего так:

    import os
    os.path.join("app", module, "templ")
    
    • –1
      Второй код, что я привёл, будет работать некорректно, когда будет больше вложенных директорий (>2).
      • –1
        ...
        i = t.index(':')
        module = t[:i]
        module_path = '/app/' + module +'/templ/'
        file = t[(i+1):]
        ...
        


        Можно проще:

        module, file = t.split(":")
        
        • 0
          Если проще, то так

          module, file = t.split(":", 1)
          
        • 0
          Да, благодарю, исправил, имела место быть недоработка. :)
      • 0
        я бы спрашивал путь у самого модуля, предварительно его заимпортировав, module.__file__, так будет меньше зависимость от структуры проекта (может захочется устанавливать модули как обычные питоновские пакеты в будущем).
        • 0
          Примерно так:

          import os, sys
          
          def get_app_root(app):
              """Returns path to app or app name."""
              if isinstance(app, (str, )):
                  __import__(app)
                  app = sys.modules[app]
              return os.path.dirname(os.path.abspath(app.__file__))
          
          tpl_path = os.path.join(get_app_root(module), 'templ')
          
          • 0
            Добавил, согласен это более универсально. Только придется писать имя модуля полностью. Но это, думаю, и к лучшему.
  • +1
    Куча вариаций расширений под которыми читаются файлы с шаблонами — это совсем не python way.
    Чтобы не изобретать велосипед по поиску шаблонов в каталогах есть класс FileSystemLoader в Жинже.
    И вообще, код для публикации в статьях наверное стоит оформлять по стандартам pep8, а то ведь его накопипастят себе разные люди и будут думать что он со всех сторон верный.
  • +8
    Каждый раз, когда вы добавляете что-то в __builtin__, рождается ещё один PHPшник.
    • 0
      Секрет популярности PHP раскрыт? :))
      Предлагаете импортировать везде функцию, которая гарантированно нужна во всем проекте?
      • +1
        explicit is better than implicit
  • 0
    Если функция load_template ничего не найдет, то она зачем-то вернет строку из одного пробела. Которая приведется к True, а не к False, как, видимо, ожидал автор.

    Рекомендуется к прочтению.
  • –1
    Что-то не с того конца начат веб фреймворк. Я бы начал все таки с приложения wsgi и маршрутизации запросов. Почему — в общем случае, особенно в сегодняшних реалиях, вы можете и не рендерить никакого HTML вообще, отдавая только JSON. А вот не работать с HTTP веб фреймворку будет сложновато.
    • –1
      Я думал об этом. Но просто описать настройку wsgi файлика не интересно таких материалов хватает. Интересно было бы описать вывод сообщений об ошибках, наподобие того как это делается в Werkzeug или Django, но было решено в последующих материалах на этом остановится.
      • 0
        Я имел в виду wsgi приложение, а не wsgi файлик.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.