-
Notifications
You must be signed in to change notification settings - Fork 3
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
GUI: add Diff-view page #112
Conversation
…er diff model subclasses
…ootTreeView model class specification
…inheritance and mixins
…o select entries for diff comparison from shared views
I'd like to request reviews here, focused mostly on the design/organization of the code. If you have the patience to QA the diff highlighting, I welcome that as well, but I do intend to circle back to that in the near future. |
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 don't think I recommended any actions in my comments, it's all questions, so maybe I'm just a confused person. The code is clean so it was easy to focus on the small things.
@@ -49,6 +56,17 @@ def __repr__(self) -> str: | |||
|
|||
return repr_str | |||
|
|||
@property | |||
def type(self) -> DiffType: | |||
if (self.original_value is not None) and (self.new_value is None): |
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.
Are there any data types in superscore
that allow None
as a valid value?
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.
This is a good point, there are some fields in our dataclasses that allow None
(optional fields). Though I don't think this changes how the DiffItem
interprets values going from something -> None
, this would still be a deletion as far as the diff types go. (or an addition for None
-> something)
elif (self.original_value is not None) and (self.new_value is not None): | ||
return DiffType.MODIFIED | ||
else: | ||
raise ValueError("DiffItem original and new values are both None") |
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.
This implies that DiffItem
is only created when there is some modification, which I assume must be true. Is there any value in including a DiffType.NOT_MODIFIED
or would this be unnecessary?
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.
Currently this is true; DiffItems
are only created when modifications are detected (or at least that's my intention). I imagine it's most concise to have only modifications show up, otherwise we'll have a DiffItem
for every single field in every dataclass.
Setpoint(pv_name="MY:SET", data=1), | ||
Readback(pv_name="MY:RBV", data=1), | ||
] | ||
) | ||
r_snapshot = Snapshot( | ||
title="r_snap", | ||
description="r desc", | ||
children=[ | ||
Setpoint(pv_name="MY:SET", data=2), | ||
Readback(pv_name="MY:RBV", data=1), | ||
Readback(pv_name="MY:RBV2", data=2), |
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 test fixture sufficient? I haven't read on yet but I imagine that this includes ADDED
and MODIFIED
but skips over DELETED
.
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.
This is certainly not a sufficient test case. I definitely need to build out the tests, I was hoping to skate by this time and circle back to it.
def __call__(cls, *args, **kwargs): | ||
if cls._instance is None: | ||
cls._instance = super().__call__(*args, **kwargs) | ||
return cls._instance |
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 writing this out because I'm trying to build understanding.
My base-level understanding of what this does is:
- When we use this as a metaclass,
QtSingleton()
(__init__
) is used instead oftype()
to construct the subclass. - Then, the resulting class's constructor runs through
__call__
- The first time we create this class, it creates like normal and caches the result onto the class itself
- The second time onward, the previously created class is returned instead
- This is done in a way that makes qt happy in that each "instance" of the singleton is literally the same object so that the signals are literally the same objects and then things work as we'd hope
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.
Yes, for the most part. The other way singletons may be created in python classes is with the __new__
dunder method, caching the first instance of the class in a similar way but with a different dunder method.
This effectively returned a singleton with each instantiation request, but also cleared signal connections along with it. I don't entirely understand why this happens, but I do know the metaclass approach with __call__
works.
A rabbit hole I'll jump down at some other point
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 bet it has something to do with the precise way pyqt bridges the gap between python and the C objects for the purpose of qt signals...
""" | ||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self.inverse = {} |
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.
General design question: does having a separate inverse
dict have some advantages over putting the inverse mapping into the same dictionary as the forward mapping?
E.g. this could have also been implemented as
dict[key] = value
(internally, also do dict[value] = key)
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.
This just made it simpler for me to inspect the dict through the normal repr, it looks like a normal dict but I can access either the key or the value. Your implementation works similarly well, I imagine.
|
||
|
||
# Entries for comparison | ||
class DiffDispatcher(QtCore.QObject, metaclass=QtSingleton): |
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.
It's not obvious to me what the value is in having this be a singleton. Will this be created and re-used in multiple contexts? What's the consequence of these contexts not referring to the same singleton object? Would it have been possible to refer to and reference the same object again?
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.
Yes, this is meant to allow us to signal between widgets without navigating through a parent-child chain.
In this case, the views
module holds onto the left/right entries used for comparison, and uses that information to create the context menus for the common table/tree views. By having this dispatcher be a singleton, we can subscribe to that signal from the top level Window and open the diff page when the DiffDispatcher
emits. We grab this singleton DiffDispatcher
(really primarily for holding a singleton signal) in the Window
widget and also each of the common views.
Without this, we'd have to fall back to the open_page_slot pattern where we pass a callable through to every page through to the common views. This has already become onerous, and I've considered making the Main window a singleton so that we can access its members easily from any child page (without walking up a questionably connected parent chain).
Would it have been possible to refer to and reference the same object again?
This is what I was trying to achieve with the singleton construction. Do you mean achieving this not with some forced singleton, but with a single object that we make/get with some factory function helper? I thought that this approach was a bit more bulletproof, guarding against unintentional module-level access. (Or maybe I've misinterpreted this comment)
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 understand now! Thanks
@ZLLentz Any other thoughts here? Bugging you because Devan is out this week and I want to build on top of this |
I have nothing else- my questions have all been fully answered and the diff looks nice |
Description
__new__
singleton creation method doesn't quite work. While the singletons are created correctly, the boundQtCore.Signal
's lose their connections every time an instance is returned__init__
doing something to the class variable signals. I am too tired to investigate furtherThis description is so minimal for the amount of time I spent spinning on this 💀
This is likely still riddled with bugs, but I want to get eyes on it before it bloats into a small planet of a diff. I'm happy to revisit this in follow-up PRs, but I feel like there are other more pressing tasks to take on. (And I'm tired of thinking about diffs)
Outstanding tasks:
Motivation and Context
#48 in part
How Has This Been Tested?
I added a smoke test. I should have probably added more
Where Has This Been Documented?
This PR. I'll go through for a docstring pass shortly
Context menus have been added to all non-main-window views
Highlighting helps, but could be more thorough / accurate
HIghlighting for entries with PV-entries
Pre-merge checklist
docs/pre-release-notes.sh
and created a pre-release documentation page