7

I'm writing a script in Python and have a bit of a problem:

class LightDMUser(QObject):
  def __init__(self, user):
    super(LightDMUser, self).__init__()
    self.user = user

  @pyqtProperty(QVariant)
  def background(self):      return self.user.get_background()

  @pyqtProperty(QVariant)
  def display_name(self):    return self.user.get_display_name()

  @pyqtProperty(QVariant)
  def has_messages(self):    return self.user.get_has_messages()

  @pyqtProperty(QVariant)
  def home_directory(self):  return self.user.get_home_directory()

  @pyqtProperty(QVariant)
  def image(self):           return self.user.get_image()

  @pyqtProperty(QVariant)
  def language(self):        return self.user.get_language()

  @pyqtProperty(QVariant)
  def layout(self):          return self.user.get_layout()

  @pyqtProperty(QVariant)
  def layouts(self):         return self.user.get_layouts()

  @pyqtProperty(QVariant)
  def logged_in(self):       return self.user.get_logged_in()

  @pyqtProperty(QVariant)
  def name(self):            return self.user.get_name()

  @pyqtProperty(QVariant)
  def real_name(self):       return self.user.get_real_name()

  @pyqtProperty(QVariant)
  def session(self):         return self.user.get_session()

As you can see, this code is horribly redundant. I tried condensing it like this:

class LightDMUser(QObject):
  attributes = ['background', 'display_name', 'has_messages', 'home_directory', 'image', 'language', 'layout', 'layouts', 'logged_in', 'name', 'real_name', 'session']

  def __init__(self, user):
    super(LightDMUser, self).__init__()
    self.user = user

    for attribute in self.attributes:
      setattr(self, attribute, pyqtProperty(QVariant, getattr(self.user, 'get_' + attribute)))

PyQt4, however, expects the class methods to be present for the class itself, not an instance. Moving the setattr code out of the __init__ block didn't work either because self wasn't defined for the class, so I don't really know what to do.

Can anyone see a way to condense this code?

7
  • There may be ways, but what you want is not compatible with the zen of python : "explicit is better than implicit"... Commented Aug 12, 2012 at 10:03
  • 1
    I'd be happy to turn a blind eye. Commented Aug 12, 2012 at 10:08
  • I sadly don't know what the property pyqtProperty does, but as a general idea, you may as well declare LightDMUser so it inherits from QObject and User, and recode pyqtProperty so it does the same thing but once in init for all attributes of User... just my two cents, HTH. Commented Aug 12, 2012 at 10:13
  • Have you tried assigning the properties to the class outside using setattr()? Commented Aug 12, 2012 at 10:19
  • 1
    Try generating closures and assigning/setting those instead. Commented Aug 12, 2012 at 10:25

6 Answers 6

5

There are number of ways to do it: class decorator, metaclass, Mixin.

Common helper function:

def set_pyqtproperties(klass, properties, proxy='user'):
    def make_prop(prop):        
        def property_(self):
            return getattr(getattr(self, proxy), 'get_' + prop)
        property_.__name__ = prop
        return property_

    if isinstance(properties, basestring):
       properties = properties.split()
    for prop in properties:
         setattr(klass, prop, pyqtProperty(QVariant, make_prop(prop)))

Class decorator

def set_properties(properties):
    def decorator(klass):
        set_pyqtproperties(klass, properties)
        return klass
    return decorator
Usage
@set_properties("display background")
class LightDMUser(QObject): pass

if there is no support for class decorators then you could try:

class LightDMUser(QObject): 
    pass
LightDMUser = set_properties("display background")(LightDMUser)

Metaclass

def set_properties_meta(properties):
    def meta(name, bases, attrs):
        cls = type(name, bases, attrs)
        set_pyqtproperties(cls, properties)
        return cls
    return meta
Usage
class LightDMUser(QObject):
    __metaclass__ =  set_properties_meta("display background")

Note: you could reuse the same metaclass if you set the list of properties as a class attribute:

def MetaClass(name, bases, attrs):
    cls = type(name, bases, attrs)
    set_pyqtproperties(cls, attrs.get('properties', ''))
    return cls

class LightDMUser(QObject):
    properties = "display background"
    __metaclass__ = MetaClass

Also you could manipulate attrs directly: attrs[name] = value before calling type() instead of setattr(cls, name, value).

The above assumes that QObject.__class__ is type.

Mixin

def properties_mixin(classname, properties):
    #note: create a new class by whatever means necessary
    # e.g., even using exec() as namedtuple does
    # http://hg.python.org/cpython/file/3.2/Lib/collections.py#l235

    # reuse class decorator here
    return set_properties(properties)(type(classname, (), {}))
Usage
PropertiesMixin = properties_mixin('PropertiesMixin', 'display background')
class LightDMUser(PropertiesMixin, QObject): pass

I haven't tried any of it. The code is here to show the amount and the kind of code it might require to implement the feature.

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you very much for this post. The first method works, but PyQt4 doesn't recognize the newly created properties. I'm trying the second method right now but the meta() function's first argument, cls, doesn't get passed at all. Is this a Python 3-specific feature?
@Blender: it is not Python 3 feature. I've introduced the error when converted this from a class-based example (that uses __new__ method that accepts metacls as the first arg).
Class decorator not works. I have attribute on class type, but no such attr on instance..
2

You could attach these methods from еру outside of the class definition:

class LightDMUser(QObject):

  def __init__(self, user):
    super(LightDMUser, self).__init__()
    self.user = user

The simplest way is to create a closure for each property, override its __name__ (just for case if @pyqtProperty needs it) and to bind it to the class:

for attribute in [
        'background',
        'display_name',
        'has_messages',
        'home_directory',
        'image',
        'language',
        'layout',
        'layouts',
        'logged_in',
        'name',
        'real_name',
        'session'
      ]:

  def delegating(self):
    return getattr(self.user, 'get_' + attribute)()

  delegating.__name__ = attribute
  delegating = pyqtProperty(QVariant)(delegating)

  setattr(LightDMUser, attribute, delegating)

2 Comments

PyQt4 is being stubborn and it segfaults when I add your code. It looks like it should work perfectly, but PyQt4 refuses to comply.
@Blender, oh, it sounds interesting. Actually I'm a Python newbie, so I have no idea on how to workaround this problem.
1

I'm pretty sure this can work if you move your loop out of the class, and create a closure to hold each of the attribute names:

class LightDMUser(QObject):
    attributes = ['background', 'display_name', 'has_messages',
                  'home_directory', 'image', 'language', 'layout',
                  'layouts', 'logged_in', 'name', 'real_name', 'session']

    def __init__(self, user):
        super(LightDMUser, self).__init__()
        self.user = user

for attribute in LightDMUser.attributes:
    closure = lambda self, attribute=attribute : getattr(self.user,
                                                         'get_' + attribute)()
    setattr(LightDMUser, attribute, pyqtProperty(QVariant, closure))

I've not tested this with the actual QT based classes you're dealing with, but a simpler version using regular Python property instances worked perfectly. I'm also not sure this is a good idea, since it would be pretty hard to figure out what's going on if you are not already familiar with it.

Comments

1

I'm not convinced I like this, but it's a possible option, isn't too difficult to understand, and removes the need for getattr's... The following can be used a bit like a macro - but might need tweaking... (eg. take funcs from a class definition that startwith get, or from an existing object etc...) One could also add a repr in there to describe it's a supporting class for interfacing with properties to user objects or whatever...)

def get_properties(name, funcs):
    get_text = """
class {name}(QObject):
""".format(name=name)
    for func in funcs:
        get_text += (
              "\n\t@pyqtProperty(QVariant)\n"
              "\tdef {func}(self): return self.user.get_{func}()\n"
              ).format(func=func)

    print get_text # this should be exec...

>>> get_properties('UserProperties', ['display', 'background'])

class UserProperties(QObject):

    @pyqtProperty(QVariant)
    def display(self): return self.user.get_display()

    @pyqtProperty(QVariant)
    def background(self): return self.user.get_background()

When that exec'd, you get the ability to write your main class as:

class LightDMUser(QObject, UserProperties):
    def __init__(self, user):
        super(LightDMUser, self).__init__()
        self.user = user

Comments

1

I tested the solution below, for Python 3. It uses the metaclass keyword

# A bit of scaffolding

def pyqtProperty(cls, method):
    return method

class QObject:
    pass

class QVariant:
    pass

class User:
    def __init__(self, name="No Name"):
        self.name = name
    def get_background(self):
        return self.name
    def get_display_name(self):
        return self.name
    def get_has_messages(self):
        return self.name
    def get_home_directory(self):
        return self.name
    def get_image(self):
        return self.name
    def get_language(self):
        return self.name
    def get_layout(self):
        return self.name
    def get_layouts(self):
        return self.name
    def get_logged_in(self):
        return self.name
    def get_name(self):
        return self.name
    def get_real_name(self):
        return self.name
    def get_session(self):
        return self.name

# The Meta Class
class MetaLightDMUser(type):
    @classmethod
    def __prepare__(cls, name, baseClasses):
        classdict = {}
        for attribute in ['background', 'display_name', 'has_messages', 'home_directory', 'image', 'language', 'layout', 'layouts', 'logged_in', 'name', 'real_name', 'session']:
            classdict[attribute] = eval("lambda self: pyqtProperty(QVariant, getattr(self.user, 'get_" + attribute +"'))()")
        return classdict

    def __new__(cls, name, baseClasses, classdict):
        return type.__new__(cls, name, baseClasses, classdict)

# The class itself
class LightDMUser(QObject, metaclass = MetaLightDMUser): 
    def __init__(self, user):
        super(LightDMUser, self).__init__()
        self.user = user

Alternatively I could have created the classdict entries like this

classdict[attribute] = lambda self, attr=attribute: pyqtProperty(QVariant, getattr(self.user, 'get_' + attr))()

but that presents an attr argument. With eval() we hard-core this argument

As well we could have used functools.partial:

classdict[attribute] = functools.partial(lambda self, attr: pyqtProperty(QVariant, getattr(self.user, 'get_' + attr))(), attr=attribute)

but then the call must be u.method(u). It cannot be u.method()

The call LightDMUser.method(u) works with all 3 implementations

Regards

2 Comments

I'm trying to get your code to run, but it seems like metaclass's implementation differs between Python 2 and Python 3 (the metaclass isn't passed the cls argument). Do you know anything about this difference?
@Blender: class methods always receive class objects as the first arg. __new__ is a special static method that also receives cls as the first arg. Note: cls here is the metaclass object itself i.e., cls == MetaLightDMUser. __prepare__ is py3k specific.
0

It's tricky to cut down on boilerplate for pyqtProperty using a metaclass or class decorator, but this is something we just got to work here that will help as a starting point. The downside, I suppose, is that you no longer get to use the @decorator syntax, but it seems like rolling this into a single line of code is more desirable in this situation.

You can set this up to call through to your user object instead of just self, or you could implement custom getattr behavior for LightDMUser that will call through to self.user automatically.

from PyQt4.QtCore import pyqtProperty
from PyQt4.QtGui import QWidget, QColor
from functools import partial

def pyqtPropertyInit(name, default):
    def _getattrDefault(default, self, attrName):
        try:
            value = getattr(self, attrName)
        except AttributeError:
            setattr(self, attrName, default)
            return default
        return value
    ga = partial(_getattrDefault, default)
    return pyqtProperty(
        default.__class__,
        fget=(lambda s: ga(s, name)),
        fset=(lambda s, v: setattr(s, name, v)),
    )

class TestClass(QWidget):
    def __init__(self, *args, **kwargs):
        super(TestClass, self).__init__(*args, **kwargs)

    stdoutColor = pyqtPropertyInit('_stdoutColor', QColor(0, 0, 255))
    pyForegroundColor = pyqtPropertyInit('_pyForegroundColor', QColor(0, 0, 255))

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.