File: has_properties.py

package info (click to toggle)
linux-show-player 0.5.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 4,896 kB
  • sloc: python: 12,408; sh: 154; makefile: 17; xml: 8
file content (249 lines) | stat: -rw-r--r-- 8,412 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# -*- coding: utf-8 -*-
#
# This file is part of Linux Show Player
#
# Copyright 2012-2016 Francesco Ceruti <ceppofrancy@gmail.com>
#
# Linux Show Player is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Linux Show Player is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Linux Show Player.  If not, see <http://www.gnu.org/licenses/>.

from abc import ABCMeta
from copy import deepcopy

from lisp.core.signal import Signal
from lisp.core.util import subclasses


class Property:
    """Descriptor to be used in HasProperties subclasses to define properties.

    .. warning::
        To be able to save properties into a session, the stored value
        MUST be JSON-serializable.

    .. warning::
        If extended any subclass *MUST*:
        1) if the __get__ method receive a None instance return self;
        2) if the __get__ is called while the value is not set, set it with a
           safe copy of 'default' and return.
        3) After the value is changed call the __changed__ method.
    """

    def __init__(self, default=None):
        self.name = 'unnamed_property'
        self.default = default

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        elif self.name not in instance.__dict__:
            instance.__dict__[self.name] = deepcopy(self.default)

        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if instance is not None:
            # Only change the value if different
            if value != instance.__dict__.get(self.name, self.default):
                instance.__dict__[self.name] = value
                self.__changed__(instance, value)

    def changed(self, instance):
        if instance is not None:
            value = self.__get__(instance)
            return value != self.default, value

        return False, self.default

    def __changed__(self, instance, value):
        instance.property_changed.emit(instance, self.name, value)
        # Get the related signal
        property_signal = instance.changed_signals.get(self.name)
        if property_signal is not None:
            property_signal.emit(value)


class WriteOnceProperty(Property):
    """Property that can be modified only once.

    Obviously this is not really "write-once", but if used as normal attribute
    will ignore any change when the stored value is different from default.
    """

    def __set__(self, instance, value):
        if self.__get__(instance) == self.default:
            super().__set__(instance, value)


class NestedProperties(Property):
    """Simplify retrieving the properties of nested HasProperties objects.

    The goal is to avoid the reimplementation of HasProperties.properties()
    and HasProperties.update_properties().

    ..note::
        When getting or setting a single property of the nested object is better
        to access it directly instead that using the nested-property.
    """

    def __init__(self, provider_name, default=None):
        super().__init__(default=default)
        self.provider_name = provider_name

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        else:
            provider = instance.__dict__.get(self.provider_name)
            if isinstance(provider, HasProperties):
                return provider.properties()

    def __set__(self, instance, value):
        if instance is not None:
            provider = instance.__dict__.get(self.provider_name)
            if isinstance(provider, HasProperties):
                provider.update_properties(value)
                self.__changed__(instance, value)

    def changed(self, instance):
        if instance is not None:
            provider = instance.__dict__.get(self.provider_name)
            if isinstance(provider, HasProperties):
                properties = provider.properties(only_changed=True)
                # If no properties is changed (empty dict) return false
                return bool(properties), properties

        return False, {}


class HasPropertiesMeta(ABCMeta):
    """Metaclass for HasProperties classes.

    This metaclass manage the 'propagation' of the properties in all subclasses,
    this process involves overwriting __properties__ with a set containing all
    the properties names.

    ..note::
        This metaclass is derived form :class:`abc.ABCMeta`, so abstract
        classes can be created without using an intermediate metaclass
    """

    def __init__(cls, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Use a set for avoiding repetitions
        cls.__properties__ = set()

        # Populate with all the properties
        for name, attribute in vars(cls).items():
            if isinstance(attribute, Property):
                cls.__properties__.add(name)
                attribute.name = name

        for base in cls.__bases__:
            cls.__properties__.update(getattr(base, '__properties__', ()))


class HasProperties(metaclass=HasPropertiesMeta):
    """Base class providing a simple way to mange object properties.

    Using the Property descriptor, subclasses, can specify a set of
    properties, that can be easily retrieved and updated via :func:`properties`
    and :func:`update_properties`.

    .. Usage::

        class MyClass(HasProperties):
            prop1 = Property(default=100)
            prop2 = Property()
    """

    __properties__ = set()

    def __init__(self):
        self.property_changed = Signal()
        #: Emitted after property change (self, name, value)

        self.changed_signals = {}
        """Contains signals that are emitted after the associated property is
        changed, the signal are create only when requested the first time.
        """

    @classmethod
    def register_property(cls, name, prop):
        """Register a new property with the given name.

        :param str name: Property name
        :param BaseProperty prop: The property
        """
        if name not in cls.__properties__:
            prop.name = name
            setattr(cls, name, prop)
            cls.__properties__.add(name)

            for subclass in subclasses(cls):
                subclass.__properties__.add(name)

    def changed(self, property_name):
        """
        :param property_name: The property name
        :return: The property change-signal
        :rtype: Signal
        """
        if property_name not in self.__class__.__properties__:
            raise ValueError('no property "{0}" found'.format(property_name))

        signal = self.changed_signals.get(property_name, None)

        if signal is None:
            signal = Signal()
            self.changed_signals[property_name] = signal

        return signal

    def properties(self, only_changed=False):
        """
        :param only_changed: when True only "changed" properties are collected
        :type only_changed: bool

        :return: The properties as a dictionary {name: value}
        :rtype: dict
        """
        if only_changed:
            properties = {}
            for name in self.__properties__:
                changed, value = getattr(self.__class__, name).changed(self)
                if changed:
                    properties[name] = value

            return properties

        return {name: getattr(self, name) for name in self.__properties__}

    @classmethod
    def properties_defaults(cls):
        """
        :return: The default properties as a dictionary {name: default_value}
        :rtype: dict
        """
        return {name: getattr(cls, name).default for name in cls.__properties__}

    def update_properties(self, properties):
        """Set the given properties.

        :param properties: The element properties
        :type properties: dict
        """
        for name, value in properties.items():
            if name in self.__properties__:
                setattr(self, name, value)