-
Notifications
You must be signed in to change notification settings - Fork 78
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
FIX: Account for custom scalar types of incoming values #737
base: master
Are you sure you want to change the base?
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## master #737 +/- ##
==========================================
- Coverage 58.65% 56.87% -1.78%
==========================================
Files 88 88
Lines 9984 9550 -434
==========================================
- Hits 5856 5432 -424
+ Misses 4128 4118 -10 ☔ View full report in Codecov by Sentry. |
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.
Minor suggestions.
for base_type in [int, float, str, np.ndarray]: | ||
if self.channeltype != base_type and issubclass(self.channeltype, base_type): | ||
# Leave bool and int separate, we're concerned about user-defined subclasses, not native Python ones | ||
if self.channeltype != bool: | ||
self.channeltype = base_type | ||
break |
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.
Not sure how often code gets called, but wouldn't it be more efficient to test for bool once and then in the loop it can just test if something is a subtype.
if self.channeltype !=bool:
for base_type in [int, float, str, np.ndarray]:
if issubclass(self.channeltype, base_type):
self.channeltype = base_type
break
Notice the check for self.channeltype!=base_type is unnecessary. If it is int
then it would pass the subclass check and set channeltype to int
which it already was and then break.
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 agree with @Thrameos's sentiment here. value_changed
gets called a lot, maybe more than any other method in all of PyDM, so it pays to make this check as lightweight as possible.
Maybe something like this?
if self.channeltype not in (int, float, bool, str, np.ndarray):
for base_type in (int, float, str, np.ndarray):
if issubclass(self.channeltype, base_type):
self.channeltype = base_type
break
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.
If this is a critical section then skipping the for
loop and the implicit loop from "not in" entirely would be the best policy. Instead use a global table cache which would be O(1).
value_type = type(value)
self.channeltype = TYPE_TABLE.get(value_type, None)
if self.channeltype is None:
# assume we will use the type as is
self.channeltype = value_type
# check for user defined subtypes
for base_type in (int, float, str, np.ndarray):
if issubclass(self.channeltype, base_type):
self.channeltype = base_type
break
# Cache the result so all future look ups are O(1)
TABLE_TYPE[value_type] = self.channeltype
The table would be prepopulated with TYPE_TABLE[bool] = bool
Each time it is called we have one hash map look up and a comparison. It it is not found then the search occurs to find the best type. That result is cached so it never has to evaluate the loop for that type again.
Wouldn't it be best to address this conversion over at the data plugin and let PyDM deal with the basic types? |
That depends on the perspective. If there were multiple plugins based on JPype, each of them would need to handle that separately. |
@hhslepicka , what would be an acceptable performance hit in this method?
|
@ivany4 Perhaps you can make up for the difference by removing the logic
Just add |
Hi @ivany4, sorry for the silence. @hhslepicka left SLAC, and I have only been able to spend tiny amounts of time on PyDM lately. I am still against including this in the base widget, I agree with @hhslepicka - this seems like it should happen at the plugin layer. I would guess you could have a common base class for a JPype-based plugin, and implement the conversion to a base type there. Your most recent implementation seems reasonable from a performance standpoint, but I'm objecting for architectural reasons. If there's a strong reason why this can't be implemented in the data plugin, I'd be willing to merge. |
Thanks for the feedback, @mattgibbs . Sad to see @hhslepicka leave :/
I'm currently researching the possibility to monkey-patch |
Monkey patching the equals for a type will undoubtedly create unexpected side effects in users code. I can't say what is would break only that I would certainly not add such a hack to JPype release nor recommend hacking to add a bad contract just to fix another libraries code. JPype is not doing anything special here as other toolkits such as numpy as doing the same. The existing code is special cases here for "np.array" and "unicode" already but I could see the exact same problem happen of "np.float32" or other external types. So this is not really a JPype issue by rather an issue with how pydm is using the type information. Using the "type" as a look up for an action as is being done here is a recipe for disaster as it violates the concept of duck typing. One should be able to define a type that meets the specification (or in this case merely derives from a base type) without breaking code. I would recommend that the hash solution with a plugin function for constructing the channel type would be the best solution. The user has the option of controlling the channel type by installing a function and the hash solution would be faster than the existing logic as it already has several special cases. Placing a user defined plug in so that they can add a type conversion with known rules is better than requiring a monkey patch. lets consider the original code
In this the type of the channel is doing 2 special cases one of which will generate an exception which is clearly not a very great performance. Also any derived types for np.ndarray will also miss the if clause. So there is already problems with the existing code. The alternative would be.
Now the user can install whatever hooks they want to ensure that derived types chose the appropriate base. This doesn't force any solution and is not particular to JPype nor is magical for ints or floats. The user has to install a handler to get the alternative behavior. |
We are using JPype, which translates Java objects to Python. Recent versions of this library convert Java
int
,double
and similar toJInt
,JDouble
, which are custom subclasses of corresponding Python types.Now, when such value arrives on a channel, it results in broken logic, once its type is recorded with
self.channeltype = type(value)
, namely:self.channeltype == float
will evaluate toFalse
, whenJDouble
is in playself.send_value_signal[self.channeltype].emit()
will enumerate through all available subscripts, not finding an overload forJDouble
, it will pick the last available, i.e.np.ndarray
. The resulting exception is very confusing:This PR tries to record
self.channeltype
as base types, where appropriate.