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](https://i.stack.imgur.com/iA0KM.png)
Таким образом, у вас есть завершение кода И 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:"), должен быть триггером. Это, однако, также подходит к вопросу документации. Использование такой машины обеспечит наличие хотя бы некоторой документации для разработанной машины.