-
Notifications
You must be signed in to change notification settings - Fork 178
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
feat(api): runtime parameters API for adding and using default parameters in protocols #14668
Conversation
…p_papi_parameter_definitions
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## edge #14668 +/- ##
==========================================
- Coverage 67.34% 67.34% -0.01%
==========================================
Files 2485 2485
Lines 71439 71411 -28
Branches 9057 9042 -15
==========================================
- Hits 48114 48095 -19
+ Misses 21173 21171 -2
+ Partials 2152 2145 -7
Flags with carried forward coverage won't be shown. Click here to find out more.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few suggestions, but LGTM!
prop = property( | ||
fget=lambda s, v=variable_name: Parameters._getparam(s, v) # type: ignore[misc] | ||
) | ||
setattr(Parameters, variable_name, prop) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of suggestions for simplifying this.
First, instead of the user-facing API being like this:
def run(context):
sample_count = context.params.sample_count
Have you considered making it this?
def run(context):
sample_count = context.params["sample_count"]
- This gives us namespacing in case we ever need to add more methods like
params.get_all()
. - This matches parameter usage more closely to parameter declaration. In the declaration, the protocol author wrote
variable_name="sample_count"
, with a string literal, so they pass the same string literal here. - This is arguably more straightforward and easier to explain.
Or, taking the context.params.sample_count
syntax for granted, an implementation-level suggestion.
Right now, when we initialize the parameter foo="bar"
, we:
- Set a private attribute
self._foo = "bar"
- Set a public attribute
self.foo
to a read-only property that returnsself._foo
- Store
"foo": "bar"
in theself._values
dict
Does this get easier if you treat self._values
as the single source of truth, and override __getattr__()
(and possibly __setattr__()
) to delegate to it? In other words, instead of dynamically adding attributes, implement dynamic attribute lookup. Then you don't need the f"_{variable_name}"
magic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now I'm going to leave the syntax the same, but I will take your suggestion to simplify things by using self._values
as the source of truth for the fget
function of the property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will be moot if you use self._values
as the source of truth, but I've also just realized that in setattr(Parameters, variable_name, prop)
, it looks like that's mutating the Parameters
class, which will leak between protocols?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefix this file name with an underscore unless you want from opentrons.protocol_api import parameter_context
to be a thing that protocol authors are allowed to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The would be no code execution reason to import the module, but since its the most public facing part of this (as far as the API) I'm alright keeping it without an underscore. Plus it would allow as user to do this pattern of typing the add_parameters
function.
from opentrons.protocol_api.parameter_context import ParameterContext
def add_parameters(parameters: ParameterContext):
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would solve the type annotation problem by re-exporting the class so it’s
from opentrons.protocol_api import ParameterContext
if keyword.iskeyword(variable_name): | ||
raise ParameterNameError("Variable name cannot be a reserved Python keyword.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure iskeyword
is the right thing to use here; it doesn't seem necessary or sufficient for preventing "bad" variable names.
Some slightly contrived examples:
def
is a keyword,but there's no technical reason someone shouldn't be able to name somethingIt might even be a reasonable thing to name a parameter for a labware definition load name.params.def
.- Meanwhile, things like
__class__
and__eq__
are potentially scarier, but they're not keywords, so they'd pass this check.
This is kind of reaffirming to me that the user-facing syntax should be params["foo"]
. It just seems inherently safer than params.foo
. It's your call, but if we stick with params.foo
, we should put some thought into a threat model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
params.def
, using the attribute method of parameter usage, cannot actually be used in code even though it can be set and will raise a SyntaxError
when called.
As for the second set of examples, those will be caught by the hasattr
check in the Parameters
class, so there is no chance of that being overwritten (and I think setattr
wouldn't even let you do that in the first place)
UNIT_MAX_LEN = 10 | ||
DISPLAY_NAME_MAX_LEN = 30 | ||
DESCRIPTION_MAX_LEN = 100 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this to protect against layout problems in the Opentrons App and ODD?
If so: It's up to you, but I think you'll have better results if you leave it to the frontend to truncate or ellipsize overly long names. Here's a contrived display name of len()==15
that will certainly not fit in a space designed to fit "up to 30 characters."
﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽﷽
Or, if this is just to protect against extreme resource usage, I would set the limits to something like 1000.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've made a deliberate choice not to truncate or ellipsize long names on the frontend to maximize the user experience for lab operators and force protocol authors to choose their names with care. But you bring up a good point with this and I'll bring it up with app&ui on the protocol authorship team.
@property | ||
def params(self) -> Parameters: | ||
return self._params |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might want a @requires_version
decorator or a ticket for it, before we forget.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can create a feature flag for now and put it behind that. Replace it with API level check before release. The feature flag will be useful for other backend things too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! I like the dynamic property creation! @SyntaxColoring has some good points that it's not as easily safe as having property getters but we'll do thorough testing and adding safeguards so that there's no scope for misuse.
I think we definitely should put the context.params
property behind a requires decorator. Are you planning on doing that in a follow-up?
…ters in protocols (#14668) Implements proposed runtime parameter API for adding and defining parameters and using them within the protocol
Overview
Addresses AUTH-67 and AUTH-68.
This PR implements both the current proposed runtime parameter API for adding and defining parameters to your protocol, as well as the means to do so. This only allows usage of the defaults in the protocol, you cannot set it to other values and they are not reported in analysis.
This is accomplished by injecting a new
ParameterContext
into anadd_parameters
function that is defined in the protocol, similar to how we injectProtocolContext
into therun
function. This contains the user facing API for adding parameters to your protocol.Once all parameters are parsed and validated (if they fail this they will raise a user friendly error with the correct line number), they are injected into the protocol context under a new
context.params
object. This will have all the parameters as dynamically set attributes of this class using the set variable name (and asproperty
s to enforce them being read only) that can then be used in the protocol as needed.For parameter definitions, both the constraints and the default values are verified to ensure the protocol will not analyze unless parameters have been defined properly.
Test Plan
This protocol is able to analyze on the app and robot, as well as run without any errors.
Changelog
ParameterContext
class for use inadd_parameters
special protocol functionProtocolContext
now has aparams
attribute that holds all the parameters as dynamically set attributesParameterDefinition
class to hold all parameter definition data that will be reported in analysis and passed to the robot serverReview requests
General code structure feedback
Risk assessment
Low/medium, this is a brand new API but it is completely encapsulated from the run context. The most dangerous part of this is the dynamically set attributes but I believe we are doing enough checks on variable name and checking existing attributes to prevent the user from doing anything dangerous.