使用 Pytransitions 时没有触发器建议
No suggestions for triggers when using Pytransitions
尝试按照此处提供的示例使用 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
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: " ) 的文档字符串的方法都应该是一个触发器。然而,它确实也解决了文档问题。使用这样的机器将确保至少存在开发机器的一些文档。
尝试按照此处提供的示例使用 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
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: " ) 的文档字符串的方法都应该是一个触发器。然而,它确实也解决了文档问题。使用这样的机器将确保至少存在开发机器的一些文档。