使用Pytransitions时不建议使用触发器

2024-04-18 16:03:13 发布

您现在位置:Python中文网/ 问答频道 /正文

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

Tags: selfadddefmachineoutstatetransitiongas
1条回答
网友
1楼 · 发布于 2024-04-18 16:03:13

transitions在运行时向模型(Matter)实例添加触发器。在初始化代码实际执行之前,IDE无法预测这一点。这是transitions工作方式的最大缺点(但同样,在处理动态状态机或运行时创建/接收的状态机时,这也是它的优点,但这是另一回事)

如果使用带代码完成的交互式shell(ipython),您将看到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! 

但我想这不是你计划的编码方式。那么,我们如何给内省提供提示呢

最简单的解决方案:为您的模型使用docstring来宣布触发器。

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来纠正错误。因此,当docstring方法(伪装为属性)被调用evaparate时,它将始终建议,即使稍后添加evaporate

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

不幸的是,PyCharm没有考虑到代码字符串中的属性,因为您正确地指出了(参见this discussion以获得更多的细节)。我们需要使用另一种方法。我们可以创建所谓的pyi文件来为PyCharm提供提示。这些文件的命名与.py对应文件相同,但仅用于IDE和其他工具,不得导入(请参见this post)。让我们创建一个名为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

备选方案:从docstrings

类似的问题已经在过渡问题跟踪中讨论过(见https://github.com/pytransitions/transitions/issues/383)。您还可以从模型的docstring生成机器配置:

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配置解析器,它不考虑可能成为docstring一部分的所有可能性。它假设具有包含(“source:”)的docstring的每个方法都应该是触发器。然而,它也处理文件问题。使用这样的机器可以确保至少有一些已开发机器的文档存在

相关问题 更多 >