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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
|
# Copyright 2007-2008 Nanorex, Inc. See LICENSE file for details.
"""
FeatureDescriptor.py - descriptor objects for program features
@author: Bruce
@version: $Id$
@copyright: 2007-2008 Nanorex, Inc. See LICENSE file for details.
"""
# == constants
_KLUGE_PERMIT_INHERITED_CLASSNAMES = (
"Command",
"basicMode",
"Select_basicCommand", # abstract, but has to be in this list
# since it doesn't have its own featurename
"Select_Command",
"selectMode",
"TemporaryCommand_preMixin", # (should this count as abstract?)
"_minimalCommand", # (ditto)
## "_Example_TemporaryCommand_useParentPM", # abstract, but doesn't have its own featurename
)
# == global state
_descriptor_for_feature_class = {}
# maps feature class to FeatureDescriptor
_descriptor_for_featurename = {}
# maps featurename (canonicalized) to FeatureDescriptor;
# (used to warn about duplicate featurenames,
# other than by subclasses not overriding one from a superclass)
_feature_classes = {}
# maps feature class (abstract class) to descriptor constructor for it;
# only contains the classes directly passed to register_abstract_feature_class
# (with descriptor_constructors), not their subclasses.
_feature_class_tuple = () # tuple of feature classes, suitable for passing to issubclass;
# recreated automatically after each call of register_abstract_feature_class
_feature_kind_subclasses = [] # list of subclasses corresponding to specific
# kinds of features to report in special ways
# (todo: permit passing different more specific descriptor_constructors
# for those -- that doesn't work now, since _feature_class_tuple doesn't
# preserve registration order)
# TODO: use this in describing kind of feature, in export command table
# TODO: register EditCommand
# [bruce 080905]
# ==
def short_class_name( clas):
# todo: refile into utilities.constants; has lots of inlined uses
# todo: generalize to non-class things
# note: used for name-mangling too, so don't change what it returns
return clas.__name__.split('.')[-1]
def canonicalize_featurename( featurename, warn = False):
# refile? does part of this function already exist elsewhere?
featurename0 = featurename
featurename = featurename.strip()
# turn underscores to blanks [bruce 080717, to work around
# erroneous underscores used in some featurename constants;
# bad effect: it also removes at least one correct one,
# in featurename = "Test Command: PM_Widgets Demo";
# nonetheless this is necessary to make sure wiki help URLs
# don't coincide (since those contain '_' in place of ' ')]
featurename = featurename.replace('_', ' ')
if warn and featurename != featurename0:
msg = "developer warning: featurename %r was canonicalized to %r" % \
( featurename0, featurename )
print msg
return featurename
# ==
def register_abstract_feature_class( feature_class, descriptor_constructor = None ):
global _feature_class_tuple
if descriptor_constructor is None:
# assert that we're a subclass of an existing feature class
assert issubclass( feature_class, _feature_class_tuple )
# 080905: record this class (assume that more general classes get
# recorded first, since this function is called immediately
# after they're defined -- first verify that assumption)
for fc in _feature_kind_subclasses:
assert not issubclass(fc, feature_class), \
"subclass %r must be registered after superclass %r" % \
( fc, feature_class)
_feature_kind_subclasses.append( feature_class)
else:
_feature_classes[ feature_class ] = descriptor_constructor
_feature_class_tuple = tuple( _feature_classes.keys() )
return
# ==
def find_or_make_descriptor_for_possible_feature_object( thing):
"""
@param thing: anything which might be found as a global value in some module
@return: descriptor (found or made) for program feature represented
by thing, or None if thing doesn't represent a program feature.
@rtype: FeatureDescriptor or None
"""
# so far, all features are represented by subclasses of
# registered abstract feature classes.
# (someday there might be other kinds of features,
# e.g. plugins discovered at runtime and represented
# by instance objects or separately created descriptors.)
try:
foundone = issubclass( thing, _feature_class_tuple )
except:
# not a class
return None
if not foundone:
# not a subclass of a registered feature class
return None
# thing is a subclass of some class in _feature_class_tuple
return find_or_make_FeatureDescriptor( thing)
# ==
def find_or_make_FeatureDescriptor( thing):
"""
@param thing: the program object or class corresponding internally to a
specific program feature, or any object of the "same kind"
(presently, any subclass of an element of _feature_class_tuple)
@return: FeatureDescriptor for thing (found or made),
or None if thing does not describe a program feature.
@rtype: FeatureDescriptor or None
@note: fast, if descriptor is already known for thing
@see: find_or_make_descriptor_for_possible_feature_object, for when
you want to call something like this on an arbitrary Python object.
"""
try:
# note: thing is hashable, since it's a class
return _descriptor_for_feature_class[thing]
except KeyError:
pass
res = _determine_FeatureDescriptor( thing) # might be None
# However we got the description (even if we're reusing one),
# cache it, to optimize future calls and prevent redundant warnings.
_descriptor_for_feature_class[thing] = res
return res
def _determine_FeatureDescriptor( thing):
"""
Determine (find or make) and return the FeatureDescriptor
to use with thing. (If thing is abstract, return None.)
@param thing: an object or class corresponding internally
to a specific program feature, and for which
no descriptor is already cached
(though we might return one which was already
cached for a different thing, e.g. a superclass).
@note: for now, this can only handle classes,
and only if they have a superclass that has been registered
with register_abstract_feature_class.
"""
assert issubclass( thing, _feature_class_tuple), \
"wrong kind of thing: %r" % (thing,)
# note: this fails with some kind of exception for non-classes,
# and with AssertionError for classes that aren't a subclass of
# a registered class
clas = thing
del thing
featurename = clas.featurename # note: not yet canonicalized
# See if class is declared abstract.
# It declares that using a name-mangled attribute, __abstract_command_class,
# so its subclasses don't accidentally inherit that declaration.
# (This is better than using command_level == CL_ABSTRACT,
# since that can be inherited mistakenly. REVIEW whether that
# attr value should survive at all.)
# [bruce 080905 new feature]
short_name = short_class_name( clas)
mangled_abstract_attrname = "_%s__abstract_command_class" % short_name
abstract = getattr(clas, mangled_abstract_attrname, False)
if abstract:
return None
if clas in _feature_class_tuple:
print "\n*** possible bug: %r probably ought to define __abstract_command_class = True" % short_name
# if not true, after review, revise this print [bruce 080905]
return None
# see if featurename is inherited
inherited_from = None # might be changed below
for base in clas.__bases__: # review: use mro?
inherited_featurename = getattr( base, 'featurename', None)
if inherited_featurename == featurename:
inherited_from = base
break
if inherited_from is not None:
# decide whether this is legitimate (use inherited description),
# or not (warn, and make up a new description).
legitimate = short_name in _KLUGE_PERMIT_INHERITED_CLASSNAMES
# maybe: also add ways to register such classes,
# and/or to mark them using __abstract_command_class = True
# (made unique to that class by name-mangling).
# maybe: point out in warning if it ends with Mode or Command
# (but not with basicCommand) (probably not worth the trouble)
if legitimate:
return find_or_make_FeatureDescriptor( inherited_from)
# return that even if it's None (meaning we're an abstract class)
# (todo: review that comment -- abstract classes are now detected earlier;
# can this still be None at this point? Doesn't matter for now.)
# make it unique
featurename = canonicalize_featurename( featurename, warn = True)
featurename = featurename + " " + short_name
print
print "developer warning: auto-extending inherited featurename to make it unique:", featurename
print " involved classes: %r and its subclass %r" % \
( short_class_name( inherited_from),
short_class_name( clas) )
print " likely fixes: either add %r to _KLUGE_PERMIT_INHERITED_CLASSNAMES," % ( short_name, )
print " or define a featurename class constant for it,"
print " or declare it as abstract by defining __abstract_command_class = True in it."
pass # use new featurename to make a new description, below
else:
# not inherited
featurename = canonicalize_featurename( featurename, warn = True)
assert not short_name in _KLUGE_PERMIT_INHERITED_CLASSNAMES, short_name
pass
# use featurename (perhaps modified above) to make a new description
descriptor_constructor = _choose_descriptor_constructor( clas)
descriptor = descriptor_constructor( clas, featurename )
# warn if featurename is duplicated (but return it anyway)
if _descriptor_for_featurename.has_key( featurename):
print "developer warning: duplicate featurename %r for %r and %r" % \
( featurename,
_descriptor_for_featurename[ featurename ].thing,
descriptor.thing
)
else:
_descriptor_for_featurename[ featurename] = descriptor
return descriptor # from _determine_FeatureDescriptor
# ==
def _choose_descriptor_constructor( subclass):
"""
subclass is a subclass of something in global _feature_classes
(but not identical to anything in it);
find the corresponding descriptor_constructor;
if more than one matches, return the most specific (or error if we can't).
"""
candidates = [(feature_class, descriptor_constructor)
for feature_class, descriptor_constructor in _feature_classes.iteritems()
if issubclass( subclass, feature_class )
]
assert candidates
if len(candidates) == 1:
return candidates[0][1]
assert 0, "nim for multiple candidates (even when all values the same!): %r" % (candidates,)
return candidates[0][1]
# ===
class FeatureDescriptor(object):
"""
Abstract superclass for various kinds of feature descriptors.
"""
def __init__(self, thing, featurename):
self.thing = thing # rename?
self.featurename = featurename
pass
# ===
def command_package_part_of_module_name(name):
"""
Given a module name like "dna.commands.InsertDna.InsertDna_EditCommand",
return the command package part, "dna.commands.InsertDna".
Return a command package name itself unchanged.
For module names, not inside a command package (or equalling one),
return None.
"""
command_package = None # default return value
name_parts = name.split('.')
if 'commands' in name_parts:
where = name_parts.index('commands')
if where not in (0, 1):
print "unusual location for 'commands' in module name:", name
if where < len(name_parts) - 1:
command_package = '.'.join(name_parts[0:where+2])
return command_package
class CommandDescriptor(FeatureDescriptor):
"""
Abstract superclass for descriptors for various kinds of comands.
"""
command_package = None
pass
class otherCommandPackage_Descriptor( CommandDescriptor):
"""
Descriptor for a command presumed to exist in a given command_package
in which no actual command was found.
"""
def __init__(self, command_package):
CommandDescriptor.__init__( self, None, None )
self.command_package = command_package
return
def sort_key(self):
return ( 0, self.command_package )
def print_plain(self):
print "command_package:", self.command_package
print "type: command package (no command found)"
pass
class basicCommand_Descriptor( CommandDescriptor): # refile with basicCommand?
"""
Descriptor for a command feature defined by any basicCommand subclass.
"""
def __init__(self, command_class, featurename):
CommandDescriptor.__init__( self, command_class, featurename )
# initialize various metainfo
modulename = command_class.__module__
self.command_package = command_package_part_of_module_name( modulename)
self.feature_type = self._get_feature_type()
# todo: more
return
def _get_command_class(self):
return self.thing
command_class = property( _get_command_class)
def _get_feature_type(self):
for fc in (list(_feature_class_tuple) + _feature_kind_subclasses)[::-1]:
# list is most general first, so we scan it backwards
if issubclass( self.command_class, fc):
return short_class_name(fc) + " subclass"
return "unknown" # bug
def _get_porting_status(self):
# temporary code during the port to USE_COMMAND_STACK
porting_status = getattr(self.command_class,
'command_porting_status',
"bug: command_porting_status not defined" )
return porting_status or ""
def sort_key(self):
# revise to group dna commands together, etc? or subcommands of one main command?
# yes, when we have the metainfo to support that.
return ( 1, self.featurename, short_class_name( self.command_class) ) #e more?
def print_plain(self):
porting_status = self._get_porting_status()
# porting_status is temporary code during the port to USE_COMMAND_STACK
fully_ported = not porting_status
if fully_ported:
print "featurename: <b>%s</b>" % self.featurename
else:
print "featurename:", self.featurename
print "classname:", short_class_name( self.command_class)
print "command_package:", self.command_package
print "type:", self.feature_type
if self.command_class.is_fixed_parent_command():
# note: it works to call this classmethod directly on the class
print "level:", self.command_class.command_level, \
"(%s)" % (self.command_class.command_parent or "no command_parent")
else:
print "level:", self.command_class.command_level
if not fully_ported:
print "porting status:", porting_status
# todo: more
return
pass # end of class basicCommand_Descriptor
# end
|