Доброе время суток. Не так давно мы начали писать большой проект на Pylons и одно из главных требований было быстрое присоединение и удаление контролеров без изменений в routing.py. Один из наших работников уже сталкивался с подобным и сделал данную функциональность через плагины. Но, как мне показалось, решение было достаточно громоздким и его тяжело было переносить в будущем из проекта в проект.
Т.к. я в прошлом имел дело с Catalyst (Perl MVC framework), да и нравилось мне, что к каждому методу можно было руками дописать URL. Собственно решил написать нечто похожее.
Требование. «Нужен минимальный функционал, с минимум телодвижений. Вызов декоратора с URL(или пустой строкой). Так же, чтобы не терялись уже готовые плюшки с параметрами в URL».
Решение пришло через декораторы. И оказалось, что на самом деле не так много писать пришлось и решение очень даже переносимое.
Собственно, начну с того, что pylons надо пропатчить. Вреда этот патч вашим текущим проектам не принесет, так что патчить можно смело.
патч исправляет баг Pylons, который не позволяет вам иметь подструктуру в project_name/controllers/*. Почему-то сделали так, что если пишешь путь до метода объекта с точкой, то эта точка исчезает. Собственно с этим патчем вы сможете делать любую структуру в вашем pylons проекте.
Далее, необходимо добавить файл библиотеки в проект (или если вы решили делать по нашему принципу все проекты, то можете вынести в отдельную либу).
# PROJECT/lib/controllers.py
Данная функция позволяет получить рекурсивный список всех *.py файлов в нужной директории. Т.е. если вы хотите, можете не хранить контроллеры только в папке controllers. Это конечно будет плохая затея, но случаи бывают разные.
Далее модифицируем файл config/routing.py и функцию make_map()
Этой вырезкой кода можно заменить всю make_map() функцию. Теперь функция берет список файлов *.py рекурсивно из папки с контроллерами. Делает проверки на существование свойств у каждого метода в каждом классе. Если необходимые свойства есть, тогда считается, что метод должен присутствовать в MAP контроллеров. Далее найденные методы передаются в MAP. Финально мы соединяем error контроллер. Да, кстати меня немного нервировала фича, с прописыванием 2-х мапов для каждого урла: со слэшем и без. Теперь она поправлена
Так, нам теперь осталось сделать декоратор и применить его.
И так, сам декоратор lib/decorators.py:
Т.е. если вы вызовете декоратор без параметра, то будет применен дефолтный путь. Если с параметром — тогда кастомизированный.
И собственно применение controllers/sample.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
Принцип действия всей этой схемы крайне прост. Декоратор создает дополнительные два свойства, в одном из который содержится ваш пусть к методу. Второй указывает на то, как именно обрабатывать этот метод. Если аргументов предано не было, то декоратор делает запись только о том, что метод локальный, т.е. путь надо генерировать. Если же таких свойств нет(нет декоратора на методе), тогда метод активным не считается и обращений к нему не будет.
Собственно, запуская проект, вы увидите в консоли список того, что у вас подключено и путь до этого метода.
Пройдя по ссылке, предварительно сделав все правильно, вы должны увидеть ожидаемый результат.
Вроде бы все, что можно было написать. Советы, пожелания?