Рубрики
Без рубрики

Создание конвейера активов в Python с помощью Paver

Из этого урока вы узнаете, как создать конвейер активов на Python с помощью Paver!

Автор оригинала: Kyle J. Roux.

Что такое асфальтоукладчик?

В последнее время я искал хорошего бегуна задач, что-то похожее на grunt или gulp, но такое, которое я мог бы использовать с Python для написания задач. Вот тут-то и появляется Асфальтоукладчик .

Paver-это библиотека Python, которая автоматизирует задачи. Он имеет чистый API для создания задач в виде функций Python, команд оболочки или любой их комбинации. Задачи также могут зависеть от других задач─они могут передавать данные между задачами, и у них могут быть значения конфигурации, которые изменяются по мере прохождения данных через каждую задачу или сбрасываются при начале каждой новой задачи. Очевидно, что это очень гибкая система, поэтому я чувствую, что она удовлетворит мои потребности.

Что мы собираемся автоматизировать?

В последнее время я много занимаюсь программированием на JavaScript, и я действительно хочу перейти к простому кодированию в CoffeeScript, так как он гораздо более выразителен и его легче читать. Я также хочу меньше использовать для предварительной обработки CSS по тем же причинам. Итак, прямо здесь у нас есть 2 вещи, которые было бы здорово автоматизировать, мы можем добавить минимизацию CSS и JavaScript и конкатенацию, чтобы все закруглить.

Как Рассказать Асфальтоукладчику О Своих Задачах

Первое, что нужно Power для начала работы, – это файл в корне вашего проекта: pavement.py . Именно здесь вы определите все свои задачи по асфальтированию. Затем мы импортируем простой модуль из Paver, который содержит все важные элементы, которые нам понадобятся из API. После этого мы начинаем писать функции.

Чтобы сообщить Paver, что функция является задачей, мы просто добавляем @easy.task декоратор в определение функции. Это позволяет нам писать вспомогательные функции, которые не будут доступны инструменту командной строки paver при выполнении задач.

Активы и трубопроводы

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

from paver import easy

def _get_files(root,ext=None,recurse=False):
    if not recurse:
        file_list = easy.path(root).files() if easy.path(root).exists() else None
    else:
       file_list =  list(easy.path(root).walkfiles()) if easy.path(root).exists() else None
    if file_list is not None:
        return file_list if ext is None else [f for f in file_list if file_list and any(map(lambda x: f.endswith(x),ext))]
    else:
        return []

Поскольку я не намерен, чтобы эта функция использовалась программистами, которые могут использовать эти задачи асфальтоукладчика, она просто здесь, чтобы помочь из-за кулис. Я использую стандартное соглашение Python о том, чтобы пометить его “частным”, добавив к его имени префикс подчеркивания. Таким образом, если кто-то придет и прочитает/использует код, он будет знать о том, что ему не следует полагаться на то, как работает эта функция , потому что я не собираюсь включать ее в общедоступный API (это означает, что если вы сказали из MyModule import* , функция _get_files будет недоступна для использования), и я могу изменить ее использование в любое время без предварительного уведомления.

Теперь давайте посмотрим, как работает эта функция, потому что мы уже используем Pavers stdlib, и мы еще даже не определили задачу. Поэтому после того, как мы импортируем easy из библиотеки Paver, мы запускаем наше определение частной функции и говорим, что для этого потребуется до трех аргументов и минимум один. Они являются:

  • root – корневая папка для начала захвата файлов
  • ext – необязательное расширение файла для фильтрации по
  • рекурсия – если true рекурсия в подкаталоги корня, по умолчанию False

Затем, внутри функции, мы сначала проверяем, следует ли нам рекурсировать или нет, и используем эту информацию для составления списка объектов пути. Если какое-либо расширение файла подходит, мы возвращаем список, но, поскольку это, вероятно, более распространено, мы фильтруем список по заданному текстовому аргументу и возвращаем этот список.

Типы сборки

И последнее, что мы хотим сделать, – это провести различие между типами сборки. Для этого можно использовать отдельные задачи разработки и производственной сборки. Таким образом, когда вы строите в производстве, вы получаете всю необходимую минимизацию и робастизацию, но в процессе разработки здание будет выписывать только заполненный CoffeeScript. Таким образом, наш последний помощник будет простым способом для наших задач определить, каков текущий тип сборки:

def _get_buildtype():
    return options.build.buildtype if getattr(options.build,'buildtype') else os.environ.get('BUILD_TYPE')                                           

Для нашей первой задачи мы соберем наши исходные файлы (в настоящее время я пишу CoffeeScript, так что это то, что мы соберем. Однако мы также создадим задачи для JavaScript и CSS). Мы также создадим переменную вне нашей задачи, которая будет хранить наши данные во время их прохождения по нашему конвейеру. Это будет экземпляр paver.options.Bunch , который является просто словарем, который позволяет нам использовать точечную нотацию для доступа к его содержимому. Таким образом, вместо того , чтобы говорить myDict['key'] , вы можете просто сказать myDict.key , который легче читать и писать.

Мы добавим наш объект Bunch в другой объект Bunch object options , который мы импортируем из paver.легко . параметры – это то, как мы должны получать доступ к параметрам командной строки или параметрам по умолчанию в наших задачах. Ниже мы также настроим наш объект options (в основном конфигурацию конвейеров активов), который мы можем легко изменить или расширить, но я покажу вам больше об этом позже.

Кроме того, обратите внимание, что мы хотим, чтобы это было доступно другим, но мы не хотим, чтобы кто-либо, кто его использует, был вынужден устанавливать все зависимости, необходимые для всех наших задач. Что, если некоторые из них им не нужны? Поэтому вместо того, чтобы просто импортировать непосредственно из модуля CoffeeScript, я помещаю импорт в блок try/except . Таким образом, если CoffeeScript не установлен в системе, мы можем просто выдать предупреждение, как только они попытаются запустить задачу, которая в этом нуждается, вместо того, чтобы создавать исключение, которое убьет всю программу.

Начальная настройка

from paver import easy,options
from paver.easy import path,sh
from paver.options import Bunch
try:
    from jsmin import jsmin
except ImportError:
    jsmin = None
try:
    from coffeescript import compile as compile_coffee
except ImportError:
    compile_coffee = None
try:
    from uglipyjs import compile as uglify
except ImportError:
    uglify = None

options.assets = Bunch(
    css='',
    js='',
    folders=Bunch(
        js='src',
        css='static/css',
    ),
    js_files=[],
    js_ext='.coffee',
    css_ext='.css',
    outfiles=Bunch(
        prod=Bunch(
            js='vendor/app.min.js',
            css=''
        ),
        dev=Bunch(
            js='vendor/app.js',
            css=''
        )            
    ),
)

options.build = Bunch(
    buildtype=None,    
)

@easy.task
def get_js():
    '''
        gather all source js files, or some other precompiled source files
                     to gather files other than javascript, set:                     
                        options.assets.js_ext
                        
                     to the extension to collect ie for coffeescript:                     
                        options.assets.js_ext = '.coffee'
                        
    '''
    ext = options.assets.js_ext or '.js'
    files = _get_files(options.assets.folders.js,ext,True)    
    options.assets.js_files = map(lambda x: (str(x),x.text()),files)    


@easy.task
def get_css():
    '''
        gather all source css files, or some other precompiled source files
                     to gather files other than css files, set:                     
                        options.assets.css_ext
                        
                     to the extension to collect ie for less:                     
                        options.assets.css_ext = '.less'\n                                            
    '''
    ext = options.assets.css_ext or '.css'
    files = _get_files(options.assets.folders.css,ext,True)
    options.assets.css_files = map(lambda x: (str(x),x.text()),files)        

Если вы заметили, все , что мы действительно делаем здесь, – это вызываем нашу вспомогательную функцию _get_files и используем либо значение options.assets.js_ext , либо options.assets.css_ext для определения типов файлов, собираемых в задаче. Это также заботится о сборе различных типов исходных файлов, таких как CoffeeScript, потому что, чтобы сказать ему собирать CoffeeScript, а не JavaScript, вам нужно обновить options.assets.js_ext до”. coffee ” и вуаля! Он собирает кофейный сценарий.

Компиляция CoffeeScript

Теперь давайте перейдем к компиляции CoffeeScript, затем к минификации и уродству.

@easy.task
@easy.needs('get_js')
def coffee():
    '''
        compile coffeescript files into javascript
    '''
    if compile_coffee is None:
        easy.info('coffee-script not installed! cannot compile coffescript')
        return None
    options.assets.js_files = map(lambda x: ((x[0],compile_coffee(x[1],True))),options.assets.js_files)    

Здесь мы сначала используем paver.easy.needs декоратор, чтобы сообщить Paver, что ему нужно будет выполнить свою задачу gather_js , прежде чем он сможет выполнить эту задачу (если мы ничего не собрали, нам нечего будет компилировать). Затем мы выдаем предупреждение и возвращаемся, если импорт для CoffeeScript ранее не удался. Затем мы сопоставляем анонимную функцию с нашими файлами, которая компилирует их с помощью функции coffeescript.compiler , которую мы импортировали как compile_coffee. Таким образом, мы не будем перезаписывать встроенную функцию compile .

Минификация, Уродство

Теперь для задач минимизации и уродства они почти точно такие же, как и в предыдущем задании.

@easy.task
def minify():
    '''
        minify javascript source with the jsmin module
    '''
    if jsmin is None:
        easy.info('Jsmin not installed! cannot minify code')
        return None
    options.assets.js_files = map(lambda x: ((x[0],jsmin(x[1]))),options.assets.js_files)

@easy.task
def uglifyjs():
    '''
        uglify javascript source code (obstification) using the ugliPyJs module
    '''
    if uglify is None:
        easy.info('ugliPyJs not installed! cannot uglify code')
        return None
    for fle,data in options.assets.js_files:        
        options.assets.js_files[options.assets.js_files.index((fle,data))] = (fle,uglify(data))        

Сцепление

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

@easy.task
def concat():
    '''
        concatenate all javascript and css files currently in memory
    '''
    options.assets.js = ''.join(map(lambda x: str(x[1]),options.assets.js_files))
    options.assets.css = ''.join(map(lambda x: str(x[1]),options.assets.css_files))

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

       
@easy.task
def write_js(buildtype=None):
    '''
        write out all gathered javascript to the file specified in options.assets.outfiles[BUILD_TYPE].js
    '''
    if not easy.path('vendor').exists():
        easy.info('making vendor dir')
        os.mkdir('vendor')
    buildtype = buildtype or _get_buildtype()    
    with open(options.assets.outfiles[buildtype].js,'w') as f:
        f.write(options.assets.js)
        easy.info('Wrote file: {}'.format(options.assets.outfiles[buildtype].js))

    
@easy.task
def build_production():
    '''
        Full Build: gather js or coffeescript, compile coffeescript, uglify, minify, concat, write out
    '''
    get_js()
    coffee()
    uglifyjs()
    minify()
    concat()
    write_js()

@easy.task
def build_dev():
    '''
        Partial Build: gather js or coffeescript, compile coffeescript, concat, write out
    '''
    get_js()
    coffee()
    concat()
    write_js()
    
@easy.task
@easy.cmdopts([
 ('buildtype=','t','build type')
])
def build(options):
    '''
        Run Build, defaults to 'dev'
    '''
    if(not hasattr(options,'build') or (options.build.get('buildtype',None) is None)):
        buildtype = 'dev'        
    else:
        buildtype = options.build.buildtype
    os.environ['BUILD_TYPE'] = buildtype
    dict(
        dev=build_dev,
        prod=build_production,
    )[buildtype]()

Здесь нужно обратить внимание на то, как мы вызываем правильную функцию задачи сборки, потому что мы знаем, что нам нужен элемент из нескольких вариантов (например, build_dev и build_production), и мы просто хотим вызвать результат. Простой способ сделать это в Python-создать файл dict , содержащий все выбранные вами имена/связанные функции.:

    dict(
            dev=build_dev,
            prod=build_production,
    )

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

dict(
        dev=build_dev,
        prod=build_production,
    )[buildtype]() #<---- this is where we access the task function we need and call it

Теперь все, что нам нужно, – это задача, которая создаст файлы вашего проекта и выполнит результат Node.js так что мы знаем, что это работает:

    
@easy.task
def run():
    '''
        Run production build and pipe generated js into nodejs for execution
    '''
    options.build = Bunch()
    buildtype = options.build.buildtype = 'prod'
    build(options)
    sh("nodejs {}".format(options.assets.outfiles[buildtype].js))

Чтобы проверить это, просто запустите:

$ paver run

И у нас есть конвейер активов. Конечно, он мог бы сделать больше, например

  • компилируйте меньше файлов/sass
  • обрабатывайте файлы с помощью ng-аннотации
  • минимизировать html или css

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

Чтобы увидеть полный код, проверьте его на github