-
Notifications
You must be signed in to change notification settings - Fork 426
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
Refactor: Avoid imports inside functions #2262
Comments
Yes, a good idea, the question is how. |
I am working on it and achieved some progress, but some imports are really hard to replace. There are several tricks to achieve it, but the code structure with tight coupling doesn't really help to achieve the goal. However, as tight coupling needs to be removed for unit tests as well, it is a necessity and part of the necessary cleanup. Simple tricks to avoid circular dependencies are
More advanced
|
Oh, I did a bit of work too. Most of what I did was to look at the major circularity in the logitech_receiver modules. The problem is that there is a central structure - DEVICES in descriptors.py - that is used in low-level code but includes information from setting_templates.py. Splitting the construction of this structure would eliminate the major (and maybe only) circularity there. I have an idea on how to do this. If you are not working on these modules then I can proceed. Then there is also the use of GUI calls in logitech_receiver. |
Yeah carry on. |
PR #2265 breaks up the imports loop with descriptors but there still remains an import loop in logitech_receiver |
There is (only) one remaining imports loop inside logitech_receiver. One way would be to have a run-time structure that contains functions that are called in loops. Is this a Pythonic way of doing things? |
PR #2265 breaks all the import loops inside logitech_receiver. There are about 10 calls to GUI functions that still need intra-function imports. |
PR #2265 breaks all the import loops inside the rest of the code, except for the calls to GUI functions from logitech_receiver code. |
PR #2265 also moves almost all of the other imports to the top of files. The exceptions are conditional imports. |
Looks like you have achieved quite much lately. Most importantly are the ones marked with the circular import comment. It's great when they are gone. |
Unfortunately these are the ones that are difficult to get rid of without changing how the code works. The question is what is the best way to proceed. One way would be to have some connection data like a class containing the entry points needed to tickle the GUI. Is that a good way to proceed? |
I don't think this will go into release 1.1.11 as it touches a lot of files in non-trivial ways. |
I have never worked with GTK and similar UI, I need to check out the code to suggest something. But Protocols are good for decoupling stuff and with callbacks that could make the components more "self contained". |
That's probably the way to decouple the GUI and the rest of the code. But a lot of code in logitech_receiver is concerned with interpreting information received from devices that support HID++ messages. A good example is hidpp20.py where just about every function and method uses the call feature_request, which gets HID++ information from a device. Testing these functions, in their current state, requires some way to simulate how this function works. About the only functions that do not use feature_request are associated with the lighting and profiles classes. This code has to build up objects from a chunk of binary data that is stored on the device and that data can be obtained in several ways. So there is code that obtains the binary data from a device and code that uses this binary data to construct useful objects. But more typical is code like:
It doesn't seem reasonable to split this into a bit that gets the binary data and a bit that extracts structure from the binary data. It is useful to do a unit test on this that checks that the code sends correct HID++ messages and does the right thing with the result. But to do a unit test here requires simulating how feature_request works, or simulating how something that feature_request calls works, and this requires some sort of HID++ I/O simulation. A separate issue here is that there needs to be a way to construct the device structure, but that can also be done if there is a way to simulate HID++ I/O. |
I'll check out the piece of code the next days at least. The feature requests can be mocked or passed as callback, need more context to propose an improvement. |
Independently, I suggest adding type hints, as this really helps with code understandability. And it is easy to grasp, what a function returns or expects. |
I was looking at how to mock these calls - this seems like a good way to set up the tests. And, yes, type hints would help a lot. I should have been adding them as I made changes, but a lot of Solaar code dates from before type hints, and I got into the habit of not using them when I made changes. |
I started a document on the implementation of Solaar, available at https://pwr-solaar.github.io/Solaar/implementation, partly for my own use. You might want to take a look at it. |
Thanks, that's helpful. |
Solaar currently stores information about device using two different mechanisms. Some information is stored in device objects. Some settings store their own inforation. (Some settings, mostly the ones that use complex data, use information stored in device objects.) Worse, changes to data that do not come from the GUI use several mechanisms to update the GUI including direct calls to GUI code that generally require in-function imports to prevent circular imports. This is not a good idea and the storage of device data should be regularized. There are two differnt ways that I see to go. The first way is to store all information directly on device objects. The device class is responsible for reading the information from a device, caching the information locally, and writing changed information to the device. Settings then use this data. The device class stores callbacks for each chunk of information, which are invoked whenever the information changes. (Except that maybe calls to change information can provide a callback not to be invoked.) This achieves separation at the cost of requiring setting up separate device information and settings. The second way is to store all information in settings of device objects. The device class is just responsible for creating these settings. The setting contains methods to read, cache, and write the setting information. Each setting can have callbacks, which are invoked when the setting information is changed. This only requires one class for each chunk of information instead of, potentially, two. I'm (reluctantly) leaning towards the first way because it allows GUIs to set up their own settings, perhaps just to have an order for settings. In any case there should be support to reduce the required boilerplate: Here is how this might work for some simple data, here a boolean value read and written by a feature call that is used only for this purpose
The information is stored under the name "CrownSmooth". The information type is bool. Data is read using command 1 of feature CROWN and written using command 2. The relevant data is in the second byte of read messages and should be prefixed with a 0x00 byte when writing. The data value for True is 0x1 and for False is 0x2.
The setting is presented using the given label and tooltip. It uses the CrownSmooth data of Device and has the datatype of that data. Here is a slightly more complex setup, using integer values that are further interpreted in the setting.
The data is an integer read as the first byte of command 0 of feature BACKLIGHT and written using command 1.
There are only a few possible values, given above along with display names for them. The currently valid values are always the same as the possible ones. Here is a situation where the data is stored as a structure and there are several settings that access the structure.
|
PR #2312 adds a callback to signal changes to settings. This removes the remaining imports inside functions, except for one to show an on-screen notification. @MattHag Any suggestions for removing this last non-top import? The notifications use icons from the GUI, but the icon code and the notify code doesn't depend on anything else in the GUI and could be moved up a level, which probably would allow the imports to be at top level. Alternatively, there could be a callback added for this purpose, freeing the code in logitech_receiver from any dependence on code in the lib/solaar tree. If this is done, what's a good way of setting up and populating the callback? |
I couldn't find the remaining part. |
It's in settings_templates around line 778
|
A callback passed to the class instantiation should be the easiest for this issue. However, to get rid of the dependency from the module, the factory build needs to be moved to a new module too. In this case (callback or object etc. from another module is required) the instantiation has to happen in another module, as this instantiation of DivertKeys requires two otherwise independent modules and tightly couples them at instantiation. The basic idea behind this is the factory pattern. In this particular case, the setup is not much code and does not have conditions, but the decoupling is what helps us here. OT: |
@MattHag The desired end-state is to disconnect the logitech_receiver code and the Solaar code so that they can be run in different processes. That's a ways off. |
I think that all these are gone now. |
Duplicating a complex code piece should never be necessary. Instantiation aka tight coupling of a class, that relies on several modules should just happen outside all the related modules e.g. in a new file factory.py, which depends on the implementation of notify and logitech_receiver. |
Yes, looks quite good. As last step for this ticket I‘d remove all those import as statements with module level imports and no prefixing with underscores. |
This has to be done a bit carefully to guard against name clashes. |
Yes. The topic of this ticket is achieved and we can close this ticket. There's still #2273 to handle code cleanup. |
Information
Is your feature request related to a problem? Please describe.
No
Describe the solution you'd like
Imports should always be on top of a module. To prevent circular dependencies, some imports were moved into function calls. Remove this code smell by refactoring the code accordingly.
The text was updated successfully, but these errors were encountered: