Нет предложений по триггерам при использовании Pytransitions - PullRequest
1 голос
/ 12 февраля 2020

Попытка использовать пакет transitions в соответствии с примерами, приведенными здесь https://github.com/pytransitions/transitions

По какой-то причине ни один из двух подходов, показанных ниже, не предлагает подсказки для зарегистрированных триггеров evaporate() (по крайней мере в PyCharm 2019.1.2 для Windows x64)

В то же время эти триггеры все еще могут использоваться.

Что можно сделать, чтобы эти триггеры предлагались при вводе текста?

class Matter(Machine):
    def say_hello(self): print("hello, new state!")
    def say_goodbye(self): print("goodbye, old state!")

    def __init__(self):
        states = ['solid', 'liquid', 'gas']
        Machine.__init__(self, states=states, initial='liquid')
        self.add_transition('melt', 'solid', 'liquid')

testmatter= Matter()
testmatter.add_transition('evaporate', 'liquid', 'gas')
testmatter.evaporate()
Out: True

testmatter.get_model_state(testmatter)
Out: <State('gas')@14748976>
class Matter2():
    pass
testmatter2 = Matter2()
machine  = Machine(model=testmatter2, states=['solid', 'liquid', 'gas', 'plasma'], initial='liquid')
machine.add_transition('evaporate', 'liquid', 'gas')

testmatter2.evaporate()
Out: True

1 Ответ

1 голос
/ 13 февраля 2020

transitions добавляет триггеры во время выполнения к экземпляру модели (Matter). Это не может быть спрогнозировано IDE до фактического выполнения кода инициализации. Имхо, это самый большой недостаток способа работы transitions (но опять же имхо, это также его сила при работе с динамическими c конечными автоматами или конечными автоматами, созданными / полученными во время выполнения, но это другая история)

Если вы используете интерактивную оболочку с дополнением кода (i python), вы увидите, что будет предложено evaporate (на основе __dir__ обращений к модели):

from transitions import Machine

class Model:
    pass

model = Model()
>>> model.e  # TAB -> nothing

# model will be decorated during machine initialization
machine = Machine(model, states=['A', 'B'], 
                  transitions=[['evaporate', 'A', 'B']], initial='A')

>>> model.e  # TAB -> completion! 

Но я предполагаю, что это не так, как вы планируете кодировать. Итак, как мы можем дать подсказки для самоанализа?

Самое простое решение: используйте строку документации для вашей модели, чтобы объявить триггеры.

from transitions import Machine

class Model:
    """My dynamically extended Model
    Attributes:
        evaporate(callable): dynamically added method
    """

model = Model()
# [1]
machine = Machine(model, states=['A', 'B'],
                  transitions=[['evaporate', 'A', 'B']], initial='A')
model.eva  # code completion! will also suggest 'evaporate' before it was added at [1]

Проблема в том, что в среде IDE правильная строка документации будет зависеть. Поэтому, когда метод docstring (маскируемый как атрибут) называется calles evaparate, он всегда будет предлагать, даже если вы позже добавите evaporate.

Использовать pyi файлы и PEP484 Обходной путь PyCharm)

К сожалению, PyCharm не учитывает атрибуты в строках документации для завершения кода, как вы правильно указали (см. это обсуждение для получения дополнительной информации). Нам нужно использовать другой подход. Мы можем создавать так называемые pyi файлы для предоставления подсказок PyCharm. Эти файлы названы идентично своим .py аналогам, но используются исключительно для IDE и других инструментов и не должны импортироваться (см. в этом посте ). Давайте создадим файл с именем sandbox.pyi

# sandbox.pyi

class Model:
    evaporate = None  # type: callable

А теперь давайте создадим фактический файл кода sandbox.py (я не называю файлы моей игровой площадки 'test', потому что это всегда пугает pytest ...)

# sandbox.py
from transitions import Machine

class Model:
    pass

## Having the type hints right here would enable code completion BUT
## would prevent transitions to decorate the model as it does not override 
## already defined model attributes and methods.
# class Model:
#     evaporate = None  # type: callable

model = Model()
# machine initialization
model.ev  # code completion 

Code completion with pyi files

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

Если вы хотите автоматически генерировать pyi файлов, вы можете взглянуть на stubgen или расширить Machine для создания окурков событий моделей для вас.

from transitions import Machine

class Model:
    pass


class PyiMachine(Machine):

    def generate_pyi(self, filename):
        with open(f'{filename}.pyi', 'w') as f:
            for model in self.models:
                f.write(f'class {model.__class__.__name__}:\n')
                for event in self.events:
                    f.write(f'    def {event}(self, *args, **kwargs) -> bool: pass\n')
                f.write('\n\n')


model = Model()
machine = PyiMachine(model, states=['A', 'B'],
                     transitions=[['evaporate', 'A', 'B']], initial='A')
machine.generate_pyi('sandbox')
# PyCharm can now correctly infer the type of success
success = model.evaporate()
model.to_A()  # A dynamically added method which is now visible thanks to the pyi file

Альтернатива: создание конфигураций машины из строк документации

Подобная проблема уже обсуждалась в системе отслеживания переходов (см. * 1056). *https://github.com/pytransitions/transitions/issues/383). Вы также можете сгенерировать конфигурацию машины из строки документации модели:

import transitions
import inspect
import re


class DocMachine(transitions.Machine):
    """Parses states and transitions from model definitions"""

    # checks for 'attribute:value' pairs (including [arrays]) in docstrings
    re_pattern = re.compile(r"(\w+):\s*\[?([^\]\n]+)\]?")

    def __init__(self, model, *args, **kwargs):
        conf = {k: v for k, v in self.re_pattern.findall(model.__doc__, re.MULTILINE)}
        if 'states' not in kwargs:
            kwargs['states'] = [x.strip() for x in conf.get('states', []).split(',')]
        if 'initial' not in kwargs and 'initial' in conf:
            kwargs['initial'] = conf['initial'].strip()
        super(DocMachine, self).__init__(model, *args, **kwargs)
        for name, method in inspect.getmembers(model, predicate=inspect.ismethod):
            doc = method.__doc__ if method.__doc__ else ""
            conf = {k: v for k, v in self.re_pattern.findall(doc, re.MULTILINE)}
            # if docstring contains "source:" we assume it is a trigger definition
            if "source" not in conf:  
                continue
            else:
                conf['source'] = [s.strip() for s in conf['source'].split(', ')]
                conf['source'] = conf['source'][0] if len(conf['source']) == 1 else conf['source']
            if "dest" not in conf:
                conf['dest'] = None
            else:
                conf['dest'] = conf['dest'].strip()
            self.add_transition(trigger=name, **conf)

    # override safeguard which usually prevents accidental overrides
    def _checked_assignment(self, model, name, func):
        setattr(model, name, func)


class Model:
    """A state machine model
    states: [A, B]
    initial: A
    """

    def go(self):
        """processes information
        source: A
        dest: B
        conditions: always_true
        """

    def cycle(self):
        """an internal transition which will not exit the current state
        source: *
        """

    def always_true(self):
        """returns True... always"""
        return True

    def on_exit_B(self):  # no docstring
        raise RuntimeError("We left B. This should not happen!")


m = Model()
machine = DocMachine(m)
assert m.is_A()
m.go()
assert m.is_B()
m.cycle()
try:
    m.go()  # this will raise a MachineError since go is not defined for state B
    assert False
except transitions.MachineError:
    pass

Это очень простой синтаксический анализатор документации-к-машине, который не учитывает все возможные варианты, которые могут быть частью строки документации. Предполагается, что каждый метод, содержащий строку документации ("source:"), должен быть триггером. Это, однако, также подходит к вопросу документации. Использование такой машины обеспечит наличие хотя бы некоторой документации для разработанной машины.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...