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?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Blender
  • 289,723
  • 53
  • 439
  • 496
  • There may be ways, but what you want is not compatible with the zen of python : "explicit is better than implicit"... – zmo Aug 12 '12 at 10:03
  • 1
    I'd be happy to turn a blind eye. – Blender Aug 12 '12 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. – zmo Aug 12 '12 at 10:13
  • Have you tried assigning the properties to the class outside using `setattr()`? – Ignacio Vazquez-Abrams Aug 12 '12 at 10:19
  • @IgnacioVazquez-Abrams: I'm trying that right now, but I'm calling `getattr` on the `self.user` object, which doesn't exist yet. I'll try subclassing the class of `self.user` and see where that takes me. – Blender Aug 12 '12 at 10:22
  • 1
    Try generating closures and assigning/setting those instead. – Ignacio Vazquez-Abrams Aug 12 '12 at 10:25
  • use `type` https://stackoverflow.com/a/55342527/2101808 – eri Mar 25 '19 at 16:45

6 Answers6

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.

jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • 1
    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 Aug 12 '12 at 17:38
  • @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). – jfs Aug 12 '12 at 18:00
  • Class decorator not works. I have attribute on class type, but no such attr on instance.. – eri Mar 25 '19 at 16:04
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)
Eldar Abusalimov
  • 24,387
  • 4
  • 67
  • 71
  • 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 Aug 12 '12 at 17:44
  • @Blender, oh, it sounds interesting. Actually I'm a Python newbie, so I have no idea on how to workaround this problem. – Eldar Abusalimov Aug 12 '12 at 18:39
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.

Blckknght
  • 100,903
  • 11
  • 120
  • 169
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
Jon Clements
  • 138,671
  • 33
  • 247
  • 280
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

Jon Clements
  • 138,671
  • 33
  • 247
  • 280
Daniel
  • 11
  • 1
  • 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 Aug 12 '12 at 17:42
  • @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. – jfs Aug 12 '12 at 18:24
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))
jbee
  • 206
  • 2
  • 8