Runscript — утилита для запуска python скриптов

    Думаю многим знакома следующая ситуация. В вашем проекте есть различные действия, которые нужно выполнять время от времени. Для каждого действия вы создаёте отдельный скрипт на питоне. Чтобы далеко не лазить, скрипт кладёте в корень проекта. Через некоторое время вся корневая директория проекта замусоривается этими скриптами и вы решаете сложить их в отдельную директорию. Теперь начинаются проблемы. Если указать интерпретатору python путь до скрипта, включающий эту новую директорию, то внутри скрипта не будут работать импорты пакетов, находящися в корне проекта т.к. корня проекта не будет в sys.path. Эту проблему можно решить несколькими способами. Можно изменять sys.path в каждом скрипте, добавляя туда корень проекта. Можно написать утилитку для запуска ваших скриптов, которая будет изменять sys.path перед запуском скрипта или просто будет лежать в корне проекта. Можно ещё что-то придумать. Мне надоело каждый раз изобретать колесо и я создал велосипед runscript на котором с удовольствием катаюсь.

    Установить библиотеку можно с помощью pip:

    $ pip install runscript

    После установки библиотеки runscript, вы получаете в вашей системе новую консольную команду run с помощью которой можно запускать скрипты. По-умолчанию, команда run ищет скрипты в под-каталоге script текущего каталога.

    Давайте рассмотрим простой пример. Создадим каталог script. Создадим пустой файл script/__init__.py, превратив этот каталог в python-пакет. Теперь создадим файл script/preved.py со следующим содержимым:

    def main(**kwargs):
        print(‘Preved, medved!’)
    


    Скрипт готов. Теперь мы можем его запустить:

    $ run preved
    Preved, medved!

    Ура! Скрипт работает. Вот собственно и всё, что делает библиотека runscript. Я серьёзно :) Команда run запускает функцию main из файла, имя которого вы ей передали в командной строке. Оказалось, что даже такой простой фунционал очень удобен. Я с удивлением заметил, что пользуюсь утилиткой run в каждом своём проекте т.к. везде есть простенькие скрипты, которые нужно запускать.

    Со временем утилита run обросла рядом полезных полезностей, о которых я сейчас расскажу.

    Получение параметров через командную строку


    Чтобы передать вашему скрипту какие-либо параметры через командную строку, вам нужно описать эти параметры в функции setup_arg_parser внутри вашего скрипта. Эта функция получает на вход объект ArgumentParser, в который вы можете добавить нужные опции. Далее, когда скрипт будет вызван, значения параметров командной строки будут переданы фунции main. Пример скрипта:

    def setup_arg_parser(parser):
        parser.add_argument(‘-w’, ‘--who’, default=’medved’)
    
    def main(who, **kwargs):
        print(‘Preved, {}’.format(who))
    

    Запускаем:

    $ run preved
    Preved, medved
    $ run preved -w anti-medved
    Preved, anti-medved

    Обратите внимание, как фунция main получила параметры командной строки — в виде обычных именованных параметров. Всегда нужно указывать **kwargs т.к. кроме нужных вам параметров, передаются значения всех глобальных для утитилы run параметров (читайте о них ниже).

    Активация Django


    Если вы пытались использовать фреймворк Django в ваших консольных скриптах, то знаете, что нужно сделать кое-что, иначе ничего не будет. Кое-что заключается в создании environment переменной DJANGO_SETTINGS_MODULE, cодержащей путь до модуля с настройками. Обычно в python скрипт добавляют следующие строки:

    import os
    os.environ[‘DJANGO_SETTINGS_MODULE’] = ‘settings’
    

    Начиная с django 1.7 нужно также выполнить

    import django
    django.setup()
    

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

    [global]
    django_settings_module = settings
    django_setup = yes
    


    Профилирование


    Добавив ключик --profile при вызове скрипта, получим файл с результатами профилирования работы нашего скрипта, который можно посмотреть в kcachegrind. Результат сохраняется в каталог var/<script_name>.prof.out, так что не забудьте создать этот каталог. Также нужно установить модуль pyprof2calltree, который нужен, чтобы сохранить результат профилирования в формате kcachegrind.

    $ run preved --profile
    Preved, medved
    $ ls var/
    preved.prof.out

    Настройка мест поиска скриптов


    По-умолчанию, утилита run ищет скрипт в двух пакетах: grab.script и script. Пакет grab.script добавлен в этот список, потому что во многих проектах парсинга сайтов я запускаю команду crawl из grab.script пакета. Если вам нужно изменить места для поиска скриптов, создайте следующую настройку в run.ini файле:

    [global]
    search_path = package1.script,foo,bar
    

    Теперь если мы выполним команду `run preved`, то утилита run попытается импортировать модуль preved в следующем порядке:

    • package1.script.preved
    • foo.preved
    • bar.preved


    Использование lock-файлов


    Иногда бывает нужно запретить одновременную работу нескольких экземпляров скрипта. Например, мы вызываем скрипт каждую минуту с помощью cron и хотим не допустить одновременной работы нескольких копий скрипта, что может произойти, если работа одной из копий затянется больше, чем на минуту. С помощью опции --lock-key мы можем передать имя lock-файла, который будет создан в каталоге var/run. Например, --lock-key foo приведёт к созданию файла var/run/foo.lock.

    Другой способ задать имя lock-файла — создание функции get_lock_key внутри вашего скрипта. Результат её работы будет использован утилитой run, для формирования имени lock-файла. Фунция будет полезна на тот, случай, если вы хотите генерировать имя lock-файла в зависимости от параметров, передаваемых скрипту.

    import time 
    
    def get_lock_key(who, **kwargs):
        return 'the-{}-lock'.format(who)
    
    
    def setup_arg_parser(parser):
        parser.add_argument('-w', '--who', default='medved')
    
    
    def main(who, **kwargs):
        print('Preved, {}'.format(who))
        time.sleep(1)
    

    Запускаем одновременно две копии скрипта и видим:

    $ run preved -w anti-medved & run preved -w anti-medved
    [1] 25277
    Trying to lock file: var/run/the-anti-medved-lock.lock
    Preved, anti-medved
    Trying to lock file: var/run/the-anti-medved-lock.lock
    File var/run/the-anti-medved-lock.lock is already locked. Terminating.
    [1]+ Done run preved -w anti-medved


    Я рассказал об основных возможностях библиотеки runscript. Надеюсь, она окажется вам полезной.

    В случае вопросов по поводу работы библиотеки можно всегда посмотреть в исходный код, который на данный момент довольно маленький: github.com/lorien/runscript/blob/master/runscript/cli.py
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 25
    • +4
      Псст, парень,
      if __name__ == '__main__':

      Только никому!
      • 0
        Не понял.
        • 0
          Имелось в виду, что это условие позволяет запускать скрипт без дополнительных приблуд:
          как-то так
          • 0
            Это условие

            if __name__ == '__main__':
                main()
            

            позволяет как раз *не* запускать фунцию main(), в том случае, если мы импортировали модуль скрипта в другом модуле. Именно эту проблему, решает вышеприведённое условие.

            Чтобы запустить на выполнение фунцию main, достаточно её запустить :)) Без всяких условий.

            main()
            
        • +3
          Как это решает проблему, описанную выше?

          ├── program.py
          └── scripts
              └── script.py
          

          program.py:
          def hello_world():
              print "Hello, world!"
          

          scripts/script.py:
          import program
          
          def actions():
              program.hello_world()
          
          actions()
          
          • +4
            Видимо, проблема настолько не очевидна, что многие, включая меня, вообще никакой проблемы не видят.
            Может я что-то не понимаю?
            В чем собственно проблема выполнить скрипт на питоне?
            • +1
              Если прописать python scripts/script.py вылетит ImportError, говорящий что нету модуля program в пределе досягаемости. Если я правильно понял, автор решает именно этот кейс.

              Так вот я и спросил, как
              if __name__ == '__main__':
              

              Решает эту проблему.
              • +3
                python -m scripts/script.py?
                • +6
                  Тфу, python -m scripts.script
                  • –1
                    Действительно, нужно просто упаковать скрипт в пакет.
                    Не знал, что так можно. Спасибо!
                    • +1
                      По сути ключ -m добавляет текущую директорию в sys.path
                      • –2
                        Для меня это ничего не меняет. У меня есть альтернатива:

                        Вариант 1:

                        def main():
                            pass
                        
                        if __name__ == '__main__':
                            main()
                        


                        $ python -m script.preved
                        


                        Вариант 2:

                        def main(**kwargs):
                            pass
                        


                        $ run preved
                        


                        Я выбираю вариант 2 — нужно меньше букв печатать, как в скриптах, так и в консоли. Для меня это важно. И мне очень приятно, что я экономлю это время. Как я уже заметил в статье, ничего революционного пакет runscript не делает, но то малое, что он делает, очень удобно.

                        Кроме, краткости, вариант 2 предоставляет упрощённую обработку аргументов командной строки и другие плюшки, о которых я написал в статье.

                        Я просто поделился тем, как запускаю скрипты в своих проектах. Никому ничего не навязываю и не говорю, что мой способ самый «лучший» (что бы это ни значило).
                        • +1
                          > Я выбираю вариант 2
                          Это «неправильный» выбор. Если вы планируете запускать скрипт из консоли, то надо писать
                          if __name__ == "__main__":
                          

                          хотя бы потому, что
                          $ python -m script.preved
                          

                          не требует установки пакета для работы.

                          Исключение — вы пишете код для себя и никто никогда не будет с ним работать и запускаться он будет на одной машине.
                          • 0
                            Очень часто пишу код именно для себя. Если мы говорим о каком-то проекте, где работает несколько людей, то тоже не вижу особенной проблемы. Обычно проект это virtualenv + requirements.txt, при разворачивании проекта на новой машине, все зависимости, и в том числе, runscript, поставятся автоматом.

                            Я, честно, плохо понимаю, зачем каждый раз писать «python -m package.module» в вашем проекте для запуска скриптов, если можно договориться написать какой-то запускальщик, который будет решать рутинные операции, специфичные для большинства скриптов этого проекта. В роли запускальщика может выступить в том числе и runscript.
        • +3
          Вы знаете несколько минусов:
          1. Взять docopt и сделать так, чтобы он делал импорт нужного модуля и вызов main. Весьма просто, и дело одного абзаца
          2. Если у вас уже есть django — то там есть стандартные commands с интерфейсом argparse. Зачем еще runscript?
          3. lock-file это наверно хорошая штука если у вас только 1 сервер, который выполняет этот код. Например, если на уровне кода нет разделения, то любой девелоперская машина выполнит тот же код что и на проде — итог, очень просто выстрелить себе в ногу.
          • 0
            1. Про decopt не понял, не использовал этот пакет. Можно подробнее?

            2. Во-первых, django я не в каждом проекте использую. Во-вторых, мне не нравятся django management commands. Мне нравится писать простые скрипты с одной функцией и складывать их в одну директорию в проекте. В случае джанго нужно запрятать скрипты подальше — в management/commands каталог какого-либо приложения. Да и сами скрипты становятся сложнее. Сравните:

            django

            from django.core.management.base import BaseCommand
            from optparse import make_option
            
            
            class Command(BaseCommand):
                option_list = BaseCommand.option_list + (
                    make_option('-w', '--who'),
                )
            
                def handle(self, who, *args, **options):
                    print(who)
            


            runscript

            def setup_arg_parser(parser):
                parser.add_argument('-w', '--who')
            
            def main(who, **kwargs):
                print(who)
            


            3. Не очень понял, что вы имели в виду. Можно пример выстрела?
            • 0
              Ok,

              1. Вот github.com/docopt/docopt, предлагается просто завести первым аргументом, что-то и делать согласно ему import_module().main(args).

              2. Ну так ок, это не так сложно вроде в папку commands бросить файл, синтаксис я бы не сказал что невыносимый. Только еще плюсом идет факт, что там стоит немаленького размера optparse, котором намного больше функций, чем runscript.add_argument.

              3. Я имел что lock-file работает только на одной машине, у другого сервера lock-file нет и следовательно ваш механизм блокировки не работает.
              • 0
                1. Если я вас правильно понял, я это и делаю, только без docopt :)

                2. По поводу optparse вы наверное не совсем поняли меня. Дело в том, что runscript тоже использует optparse, вернее даже не optparse, а argpase. Я в статье писал про это. Сложно или не сложно вам создать файл, кажется ли вам синтаксис django management commands выносимым или нет, я не обсуждаю. Это ваше личное дело. Я просто рассказал, что лично я думаю о django management commands. Вообще этот пункт особо не имеет смысла обсуждать т.к. я использую джангу не во всех проектах, я чуть выше уже говорил об этом.

                3. Да, блокировка через lock-файл работает только в пределах одной машины. Меня это устраивает. Фунциональность runscript — это решение моих задач. Задачи лочить выполнение скриптов сразу на нескольких машинах у меня не воникало.
                • 0
                  1. да
                  2. используйте docopt или click (http://click.pocoo.org/3/)
                  3. да

                  Ну мы говорим не только о Вас, а о нас программистах в целом.
                  • 0
                    2. На данный момент меня вполне удовлетворяет argparse, идущий в stdlib питона.

                    Я всё же скорее про себя говорю. Мол были такие-то задачи, я их так-то решил. Это конкретный разговор. О программистах в целом мне рассуждать не интересно. У каждого свои проблемы и каждый решает их по разному. Если кому-то понравится идея runscript, он может использовать эту библиотеку. Если кто-то считает, что она стреляет в ногу или не делает ничего полезного, он не будет её использовать
                    • +1
                      > Если кому-то понравится идея runscript, он может использовать эту библиотеку

                      Хей, да в чем идея то? Я же Вам привожу и привожу ссылки, а Вам неинтересно рассуждать о программистах в целом. Экий, Вы не уловимый Джо.
                      • 0
                        Извините, я не понимаю, что вы хотите сказать.
              • +2
                В django_extensions есть команда runscript
            • 0
              Однажды я взял и потратил день на изучение virtualenv и setuptools. С тех пор на тех кто манипулирует sys.path я смотрю свысока
              • 0
                Держите нас в курсе, на кого вы смотрите свысока :)

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