1
"""This module provides a class to parse/resolve osc url-like
2
arguments.
3
4
Some notes about terminology:
5
 'foo/bar' is called an entry. The entry 'foo/bar' consists of
6
 two components; the first component is 'foo' and the second
7
 component is 'bar'.
8
 'api://project/package?' is also an entry which consists of 3
9
 components; the first component is 'api://', the second is 'project'
10
 and the third component is 'package'. The '?' indicates that
11
 'package' is an optional component
12
13
"""
14
15
import re
16
import urlparse
17
import logging
18
19
from osc.wc.util import (wc_is_project, wc_is_package, wc_read_project,
20
                         wc_read_package, wc_read_apiurl)
21
22
23
class ResolvedInfo(object):
24
    """Encapsulate resolved arguments"""
25
26
    def __init__(self):
27
        super(ResolvedInfo, self).__init__()
28
        self._data = {}
29
30
    def add(self, name, value):
31
        """Add additional components.
32
33
        name is the name of the component and value
34
        the actual value.
35
        Note: if a component already exists it will
36
        be overriden with the new value.
37
38
        """
39
        self._data[name] = value
40
41
    def __getattr__(self, name):
42
        return self._data[name]
43
44
    def __str__(self):
45
        return str(self._data)
46
47
48
class Entry(object):
49
    """Manages Component objects."""
50
51
    def __init__(self, path=''):
52
        """Constructs a new Entry object.
53
54
        path, if specified, is a path to a project or package
55
        working copy. If the following conditions are met,
56
        path is taken into consideration for resolving:
57
        - 'project' and 'package' components are present _and_
58
          path is a package working copy
59
        or
60
        - a 'project' component is present _and_ (path is a
61
          project _or_ package working copy)
62
63
        """
64
        super(Entry, self).__init__()
65
        self._components = []
66
        self._path = path
67
68
    def append(self, component):
69
        """Add a Component object."""
70
        self._components.append(component)
71
72
    def _build_regex(self):
73
        regex = ''
74
        prev = None
75
        for c in self._components:
76
            sep = '/'
77
            if c.opt:
78
                sep += '?'
79
            if prev is not None and prev.api:
80
                # no separator if preceeding component was a api
81
                sep = ''
82
            regex += sep + c.regex
83
            prev = c
84
        regex = regex.lstrip('/').lstrip('?')
85
        regex = '^' + regex
86
        regex += '$'
87
        return regex
88
89
    def match(self, arg):
90
        """Match entry against arg.
91
92
        If it does not match None is returned. Otherwise
93
        a dict is returned which contains the matches
94
        (some optional matches might be None).
95
96
        """
97
        regex = self._build_regex()
98
#        print regex, arg
99
        m = re.match(regex, arg)
100
        if m is None:
101
            return None
102
        return m.groupdict()
103
104
    def wc_resolve(self, path=''):
105
        """Try to read components from path.
106
107
        If path is specified it overrides the path which was passed
108
        to __init__.
109
        If all conditions are met (see class description for details)
110
        a dict is returned which contains the matches
111
        (some optional and _non_optional matches might be None).
112
        Otherwise None is returned.
113
114
        """
115
        path = path or self._path
116
        if not path:
117
            return None
118
        unresolved = dict([(comp.name, None) for comp in self._components])
119
        has_prj = 'project' in unresolved
120
        has_pkg = 'package' in unresolved and 'project' in unresolved
121
        if has_pkg:
122
            if wc_is_package(path):
123
                ret = {'apiurl': wc_read_apiurl(path),
124
                       'project': wc_read_project(path),
125
                       'package': wc_read_package(path)}
126
                unresolved.update(ret)
127
                return unresolved
128
        pkg_opt = True
129
        for comp in self._components:
130
            if comp.name == 'package':
131
                pkg_opt = comp.opt
132
        if has_prj and pkg_opt:
133
            if wc_is_project(path):
134
                ret = {'apiurl': wc_read_apiurl(path),
135
                       'project': wc_read_project(path)}
136
                unresolved.update(ret)
137
                return unresolved
138
        return None
139
140
    def __str__(self):
141
        return self._build_regex()
142
143
144
class Component(object):
145
    """Represents a regex for a component"""
146
    APIURL_RE = "(?P<%s>.+)://"
147
    COMPONENT_RE = "(?P<%s>[^/]+)"
148
149
    def __init__(self, format, api=False):
150
        """Constructs a new Regex object.
151
152
        format is a component.
153
154
        Keyword arguments:
155
        api -- if True APIURL_RE will be used (default: False)
156
157
        """
158
        super(Component, self).__init__()
159
        self.opt = format.endswith('?')
160
        self.api = api
161
        self.name = format.rstrip('?')
162
        self.regex = Component.COMPONENT_RE % self.name
163
        if self.api:
164
            if self.name:
165
                self.name += '_'
166
            self.name += 'apiurl'
167
            self.regex = Component.APIURL_RE % self.name
168
        if self.opt:
169
            self.regex += '?'
170
171
172
class OscArgs(object):
173
    """Resolves url-like arguments into its components.
174
175
    Note: to avoid name clashes when defining 2 or more api entries
176
    use the following syntax:
177
     'api://project', 'api(tgt)://tgt_project/tgt_package'
178
    The ResolvedInfo object will contain a "apiurl" and a
179
    "tgt_apiurl" attribute.
180
181
    """
182
    APIURL_RE = "api\(?([^)]+)?\)?://"
183
184
    def __init__(self, *format_entries, **kwargs):
185
        """Constructs a new OscArgs instance.
186
187
        *format_entries contains the formatting entries.
188
189
        Keyword arguments:
190
        path -- path to a project or package working copy
191
                (default: ''). path might be used for component
192
                resolving.
193
194
        """
195
        super(OscArgs, self).__init__()
196
        self._logger = logging.getLogger(__name__)
197
        self._entries = []
198
        self._parse_entries(format_entries, kwargs.pop('path', ''))
199
200
    def _parse_entries(self, format_entries, path):
201
        """Parse each entry and each component into a Entry or
202
        Component object.
203
204
        """
205
        for entry in format_entries:
206
            e = Entry(path)
207
            m = re.match(OscArgs.APIURL_RE, entry)
208
            if m is not None:
209
                api = m.group(1) or ''
210
                e.append(Component(api, api=True))
211
                entry = re.sub(OscArgs.APIURL_RE, '', entry)
212
            for component in entry.split('/'):
213
                r = Component(component)
214
                e.append(r)
215
            self._entries.append(e)
216
217
    def unresolved(self, info, name):
218
        """Resolve unresolved components "manually".
219
220
        Subclasses may override this method to assign a
221
        default value to a unresolved component.
222
223
        """
224
        pass
225
226
    def _check_resolved(self, info, resolved):
227
        """Check for unresolved components.
228
229
        If a component is not resolved (that is its value in the
230
        resolved dict is None) the unresolved method is called
231
        which can assign a default value (for instance). It is
232
        up to the implementation of the unresolved method whether
233
        the unresolved component is added to the info object or not.
234
        All resolved data is added to the info object.
235
236
        """
237
        unresolved = []
238
        for k, v in resolved.iteritems():
239
            if v is None:
240
                unresolved.append(k)
241
            else:
242
                info.add(k, v)
243
        for name in unresolved:
244
            self.unresolved(info, k)
245
246
    def _resolve(self, args, use_wc=False, path=''):
247
        entries = self._entries[:]
248
        args = list(args)
249
        info = ResolvedInfo()
250
        while args:
251
            arg = args.pop(0)
252
            self._logger.debug("argument: " + arg)
253
            if not entries:
254
                raise ValueError('Too many args')
255
            entry = entries.pop(0)
256
            self._logger.debug("entry: %s" % entry)
257
            resolved = None
258
            if use_wc:
259
                resolved = entry.wc_resolve(path)
260
            if resolved is not None:
261
                # push arg back unless arg == ''
262
                if arg:
263
                    args.insert(0, arg)
264
                self._check_resolved(info, resolved)
265
                continue
266
            resolved = entry.match(arg)
267
            if resolved is None:
268
                msg = "'%s' and %s do not match" % (arg, entry)
269
                raise ValueError(msg)
270
            self._check_resolved(info, resolved)
271
        if entries:
272
            raise ValueError('Too few args')
273
        return info
274
275
    def resolve(self, *args, **kwargs):
276
        """Resolve each entry in *args.
277
278
        If an entry cannot be resolved a ValueError is
279
        raised. Otherwise a ResolvedInfo object is returned.
280
281
        Keyword arguments:
282
        path -- if specified it overrides the path which was passed
283
                to __init__ (default: '')
284
285
        """
286
        try:
287
            info = self._resolve(args, use_wc=False)
288
        except ValueError:
289
            path = kwargs.pop('path', '')
290
            info = self._resolve(args, use_wc=True, path=path)
291
        return info