Release 0.4.0 (2016-11-24)

Developer Changes

Getting rid of Facade

В предыдущих версиях фреймворка ram функции для взаимодействия с пользователем были доступны как методы класса Facade. Разработчику требовалось создать объект этого класса в коде юнита {unit}, а при необходимости взаимодействия с пользователем из вызываемых в коде функций - передать созданный экземпляр Facade {instance of Facade} в качестве аргумента.

Помимо функций для взаимодействия с пользователем, Facade также предоставлял архаичные методы для вызова процессов: RunCommand для запуска процесса в приоритетном режиме {in foreground} и ExecTask для запуска процесса в фоновом режиме {in background}.

В версии 0.4.0 фреймворка ram проведен рефакторинг целью устранения объектов класса Facade. Методы для взаимодействия с пользователем вынесены в модуль {module} ram.widgets и доступны из любого места программы как функции этого модуля. Методы для вызова внешних процессов расширены и вынесены в модуль ram.process.

Например, код для отображения диалогового окна с вопросом к пользователю и вызовом процесса в предыдущих версиях фреймворка мог выглядеть так:

import ram.unitlib

def Run(facade):
    if facade.RunCommand('rm -rf /'):
        facade.ShowMessage("Success", "All files removed!")
    else:
        facade.ShowError("Failure", "Failed to remove all files!")

if __name__ == '__main__':
    facade = ram.unitlib.Facade()

    if facade.AskViaButtons(
        "Remove all files",
        "Would you like to remove all files?"
    ):
        Run(facade)

В результате изменений, доступных в новой версии фреймворка, этот код будет выглядеть так:

import ram.widgets
import ram.process

def Run():
    if ram.process.launch('rm -rf /'):
        ram.widgets.ShowMessage("Success", "All files removed!")
    else:
        ram.widgets.ShowError("Failure", "Failed to remove all files!")

if __name__ == '__main__':
    if ram.widgets.AskViaButtons(
        "Remove all files",
        "Would yo like to remove all files?
    ):
        Run()

Process management

Все функции для работы с внешними процессами теперь сосредоточены в модуле ram.process. В предыдущих релизах фреймворка ram они были не систематизированы. Часть из этих функций была доступна в качестве методов упразденного класса Facade. Другая часть - в виде статических функций класса Process из уже упомянутого модуля ram.process (класс Process также был упразднен). Работа с процессами реализована в виде функций верхнего уровня.

Ниже представлено несколько примеров того, как выглядела работа с процессами при использовании предыдущих версий фреймворка:

import ram.unitlib
from ram.process import Process

facade = Facade()

# run process and get it's exit code
pinged = facade.RunCommand('ping -c1 localhost')

# same but using Process
pinged = Process.launch('ping -c1 localhost')

# get process output or errors
try:
    output = Process.iopipe('ls -1 /tmp')
except RuntimeError as err:
    facade.ShowMessage("errors", err)

При использовании версии фреймворка ram 0.4.0 эти команды выглядят так:

import ram.process
import ram.widgets

# run process and get it's exit code
pinged = ram.process.launch('ping -c1 localhost')

# get process output or errors
try:
    output = ram.process.output('ls -1 /tmp')
except RuntimeError as err:
    ram.widgets.ShowMessage("errors", err)

# get process exit code, output and errors
status, output, errors = ram.process.run('rm -rf /')

На момент релиза версии 0.4.0 разработчикам доступны следующие основные функции:

ram.process.launch(command)

Вызывает внешний процесс, заданный параметром command, в приоритетном режиме.

Вызов этой функции идентичен вызову os.system, но по умолчанию функция использует реализацию на основе библиотеки subprocess. Для отладочных целей можно переключить реализацию этого вызова на использование os.system. Для этого необходимо включить опцию shell:

# ram tweak shell on
Parameters:command

Команда, которая должна быть выполнена при вызове функции. Может быть задана строкой или списком строк.

Если команда задана в виде строки, то параметр в неизменном виде передается в качестве аргумента вызываемому shell-у.

Если команда задана в виде списка строк, то элементы списка при необходимости заключаются в кавычки {quoted}, а затем объединяются в строку. Далее функция выполняется так же, как с изначально полученной строкой.

Returns:Код возврата {exit code} завершенного процесса.
ram.process.output(command, input=None)

Вызывает внешний процесс, заданный параметром command, и возвращает его вывод, направленный в поток stdout.

В случае неуспешного {failure} завершения процесса функция порождает исключение {raises exception} c текстом вывода, направленного в поток stderr.

Parameters:
  • command

    Команда, которая должна быть выполнена при вызове функции. Может быть задана строкой или списком строк.

    Если команда задана в виде строки, то параметр в неизменном виде передается в качестве аргумента вызываемому shell-у.

    Если команда задана в виде списка строк, то элементы списка при необходимости заключаются в кавычки {quoted}, а затем объединяются в строку. Далее функция выполняется так же, как с изначально полученной строкой.

  • input – Текст, который направляется в поток stdin процесса.
Returns:

Текст, полученный из потока stdout процесса.

Raises:

RuntimeError – Исключение с текстом из потока stderr процесса, порождаемое в случае неудачного завершения процесса.

ram.process.run(command, input=None)

Вызывает внешний процесс, заданный параметром command. Возвращает кортеж {tuple} из трех элементов - код возврата, вывод в поток stdout и вывод в поток stderr.

Parameters:
  • command

    Команда, которая должна быть выполнена при вызове функции. Может быть задана строкой или списком строк.

    Если команда задана в виде строки, то параметр в неизменном виде передается в качестве аргумента вызываемому shell-у.

    Если команда задана в виде списка строк, то элементы списка при необходимости заключаются в кавычки {quoted}, а затем объединяются в строку. Далее функция выполняется так же, как с изначально полученной строкой.

  • input – Текст, который выводится в поток stdin процесса.
Returns:

(exit code, output, errors)

Watching process events

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

Для асинхронной работы с процессами в модуле ram.process определены Watch-объекты. Эти объекты инкапсулируют логику работы с внешними источниками событий. Каждый экземпляр класса Watch ассоциирован с файловым дескриптором, который и является источником событий (например, может быть использован совместно с вызовом select). Помимо этого, Watch-объекты на основе своей логики интерпретируют данные, поступающие из файлового дескриптора, и формируют из них очередь поступающих значений.

Watch-объекты не должны создаваться в коде явно. Для их создания модуль ram.process предоставляет набор контекстных менеджеров. Помимо этого, контекстные менеджеры также обеспечивают освобождение связанных с Watch-объектами ресурсов (например, завершение процессов, запущенных в фоновом режиме). Для отслеживания событий, поступающих от внешних процессов, в релизе фреймворка ram 0.4.0 определены следующие контекстные менеджеры:

ram.process.watch_status(command)

Порождает Watch-объект для отслеживания завершения процесса, запущенного в фоновом режиме.

ram.process.watch_stdout(command)

Порождает Watch-объект для отслеживания данных, поступающих в поток stdout процесса.

ram.process.watch_stderr(command)

Порождает Watch-объект для отслеживания данных, поступающих в поток stderr процесса.

Например, работа с Watch-объектами может выглядеть следующим образом:

from ram.process import watch_status

# using context manager to get Watch object
# once background process is executed, watch is returned
with watch_status('ping -c1 localhost') as watch:

    # blocking call to wait until process exited
    # exit status of the process will be printed
    print watch()
import time

from ram.process import watch_output

with watch_stdout('ping -c1 localhost') as watch:

    # check background process is active
    while watch:
        # non-blocking iterate over incoming data
        for data in watch:
            print data

        # give a chance for background process
        # to continue execution
        time.sleep(1.0)

Watch objects advanced

Публичный интерфейс для работы с Watch-объектами:

class ram.process.Watch

Объект, инкапсулирующий работу с внешними источниками событий.

__nonzero__()

Возвращает статус Watch-объекта: True, если источник активен и может посылать события, или False, если источник неактивен (например, если данные, поступающие из файлового дескриптора, закончились).

__call__(timeout=None, iterate=True)

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

Если параметр iterate принимает значение True, то объект возвращает первое непрочитанное значение из очереди. В ином случае очередь поступивших значений не изменяется, и метод возвращает значение None.

Время ожидания события можно ограничить с помощью параметра timeout. По умолчанию значение параметра равно None, что соответствует бесконечному ожиданию. В качестве значения параметра можно указывать время ожидания в секундах. Если в течение заданного времени события не поступают, функция порождает исключение.

__iter__()

Итерация по очереди событий в неблокирующем режиме.

Функции для отслеживания событий от внешних процессов, описанные в предыдущем разделе, используют следующие классы Watch-объектов:

class ram.process.ExitWatch(ram.process.Watch)

Watch-объект для отслеживания завершения процесса, запущенного в фоновом режиме. Значением, получаемым в результате вызова __call__, является код возврата процесса.

__init__(proc):

Для инициализации этого объекта ему необходимо передать объект процесса, порожденный в результате вызова subprocess.Popen.

Этот объект генерирует событие в момент завершения внешнего процесса. Одновременно с этим объект становится неактивным. Все последующие вызовы __call__ для этого объекта будут неблокирующими, функция будет возвращать код возврата процесса.

class ram.process.PipeWatch(ram.process.Watch)

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

__init__(file):

Для инициализации этого объекта ему необходимо передать файловый объект {file-like object}. Объект должен поддерживать метод fileno() для получения ассоциированного файлового дескриптора.

Using watches with RunMenu

Watch-объекты, описанные в предыдущих разделах, могут использоваться в качестве источника событий для функций построения псевдографического интерфейса. В релизе 0.4.0 фреймворка ram реализована поддержка этих объектов для функции ram.widgets.RunMenu:

ram.widgets.RunMenu(..., watches=None, ...)

Функция для отображения иерархического меню с использованием псевдографического интерфейса. По умолчанию (при значении watches=None) меню, построенное с помощью этого вызова, реагирует только на пользовательский ввод. Параметр watches позволяет также отслеживать события из Watch-объектов.

Parameters:watches

Заданный словарем набор Watch-объектов, события которых будут отслеживаться.

В качестве ключа словаря должен быть указан непосредственно Watch-объект.

В качестве значения словаря может быть указан вызываемый {callable} объект или булевское значение True/False.

Вызываемый объект должен принимать один аргумент - значение, полученное из очереди Watch-объекта. Объект вызывается в момент поступления события последовательно для всех значений, полученных из очереди.

Если значение равно True, то при поступлении события очередь полученных значений очищается.

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

Например, следующий код отображает меню с одним элементом и запускает процесс ping. Пока процесс запущен, элемент меню отображает сообщение “Waiting …”. Когда процесс завершается, элемент меню отображает сообщение “Exited” и код возврата процесса.

with ram.process.watch_status('ping -c3 localhost') as watch:

    def _mk_menu():
        caption = (
            "Waiting ..." if watch else
            "Exited: %s" % watch()
        )

        return [(caption, "")]

    ram.widgets.RunMenu(
        "Testing watch status",
        _mk_menu,
        watches={
            watch: True
        }
    )

Следует обратить внимание, что для получения статуса процесса используется потенциально-блокирующий вызов watch(). Однако он будет выполнен, только если условие watch в конструкции if/else ложно. Это означает, что Watch-объект неактивен, т.к. ассоциированный с ним процесс завершился и получение его статуса не блокирует выполнение кода.

Другой пример показывает пустое меню и отслеживает события вывода процесса ping. Данные, поступающие от Watch-объекта, сохраняются в список. Как только Watch-объект сообщает о завершении потока данных, накопившиеся данные выводятся в диалоговом окне.

with ram.process.watch_stdout('ping -c3 localhost') as watch:

    output = []

    def _read_stdout(data, watch=watch):
        output.append(data)
        if not watch:
            ram.widgets.ShowMessage("".join(output))

    ram.widgets.RunMenu(
        "Testing watch stdout",
        [],
        watches={
            watch: _read_stdout
        }
    )

Tracking watches

Описанные выше функции watch_status, watch_stdout и watch_stderr удобны для отслеживания событий, поступающих от внешних процессов. Используя их совместно с произвольными shell-скриптами, можно создавать более сложные конструкции. Например, наивная реализация интервального таймера, генерирующего события каждую секунду, могла бы выглядеть так:

from ram.process import watch_stdout

# run script that repeatedly:
#   sleeps for a second
#   prints current date and time
with watch_stdout('while true; do sleep 1; date; done') as watch:
    # wait for timer tick
    # print current time
    print watch()

Однако подобное использование Watch-объектов имеет следующие проблемы:

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

Для преодоления этих ограничений модуль ram.process предоставляет функцию watch_iterable. С ее помощью можно построить Watch-объекты на основе произвольного блокирующего генератора, реализованного на языке Python. Запуск генератора осуществляется асинхронно с использованием библиотеки multiprocessing. Объекты, возвращаемые оператором yield в коде генератора, поступают в очередь Watch-объекта в неизменном виде. Возвращаемые объекты должны быть сериализуемы с помощью модуля pickle {picklable}. При этом не требуется специальной адаптации кода функции-генератора для создания Watch-объекта - оригинальный генератор по-прежнему можно использовать в синхронном режиме.

Для реализации аналогичной функциональности наивного интервального таймера средствами языка Python можно использовать такой код:

import time

def naive_timer():
    while True:
        time.sleep(1.0)
        yield time.time()

Для того чтобы сделать Watch-объект на основе этого кода, необходимо воспользоваться функцией watch_iterable:

watch_iterable(iterable, name=None)

Запускает итерацию по переданному итерируемому объекту в параллельном процессе. Порождает Watch-объект для отслеживания событий и получения данных от этого генератора.

Parameters:
  • iterable – Итерируемый объект (например, инициализированный экземпляр функции-генератора).
  • name – Имя, используемое для создаваемого процесса в сообщениях об ошибках.

Например, чтобы сделать Watch-объект на основе функции-генератора naive_timer(), можно использовать следюущий код:

from ram.process import watch_iterable

# create watch based on naive_timer generator
with watch_iterable(naive_timer()) as watch:

    # wait for timer tick
    # print current time
    print watch()

Помимо возвращаемых итератором значений, контекстный менеджер watch_iterable передает в основной процесс необработанные исключения, возникшие в результате выполнения генератора. Исключение, порождаемое в асинхронно-запущенном генераторе, передается в основной процесс при попытке получить следующее сообщение из очереди Watch-объекта и повторно выкидывается в основном процессе. Объект стека {stack trace} для порожденного исключения не сериализуем, поэтому он форматируется и передается в основной процесс в виде текста. Когда исключение повторно генерируется на стороне основного процесса, эта строка добавляется к тексту исключения. Тип исходного исключения при этом сохраняется.

>>> from ram.process import watch_iterable
>>>
>>> def faulty():
...     raise ValueError('error!')
...     yield None
...
>>> with watch_iterable(faulty(), name='run-faulty') as watch:
...     print watch()
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "ram/process.py", line 176, in __call__
    return self.update()
  File "ram/process.py", line 276, in update
    raise exc(_ev)
ValueError: error!
Process: run-faulty
Traceback (most recent call last):
  File "ram/process.py", line 296, in _wrap_iter
    for index, obj in enumerate(iterable):
  File "<stdin>", line 2, in faulty
ValueError: error!

Batteries included!

Помимо средств для построения Watch-объектов на основе произвольного генератора, модуль ram.process содержит набор предопределенных генераторов и специализированных контекстных менеджеров:

track_timer(timeout=None)

Блокирующий генератор, реализующий интервальный таймер. В качестве возвращаемых значений используется результат выполения функции time.time.

Parameters:timeout – Интервал таймера. Значение по умолчаню: 1 секунда.
>>> from ram.process import track_timer
>>>
>>> for event in track_timer():
...     print event
...
1479909647.81
1479909648.81
1479909649.81
...
watch_timer(timeout=None)

Контекстный менеджер для создания Watch-объекта на основе генератора track_timer.

track_output(command, timeout=None)

Блокирующий генератор для отслеживания изменений в выводе программы. Может быть удобен для поллинга команд, отображающих какое-либо состояние. В качестве возвращаемых значений используется актуальный вывод команды.

Parameters:
  • command – Команда, изменения в выводе которой требуется отслеживать.
  • timeout – Интервал между запусками команды. Значение по умолчанию: 1 секунда.
>>> from ram.process import track_output
>>>
>>> for event in track_output('dmesg | wc -l'):
...     print event,
1124
1127
1131
...
watch_output(command, timeout=None)

Контекстный менеджер для создания Watch-объекта на основе генератора track_output.

track_dir(dirname, match=None, files=True, dirs=False, rec=False)

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

  • путь к отслеживаемой директории;
  • имя созданного или удаленного файла (директории);
  • булевское значение True для директорий и False для файлов;
  • булевское значение True для созданных объектов и False для удаленных объектов.
Parameters:
  • dirname – Путь к отслеживаемой директории.
  • match

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

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

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

  • files – Булевское значение. Если оно равно True, то генерируются события для файлов.
  • dirs – Булевское значение. Если оно равно True, то генерируются события для директорий.
  • rec – Булевское значение. Если оно равно True, то отслеживаются события в поддиректориях.
>>> from ram.process import track_dir
>>>
>>> for event in track_dir('/tmp'):
...     print event
('/tmp', 'q7p8AD', False, True)
('/tmp', 'q7p8AD', False, False)
...
watch_dir(dirname, match=None, files=True, dirs=False, rec=False)

Контекстный менеджер для создания Watch-объекта на основе генератора track_dir.