summaryrefslogtreecommitdiff
path: root/morphlib/morphset.py
blob: 468bcbe94024b080bc6d107036692b194ee4e5a0 (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
# Copyright (C) 2013  Codethink Limited
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# =*= License: GPL-2 =*=


import morphlib


class StratumNotInSystemError(morphlib.Error):

    def __init__(self, system_name, stratum_name):
        self.msg = (
            'System %s does not contain %s' % (system_name, stratum_name))


class StratumNotInSetError(morphlib.Error):

    def __init__(self, stratum_name):
        self.msg = 'Stratum %s is not in MorphologySet' % stratum_name


class ChunkNotInStratumError(morphlib.Error):

    def __init__(self, stratum_name, chunk_name):
        self.msg = (
            'Stratum %s does not contain %s' % (stratum_name, chunk_name))


class MorphologySet(object):

    '''Store and manipulate a set of Morphology objects.'''

    def __init__(self):
        self.morphologies = []

    def add_morphology(self, morphology):
        '''Add a morphology object to the set, unless it's there already.'''

        triplet = (
            morphology.repo_url,
            morphology.ref,
            morphology.filename
        )
        for existing in self.morphologies:
            existing_triplet = (
                existing.repo_url,
                existing.ref,
                existing.filename
            )
            if existing_triplet == triplet:
                return

        self.morphologies.append(morphology)

    def has(self, repo_url, ref, filename):
        '''Does the set have a morphology for the given triplet?'''
        return self._get_morphology(repo_url, ref, filename) is not None

    def _get_morphology(self, repo_url, ref, filename):
        for m in self.morphologies:
            if (m.repo_url == repo_url and
                m.ref == ref and
                m.filename == filename):
                return m
        return None

    def _find_spec(self, specs, wanted_name):
        for spec in specs:
            name = spec.get('morph', spec.get('name'))
            if name == wanted_name:
                return spec['repo'], spec['ref'], name
        return None, None, None

    def get_stratum_in_system(self, system_morph, stratum_name):
        '''Return morphology for a stratum that is in a system.

        If the stratum is not in the system, raise StratumNotInSystemError.
        If the stratum morphology has not been added to the set,
        raise StratumNotInSetError.

        '''

        repo_url, ref, morph = self._find_spec(
            system_morph['strata'], stratum_name)
        if repo_url is None:
            raise StratumNotInSystemError(system_morph['name'], stratum_name)
        m = self._get_morphology(repo_url, ref, '%s.morph' % morph)
        if m is None:
            raise StratumNotInSetError(stratum_name)
        return m

    def get_chunk_triplet(self, stratum_morph, chunk_name):
        '''Return the repo url, ref, morph name triplet for a chunk.

        Given a stratum morphology, find the triplet used to refer to
        a given chunk. Note that because of how the chunk may be
        referred to using either name or morph fields in the morphology,
        the morph field (or its computed value) is always returned.
        Note also that the morph field, not the filename, is returned.

        Raise ChunkNotInStratumError if the chunk is not found in the
        stratum.

        '''

        repo_url, ref, morph = self._find_spec(
            stratum_morph['chunks'], chunk_name)
        if repo_url is None:
            raise ChunkNotInStratumError(stratum_morph['name'], chunk_name)
        return repo_url, ref, morph

    def _traverse_specs(self, cb_process, cb_filter=lambda s: True):
        '''Higher-order function for processing every spec.
        
        This traverses every spec in all the morphologies, so all chunk,
        stratum and stratum-build-depend specs are visited.

        It is to be passed one or two callbacks. `cb_process` is given
        a spec, which it may alter, but if it does, it must return True.

        `cb_filter` is given the morphology, the kind of spec it is
        working on in addition to the spec itself.

        `cb_filter` is expected to decide whether to run `cb_process`
        on the spec.

        Arguably this could be checked in `cb_process`, but it can be less
        logic over all since `cb_process` need not conditionally return.

        If any specs have been altered, at the end of iteration, any
        morphologies in the MorphologySet that are referred to by an
        altered spec are also changed.

        This requires a full iteration of the MorphologySet, so it is not a
        cheap operation.

        A coroutine was attempted, but it required the same amount of
        code at the call site as doing it by hand.
        
        '''

        altered_references = {}

        def process_spec_list(m, kind):
            specs = m[kind]
            for spec in specs:
                if cb_filter(m, kind, spec):
                    orig_spec = (spec['repo'], spec['ref'], spec['morph'])
                    dirtied = cb_process(m, kind, spec)
                    if dirtied:
                        m.dirty = True
                        altered_references[orig_spec] = spec

        for m in self.morphologies:
            if m['kind'] == 'system':
                process_spec_list(m, 'strata')
            elif m['kind'] == 'stratum':
                process_spec_list(m, 'build-depends')
                process_spec_list(m, 'chunks')

        for m in self.morphologies:
            tup = (m.repo_url, m.ref, m.filename[:-len('.morph')])
            if tup in altered_references:
                spec = altered_references[tup]
                if m.ref != spec['ref']:
                    m.ref = spec['ref']
                    m.dirty = True
                assert (m.filename == spec['morph'] + '.morph'
                        or m.repo_url == spec['repo']), \
                       'Moving morphologies is not supported.'

    def change_ref(self, repo_url, orig_ref, morph_filename, new_ref):
        '''Change a triplet's ref to a new one in all morphologies in a ref.

        Change orig_ref to new_ref in any morphology that references the
        original triplet. This includes stratum build-dependencies.

        '''

        def wanted_spec(m, kind, spec):
            return (spec['repo'] == repo_url and
                    spec['ref'] == orig_ref and
                    spec['morph'] + '.morph' == morph_filename)

        def process_spec(m, kind, spec):
            spec['unpetrify-ref'] = spec['ref']
            spec['ref'] = new_ref
            return True

        self._traverse_specs(process_spec, wanted_spec)