module exports a property instead of a class -- Evil?

G

gry

I often find myself wanting an instance attribute that can take on only
a few fixed symbolic values. (This is less functionality than an enum,
since there are no *numbers* associated with the values). I do want
the thing to fiercely object to assignments or comparisons with
inappropriate values. My implementation below gets me:

..import mode
..class C(object):
.. status = mode.Mode('started', 'done', 'on-hold')
..
..c=C()
..c.status = 'started'
..c.status = 'stated': #Exception raised
..if c.status == 'done': something
..if c.status == 'stated': #Exception raised
..if c.status.done: something #simpler and clearer than string compare
..if c.status < 'done': something # Mode arg strings define ordering

I would appreciate comments on the overall scheme, as well as about the
somewhat sneaky (I think) exporting of a property-factory instead of a
class. My intent is to provide a simple clear interface to the client
class ("C" above), but I don't want to do something *too* fragile or
confusing...
(I'd also welcome a better name than "Mode"...)

-------------------------- mode.py ----------------------
class _Mode: #internal use only, not exported.
def __init__(self, *vals):
if [v for v in vals if not isinstance(v, str)]:
raise ValueError, 'Mode values must be strings'
else:
self.values = list(vals)

def set(self, val):
if val not in self.values:
raise ValueError, 'bad value for Mode: "%s"' % val
else:
self.state = val

def __cmp__(self, other):
if other in self.values:
return cmp(self.values.index(self.state),
self.values.index(other))
else:
raise ValueError, 'bad value for Mode comparison'

def __getattr__(self, name):
if name in self.values:
return self.state == name
else:
raise AttributeError, 'no such attribute: "%s"' % name


def Mode(*vals): # *function* returning a *property*, not a class.
m = _Mode(*vals)
def _insert_mode_get(self):
return m
def _insert_mode_set(self, val):
m.set(val)
return property(_insert_mode_get, _insert_mode_set)
 
L

Lonnie Princehouse

The property factory is nice, but have you considered subclassing
property?

class Mode(property):
def __init__(self, *vals):
if [v for v in vals if not isinstance(v,str)]:
raise ValueError, 'Mode values must be strings'
else:
self.values = list(vals)
property.__init__(self, self.get, self.set)
def get(self, instance):
...
def set(self, instance, value):
...
 
G

gry

Hmm, I had no idea that "property" was a class. It's listed in the
library
reference manual under builtin-functions. That will certainly make
things neater. Thanks!

-- George
 
B

Bengt Richter

I often find myself wanting an instance attribute that can take on only
^^^^^^^^
without checking deeply, are you not sharing state among all instance?
See following for an alternative way, allowing initialization by a
first assignment of a name sequence, followed by normal operation on
a per instance basis.
a few fixed symbolic values. (This is less functionality than an enum,
since there are no *numbers* associated with the values). I do want
the thing to fiercely object to assignments or comparisons with
inappropriate values. My implementation below gets me:

.import mode
.class C(object):
. status = mode.Mode('started', 'done', 'on-hold')
.
.c=C()
.c.status = 'started'
.c.status = 'stated': #Exception raised
.if c.status == 'done': something
.if c.status == 'stated': #Exception raised
.if c.status.done: something #simpler and clearer than string compare
.if c.status < 'done': something # Mode arg strings define ordering

I would appreciate comments on the overall scheme, as well as about the
somewhat sneaky (I think) exporting of a property-factory instead of a
class. My intent is to provide a simple clear interface to the client
class ("C" above), but I don't want to do something *too* fragile or
confusing...
(I'd also welcome a better name than "Mode"...)

-------------------------- mode.py ----------------------
class _Mode: #internal use only, not exported.
def __init__(self, *vals):
if [v for v in vals if not isinstance(v, str)]:
raise ValueError, 'Mode values must be strings'
else:
self.values = list(vals)

def set(self, val):
if val not in self.values:
raise ValueError, 'bad value for Mode: "%s"' % val
else:
self.state = val

def __cmp__(self, other):
if other in self.values:
return cmp(self.values.index(self.state),
self.values.index(other))
else:
raise ValueError, 'bad value for Mode comparison'

def __getattr__(self, name):
if name in self.values:
return self.state == name
else:
raise AttributeError, 'no such attribute: "%s"' % name


def Mode(*vals): # *function* returning a *property*, not a class.
m = _Mode(*vals)
def _insert_mode_get(self):
return m
def _insert_mode_set(self, val):
m.set(val)
return property(_insert_mode_get, _insert_mode_set)
Not tested beyond what you see ;-)

----< state.py >----------------------------------------------------------------------------------------------
# set up to validate state name strings
namerefcode = compile('a','','eval').co_code
non_name_chars = []
for c in (chr(i) for i in xrange(256)):
try:
if compile(c, '', 'eval').co_code != namerefcode:
non_name_chars.append(c)
except (SyntaxError, TypeError):
non_name_chars.append(c)
non_name_chars = ''.join(non_name_chars)
idem = ''.join([chr(i) for i in xrange(256)])

class Status(object):
def __get__(self, inst, cls=None):
if inst is None: return self
if not '_state' in inst.__dict__:
raise ValueError, 'Uninitialized instance state names'
return inst._state
def __set__(self, inst, value):
if not hasattr(inst, '_state'): inst._state = self.State(*value)
else: inst._state._setv(value)

class State(object):
def __init__(self, *names):
for s in names:
if s[:1].isdigit() or s!= s.translate(idem, non_name_chars):
raise ValueError, '%r is not a valid name'%s
self._names = list(names)
self._value = names[0]
def _name_ck(self, name):
if name not in self._names:
raise AttributeError(
'Legal names are: %s -- not %r' % (', '.join(map(repr, self._names)), name))
def __getattr__(self, attr):
if attr.startswith('_'):
return object.__getattribute__(self, attr)
self._name_ck(attr)
return self._value == attr
def _setv(self, value):
self._name_ck(value)
self._value = value
def __cmp__(self, other):
self._name_ck(other)
return cmp(self._names.index(self._value), self._names.index(other))
def __str__(self):
return self._value

def test():
class C(object):
status = Status()
instances = [C(), C(), C()]
nameslist = map(str.split, ['started done on_hold', 'one two three', 'UNK running stopped'])
for i, (inst, names) in enumerate(zip(instances, nameslist)):
inst.status = names
inst.status = names
print 'Instance %s names: %r' %(i, inst.status._names)
for i, inst in enumerate(instances):
print i, 'names:',inst.status._names
print i, 'current:', inst.status
print i, ' '.join([' .%s? -> %s'% (name, getattr(inst.status, name)) for name in inst.status._names])
print i, ' '.join(['==%s? -> %s'% (name, inst.status == name) for name in inst.status._names])
print i, ' '.join([' >%s? -> %s'% (name, inst.status > name) for name in inst.status._names])
print i, ' '.join([' <%s? -> %s'% (name, inst.status < name) for name in inst.status._names])
try: inst.status = 'fini'
except Exception, e:
print 'Exception %s: %s' %(e.__class__.__name__, e)
class D(object):
hownow = Status()
dinst = D()
for bogus in ['this$', 'an#d', 'with space', '4digitstart', 'bad\x00']:
try: dinst.hownow = ('verily', bogus, 'will', 'fail')
except Exception, e:
print 'Exception %s: %s' %(e.__class__.__name__, e)

if __name__ == '__main__': test()
--------------------------------------------------------------------------------------------------------------
Result:

[ 0:00] C:\pywk\clp>py24 state.py
Instance 0 names: ['started', 'done', 'on_hold']
Instance 1 names: ['one', 'two', 'three']
Instance 2 names: ['UNK', 'running', 'stopped']
0 names: ['started', 'done', 'on_hold']
0 current: started
0 .started? -> True .done? -> False .on_hold? -> False
0 ==started? -> True ==done? -> False ==on_hold? -> False
0 >started? -> False >done? -> False >on_hold? -> False
0 <started? -> False <done? -> True <on_hold? -> True
Exception AttributeError: Legal names are: 'started', 'done', 'on_hold' -- not 'fini'
1 names: ['one', 'two', 'three']
1 current: two
1 .one? -> False .two? -> True .three? -> False
1 ==one? -> False ==two? -> True ==three? -> False
1 >one? -> True >two? -> False >three? -> False
1 <one? -> False <two? -> False <three? -> True
Exception AttributeError: Legal names are: 'one', 'two', 'three' -- not 'fini'
2 names: ['UNK', 'running', 'stopped']
2 current: stopped
2 .UNK? -> False .running? -> False .stopped? -> True
2 ==UNK? -> False ==running? -> False ==stopped? -> True
2 >UNK? -> True >running? -> True >stopped? -> False
2 <UNK? -> False <running? -> False <stopped? -> False
Exception AttributeError: Legal names are: 'UNK', 'running', 'stopped' -- not 'fini'
Exception ValueError: 'this$' is not a valid name
Exception ValueError: 'an#d' is not a valid name
Exception ValueError: 'with space' is not a valid name
Exception ValueError: '4digitstart' is not a valid name
Exception ValueError: 'bad\x00' is not a valid name

Regards,
Bengt Richter
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,774
Messages
2,569,598
Members
45,150
Latest member
MakersCBDReviews
Top