使用 Pytransitions 时没有触发器建议

No suggestions for triggers when using Pytransitions

尝试按照此处提供的示例使用 transitionshttps://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

transitions 在运行时将触发器添加到模型 (Matter) 实例。在初始化代码实际执行之前,IDEs 无法预测到这一点。恕我直言,这是 transitions 工作方式的最大缺点(但再次恕我直言,这也是它在运行时处理动态状态机或状态机 created/received 时的优势,但这是另一回事)

如果您使用带有代码完成 (ipython) 的交互式 shell,您将看到 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 方法(伪装成属性)调用 evaparate 时,它总是会提示,即使你稍后添加 evaporate.

使用 pyi 个文件和 PEP484(PyCharm 解决方法)

不幸的是,PyCharm 没有考虑文档字符串中的属性来完成代码,正如您正确指出的那样(参见 this discussion for more details). We need to use another approach. We can create so called pyi files to provide hints to PyCharm. Those files are named identically to their .py counterparts but are solely used for IDEs and other tools and must not be imported (see )。让我们创建一个名为 sandbox.pyi

的文件
# sandbox.pyi

class Model:
    evaporate = None  # type: callable

现在让我们创建实际的代码文件 sandbox.py(我没有将我的 playground 文件命名为 '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 

这样您就可以完成代码并且 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

备选方案:从文档字符串生成机器配置

转换的问题跟踪器中已经讨论了类似的问题(请参阅 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

这是一个非常简单的 docstring-to-machine-configuration 解析器,它不会处理可能成为 docstring 一部分的所有可能性。它假定每个带有包含 ("source: " ) 的文档字符串的方法都应该是一个触发器。然而,它确实也解决了文档问题。使用这样的机器将确保至少存在开发机器的一些文档。