##// END OF EJS Templates
Added SingletonConfigurable with instance method....
Brian Granger -
Show More
@@ -1,173 +1,234 b''
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 """
4 4 A base class for objects that are configurable.
5 5
6 6 Authors:
7 7
8 8 * Brian Granger
9 9 * Fernando Perez
10 10 """
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Copyright (C) 2008-2010 The IPython Development Team
14 14 #
15 15 # Distributed under the terms of the BSD License. The full license is in
16 16 # the file COPYING, distributed as part of this software.
17 17 #-----------------------------------------------------------------------------
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Imports
21 21 #-----------------------------------------------------------------------------
22 22
23 23 from copy import deepcopy
24 24 import datetime
25 25
26 26 from loader import Config
27 27 from IPython.utils.traitlets import HasTraits, Instance
28 28 from IPython.utils.text import indent
29 29
30 30
31 31 #-----------------------------------------------------------------------------
32 32 # Helper classes for Configurables
33 33 #-----------------------------------------------------------------------------
34 34
35 35
36 36 class ConfigurableError(Exception):
37 37 pass
38 38
39 39
40 class MultipleInstanceError(ConfigurableError):
41 pass
42
40 43 #-----------------------------------------------------------------------------
41 44 # Configurable implementation
42 45 #-----------------------------------------------------------------------------
43 46
44 47
45 48 class Configurable(HasTraits):
46 49
47 50 config = Instance(Config,(),{})
48 51 created = None
49 52
50 53 def __init__(self, **kwargs):
51 54 """Create a conigurable given a config config.
52 55
53 56 Parameters
54 57 ----------
55 58 config : Config
56 59 If this is empty, default values are used. If config is a
57 60 :class:`Config` instance, it will be used to configure the
58 61 instance.
59 62
60 63 Notes
61 64 -----
62 65 Subclasses of Configurable must call the :meth:`__init__` method of
63 66 :class:`Configurable` *before* doing anything else and using
64 67 :func:`super`::
65 68
66 69 class MyConfigurable(Configurable):
67 70 def __init__(self, config=None):
68 71 super(MyConfigurable, self).__init__(config)
69 72 # Then any other code you need to finish initialization.
70 73
71 74 This ensures that instances will be configured properly.
72 75 """
73 76 config = kwargs.pop('config', None)
74 77 if config is not None:
75 78 # We used to deepcopy, but for now we are trying to just save
76 79 # by reference. This *could* have side effects as all components
77 80 # will share config. In fact, I did find such a side effect in
78 81 # _config_changed below. If a config attribute value was a mutable type
79 82 # all instances of a component were getting the same copy, effectively
80 83 # making that a class attribute.
81 84 # self.config = deepcopy(config)
82 85 self.config = config
83 86 # This should go second so individual keyword arguments override
84 87 # the values in config.
85 88 super(Configurable, self).__init__(**kwargs)
86 89 self.created = datetime.datetime.now()
87 90
88 91 #-------------------------------------------------------------------------
89 92 # Static trait notifiations
90 93 #-------------------------------------------------------------------------
91 94
92 95 def _config_changed(self, name, old, new):
93 96 """Update all the class traits having ``config=True`` as metadata.
94 97
95 98 For any class trait with a ``config`` metadata attribute that is
96 99 ``True``, we update the trait with the value of the corresponding
97 100 config entry.
98 101 """
99 102 # Get all traits with a config metadata entry that is True
100 103 traits = self.traits(config=True)
101 104
102 105 # We auto-load config section for this class as well as any parent
103 106 # classes that are Configurable subclasses. This starts with Configurable
104 107 # and works down the mro loading the config for each section.
105 108 section_names = [cls.__name__ for cls in \
106 109 reversed(self.__class__.__mro__) if
107 110 issubclass(cls, Configurable) and issubclass(self.__class__, cls)]
108 111
109 112 for sname in section_names:
110 113 # Don't do a blind getattr as that would cause the config to
111 114 # dynamically create the section with name self.__class__.__name__.
112 115 if new._has_section(sname):
113 116 my_config = new[sname]
114 117 for k, v in traits.iteritems():
115 118 # Don't allow traitlets with config=True to start with
116 119 # uppercase. Otherwise, they are confused with Config
117 120 # subsections. But, developers shouldn't have uppercase
118 121 # attributes anyways! (PEP 6)
119 122 if k[0].upper()==k[0] and not k.startswith('_'):
120 123 raise ConfigurableError('Configurable traitlets with '
121 124 'config=True must start with a lowercase so they are '
122 125 'not confused with Config subsections: %s.%s' % \
123 126 (self.__class__.__name__, k))
124 127 try:
125 128 # Here we grab the value from the config
126 129 # If k has the naming convention of a config
127 130 # section, it will be auto created.
128 131 config_value = my_config[k]
129 132 except KeyError:
130 133 pass
131 134 else:
132 135 # print "Setting %s.%s from %s.%s=%r" % \
133 136 # (self.__class__.__name__,k,sname,k,config_value)
134 137 # We have to do a deepcopy here if we don't deepcopy the entire
135 138 # config object. If we don't, a mutable config_value will be
136 139 # shared by all instances, effectively making it a class attribute.
137 140 setattr(self, k, deepcopy(config_value))
138 141
139 142 @classmethod
140 143 def class_get_shortnames(cls):
141 144 """Return the shortname to fullname dict for config=True traits."""
142 145 cls_traits = cls.class_traits(config=True)
143 146 shortnames = {}
144 147 for k, v in cls_traits.items():
145 148 shortname = v.get_metadata('shortname')
146 149 if shortname is not None:
147 150 longname = cls.__name__ + '.' + k
148 151 shortnames[shortname] = longname
149 152 return shortnames
150 153
151 154 @classmethod
152 155 def class_get_help(cls):
153 156 """Get the help string for this class in ReST format."""
154 157 cls_traits = cls.class_traits(config=True)
155 158 final_help = []
156 159 final_help.append(u'%s options' % cls.__name__)
157 160 final_help.append(len(final_help[0])*u'-')
158 161 for k, v in cls_traits.items():
159 162 help = v.get_metadata('help')
160 163 shortname = v.get_metadata('shortname')
161 164 header = "%s.%s : %s" % (cls.__name__, k, v.__class__.__name__)
162 165 if shortname is not None:
163 166 header += " (shortname=" + shortname + ")"
164 167 final_help.append(header)
165 168 if help is not None:
166 169 final_help.append(indent(help))
167 170 return '\n'.join(final_help)
168 171
169 172 @classmethod
170 173 def class_print_help(cls):
171 174 print cls.class_get_help()
172 175
173 No newline at end of file
176
177 class SingletonConfigurable(Configurable):
178 """A configurable that only allows one instance.
179
180 This class is for classes that should only have one instance of itself
181 or *any* subclass. To create and retrieve such a class use the
182 :meth:`SingletonConfigurable.instance` method.
183 """
184
185 _instance = None
186
187 @classmethod
188 def instance(cls, *args, **kwargs):
189 """Returns a global instance of this class.
190
191 This method create a new instance if none have previously been created
192 and returns a previously created instance is one already exists.
193
194 The arguments and keyword arguments passed to this method are passed
195 on to the :meth:`__init__` method of the class upon instantiation.
196
197 Examples
198 --------
199
200 Create a singleton class using instance, and retrieve it::
201
202 >>> from IPython.config.configurable import SingletonConfigurable
203 >>> class Foo(SingletonConfigurable): pass
204 >>> foo = Foo.instance()
205 >>> foo == Foo.instance()
206 True
207
208 Create a subclass that is retrived using the base class instance::
209
210 >>> class Bar(SingletonConfigurable): pass
211 >>> class Bam(Bar): pass
212 >>> bam = Bam.instance()
213 >>> bam == Bar.instance()
214 True
215 """
216 # Create and save the instance
217 if cls._instance is None:
218 inst = cls(*args, **kwargs)
219 # Now make sure that the instance will also be returned by
220 # the subclasses instance attribute.
221 for subclass in cls.mro():
222 if issubclass(cls, subclass) and \
223 issubclass(subclass, SingletonConfigurable) and \
224 subclass != SingletonConfigurable:
225 subclass._instance = inst
226 else:
227 break
228 if isinstance(cls._instance, cls):
229 return cls._instance
230 else:
231 raise MultipleInstanceError(
232 'Multiple incompatible subclass instances of '
233 '%s are being created.' % cls.__name__
234 )
@@ -1,143 +1,168 b''
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 """
4 4 Tests for IPython.config.configurable
5 5
6 6 Authors:
7 7
8 8 * Brian Granger
9 9 * Fernando Perez (design help)
10 10 """
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Copyright (C) 2008-2010 The IPython Development Team
14 14 #
15 15 # Distributed under the terms of the BSD License. The full license is in
16 16 # the file COPYING, distributed as part of this software.
17 17 #-----------------------------------------------------------------------------
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Imports
21 21 #-----------------------------------------------------------------------------
22 22
23 23 from unittest import TestCase
24 24
25 from IPython.config.configurable import Configurable, ConfigurableError
25 from IPython.config.configurable import (
26 Configurable,
27 SingletonConfigurable
28 )
29
26 30 from IPython.utils.traitlets import (
27 TraitError, Int, Float, Str
31 Int, Float, Str
28 32 )
33
29 34 from IPython.config.loader import Config
30 35
31 36
32 37 #-----------------------------------------------------------------------------
33 38 # Test cases
34 39 #-----------------------------------------------------------------------------
35 40
36 41
37 42 class MyConfigurable(Configurable):
38 43 a = Int(1, config=True, shortname="a", help="The integer a.")
39 44 b = Float(1.0, config=True, shortname="b", help="The integer b.")
40 45 c = Str('no config')
41 46
42 47
43 48 mc_help=u"""MyConfigurable options
44 49 ----------------------
45 50 MyConfigurable.a : Int (shortname=a)
46 51 The integer a.
47 52 MyConfigurable.b : Float (shortname=b)
48 53 The integer b."""
49 54
50 55 class Foo(Configurable):
51 56 a = Int(0, config=True, shortname="a", help="The integer a.")
52 57 b = Str('nope', config=True)
53 58
54 59
55 60 class Bar(Foo):
56 61 b = Str('gotit', config=False, shortname="b", help="The string b.")
57 62 c = Float(config=True, shortname="c", help="The string c.")
58 63
59 64
60 class TestConfigurableConfig(TestCase):
65 class TestConfigurable(TestCase):
61 66
62 67 def test_default(self):
63 68 c1 = Configurable()
64 69 c2 = Configurable(config=c1.config)
65 70 c3 = Configurable(config=c2.config)
66 71 self.assertEquals(c1.config, c2.config)
67 72 self.assertEquals(c2.config, c3.config)
68 73
69 74 def test_custom(self):
70 75 config = Config()
71 76 config.foo = 'foo'
72 77 config.bar = 'bar'
73 78 c1 = Configurable(config=config)
74 79 c2 = Configurable(config=c1.config)
75 80 c3 = Configurable(config=c2.config)
76 81 self.assertEquals(c1.config, config)
77 82 self.assertEquals(c2.config, config)
78 83 self.assertEquals(c3.config, config)
79 84 # Test that copies are not made
80 85 self.assert_(c1.config is config)
81 86 self.assert_(c2.config is config)
82 87 self.assert_(c3.config is config)
83 88 self.assert_(c1.config is c2.config)
84 89 self.assert_(c2.config is c3.config)
85 90
86 91 def test_inheritance(self):
87 92 config = Config()
88 93 config.MyConfigurable.a = 2
89 94 config.MyConfigurable.b = 2.0
90 95 c1 = MyConfigurable(config=config)
91 96 c2 = MyConfigurable(config=c1.config)
92 97 self.assertEquals(c1.a, config.MyConfigurable.a)
93 98 self.assertEquals(c1.b, config.MyConfigurable.b)
94 99 self.assertEquals(c2.a, config.MyConfigurable.a)
95 100 self.assertEquals(c2.b, config.MyConfigurable.b)
96 101
97 102 def test_parent(self):
98 103 config = Config()
99 104 config.Foo.a = 10
100 105 config.Foo.b = "wow"
101 106 config.Bar.b = 'later'
102 107 config.Bar.c = 100.0
103 108 f = Foo(config=config)
104 109 b = Bar(config=f.config)
105 110 self.assertEquals(f.a, 10)
106 111 self.assertEquals(f.b, 'wow')
107 112 self.assertEquals(b.b, 'gotit')
108 113 self.assertEquals(b.c, 100.0)
109 114
110 115 def test_override1(self):
111 116 config = Config()
112 117 config.MyConfigurable.a = 2
113 118 config.MyConfigurable.b = 2.0
114 119 c = MyConfigurable(a=3, config=config)
115 120 self.assertEquals(c.a, 3)
116 121 self.assertEquals(c.b, config.MyConfigurable.b)
117 122 self.assertEquals(c.c, 'no config')
118 123
119 124 def test_override2(self):
120 125 config = Config()
121 126 config.Foo.a = 1
122 127 config.Bar.b = 'or' # Up above b is config=False, so this won't do it.
123 128 config.Bar.c = 10.0
124 129 c = Bar(config=config)
125 130 self.assertEquals(c.a, config.Foo.a)
126 131 self.assertEquals(c.b, 'gotit')
127 132 self.assertEquals(c.c, config.Bar.c)
128 133 c = Bar(a=2, b='and', c=20.0, config=config)
129 134 self.assertEquals(c.a, 2)
130 135 self.assertEquals(c.b, 'and')
131 136 self.assertEquals(c.c, 20.0)
132 137
133 138 def test_shortnames(self):
134 139 sn = MyConfigurable.class_get_shortnames()
135 140 self.assertEquals(sn, {'a': 'MyConfigurable.a', 'b': 'MyConfigurable.b'})
136 141 sn = Foo.class_get_shortnames()
137 142 self.assertEquals(sn, {'a': 'Foo.a'})
138 143 sn = Bar.class_get_shortnames()
139 144 self.assertEquals(sn, {'a': 'Bar.a', 'c': 'Bar.c'})
140 145
141 146 def test_help(self):
142 147 self.assertEquals(MyConfigurable.class_get_help(), mc_help)
143 148
149
150 class TestSingletonConfigurable(TestCase):
151
152 def test_instance(self):
153 from IPython.config.configurable import SingletonConfigurable
154 class Foo(SingletonConfigurable): pass
155 foo = Foo.instance()
156 self.assertEquals(foo, Foo.instance())
157 self.assertEquals(SingletonConfigurable._instance, None)
158
159 def test_inheritance(self):
160
161 class Bar(SingletonConfigurable): pass
162 class Bam(Bar): pass
163 bam = Bam.instance()
164 bam == Bar.instance()
165 self.assertEquals(bam, Bam._instance)
166 self.assertEquals(bam, Bar._instance)
167 self.assertEquals(SingletonConfigurable._instance, None)
168 No newline at end of file
@@ -1,208 +1,207 b''
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 """
4 4 Tests for IPython.config.loader
5 5
6 6 Authors:
7 7
8 8 * Brian Granger
9 9 * Fernando Perez (design help)
10 10 """
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Copyright (C) 2008-2009 The IPython Development Team
14 14 #
15 15 # Distributed under the terms of the BSD License. The full license is in
16 16 # the file COPYING, distributed as part of this software.
17 17 #-----------------------------------------------------------------------------
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Imports
21 21 #-----------------------------------------------------------------------------
22 22
23 23 import os
24 24 from tempfile import mkstemp
25 25 from unittest import TestCase
26 26
27 27 from IPython.utils.traitlets import Int, Unicode
28 28 from IPython.config.configurable import Configurable
29 29 from IPython.config.loader import (
30 30 Config,
31 31 PyFileConfigLoader,
32 32 KeyValueConfigLoader,
33 33 ArgParseConfigLoader,
34 34 ConfigError
35 35 )
36 36
37 37 #-----------------------------------------------------------------------------
38 38 # Actual tests
39 39 #-----------------------------------------------------------------------------
40 40
41 41
42 42 pyfile = """
43 43 c = get_config()
44 44 c.a=10
45 45 c.b=20
46 46 c.Foo.Bar.value=10
47 47 c.Foo.Bam.value=range(10)
48 48 c.D.C.value='hi there'
49 49 """
50 50
51 51 class TestPyFileCL(TestCase):
52 52
53 53 def test_basic(self):
54 54 fd, fname = mkstemp('.py')
55 55 f = os.fdopen(fd, 'w')
56 56 f.write(pyfile)
57 57 f.close()
58 58 # Unlink the file
59 59 cl = PyFileConfigLoader(fname)
60 60 config = cl.load_config()
61 61 self.assertEquals(config.a, 10)
62 62 self.assertEquals(config.b, 20)
63 63 self.assertEquals(config.Foo.Bar.value, 10)
64 64 self.assertEquals(config.Foo.Bam.value, range(10))
65 65 self.assertEquals(config.D.C.value, 'hi there')
66 66
67 67 class MyLoader1(ArgParseConfigLoader):
68 68 def _add_arguments(self):
69 69 p = self.parser
70 70 p.add_argument('-f', '--foo', dest='Global.foo', type=str)
71 71 p.add_argument('-b', dest='MyClass.bar', type=int)
72 72 p.add_argument('-n', dest='n', action='store_true')
73 73 p.add_argument('Global.bam', type=str)
74 74
75 75 class MyLoader2(ArgParseConfigLoader):
76 76 def _add_arguments(self):
77 77 subparsers = self.parser.add_subparsers(dest='subparser_name')
78 78 subparser1 = subparsers.add_parser('1')
79 79 subparser1.add_argument('-x',dest='Global.x')
80 80 subparser2 = subparsers.add_parser('2')
81 81 subparser2.add_argument('y')
82 82
83 83 class TestArgParseCL(TestCase):
84 84
85 85 def test_basic(self):
86 86 cl = MyLoader1()
87 87 config = cl.load_config('-f hi -b 10 -n wow'.split())
88 88 self.assertEquals(config.Global.foo, 'hi')
89 89 self.assertEquals(config.MyClass.bar, 10)
90 90 self.assertEquals(config.n, True)
91 91 self.assertEquals(config.Global.bam, 'wow')
92 92 config = cl.load_config(['wow'])
93 93 self.assertEquals(config.keys(), ['Global'])
94 94 self.assertEquals(config.Global.keys(), ['bam'])
95 95 self.assertEquals(config.Global.bam, 'wow')
96 96
97 97 def test_add_arguments(self):
98 98 cl = MyLoader2()
99 99 config = cl.load_config('2 frobble'.split())
100 100 self.assertEquals(config.subparser_name, '2')
101 101 self.assertEquals(config.y, 'frobble')
102 102 config = cl.load_config('1 -x frobble'.split())
103 103 self.assertEquals(config.subparser_name, '1')
104 104 self.assertEquals(config.Global.x, 'frobble')
105 105
106 106 def test_argv(self):
107 107 cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
108 108 config = cl.load_config()
109 109 self.assertEquals(config.Global.foo, 'hi')
110 110 self.assertEquals(config.MyClass.bar, 10)
111 111 self.assertEquals(config.n, True)
112 112 self.assertEquals(config.Global.bam, 'wow')
113 113
114 114
115 115 class TestKeyValueCL(TestCase):
116 116
117 117 def test_basic(self):
118 118 cl = KeyValueConfigLoader()
119 119 argv = [s.strip('c.') for s in pyfile.split('\n')[2:-1]]
120 print argv
121 120 config = cl.load_config(argv)
122 121 self.assertEquals(config.a, 10)
123 122 self.assertEquals(config.b, 20)
124 123 self.assertEquals(config.Foo.Bar.value, 10)
125 124 self.assertEquals(config.Foo.Bam.value, range(10))
126 125 self.assertEquals(config.D.C.value, 'hi there')
127 126
128 127 def test_shortname(self):
129 128 class Foo(Configurable):
130 129 i = Int(0, config=True, shortname="i")
131 130 s = Unicode('hi', config=True, shortname="s")
132 131 cl = KeyValueConfigLoader()
133 132 config = cl.load_config(["i=20", "s=there"], classes=[Foo])
134 133 self.assertEquals(config.Foo.i, 20)
135 134 self.assertEquals(config.Foo.s, "there")
136 135
137 136 def test_duplicate(self):
138 137 class Foo(Configurable):
139 138 i = Int(0, config=True, shortname="i")
140 139 class Bar(Configurable):
141 140 i = Int(0, config=True, shortname="i")
142 141 cl = KeyValueConfigLoader()
143 142 self.assertRaises(KeyError, cl.load_config, ["i=20", "s=there"], classes=[Foo, Bar])
144 143
145 144
146 145 class TestConfig(TestCase):
147 146
148 147 def test_setget(self):
149 148 c = Config()
150 149 c.a = 10
151 150 self.assertEquals(c.a, 10)
152 151 self.assertEquals(c.has_key('b'), False)
153 152
154 153 def test_auto_section(self):
155 154 c = Config()
156 155 self.assertEquals(c.has_key('A'), True)
157 156 self.assertEquals(c._has_section('A'), False)
158 157 A = c.A
159 158 A.foo = 'hi there'
160 159 self.assertEquals(c._has_section('A'), True)
161 160 self.assertEquals(c.A.foo, 'hi there')
162 161 del c.A
163 162 self.assertEquals(len(c.A.keys()),0)
164 163
165 164 def test_merge_doesnt_exist(self):
166 165 c1 = Config()
167 166 c2 = Config()
168 167 c2.bar = 10
169 168 c2.Foo.bar = 10
170 169 c1._merge(c2)
171 170 self.assertEquals(c1.Foo.bar, 10)
172 171 self.assertEquals(c1.bar, 10)
173 172 c2.Bar.bar = 10
174 173 c1._merge(c2)
175 174 self.assertEquals(c1.Bar.bar, 10)
176 175
177 176 def test_merge_exists(self):
178 177 c1 = Config()
179 178 c2 = Config()
180 179 c1.Foo.bar = 10
181 180 c1.Foo.bam = 30
182 181 c2.Foo.bar = 20
183 182 c2.Foo.wow = 40
184 183 c1._merge(c2)
185 184 self.assertEquals(c1.Foo.bam, 30)
186 185 self.assertEquals(c1.Foo.bar, 20)
187 186 self.assertEquals(c1.Foo.wow, 40)
188 187 c2.Foo.Bam.bam = 10
189 188 c1._merge(c2)
190 189 self.assertEquals(c1.Foo.Bam.bam, 10)
191 190
192 191 def test_deepcopy(self):
193 192 c1 = Config()
194 193 c1.Foo.bar = 10
195 194 c1.Foo.bam = 30
196 195 c1.a = 'asdf'
197 196 c1.b = range(10)
198 197 import copy
199 198 c2 = copy.deepcopy(c1)
200 199 self.assertEquals(c1, c2)
201 200 self.assert_(c1 is not c2)
202 201 self.assert_(c1.Foo is not c2.Foo)
203 202
204 203 def test_builtin(self):
205 204 c1 = Config()
206 205 exec 'foo = True' in c1
207 206 self.assertEquals(c1.foo, True)
208 207 self.assertRaises(ConfigError, setattr, c1, 'ValueError', 10)
General Comments 0
You need to be logged in to leave comments. Login now