-
Notifications
You must be signed in to change notification settings - Fork 525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide features to ease static type checking #658
Comments
Thanks! It'd also help if the code would be typed in general |
Thank you for quick responese! I see it now, in my case I am using VScode with mypy and as you mentioned - VSCode does not always consider stub files. Now the issue I'm having (apart from mentioned runtime decorated functions) is related to subclassing from transitions import EventData
from transitions.extensions.asyncio import AsyncTransition
class MyTransition(AsyncTransition):
async def _change_state(self, event_data: EventData) -> None:
if hasattr(event_data.machine, "model_graphs"):
graph = event_data.machine.model_graphs[id(event_data.model)]
graph.reset_styling()
graph.set_previous_transition(self.source, self.dest)
await event_data.machine.get_state(self.source).exit(event_data)
event_data.machine.set_state(self.dest, event_data.model)
event_data.update(getattr(event_data.model, event_data.machine.model_attribute))
await event_data.machine.get_state(self.dest).enter(event_data) with the return type set to
I've tried different return type, e.g. with
This is apart from the rest of the typing issues with missing attributes. Could it be that stub files are outdated? Thank you! |
Hi @sy-be,
they are not outdated but could use improvements. As transitions was initially built rather "duck-ish" and very agnostic towards passed arguments, "post-mortem typing" isn't trivial. Furthermore, it reveals some weaknesses the inheritance approach which heavily relies on overriding has (e.g. sync to async or Liskov substitution principle violations) that are not easy to fix. That's also a good thing but requires some refactoring. I made some minor changes in from transitions.extensions.asyncio import AsyncTransition, AsyncEventData
class MyTransition(AsyncTransition):
async def _change_state(self, event_data: AsyncEventData) -> None: # type: ignore[override]
assert self.dest is not None
if hasattr(event_data.machine, "model_graphs"): # [1]
graph = event_data.machine.model_graphs[id(event_data.model)]
graph.reset_styling()
graph.set_previous_transition(self.source, self.dest)
await event_data.machine.get_state(self.source).exit(event_data)
event_data.machine.set_state(self.dest, event_data.model)
event_data.update(getattr(event_data.model, event_data.machine.model_attribute))
await event_data.machine.get_state(self.dest).enter(event_data) However, I guess that block |
Excellent! Thanks for your input! I like the library a lot, with better typing it'd be even better. Any plans on releasing these changes? I'm working on adding types to a large project and ideally would love to minimise the amount of |
Hi @sy-be,
end of this week. I need to check open issues and see whether there are some major issues to be tackled. features will be postponed to the next release though. |
Hello @sy-be, @quantitative-technologies, @lucaswhipple-sfn! I am working on some utilities that hopefully ease the work with transitions when doing static type checks. The current state of the process can be found in the dev-experimental-model-creation branch. Here is the relevant section from the documentation: Typing supportAs you probably noticed, But don't worry! You can use the machine constructor parameter from transitions import Machine
# Dynamic assignment
class Model:
pass
model = Model()
default_machine = Machine(model, states=["A", "B"], transitions=[["go", "A", "B"]], initial="A")
print(model.__dict__.keys()) # all convenience functions have been assigned
# >> dict_keys(['trigger', 'to_A', 'may_to_A', 'to_B', 'may_to_B', 'go', 'may_go', 'is_A', 'is_B', 'state'])
assert model.is_A() # Unresolved attribute reference 'is_A' for class 'Model'
# Predefined assigment: We are just interested in calling our 'go' event and will trigger the other events by name
class PredefinedModel:
# state (or another parameter if you set 'model_attribute') will be assigned anyway
# because we need to keep track of the model's state
state: str
def go(self) -> bool:
raise RuntimeError("Should be overridden!")
def trigger(self, trigger_name: str) -> bool:
raise RuntimeError("Should be overridden!")
model = PredefinedModel()
override_machine = Machine(model, states=["A", "B"], transitions=[["go", "A", "B"]], initial="A", model_override=True)
print(model.__dict__.keys())
# >> dict_keys(['trigger', 'go', 'state'])
model.trigger("to_B")
assert model.state == "B" If you want to use all the convenience functions and throw some callbacks into the mix, defining a model can get pretty complicated when you have a lot of states and transitions defined. from transitions.experimental.utils import generate_base_model
simple_config = {
"states": ["A", "B"],
"transitions": [
["go", "A", "B"],
],
"initial": "A",
"before_state_change": "call_this",
"model_override": True,
}
class_definition = generate_base_model(simple_config)
with open("base_model.py", "w") as f:
f.write(class_definition)
# ... in another file
from transitions import Machine
from base_model import BaseModel
class Model(BaseModel): # call_this will be an abstract method in BaseModel
def call_this(self) -> None:
# do something
model = Model()
machine = Machine(model, **simple_config) Where I need your feedbackI plan to add some model decorators that enables the definition of trigger/events in a model class. I have two approaches: a) via function decorators and b) via functions that return a callable placeholder. This is how they could look like: from transitions.experimental.utils import trigger_decorator, trigger, transition, with_trigger_decorator
from transitions import Machine
class Model:
@trigger_decorator(source="A", dest="B") # version A
@trigger_decorator(source="B", dest="C")
def foo(self):
raise RuntimeError("Trigger was not initialized correctly.")
bar = trigger(transition(source="B", dest="A", conditions=lambda: False),
transition(source="B", dest="A")) # version B
@with_trigger_decorator
class MyMachine(Machine):
pass
model = Model()
machine = MyMachine(model, states=["A", "B"], initial="A")
model.foo()
model.bar()
assert model.state == "A" What do you think?Does this look useful to you? Which version to you prefer?What do you think about
|
Excellent! Thank you for this consideration. I quite like the idea of For better or worse, I generally don't make a separate Model class when defining my state machine - I typically just override the My $.02 on Version A vs. Version B is that version A seems to have a method definition syntax that is less hidden to me that the magic of declaring a trigger as a class variable (which is what I think version B is doing?) but my limited knowledge of the behind-the-scenes magic may be compounding my opinion. Thank you for doing this work and I hope my feedback helps. |
Hello @lucaswhipple-sfn and thank you for the feedback.
do you mean you inherit from
It depends. from transitions import Machine
# You could also use a BaseModel here
# class MyMachine(BaseModel, Machine):
class MyMachine(Machine):
def __init__(self):
super(MyMachine, self).__init__(states=["A", "B"], initial="A", model_override=True)
self.add_transition("go", "A", "B")
def go(self) -> bool:
raise RuntimeError("Should be overridden!")
def is_A(self) -> bool:
raise RuntimeError("Should be overridden!")
m = MyMachine()
assert m.go()
assert not m.is_A()
# m.is_B() # AttributeError: 'is_B' does not exist on
You are right. Version B more or less returns a I also prefer version A when it comes to comprehensibility. When many transitions need to be defined, version B will be more compact as you don't need empty lines between definitions and could also add multiple transitions in one line. |
Thank you for your work! I like the On As per version A or B for triggers, I'd generally prefer version A as it's more clearly defined and improves visibility. However in the project I'm working on, historically the whole model was designed in a very unusual way, so I doubt I'll be able to use any of these approaches unless I refactor the code (which I hope to do one day!). On another noteI tried running from the branch and also on version 0.9.1, I now get extra mypy warnings like:
I get errors (line numbers aligned):
lines 9 and the last 11 are related to the above - transition.dest potentially being This second issue was there before, I guess the code needs updating as well as |
internal transitions have no destination 1. In this case The other issues look like incomplete typing for 'protected' functions. I'll have a look. |
What's your type checking setup? VSCode (Pylance?) can infer from |
I reworked the naming and signatures a bit. I wanted to keep the signatures comparable to how transitions can be defined as of now (lists of lists and dicts) but wanted to provide the convenience of from transitions.experimental.utils import with_model_definitions, event, add_transitions, transition
from transitions import Machine
class Model:
state: str = ""
@add_transitions(transition(source="A", dest="B"), ["C", "A"])
@add_transitions({"source": "B", "dest": "A"})
def foo(self): ... # version A
bar = event(
{"source": "B", "dest": "A", "conditions": lambda: False},
transition(source="B", dest="C")
) # version B
@with_model_definitions
class MyMachine(Machine):
pass
model = Model()
machine = MyMachine(model, states=["A", "B", "C"], initial="A")
model.foo()
model.bar()
assert model.state == "C"
model.foo()
assert model.state == "A"
|
…ils.generate_base_model` When `model_override` is set to True, Machine will assign only methods to a model that are already defined. This eases static type checking (#658) and enables tailored helper function assigment. Default value is False which prevents override of already defined model functions.
…ns, event, with_model_definitions, transition} Add helper functions to define transitions on a model class for better type checking (#658)
…ils.generate_base_model` When `model_override` is set to True, Machine will assign only methods to a model that are already defined. This eases static type checking (#658) and enables tailored helper function assigment. Default value is False which prevents override of already defined model functions.
…ns, event, with_model_definitions, transition} Add helper functions to define transitions on a model class for better type checking (#658)
The mentioned features have been merged into master. I will close this issue. If you face other obstacles -- typing-related or other -- please open a new issue. Thanks again for your feedback! |
Since transitions uses runtime decoration quite excessively static type checkers have a hard time to analyse trigger and convenience functions. This has been discussed in previous issues:
Functions decorators or wrapper functions might help here:
Furthermore one could consider a new flag that attempts to create convenience functions that follow common style guides. See for instance #385 where the use of enums result in functions such as
is_ERROR
that most linters will complain about.The text was updated successfully, but these errors were encountered: