1 ## This file is part of Crapvine.
3 ## Copyright (C) 2007 Andrew Sayman <lorien420@myrealbox.com>
5 ## Crapvine is free software; you can redistribute it and/or modify
6 ## it under the terms of the GNU General Public License as published by
7 ## the Free Software Foundation; either version 3 of the License, or
8 ## (at your option) any later version.
10 ## Crapvine is distributed in the hope that it will be useful,
11 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
12 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 ## GNU General Public License for more details.
15 ## You should have received a copy of the GNU General Public License
16 ## along with this program. If not, see <http://www.gnu.org/licenses/>.
21 from datetime
import datetime
22 from dateutil
.parser
import parse
24 import inspect
, types
, __builtin__
26 ############## preliminary: two utility functions #####################
28 def skip_redundant(iterable
, skipset
=None):
29 "Redundant items are repeated items or items in the original skipset."
30 if skipset
is None: skipset
= set()
32 if item
not in skipset
:
36 def remove_redundant(metaclasses
):
37 skipset
= set([types
.ClassType
])
38 for meta
in metaclasses
: # determines the metaclasses to be skipped
39 skipset
.update(inspect
.getmro(meta
)[1:])
40 return tuple(skip_redundant(metaclasses
, skipset
))
42 ##################################################################
43 ## now the core of the module: two mutually recursive functions ##
44 ##################################################################
46 memoized_metaclasses_map
= {}
48 def get_noconflict_metaclass(bases
, left_metas
, right_metas
):
49 """Not intended to be used outside of this module, unless you know
50 what you are doing."""
51 # make tuple of needed metaclasses in specified priority order
52 metas
= left_metas
+ tuple(map(type, bases
)) + right_metas
53 needed_metas
= remove_redundant(metas
)
55 # return existing confict-solving meta, if any
56 if needed_metas
in memoized_metaclasses_map
:
57 return memoized_metaclasses_map
[needed_metas
]
58 # nope: compute, memoize and return needed conflict-solving meta
59 elif not needed_metas
: # wee, a trivial case, happy us
61 elif len(needed_metas
) == 1: # another trivial case
62 meta
= needed_metas
[0]
63 # check for recursion, can happen i.e. for Zope ExtensionClasses
64 elif needed_metas
== bases
:
65 raise TypeError("Incompatible root metatypes", needed_metas
)
66 else: # gotta work ...
67 metaname
= '_' + ''.join([m
.__name
__ for m
in needed_metas
])
68 meta
= classmaker()(metaname
, needed_metas
, {})
69 memoized_metaclasses_map
[needed_metas
] = meta
72 def classmaker(left_metas
=(), right_metas
=()):
73 def make_class(name
, bases
, adict
):
74 metaclass
= get_noconflict_metaclass(bases
, left_metas
, right_metas
)
75 return metaclass(name
, bases
, adict
)
79 class BaseAttr(object):
80 def __init__(self
, default
= None, linked_default
= None):
81 self
.__default
= default
82 self
.__linked
_default
= linked_default
87 def linked_default(self
):
88 return self
.__linked
_default
90 class TextAttr(BaseAttr
):
91 def __init__(self
, name
, default
= '', linked_default
= None):
92 BaseAttr
.__init
__(self
, default
, linked_default
)
94 self
.inst_attr
= "__%s" % name
95 def __set__(self
, instance
, value
):
96 setattr(instance
, self
.inst_attr
, str(value
))
97 def __get__(self
, instance
, owner
):
100 if hasattr(instance
, self
.inst_attr
):
101 return getattr(instance
, self
.inst_attr
)
103 if self
.linked_default
:
104 return getattr(instance
, self
.linked_default
)
107 def __delete__(self
, instance
):
108 raise AttributeError('Cannot delete attribute')
110 class NumberAsTextAttr(BaseAttr
):
111 def __init__(self
, name
, default
= '0', linked_default
= None, enforce_as
= 'grapevine_float', simplify
= True):
112 BaseAttr
.__init
__(self
, default
, linked_default
)
114 self
.inst_attr
= "__%s" % name
115 self
.enforce_as
= enforce_as
116 self
.simplify
= simplify
117 def __set__(self
, instance
, value
):
118 if self
.enforce_as
== 'grapevine_float':
119 if not self
.__is
_valid
_grapevine
_float
(value
):
120 raise ValueError('Cannot set attribute to value %s, no valid numbers' % (value
))
121 elif self
.enforce_as
== 'float':
125 raise ValueError('Cannot set attribute to value %s, not a float value' % (value
))
126 elif self
.enforce_as
== 'int':
130 raise ValueError('Cannot set attribute to value %s, not an int value' %(value))
132 setattr(instance
, self
.inst_attr
, self
.__simplify
_float
_str
(value
))
134 setattr(instance
, self
.inst_attr
, value
)
135 def __get__(self
, instance
, owner
):
138 if hasattr(instance
, self
.inst_attr
):
139 return getattr(instance
, self
.inst_attr
)
141 if self
.linked_default
:
142 return getattr(instance
, self
.linked_default
)
145 def __is_valid_grapevine_float(self
, value
):
146 """Grapevine can store an integer value, a float value, a range specified by
147 by a '-', and two option values. Any number style string needs to be checked
148 for all of these options.
150 returns True if value should be accepted by as a grapevine numeric"""
156 for separator_str
in ['-', ' or ']:
157 for innerval
in value
.split(separator_str
):
164 def __simplify_float_str(self
, value
):
166 if float(value
) == round(float(value
)):
167 return unicode(int(round(float(value
))))
173 class BoolAttr(BaseAttr
):
174 def __init__(self
, name
, default
= False, linked_default
= None):
175 BaseAttr
.__init
__(self
, default
, linked_default
)
177 self
.inst_attr
= "__%s" % name
178 def __set__(self
, instance
, value
):
186 setattr(instance
, self
.inst_attr
, value
)
187 def __get__(self
, instance
, owner
):
190 if hasattr(instance
, self
.inst_attr
):
191 return getattr(instance
, self
.inst_attr
)
193 if self
.linked_default
:
194 return getattr(instance
, self
.linked_default
)
197 def __delete__(self
, instance
):
198 raise AttributeError('Cannot delete attribute')
200 class DateAttr(BaseAttr
):
201 def __init__(self
, name
, default
= None, linked_default
= None):
202 BaseAttr
.__init
__(self
, default
, linked_default
)
204 self
.inst_attr
= "__%s" % name
205 def __set__(self
, instance
, value
):
206 if not isinstance(value
, datetime
):
207 setattr(instance
, self
.inst_attr
, parse(value
))
209 setattr(instance
, self
.inst_attr
, value
)
210 def __get__(self
, instance
, owner
):
213 if hasattr(instance
, self
.inst_attr
):
214 return getattr(instance
, self
.inst_attr
)
216 if self
.linked_default
:
217 return getattr(instance
, self
.linked_default
)
220 def __delete__(self
, instance
):
221 raise AttributeError('Cannot delete attribute')
223 class AttributeBuilder(type):
224 def __init__(cls
, name
, bases
, dict):
225 super(AttributeBuilder
, cls
).__init
__(name
, bases
, dict)
226 attribute_class_map
= [
227 ('required_attrs', TextAttr
),
228 ('text_attrs', TextAttr
),
229 ('number_as_text_attrs', NumberAsTextAttr
),
230 ('bool_attrs', BoolAttr
),
231 ('date_attrs', DateAttr
),
232 ('text_children', TextAttr
)
234 defaults
= getattr(cls
, 'defaults', {})
235 linked_defaults
= getattr(cls
, 'linked_defaults', {})
236 for pair
in attribute_class_map
:
237 current_desired_properties
= getattr(cls
, pair
[0], [])
238 for prop
in current_desired_properties
:
240 attr_name
= prop
if not isinstance(prop
, tuple) else prop
[0]
242 if defaults
.has_key(attr_name
):
243 local_kargs
['default'] = defaults
[attr_name
]
244 if linked_defaults
.has_key(attr_name
):
245 local_kargs
['linked_default'] = linked_defaults
[attr_name
]
247 extra_kargs
= {} if not isinstance(prop
, tuple) else prop
[1]
248 local_kargs
.update(extra_kargs
)
250 new_attr
= pair
[1](attr_name
, **local_kargs
)
251 setattr(cls
, attr_name
, new_attr
)