使用Pytransitions时没有触发器的建议 [英] No suggestions for triggers when using Pytransitions

查看:91
本文介绍了使用Pytransitions时没有触发器的建议的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

根据此处提供的示例尝试使用transitions https://github.com/pytransitions/transitions

Trying to use transitions package as per examples provided here https://github.com/pytransitions/transitions

由于某种原因,下面显示的两种方法均未为注册的evaporate()触发器提供键入建议(至少在Windows x64的PyCharm 2019.1.2中)

For some reason, neither of the two approaches shown below provide typing suggestions for registered evaporate() trigger (at least in PyCharm 2019.1.2 for Windows x64)

同时,这些触发器仍然可以使用.

At the same time, these triggers can still be used.

如何在键入时建议这些触发器?

What can be done to have these triggers suggested as I type?

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)实例.在实际执行初始化代码之前,IDE无法预测到这一点.恕我直言,这是transitions工作方式的最大缺点(但是再次恕我直言,这也是处理动态状态机或在运行时创建/接收的状态机时的优势).

transitions adds triggers at runtime to the model (Matter) instance. This cannot be predicted by IDEs before the initialization code is actually executed. Imho, this is the biggest disadvantage of the way in which transitions works (but again imho, it is also its strength when dealing with dynamic state machines or state machines created/received during runtime but that's another story)

如果您使用具有代码完成功能(ipython)的交互式外壳,您将看到evaporate(基于对模型的__dir__调用) 建议:

If you use an interactive shell with code completion (ipython), you will see that evaporate (based on __dir__ calls to the model) will be suggested:

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! 

但是我认为那不是您打算编码的方式.那么我们怎样才能给内省一些暗示呢?

But I assume that's not how you plan to code. So how can we give hints to the introspection?

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,它也总是会提示您.

The problem here is that the IDE will rely on the docstring to be correct. So when the docstring method (masked as attribute) is calles evaparate, it will always suggests that even though you later add evaporate.

不幸的是,正如您正确指出的那样,PyCharm不会考虑文档字符串中的属性来完成代码(请参见这篇文章).让我们创建一个名为sandbox.pyi

Unfortunately, PyCharm does not consider attributes in docstrings for code completion as you correctly pointed out (see 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 this post). Let's create a file called sandbox.pyi

# sandbox.pyi

class Model:
    evaporate = None  # type: callable

现在,让我们创建实际的代码文件sandbox.py(我没有将我的游乐场文件命名为"test",因为这总是让pytest感到震惊...)

And now let's create the actual code file sandbox.py (I don't name my playground files 'test' because that always startles 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将正确装饰模型.缺点是您还有另一个文件要担心哪个文件可能会使您的项目混乱.

This way you have code completion AND transitions will correctly decorate the model. The downside is that you have another file to worry about which might clutter your project.

如果要自动生成pyi文件,可以查看 stubgen 或扩展Machine为您生成模型的事件存根.

If you want to generate pyi files automatically, you could have a look at stubgen or extend Machine to generate event stubs of models for you.

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 ).您还可以从模型的文档字符串生成机器配置:

Alternative: Generate machine configurations FROM docstrings

A similar issue has been already discussed in the issue tracker of transitions (see https://github.com/pytransitions/transitions/issues/383). You could also generate the machine configuration from the model's 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-configration解析器,它不会处理可能是docstring一部分的所有事件.它假定每个文档字符串包含("source:")的方法都应被视为触发器.但是,它也确实处理了文档问题.使用这样的机器将确保至少存在一些有关已开发机器的文档.

This is a very simple docstring-to-machine-configration parser which does not take care of all eventualities that could be part of a docstring. It assumes that every method with a docstring containing ("source: " ) is supposed to be a trigger. It does however also approaches the issue of documentation. Using such a machine would make sure that at least some documentation for the developed machine exists.

这篇关于使用Pytransitions时没有触发器的建议的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆