summaryrefslogtreecommitdiff
path: root/cad/src/foundation/FeatureDescriptor.py
blob: 1013dfaa55602494c614ada851e1e0f8de277855 (plain)
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