File: preference_binding.py

package info (click to toggle)
python-apptools 5.3.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,552 kB
  • sloc: python: 9,868; makefile: 80
file content (168 lines) | stat: -rw-r--r-- 6,086 bytes parent folder | download
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
# (C) Copyright 2005-2025 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
""" A binding between a trait on an object and a preference value. """

from ast import literal_eval

# Enthought library imports.
from traits.api import Any, HasTraits, Instance, Str, Undefined

# Local imports.
from .i_preferences import IPreferences
from .package_globals import get_default_preferences


class PreferenceBinding(HasTraits):
    """ A binding between a trait on an object and a preference value. """

    #### 'PreferenceBinding' interface ########################################

    # The object that we are binding the preference to.
    obj = Any

    # The preferences node used by the binding. If this trait is not set then
    # the package-global default preferences node is used (and if that is not
    # set then the binding won't work ;^)
    preferences = Instance(IPreferences)

    # The path to the preference value.
    preference_path = Str

    # The name of the trait that we are binding the preference to.
    trait_name = Str

    ###########################################################################
    # 'object' interface.
    ###########################################################################

    def __init__(self, **traits):
        """ Constructor. """

        super(PreferenceBinding, self).__init__(**traits)

        # Initialize the object's trait from the preference value.
        self._set_trait(notify=False)

        # Wire-up trait change handlers etc.
        self._initialize()

    ###########################################################################
    # 'PreferenceBinding' interface.
    ###########################################################################

    #### Trait initializers ###################################################

    def _preferences_default(self):
        """ Trait initializer. """

        return get_default_preferences()

    ###########################################################################
    # Private interface.
    ###########################################################################

    #### Trait change handlers ################################################

    def _on_trait_changed(self, event):
        """ Dynamic trait change handler. """

        self.preferences.set(self.preference_path, event.new)

    #### Other observer pattern listeners #####################################

    def _preferences_listener(self, node, key, old, new):
        """ Listener called when a preference value is changed. """

        components = self.preference_path.split(".")
        if key == components[-1]:
            self._set_trait()

    #### Methods ##############################################################

    # fixme: This method is mostly duplicated in 'PreferencesHelper' (the only
    # difference is the line that gets the handler).
    def _get_value(self, trait_name, value):
        """Get the actual value to set.

        This method makes sure that any required work is done to convert the
        preference value from a string.

        """

        handler = self.obj.trait(trait_name).handler

        # If the trait type is 'Str' then we just take the raw value.
        if type(handler) is Str:
            pass

        # Otherwise, we literal_eval it!  This is safe against arbitrary code
        # execution, but it does limit values to core Python data types.
        else:
            try:
                value = literal_eval(value)

            # If the eval fails then there is probably a syntax error, but
            # we will let the handler validation throw the exception.
            except Exception:
                pass

        return handler.validate(self, trait_name, value)

    def _initialize(self):
        """ Wire-up trait change handlers etc. """

        # Listen for the object's trait being changed.
        self.obj.observe(self._on_trait_changed, self.trait_name)

        # Listen for the preference value being changed.
        components = self.preference_path.split(".")
        node = ".".join(components[:-1])

        self.preferences.add_preferences_listener(
            self._preferences_listener, node
        )

    def _set_trait(self, notify=True):
        """ Set the object's trait to the value of the preference. """

        value = self.preferences.get(self.preference_path, Undefined)
        if value is not Undefined:
            trait_value = self._get_value(self.trait_name, value)
            traits = {self.trait_name: trait_value}

            self.obj.trait_set(trait_change_notify=notify, **traits)


# Factory function for creating bindings.
def bind_preference(obj, trait_name, preference_path, preferences=None):
    """ Create a new preference binding. """

    # This may seem a bit wierd, but we manually build up a dictionary of
    # the traits that need to be set at the time the 'PreferenceBinding'
    # instance is created.
    #
    # This is because we only want to set the 'preferences' trait iff one
    # is explicitly specified. If we passed it in with the default argument
    # value of 'None' then it counts as 'setting' the trait which prevents
    # the binding instance from defaulting to the package-global preferences.
    # Also, if we try to set the 'preferences' trait *after* construction time
    # then it is too late as the binding initialization is done in the
    # constructor (we could of course split that out, which may be the 'right'
    # way to do it ;^).
    traits = {
        "obj": obj,
        "trait_name": trait_name,
        "preference_path": preference_path,
    }

    if preferences is not None:
        traits["preferences"] = preferences

    return PreferenceBinding(**traits)