How to modularize property creation in PySide - python

I am largely following the instructions here for using properties and I could just use the Person object given there as the backend, but that is not very useful. I am trying to figure out how to do the following two things:
Use multiple instances of multiple such classes in a backend and connect them in a way that PySide/QML doesn't complain
Allow customization of the backend by modules to be determined at runtime (i.e. I eventually want to componentize the application - have different components implementing an interface, the component contributes separately to both the GUI and the backend; but this question concerns only the backend)
This is in contrast to simply defining all of these Properties along with their setter and getter on the main backend class (which I was able to do), which is what I mean by modularize in the question.
I modify the Person example from the link to make it something the UI can change and give it an extra attribute for kicks...
person.py
from PySide2.QtCore import QObject, Signal, Property
class Person(QObject):
def __init__(self, name, age):
QObject.__init__(self)
self._name = name
self._age = age
def getName(self):
return self._name
def setName(self, name):
print(f"Setting name to {name}")
self._name = name
def getAge(self):
return self._age
def setAge(self, age):
print(f"Setting age to {age}")
self._age = age
#Signal
def name_changed(self):
pass
#Signal
def age_changed(self):
pass
name = Property(str, getName, setName, notify=name_changed)
age = Property(str, getAge, setAge, notify=age_changed)
Just as an example I'll create two instances of Person. The first instance I have created as a class member. This is not really what I want, but closer resembles the way properties were used in the link. The second instance is what I really want which is that the properties are instance members, so that I can add them from elsewhere in the application at runtime. Neither method currently works
main.py
import sys
from os.path import abspath, dirname, join
from PySide2.QtCore import QObject, Property, Signal
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from person import Person
class Backend(QObject):
def __init__(self):
QObject.__init__(self)
def registerProperty(self, name : str, prop):
setattr(self, name, prop)
person1 = Person("Jill", 29)
if __name__ == '__main__':
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
context = engine.rootContext()
# Instance of the Python object
backend = Backend()
# simulate properties added by another module
backend.registerProperty("person2", Person("Jack", 30))
qmlFile = join(dirname(__file__), 'view3.qml')
engine.load(abspath(qmlFile))
# Expose the Python object to QML
context.setContextProperty("backend", backend)
# I tried this but it did nothing
# context.setContextProperty("backend.jack", backend.jack)
# context.setContextProperty("backend.jill", backend.jill)
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
finally the view3.qml file is simply
import QtQuick 2.0
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtQuick.Window 2.12
ApplicationWindow {
visible: true
ColumnLayout {
TextField {
implicitWidth: 200
onAccepted: {
backend.person1.name = text
}
}
TextField {
implicitWidth: 200
onAccepted: {
backend.person1.age = text
}
}
TextField {
implicitWidth: 200
onAccepted: {
backend.person2.name = text
}
}
TextField {
implicitWidth: 200
onAccepted: {
backend.person2.age = text
}
}
}
}
When I try to set any of the values in the UI the error is always the same (the error appears against the QML file)
TypeError: Value is undefined and could not be converted to an object
Ultimately I would like to have such objects nested to any arbitrary depth. Is there a way to achieve what I am trying to do here? Or am I maybe completely off track with the way I'm setting about this?

I don't know that I'm qualified to advise you on overall architecture design for GUI apps, but I think I can explain what's going wrong, and suggest a way to do what you describe. Your registerProperty method adds a Python attribute, but as you've seen, that doesn't make it visible from QML.
The bad news: Qt properties cannot be added to an object after it's created.
The good news: You can create a Qt property that's a list (or a dictionary), and add to it.
One pitfall to be aware of is that to expose a list to QML, you specify its type as 'QVariantList'. (For dictionaries, use 'QVariantMap', and make sure your keys are strings.)
Here's an example of how your Backend class could look. (Using super() to access the parent class means you don't have to pass self to its initializer.)
from Pyside2.QtCore import QObject, Property, Signal
class Backend(QObject):
def __init__(self):
super().__init__()
self.people = []
people_changed = Signal('QVariantList')
#Property('QVariantList', notify=people_changed)
def people(self):
return self._people
#value.setter
def people(self, new_people):
self._people = new_people
self.people_changed.emit(new_people)
def add_person(self, name, age):
self.people.append(Person(name, age, self))
# Note: only ASSIGNING will automatically fire the changed
# signal. If you append, you have to fire it yourself.
self.people_changed.emit(self.people)
This will keep QML up to date as you add people; you could also create a similar method to remove people. Parenting each person to the Backend object makes sure Qt will keep references to them as long as your Backend still exists.
For a truly dynamic collection of properties, perhaps you could give your top-level Backend object a dictionary that your other components add to. So backend.people would become backend.properties['people'], and a specific module would be responsible for adding that key to the properties dictionary, then adding to and removing entries from it.
Specifying all those getters and setters is a hassle, isn't it? I spent so long feeling like there must be a better way, and only recently came across a solution here on Stack Overflow. Using this metaclass, your Person class and the Backend I posted above could be simplified to:
from PySide2.QtCore import QObject
# Assuming you save the linked code as properties.py:
from properties import PropertyMeta, Property
class Person(QObject, metaclass=PropertyMeta):
name = Property(str)
age = Property(int)
def __init__(self, name, age, parent=None):
super().__init__(parent)
self.name = name
self.age = age
class Backend(QObject, metaclass=PropertyMeta):
people = Property(list)
def __init__(self):
super().__init__()
self.people = []
def add_person(self, name, age):
self.people.append(Person(name, age, self))
# Automatically emits changed signal, even for in-place changes
(I also changed age to an int, but it could still be a str if you need it to be.)

Related

Can I add specificity to a kwarg in a subclass' constructor?

I am trying to get PyCharm to understand that the subclass of my base controller class only takes a specific type of widget.
Minimal example:
import tkinter as tk
class BaseWidgetController:
def __init__(self, parent: 'tk.Widget'): # Parent is always __some__ kind of widget
self._parent = parent
class EntryWidgetController(BaseWidgetController):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._parent: 'tk.Entry' # On this class, I want Pycharm to understand _parent is only ever an Entry (a subclass of tk.Widget), but even adding this line doesn't change its mind.
def say_type(self) -> None:
print(type(self._parent)) # PyCharm still thinks _parent is a tk.Widget
ew = EntryWidgetController(parent=tk.Frame())
ew.say_type() # Obviously this works fine at runtime.
If you want to constrain the EntryWidgetController so that it only accepts tk.Entry or subclasses, the fix is rather simple - just do
class EntryWidgetController(BaseWidgetController):
def __init__(self, parent: 'tk.Entry', **kwargs):
super().__init__(parent=parent, **kwargs)
That way
ew = EntryWidgetController(parent=tk.Frame())
will make PyCharm complain that Expected type 'Entry', got 'Frame' instead.

Auto-generate unique ID for each class type

I have an interesting problem. I would like to write a class, which when inherited provides to all children classes the following behavior:
sets its self.id attribute to a UUID value
-- if this is the first time class got instantiated, a new UUID is generated
-- the UUID value is reused for the same class, when it is instantiated many times
Now, the fun part: I want the mechanism above to work, regardless of the path used to instantiate this class. Let's assume the following:
from .Package.Class1 import Class1
from TopPackage.Package.Class1 import Class1
from .Package.Class2 import Class2
from TopPackage.Package.Class2 import Class2
In both situations, I would like Class1 to generate the same self.id value in both import styles. I would also like Class2 to generate a different self.id value from Class1, but the same between its own import styles.
So far, I wrote the following code for a class classes 1 and 2 would inherit from:
class ClassWithId(ABC):
_EXISTING_OBJECT_IDS = dict()
def __init__(self):
if self in ClassWithId._EXISTING_OBJECT_IDS.keys():
self.id = ClassWithId._EXISTING_OBJECT_IDS[self]
else:
self.id = uuid.uuid4()
ClassWithId[self] = self.id
However, I have a few problems in here:
ClassWithId must inherit from class ABC because it is also an interface for classes 1 and 2
trying to put self as key in dict() results in TypeError: 'ABCMeta' object does not support item assignment
I am generally unsure, if this approach is going to be resistant to different import styles, because from Python's perspective class type .Package.Class1.Class1 and TopPackage.Package.Class1.Class1 are 2 different objects
Any ideas?
UPDATE:
I have integrated Elrond's suggestion into my code, and but different import levels (package-wise) yield different UUID values for the same class:
<class 'StageTwo.Steps.SsTestHandler1.SsTestHandler1'> 3583c89c-5ba8-4b28-a909-31cc27628370
<class 'tests.TestStages.StageTwo.Steps.SsTestHandler1.SsTestHandler1'> f4ead4a0-f5f7-4d95-8252-0de47104cb2f
<class 'StageTwo.Steps.SsTestHandler2.SsTestHandler2'> 8bd9a774-0110-4eee-a30c-a4263ad546cf
<class 'tests.TestStages.StageTwo.Steps.SsTestHandler2.SsTestHandler2'> 773d84c4-82a3-4684-92b5-51509e6db545
Maybe I forgot to mention, but my ClassWithId is inherited by ClassX and ClassY down the line, and it is these 2 classes, which are expected to be resistant to the situation I have shown above (being imported with different path, yet still retaining the same UUID).
You will need to use a metaclass for this:
import uuid
import abc
class BaseMeta(abc.ABCMeta):
def __new__(mcs, name, bases, attrs):
attrs['_class_uuid'] = uuid.uuid4()
return super().__new__(mcs, name, bases, attrs)
class Base(metaclass=BaseMeta):
def __init__(self):
print(self.__class__.__name__, self._class_uuid)
Now all classes that inherit from Base will be assigned a uuid via the _class_uuid property, once per subclass:
from package1.class1 import Class1
from package2.class2 import Class2
Class1() # 6e0852c8-61c9-4f8b-9817-eeeda4b49d56
Class1() # 6e0852c8-61c9-4f8b-9817-eeeda4b49d56
Class2() # 73012f1a-a984-4f76-96f1-ef5225a38fbe
Class2() # 73012f1a-a984-4f76-96f1-ef5225a38fbe
Using absolute/relative imports shouldn't make a difference in either case.
from Python's perspective class type .Package.Class1.Class1 and TopPackage.Package.Class1.Class1 are 2 different objects
If I understand what you're saying here, I don't think this statement is true. Hopefully, my example below will clarify.
There might be other approaches, such as using the class name as your dictionary key, but perhaps a more extendable approach is with a metaclass.
I placed everything in one file, for the sake of simplicity, but the approach remains the same for a module with multiple levels:
TopPackage.py:
import uuid
class ABC:
pass
def id_assigner(class_name, class_parents, class_attrs):
class_attrs['id'] = str(uuid.uuid4())
return type(class_name, class_parents, class_attrs)
class Class1(ABC, metaclass=id_assigner):
pass
class Class2(ABC, metaclass=id_assigner):
pass
def foo():
c2 = Class2()
print('c2.id from foo: ', c2.id)
And a script to test it:
import TopPackage
c1_a = TopPackage.Class1()
c1_b = TopPackage.Class1()
print(c1_a.id)
print(c1_b.id)
c2_a = TopPackage.Class2()
c2_b = TopPackage.Class2()
print(c2_a.id)
print(c2_b.id)
TopPackage.foo()
The call to TopPackage.foo(), I hope, shows that using Class2 from different location will result in the same class definition, and hence the same id. When I run this test script an example output I get is:
c69b17e0-9ff0-4276-bcce-6ac4f5e5a2e5
c69b17e0-9ff0-4276-bcce-6ac4f5e5a2e5
86fbe02e-d411-4ba1-b292-d2b1ec2100bd
86fbe02e-d411-4ba1-b292-d2b1ec2100bd
c2.id from foo: 86fbe02e-d411-4ba1-b292-d2b1ec2100bd

How to design a complex Bokeh Application?

I am programming a bokeh application. I want to split the functionalities into different files. But I want to have some attributes accesible from every class, these attributes should be shared and always updated. For example an attribute which stores a dataframe that all the plots are going to use. So I think I have at least two possible solutions:
Use a big class and include the attributes and methods of other files:
class Bigclass(object):
from bk_plots import p1, p2, p3
from bk_data import d1, d2, d3
from bk_layout import ly1, ly2
from bk_events import ev1, ev2
# unfortunately, "from classdefn import *" is an error or warning
num = 42 # add more members here if you like
Note: this solution was copied from here (partial classes)
Or I could use inheritance. The parent will have the shared attributes. The perk of this system is that I would need to send the rest of the object references to every subclass
class Parent():
shared = 'parent'
class Plot(Parent):
def __init__(self):
Parent.shared = 'plots' # update class variable from this class
# I would need to have references to the objects of other classes
class Data(Parent):
def __init__(self):
Parent.shared = 'second'
# [...]
Is there a better way to do this? Which option will bring me less problems?
Finally I have created an my_bokeh_app folder. There I have an __init__.py file with this content for the initialization:
from my_bokeh_app.bokeh_data import BokehData
from my_bokeh_app.bokeh_plots import BokehPlots
from my_bokeh_app.bokeh_table import BokehDataTable
from my_bokeh_app.bokeh_events import BokehEvents
from my_bokeh_app.bokeh_layout import BokehLayout
BokehData()
BokehPlots()
BokehDataTable()
BokehEvents()
BokehLayout()
I have created a Class to share data among all the objects. This is the class:
class BokehSharedData(object):
# ------------------- CLASS VARIABLES ---------------------- #
# This variables are shared. So all the children can access them
data_source = None
bk_layout = None
bk_data = None
bk_plot = None
bk_table = None
bk_events = None
In every class I make a reference to the BokehSharedData class. I also inherit from that class to access to the class variables.
from my_bokeh_app.bokeh_shared_data import BokehSharedData
class BokehData(BokehSharedData):
def __init__(self, **kwargs):
self.env = BokehSharedData
self.env.bk_data = self
# If for example I want to access to the source attribute from the rest of objects
# I could make this shortcut on the shared class
self.env.data_source = ColumnDataSource(...)
def update_data_source(self):
# [...]
And I could read the shared attributes or execute methods from other object:
from my_bokeh_app.bokeh_shared_data import BokehSharedData
class BokehPlots(BokehSharedData):
def __init__(self, **kwargs):
self.env = BokehSharedData
self.env.bk_plots = self
# I could use self.env.data_source here or run some method of BokehData class like this
self.env.bk_data.update_data_source()
The complete app where you can see all the classes working is here

Dynamically updated Kivy settings entry

Kivy has this awesome built-in functionality for creating a settings panel for your app.
It gives you a set of entry types you can use like string, bool, options, etc.
But all of these options are hard coded in json files, and if there something dynamic going on, what do you do?
How can you have a dynamically changing settings menu in Kivy?
Specifically, I need to have a settings panel for serial connectivity. My app's user would need to choose to which existing serial ports he wants to connect. This list can be obtained in python, but it can change at any time, so how can I keep my settings menu up to date with the current com ports availability?
There are probably several ways of doing it. Here's one of them:
Create a new type of setting, that accepts a function as a string, that will contain a full path to the function you want to call each time the user wants to look at the list:
class SettingDynamicOptions(SettingOptions):
'''Implementation of an option list that creates the items in the possible
options list by calling an external method, that should be defined in
the settings class.
'''
function_string = StringProperty()
'''The function's name to call each time the list should be updated.
It should return a list of strings, to be used for the options.
'''
def _create_popup(self, instance):
# Update the options
mod_name, func_name = self.function_string.rsplit('.',1)
mod = importlib.import_module(mod_name)
func = getattr(mod, func_name)
self.options = func()
# Call the parent __init__
super(SettingDynamicOptions, self)._create_popup(instance)
It's subclassed from SettingOptions, which lets the user choose from a drop-down list. Each time the user presses the setting to see the possible options, the _create_popup method is called. The new overriden method dynamically imports the function and calls it to update the class's options attribute (which is reflected in the drop-down list).
Now it's possible to create such a settings item in json:
{
"type": "dynamic_options",
"title": "options that are always up to date",
"desc": "some desc.",
"section": "comm",
"key": "my_dynamic_options",
"function_string": "my_module.my_sub_module.my_function"
},
It's also necessary to register the new settings type by subclassing Kivy's settings class:
class MySettings(SettingsWithSidebar):
'''Customized settings panel.
'''
def __init__(self, *args, **kargs):
super(MySettings, self).__init__(*args, **kargs)
self.register_type('dynamic_options', SettingDynamicOptions)
and to use it for your app:
def build(self):
'''Build the screen.
'''
self.settings_cls = MySettings

Making two classes reference eachother

I'm trying to make a text adventure where different "place" classes can point to eachother.
For instance, I have a Manager class that has a reference to each place. Then I have a Home class, and a Club class, with references to eachother through manager. The problem is that I can't instantiate them due to the circular reference.
Here's how I solved it, but it's ugly, because I have to create the places member inside of a method instead of __init__.
class Manager:
def __init__(self):
self.home = Home(self)
self.club = Club(self)
class Home:
def __init__(self, manager):
self.places = {}
self.manager = manager
def display_plot_and_get_option (self):
print "where do you want to go?"
return 'club' #get this from user
def get_next_place(self, place_name):
self.places = { #THIS IS THE BAD PART, which should be in __init__ but can't
'home':self.manaer.home
'club':self.manaer.club }
return self.places[place_name]
class Club:
#similar code to Home
pass
manager = Manager()
while (True):
place_name = manager.current_place.display_plot_and_get_option()
manager.current_place = manager.current_place.get_next_place(place_name)
In c++ I would set my dict up in the constructor, where it should be, and it would use the pointer of the Manager's home or club members, since I only want 1 instance of each place. How can I do this in python?
edit: expanded code example
You can just have a dictionary that holds the references, and call the methods straight from the Manager (which shouldn't really be named Manager, as it does not serve that purpose now) instance.
class Home(object):
pass
class Club(object):
pass
PLACES = {
'home': Home(),
'club': Club()
}
class Manager(object):
def display_plot_and_get_option(self):
return raw_input('Where do you want to go?')
def get_next_place(self, place_name):
return PLACES[place_name]
m = Manager()
while 1:
place_name = m.display_plot_and_get_option()
m.get_next_place(place_name)
Assuming that Home and Club are just a couple of the many places you plan to include in your game, it would probably be advantageous to create a Place class. Specific classes can either inherit from Place or have a name as a data member. Then you can model connections with a tree or graph.

Categories

Resources