Pull to refresh

Pylons. Альтернатива routing.py

Reading time 5 min
Views 1.8K
Доброе время суток. Не так давно мы начали писать большой проект на Pylons и одно из главных требований было быстрое присоединение и удаление контролеров без изменений в routing.py. Один из наших работников уже сталкивался с подобным и сделал данную функциональность через плагины. Но, как мне показалось, решение было достаточно громоздким и его тяжело было переносить в будущем из проекта в проект.

Т.к. я в прошлом имел дело с Catalyst (Perl MVC framework), да и нравилось мне, что к каждому методу можно было руками дописать URL. Собственно решил написать нечто похожее.


Требование. «Нужен минимальный функционал, с минимум телодвижений. Вызов декоратора с URL(или пустой строкой). Так же, чтобы не терялись уже готовые плюшки с параметрами в URL».

Решение пришло через декораторы. И оказалось, что на самом деле не так много писать пришлось и решение очень даже переносимое.

Собственно, начну с того, что pylons надо пропатчить. Вреда этот патч вашим текущим проектам не принесет, так что патчить можно смело.
--- pylons/util.py	2009-12-29 14:28:20.000000000 +0600
+++ dev/null	2010-02-05 03:44:26.000000000 +0600
@@ -122,6 +122,8 @@
         'Oneword'
 
     """
+    module_name = module_name.split('.')[-1]
     words = module_name.replace('-', '_').split('_')
     return ''.join([w.title() for w in words])


патч исправляет баг Pylons, который не позволяет вам иметь подструктуру в project_name/controllers/*. Почему-то сделали так, что если пишешь путь до метода объекта с точкой, то эта точка исчезает. Собственно с этим патчем вы сможете делать любую структуру в вашем pylons проекте.

Далее, необходимо добавить файл библиотеки в проект (или если вы решили делать по нашему принципу все проекты, то можете вынести в отдельную либу).

# PROJECT/lib/controllers.py
# -*- coding: utf-8 -*- 
                                                                                                                                                             
import os                                                                                                                                                                               
                                                                                                                                                                                        
def scan_folder_recurse(folder, excl_names=['__']):
    """ Recurse search for PY files in given folder """                                                                                                                                   
    all_files = []                                                                                                                                                                      
    for root, dir, files in os.walk(folder):                                                                                                                                            
        filelist = \                                                                                                                                                                    
            [os.path.join(root, fi) for fi in files if fi.endswith('.py')                                                                                                               
              and not any(fi.startswith(prefix) for prefix in excl_names)]                                                                                                              
        for f in filelist:                                                                                                                                                              
            all_files.append(f)                                                                                                                                                         
    return all_files                                                                                                                                                                                                                                                                                                                                                      

Данная функция позволяет получить рекурсивный список всех *.py файлов в нужной директории. Т.е. если вы хотите, можете не хранить контроллеры только в папке controllers. Это конечно будет плохая затея, но случаи бывают разные.

Далее модифицируем файл config/routing.py и функцию make_map()

    map = Mapper(directory=config['pylons.paths']['controllers'],
                 always_scan=config['debug'])
    map.minimization = False

    proj_root = os.path.dirname(config['pylons.paths']['root'])
    sep = os.path.sep

    """ GETTING ALL FILES ENTIRE CONTROLLERS FOLDER """
    all_files = scan_folder_recurse(
            config['pylons.paths']['controllers'],
            excl_names=['__', 'daemon', 'models'])

    log.debug("Found %d controllers" % len(all_files))
    log.debug("Building route map")

    cfg = ConfigParser.RawConfigParser()
    cfg.read(config['global_conf']['__file__'])

    for file in all_files:
	t_controller_name = module_path.split('.')[-1]
        controller_name = '/'.join(module_path.split('.')[2:])

        """ IMPORTING MODULE """
	controller = __import__(module_path)
        """ IMPORTING MODULE ENVIRONMENT """
	controller = sys.modules[module_path]

	my_list = dir(controller)
	name_re = re.compile(t_controller_name, re.IGNORECASE)
        
        """ We need classes with methods """
	control = None
	for element in my_list:

	    if name_re.search(element) and controller_re.search(element):
	        control = getattr(controller, element)
        """ If class found """
	if control:
          """ Searching for need property """
	  for item in control.__dict__:
	      try:
	          attrib = control.__dict__[item].__dict__['method']

                  """ If Class has method property """
	          if attrib=='path':
	              route_path = getattr(control,item).route_path

	          else:
	              route_path = "/%s/%s" % (controller_name,item)

	          route_path = route_path.rstrip('/')

                  """ Two method to create two variations of path """
	          map.connect(
	                  route_path,
	                  controller=controller_name,
	                  action=item)
	          map.connect(
	                  "%s/" % route_path,
	                  controller=controller_name,
	                  action=item)

	          log.info("%s::%s ---->>>> %s ..... connected" % \
	              (controller_name,item,route_path))
	      except:
	          pass


    log.info('Route map complite...')
    # The ErrorController route (handles 404/500 error pages); it should
    # likely stay at the top, ensuring it can always be resolved
    map.connect('/error/{action}', controller='error')
    map.connect('/error/{action}/{id}', controller='error')

    return map



Этой вырезкой кода можно заменить всю make_map() функцию. Теперь функция берет список файлов *.py рекурсивно из папки с контроллерами. Делает проверки на существование свойств у каждого метода в каждом классе. Если необходимые свойства есть, тогда считается, что метод должен присутствовать в MAP контроллеров. Далее найденные методы передаются в MAP. Финально мы соединяем error контроллер. Да, кстати меня немного нервировала фича, с прописыванием 2-х мапов для каждого урла: со слэшем и без. Теперь она поправлена

Так, нам теперь осталось сделать декоратор и применить его.

И так, сам декоратор lib/decorators.py:

# -*- coding: utf-8 -*-
"""Decorators for project
"""

def route_action(path=None):
    def decorate(f):
        if path==None:
            setattr(f,'method','local')
        else:
            setattr(f, 'method', 'path')
            setattr(f, 'route_path', path)
        return f
    return decorate



Т.е. если вы вызовете декоратор без параметра, то будет применен дефолтный путь. Если с параметром — тогда кастомизированный.

И собственно применение controllers/sample.py

from PROJECT.lib.decorators import route_action

class SampleController(BaseController):
    @route_action('/sample/hello_world')                                                                                                                                                  
    def hello(self):
        return "This is Sample/Hello method"

    @route_action()                                                                                                                                                  
    def hello_to_me(self):
        return "This is Sample/Hello_to_me local method"

    @route_action('/sample/hello_world/{id}')                                                                                                                                                  
    def hello(self, id):
        return "This is Sample/Hello World with ID: %d method" % id


Принцип действия всей этой схемы крайне прост. Декоратор создает дополнительные два свойства, в одном из который содержится ваш пусть к методу. Второй указывает на то, как именно обрабатывать этот метод. Если аргументов предано не было, то декоратор делает запись только о том, что метод локальный, т.е. путь надо генерировать. Если же таких свойств нет(нет декоратора на методе), тогда метод активным не считается и обращений к нему не будет.

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

Вроде бы все, что можно было написать. Советы, пожелания?
Tags:
Hubs:
+16
Comments 21
Comments Comments 21

Articles