add vcs-* fields to debian/control
[debian/make-magic.git] / core / marshal.py
1 #! /usr/bin/env python
2
3 '''Marshal in and out of internal object representations
4
5 Internally, we use the object types defined in core.bits,
6 however we need some way of getting stuff in and out of that format
7 both for IPC, and so that people don't have to write their
8 item definitions in Python[0].  We also need to be able to talk in 
9 different formats to build APIs with.
10
11 To do this, we're going to use a simple, data based common format that
12 should be able to be represented in several different formats (e.g.
13 python objects, json, xml etc). Bonus points for being able to use 
14 off-the-shelf encoders and decoders.
15
16 The internal format for item classes is based is a list of dicts in
17 the form:
18
19 items = [
20    { 'name':         'itemname',                           # required
21      'depends':      ['itemname2', 'itemname43', 'groupname']  # optional
22      'description':  'multi-line description of the item', # optional
23      'if':           '<predicate definition>'              # optional
24    },
25
26    { 'name':         'itemname2',
27      'depends':      []
28    },
29
30    { 'group':        'groupname',                          # required
31      'contains':     ['itemname43', itemname32','groupname5']  # required
32      'depends':      ['itemname47', 'groupname2' ...]      # optional
33      'description':  'multi-line description of the item', # optional
34      'if':           '<predicate definition>'              # optional
35    },
36    ...
37 ]
38
39 where all dependencies refered to must be defined in the list.
40 This is equivalent to the internal definition:
41
42         class itemname(bits.Item):
43                 description = 'multi-line description of the item'
44                 depends = (itemname2, itemname43, groupname)
45                 predicate = <callable that returns True iff predicate holds over passed requirements>
46
47         class itemname2(bits.Item):
48                 pass
49
50         class groupname(bits.Group):
51                 description = 'multi-line description of the item'
52                 depends = (itemname47, groupname2)
53                 contains = (itemname43, itemname32, groupname5)
54                 predicate = <callable that returns True iff predicate holds over passed requirements>
55
56         items = [ itemname, itemname2, groupname ]
57
58 Item instances are represented in the same way, but the dicts can have 
59 extra key/value pairs for item state and metadata. These are available
60 as as a dict as the 'data' property on the python object instances.
61
62 Group instances are not currently able to be marshalled; Only classes.
63 Groups should be reduced out during the process of Task creation.
64
65 Predicate definitions as strings are currently as defined in the digraphtools.predicate module
66 We use the PredicateContainsFactory to generate the predicates. This also allows us to marshal
67 them back and forward pretty easily from strings
68
69 [0] Although it's extensible inside so that you can do things like
70 write predicates in pure python, the whole system has to be usable
71 by someone that doesn't know a line of python.
72 '''
73 import core.bits
74 from digraphtools.predicate import PredicateContainsFactory
75
76 class ItemConverter(object):
77         '''Convert items to and from Item objects
78         '''
79
80         identifier_chrs = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_")
81         reserved_keys = set(('name','group','depends','description','contains','if'))
82         def normalise_item_name(self, name):
83                 '''return a passed string that can be used as a python class name'''
84                 name = str(name)
85                 name = filter(self.identifier_chrs.__contains__, name)
86                 if name[:1].isdigit(): 
87                         name = '_'+name
88                 return name
89
90         def predicate_string_to_callable(self, predicate):
91                 '''turn a predicate into a callable'''
92                 pf = PredicateContainsFactory()
93                 pred = pf.predicate_from_string(predicate)
94                 pred._predicate_string = predicate  # Save for marshalling back the other way
95                 return pred
96
97         def predicate_callable_to_string(self, predicate):
98                 '''turn a predicate into a callable'''
99                 if hasattr(predicate, '_predicate_string'):
100                         return predicate._predicate_string      # Restore previous knowledge. Mwahahahah
101                 raise ValueError('Cannot marshal strange predicate into a string.')
102
103         def itemdict_to_group(self, itemdict):
104                 '''return a Group subclass from an item dict datastructure
105                 This does not unroll dependencies or group contents from strings into classes
106                 pre: itemdict is valid
107                 '''
108                 assert not itemdict.has_key('name')
109                 name = self.normalise_item_name(itemdict['group'])
110                 attrs = dict(contains=itemdict['contains'])
111                 if itemdict.has_key('depends'): attrs['depends'] = tuple(itemdict['depends'])
112                 if itemdict.has_key('description'): attrs['description'] = itemdict['description']
113                 if itemdict.has_key('if'): attrs['predicate'] = self.predicate_string_to_callable(itemdict['if'])
114                 return type.__new__(type, name, (core.bits.Group,), attrs)
115                 
116         def itemdict_to_item_class(self, itemdict):
117                 '''return an Item subclass from an item dict datastructure
118                 This does not unroll item dependencies from strings into classes
119
120                 pre: itemdict is valid
121                 '''
122                 if itemdict.has_key('group'): 
123                         return self.itemdict_to_group(itemdict)
124
125                 name = self.normalise_item_name(itemdict['name'])
126                 if name == 'TaskComplete':
127                         itemsuper = core.bits.TaskComplete
128                 else:
129                         itemsuper = core.bits.Item
130                 attrs = dict()
131                 if itemdict.has_key('depends'): attrs['depends'] = tuple(itemdict['depends'])
132                 if itemdict.has_key('description'): attrs['description'] = itemdict['description']
133                 if itemdict.has_key('if'): attrs['predicate'] = self.predicate_string_to_callable(itemdict['if'])
134                 return type.__new__(type, name, (itemsuper,), attrs)
135
136         def itemdict_to_item_instance(self, itemdict):
137                 cl = self.itemdict_to_item_class(itemdict)
138                 data = dict((k,v) for k,v in itemdict.items() if k not in self.reserved_keys)
139                 return cl(data=data)
140
141         def itemclass_to_itemdict(self, item):
142                 '''return an item dict datastructure from an Item or Group subclass'''
143                 if issubclass(item,core.bits.Group):
144                         itemdict = dict(group=item.__name__, contains=[c.__name__ for c in item.contains])
145                 else:
146                         itemdict = dict(name=item.__name__)
147                 if item.depends: itemdict['depends'] = list(d.__name__ for d in item.depends)
148                 if item.description: itemdict['description'] = item.description
149                 if item.predicate != core.bits.BaseItem.predicate:
150                         # This might fail if someone has put their own callable in as a predicate
151                         # That's okay; it just means they can't marshal their classes back to json
152                         itemdict['if'] = self.predicate_callable_to_string(item.predicate)
153                 return itemdict
154
155         def item_to_itemdict(self, item):
156                 '''return an item dict datastructure from an Item instance
157                 Note: Does not work on groups and does not convert predicates
158                 '''
159                 assert not isinstance(item, core.bits.Group)
160                 itemdict = dict(name=item.name)
161                 itemdict.update(dict((k,v) for k,v in item.data.items() if k not in self.reserved_keys))
162                 if item.description: itemdict['description'] = item.description
163                 if len(item.depends):
164                         itemdict['depends'] = [d.name for d in item.depends]
165                 return itemdict
166
167 class TaskConverter(ItemConverter):
168         def taskdict_to_task(self, taskdict):
169                 # turn the items into instances
170                 items = map(self.itemdict_to_item_instance, taskdict['items'])
171
172                 # reference them to each other correctly
173                 item_by_name = dict((item.name,item) for item in items)
174                 for item in items:
175                         item.depends = tuple(item_by_name[dep] for dep in item.depends)
176
177                 # Find the goal node
178                 metadata = taskdict['metadata']
179                 goal = item_by_name['TaskComplete']
180                 requirements = metadata['requirements']
181                 uuid = metadata['uuid']
182                 return core.bits.Task(items, requirements, goal, uuid, metadata)