Dynamically updated Kivy settings entry - python

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

Related

How to modularize property creation in PySide

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.)

how to create a set of classes and its vars and methods from a given text in python

I want to create a set of classes, its vars and methodes just from a given text configuration, espcially with django models, for exmaple i have a list of models to create in models.py
classes=["users", "posts", "commnets"]
vars= [{"a","b"},{"bb","vv"},{"aa"}]
#methods=[{....},{....},{....}] not now
in models.py
i want to make something like this to create that classes
for i,j in zip(classes,vars):
create_classes_from_string(i,j)
how can i program #create_classes_from_string assuring that it creates tables in my database with that configuration
I can view this question in 2 perspectives
Normal way of dynamically creating python class
Create dynamic django models specifically
But in both cases, the attrs should be defined as dict with the variable name and its value. Because defining a variable without a value is meaningless here.
1. Normal way of dynamically creating python class
Here we can simply use type() method to generate a python class. This can be later used to create object with their own name by adding them to locals() builtin function.
An example is mentioned below
classes = ["Class1", "Class2"]
class_fileds = [
{
'cl1_var1': "test",
'cl1_var2': 123,
},
{
'cl2_var1': [1, 2, 3],
}
]
classes_details = list(zip(classes, class_fileds)) # Python3 format
for class_details in classes_details:
class_name = class_details[0]
class_attrs = class_details[1]
class_def = type(
class_name,
(object, ), # Base classes tuple
class_attrs
)
locals().update({class_name: class_def}) # To associate the class with the script running
instance1 = Class1()
instance2 = Class2()
Outputs
>>> instance1 = Class1()
>>> instance2 = Class2()
>>>
>>> instance1.cl1_var1
'test'
>>> instance1.cl1_var2
123
>>> instance2.cl2_var1
[1, 2, 3]
Here the class names in the list, classes = ["Class1", "Class2"], can be used as such as given i.e. Class1(), Class2() etc. This is achieved by adding the variables Class1 and Class2 to the running script dynamically by using local() inbuilt function
2. Create dynamic django models specifically
Even though the basic logic remains the same there are a couple of changes required.
First of all we need to understand the dynamic model creations in Django. Django provides a clear documentation for this.
Please refer, https://code.djangoproject.com/wiki/DynamicModels
An example can be seen as below, you can directly add the below script to models.py file
from django.db import models
from django.db.models import CharField, IntegerField
# This is taken from https://code.djangoproject.com/wiki/DynamicModels#Ageneral-purposeapproach
def create_model(name, fields=None, app_label='', module='', options=None, admin_opts=None):
class Meta:
pass
if app_label:
setattr(Meta, 'app_label', app_label)
if options is not None:
for key, value in options.iteritems():
setattr(Meta, key, value)
attrs = {'__module__': module, 'Meta': Meta} # Set up a dictionary to simulate declarations within a class
if fields: # Add in any fields that were provided
attrs.update(fields)
model = type(name, (models.Model,), attrs) # Create the class, which automatically triggers ModelBase processing
return model
classes = ["Class1", "Class2"]
class_fileds = [
{
'cl1_var1': CharField(max_length=255),
'cl1_var2': IntegerField(),
},
{
'cl2_var2': IntegerField(),
}
]
models_details = list(zip(classes, class_fileds))
for model_detail in models_details:
model_name = model_detail[0]
model_attrs = model_detail[1]
model_def = create_model(
model_name,
fields=model_attrs,
app_label=__package__,
module= __name__,
)
locals()[model_name] = model_def
Output at django shell
>>> from my_app.models import Class1
>>> Class1(cl1_var1="Able to create dynamic class", cl1_var2=12345).save()
>>> Class1.objects.all().values()
<QuerySet [{'cl1_var1': 'Able to create dynamic class', 'id': 3, 'cl1_var2': 12345}]>
This model is added to django app, my_app and this would work fine and there are a few things to be noted
field attrs should be handled carefully as you are going to read that from text file
The models should be added using locals() to import that from app
Method, create_model should be taken from the reference link as it supports more features like adding admin pages etc
Data migration also works with this kind of model
My Suggestion
The above-explained methods would work without any issue and all of them are supported but one thing to be not forgotten that, there is performance difference in dynamically imported classes and real import. Also, this is a bit complex structure and any change in the code should be done very carefully to not break it up.
So my suggestion is to read the text file with configurations and generate models.py file from the configuration file using some magic script(that can also be created in python).
So every time there is a change in the text-config file you have to generate the models.py script. This way you can also ensure the Model definitions

Dynamically reload django-filter (Model)ChoiceFilters drop-down contents

I'm using a django-filter to present a drop-down list (using ModelChoiceFilter). I'm wanting to re-load the drop down contents dynamically when the user selects a certain option (that is stored in the session; not shown in the code below). Unfortunately I'm not having any luck in achieving this. I seem to be unable to address the class (as opposed to the instance) variable that defines the drop-down list. This is what I have so far (AnotherModel contains a ForeignKey relationship to MyModel)
(filter.py)
import django_filters
class MyFilter(django_filters.FilterSet)
drop_down = ModelChoiceFilter(queryset = MyModel.objects.filter(filter1))
i = 3 # A non-filter class variable
def __init__(self, *args, **kwargs):
self.drop_down = ModelChoiceFilter(queryset = MyModel.objects.filter(filter2))
self.i = 5
#classmethod
def class_method(cls):
# Throws AttributeError; type object 'MyFilter' has no attribute 'drop_down'
<edit: printing drop-down throws the error; assigning to it works>
print cls.drop_down
cls.drop_down = ModelChoiceFilter(queryset = MyModel.objects.filter(filter2))
cls.i=4 # Works fine
(views.py)
from project.filters import MyFilter
def my_filter(request):
print MyFilter.i # prints 3
# Throws AttributeError; type object 'MyFilter' has no attribute 'drop_down'
print MyFilter.drop_down
f =MyFilter(request.GET,
queryset = AnotherModel.objects.filter(xyz))
print MyFilter.i # prints 3
print f.i # prints 5 (as expected)
# Renders a drop-down populated with the values from filter1, not filter2
return render(request,'template.html', {'filter' : f})
I had initially hoped the re-assigning the instance of drop_down would work, but since that didn't I started looking at the class variable. However it's unclear to me why I'm unable to access the class variable; in both cases where its addressed directly through MyFilter.drop_down and through the #classmethod function it throws an AttributeError, that the type object 'MyFilter' has no attribute 'drop_down'. As shown above I can access the class variable i as expcted.
Is there something obvious that I'm missing? Or is there another approach that can be recommended, or should I give up on the django-filter and use the standard django filters and solve the problem as described here: [http://www.ilian.io/django-forms-choicefield-with-dynamic-values/]?
[EDIT]
# Alex Morozov
Yes, my goal is to change the content of the drop down at runtime. In this case, this would involve a new queryset being assigned to the ModelChoiceFilter. Something like this
__init__(self,*args,**kwargs):
self.session = kwargs.pop('session', None)
super(MyFilter,self).__init__(*args, **kwargs)
self.drop_down = ModelChoiceFilter(queryset = MyModel.my_custom_manager.my_customer_filter_that_uses_session(self.session))
I know that this example simply initializes the instance's copy of drop_down (and thus does not have the desired effect), but hopefully the intent is clear?

Best practices for passing model names as strings

I have two different models that I would like to filter similarly by a common field name at different times, so i've written a single context function that handles both models by taking a string as an argument to use as the model name. Right now I'm using eval(), but something in my gut tells me that's a grave error. Is there a more pythonic way to do what I'm describing?
Here's a shortened version of what my code looks like at the moment:
def reference_context(model, value):
menu = main_menu()
info = company_info()
pages = get_list_or_404(eval(model), category = value)
Secondly, is there a way to pass a keyword in a similar fashion, so I could have something along the lines of:
def reference_context(model, category, value):
menu = main_menu()
info = company_info()
pages = get_list_or_404(eval(model), eval(category) = value)
And commentary on any other issue is welcome and greatly encouraged.
If they are come from the same module (models.py), you can use getattr to retrieve the model class, and kwargs (a dict with double asterisk) this way:
from myapp import models
def reference_context(model, value):
menu = main_menu()
info = company_info()
pages = get_list_or_404(getattr(models, model), **{category: value})
You can use the get_model utility function, which takes the app name and model name.
from django.db.models import get_model
User = get_model("auth", "User") # returns django.contrib.auth.models.User
I don't really see why you need to pass the model as a string - just pass the model reference. E.g.
class ModelA(models.Model):
...
class ModelB(models.Model):
...
def reference_context(model, **kw):
menu = main_menu()
info = company_info()
pages = get_list_or_404(model, **kw)
# ...
In this setup you can pass any model and any query you want, e.g.
reference_context(ModelA, category="Hello")
or
reference_context(ModelB, item__ordered__lte=now)
As explained in my comment, if you really need to map strings to models, use an explicit registry/mapping. This prevents people from manipulating form data which might allow them to create a User in stead of, for example, a "Book":
model_map = dict(book=ModelA, magazine=ModelB)
reference_context(model_map[model_as_string], ...)

Why are form field __init__ methods being called on Django startup?

I have a Django app with custom form fields, some of which have slow operations in their constructors. I was surprised recently to find out that those constructors were getting called when Django itself was starting up, even before a user does something that requires that form in a view.
Why are they getting instantiated at server start?
Example:
urls.py:
from myapp.views import view1
...
url(r'^test$', view1.test),
views/view1.py:
class MyForm(ModelForm):
class Meta:
model = MyModel
field1 = MyChoiceField()
class MyChoiceField(ChoiceField):
def __init__(self, choices=(), required=True, widget=None, label=None,
initial=None, help_text=None, *args, **kwargs):
super(ChoiceField, self).__init__(required, widget, label, initial,
help_text, *args, **kwargs)
self.choices = [(m.id, m.name) for m in ReallyLargeTableModel.objects.all()]
If I set a break point inside that field constructor, then start up Django, it breaks the first time I request any page, even if the view in question does not need that form or field. The stacktrace leads back to the import line in urls.py.
Is this because I'm importing view1 in urls.py, instead of importing view1.test?
Edit: This isn't Django specific, here is a test case the illustrates the behavior:
class Something():
def __init__(self):
print "Something __init__() called"
class UsesSomething():
field = Something()
If you run this in the interactive terminal, it will print "Something init() called". This was surprising to me because I have not actually instantiated a UsesSomething object.
Because you instantiate the fields in the form definition, which is presumably being imported by one of your views.
The field init is the wrong place to do this sort of dynamic initialization, for this exact reason. You want something that is called when the form is initialized: ie, the form's __init__.
That said, you don't actually want to do this at all - you just need to use forms.ModelChoiceField, which takes a queryset and does the dynamic assignment of choices for you.
class MyForm(ModelForm):
field1 = forms.ModelChoiceField(queryset=ReallyLargeTableModel.objects.all())
In your example:
class UsesSomething():
field = Something()
The line of code field = Something() will execute when you import the containing module as Python processes the class definition. This is just how Python works. You can actually put arbitrary code inside a class definition.
module: test.py:
class UsesSomething():
print "wow!"
>>> import test
wow!

Categories

Resources