Update to genscripts rev 382. This has more fixes for py3k and the modular rewrite...
[gentoolkit.git] / pym / gentoolkit / metadata.py
1 #!/usr/bin/python
2 #
3 # Copyright(c) 2009, Gentoo Foundation
4 #
5 # Licensed under the GNU General Public License, v2
6 #
7 # $Header$
8
9 """Provides an easy-to-use python interface to Gentoo's metadata.xml file.
10
11         Example usage:
12                 >>> from gentoolkit.metadata import MetaData
13                 >>> pkg_md = MetaData('/usr/portage/app-misc/gourmet/metadata.xml')
14                 >>> pkg_md
15                 <MetaData '/usr/portage/app-misc/gourmet/metadata.xml'>
16                 >>> pkg_md.herds()
17                 ['no-herd']
18                 >>> for maint in pkg_md.maintainers():
19                 ...     print "{0} ({1})".format(maint.email, maint.name)
20                 ...
21                 nixphoeni@gentoo.org (Joe Sapp)
22                 >>> for flag in pkg_md.use():
23                 ...     print flag.name, "->", flag.description
24                 ...
25                 rtf -> Enable export to RTF
26                 gnome-print -> Enable printing support using gnome-print
27                 >>> upstream = pkg_md.upstream()
28                 >>> upstream
29                 [<_Upstream {'docs': [], 'remoteid': [], 'maintainer':
30                  [<_Maintainer 'Thomas_Hinkle@alumni.brown.edu'>], 'bugtracker': [],
31                  'changelog': []}>]
32                 >>> upstream[0].maintainer[0].name
33                 'Thomas Mills Hinkle'
34 """
35
36 # Move to Imports section after Python-2.6 is stable
37
38
39 __all__ = ('MetaData',)
40 __docformat__ = 'epytext'
41
42 # =======
43 # Imports
44 # =======
45
46 import re
47 import xml.etree.cElementTree as etree
48
49 from portage import os, settings
50
51 # =======
52 # Classes
53 # =======
54
55 class _Maintainer(object):
56         """An object for representing one maintainer.
57
58         @type email: str or None
59         @ivar email: Maintainer's email address. Used for both Gentoo and upstream.
60         @type name: str or None
61         @ivar name: Maintainer's name. Used for both Gentoo and upstream.
62         @type description: str or None
63         @ivar description: Description of what a maintainer does. Gentoo only.
64         @type restrict: str or None
65         @ivar restrict: e.g. &gt;=portage-2.2 means only maintains versions
66                 of Portage greater than 2.2. Should be DEPEND string with < and >
67                 converted to &lt; and &gt; respectively. 
68         @type status: str or None
69         @ivar status: If set, either 'active' or 'inactive'. Upstream only.
70         """
71
72         def __init__(self, node):
73                 self.email = None
74                 self.name = None
75                 self.description = None
76                 self.restrict = node.get('restrict')
77                 self.status = node.get('status')
78                 maint_attrs = node.getchildren()
79                 for attr in maint_attrs:
80                         setattr(self, attr.tag, attr.text)
81
82         def __repr__(self):
83                 return "<%s %r>" % (self.__class__.__name__, self.email)
84
85
86 class _Useflag(object):
87         """An object for representing one USE flag.
88
89         @todo: Is there any way to have a keyword option to leave in
90                 <pkg> and <cat> for later processing?
91         @type name: str or None
92         @ivar name: USE flag
93         @type restrict: str or None
94         @ivar restrict: e.g. &gt;=portage-2.2 means flag is only avaiable in
95                 versions greater than 2.2
96         @type description: str
97         @ivar description: description of the USE flag
98         """
99
100         def __init__(self, node):
101                 self.name = node.get('name')
102                 self.restrict = node.get('restrict')
103                 _desc = ''
104                 if node.text:
105                         _desc = node.text
106                 for child in node.getchildren():
107                         _desc += child.text if child.text else ''
108                         _desc += child.tail if child.tail else ''
109                 # This takes care of tabs and newlines left from the file
110                 self.description = re.sub('\s+', ' ', _desc)
111
112         def __repr__(self):
113                 return "<%s %r>" % (self.__class__.__name__, self.name)
114
115
116 class _Upstream(object):
117         """An object for representing one package's upstream.
118
119         @type maintainers: list
120         @ivar maintainers: L{_Maintainer} objects for each upstream maintainer
121         @type changelogs: list
122         @ivar changelogs: URLs to upstream's ChangeLog file in str format
123         @type docs: list
124         @ivar docs: Sequence of tuples containing URLs to upstream documentation
125                 in the first slot and 'lang' attribute in the second, e.g.,
126                 [('http.../docs/en/tut.html', None), ('http.../doc/fr/tut.html', 'fr')]
127         @type bugtrackers: list
128         @ivar bugtrackers: URLs to upstream's bugtracker. May also contain an email
129                 address if prepended with 'mailto:'
130         @type remoteids: list
131         @ivar remoteids: Sequence of tuples containing the project's hosting site
132                 name in the first slot and the project's ID name or number for that
133                 site in the second, e.g., [('sourceforge', 'systemrescuecd')]
134         """
135
136         def __init__(self, node):
137                 self.node = node
138                 self.maintainers = self.upstream_maintainers()
139                 self.changelogs = self.upstream_changelogs()
140                 self.docs = self.upstream_documentation()
141                 self.bugtrackers = self.upstream_bugtrackers()
142                 self.remoteids = self.upstream_remoteids()
143
144         def __repr__(self):
145                 return "<%s %r>" % (self.__class__.__name__, self.__dict__)
146
147         def upstream_bugtrackers(self):
148                 """Retrieve upstream bugtracker location from xml node."""
149                 return [e.text for e in self.node.findall('bugs-to')]
150
151         def upstream_changelogs(self):
152                 """Retrieve upstream changelog location from xml node."""
153                 return [e.text for e in self.node.findall('changelog')]
154
155         def upstream_documentation(self):
156                 """Retrieve upstream documentation location from xml node."""
157                 result = []
158                 for elem in self.node.findall('doc'):
159                         lang = elem.get('lang')
160                         result.append((elem.text, lang))
161                 return result
162
163         def upstream_maintainers(self):
164                 """Retrieve upstream maintainer information from xml node."""
165                 return [_Maintainer(m) for m in self.node.findall('maintainer')]
166
167         def upstream_remoteids(self):
168                 """Retrieve upstream remote ID from xml node."""
169                 return [(e.text, e.get('type')) for e in self.node.findall('remote-id')]
170
171
172 class MetaData(object):
173         """Access metadata.xml"""
174
175         def __init__(self, metadata_path):
176                 """Parse a valid metadata.xml file.
177
178                 @type metadata_path: str
179                 @param metadata_path: path to a valid metadata.xml file
180                 @raise IOError: if C{metadata_path} can not be read
181                 """
182
183                 self.metadata_path = metadata_path
184                 self._xml_tree = etree.parse(metadata_path)
185
186                 # Used for caching
187                 self._herdstree = None
188                 self._descriptions = None
189                 self._maintainers = None
190                 self._useflags = None
191                 self._upstream = None
192
193         def __repr__(self):
194                 return "<%s %r>" % (self.__class__.__name__, self.metadata_path)
195
196         def _get_herd_email(self, herd):
197                 """Get a herd's email address.
198
199                 @type herd: str
200                 @param herd: herd whose email you want
201                 @rtype: str or None
202                 @return: email address or None if herd is not in herds.xml
203                 @raise IOError: if $PORTDIR/metadata/herds.xml can not be read
204                 """
205
206                 if self._herdstree is None:
207                         herds_path = os.path.join(settings['PORTDIR'], 'metadata/herds.xml')
208                         try:
209                                 self._herdstree = etree.parse(herds_path)
210                         except IOError:
211                                 # For some trees, herds.xml may not exist. Bug #300108.
212                                 return None
213
214                 # Some special herds are not listed in herds.xml
215                 if herd in ('no-herd', 'maintainer-wanted', 'maintainer-needed'):
216                         return None
217
218                 for node in self._herdstree.getiterator('herd'):
219                         if node.findtext('name') == herd:
220                                 return node.findtext('email')
221
222         def herds(self, include_email=False):
223                 """Return a list of text nodes for <herd>.
224
225                 @type include_email: bool
226                 @keyword include_email: if True, also look up the herd's email
227                 @rtype: list
228                 @return: if include_email is False, return a list of strings;
229                          if include_email is True, return a list of tuples containing:
230                                          [('herd1', 'herd1@gentoo.org'), ('no-herd', None);
231                 """
232
233                 result = []
234                 for elem in self._xml_tree.findall('herd'):
235                         text = elem.text
236                         if text is None:
237                                 text = ''
238                         if include_email:
239                                 herd_mail = self._get_herd_email(text)
240                                 result.append((text, herd_mail))
241                         else:
242                                 result.append(text)
243
244                 return result
245
246         def descriptions(self):
247                 """Return a list of text nodes for <longdescription>.
248
249                 @rtype: list
250                 @return: package description in string format
251                 @todo: Support the C{lang} attribute
252                 """
253
254                 if self._descriptions is not None:
255                         return self._descriptions
256
257                 long_descriptions = self._xml_tree.findall("longdescription")
258                 self._descriptions = [e.text for e in long_descriptions]
259                 return self._descriptions
260
261         def maintainers(self):
262                 """Get maintainers' name, email and description.
263
264                 @rtype: list
265                 @return: a sequence of L{_Maintainer} objects in document order.
266                 """
267
268                 if self._maintainers is not None:
269                         return self._maintainers
270
271                 self._maintainers = []
272                 for node in self._xml_tree.findall('maintainer'):
273                         self._maintainers.append(_Maintainer(node))
274
275                 return self._maintainers
276
277         def use(self):
278                 """Get names and descriptions for USE flags defined in metadata.
279
280                 @rtype: list
281                 @return: a sequence of L{_Useflag} objects in document order.
282                 """
283
284                 if self._useflags is not None:
285                         return self._useflags
286
287                 self._useflags = []
288                 for node in self._xml_tree.getiterator('flag'):
289                         self._useflags.append(_Useflag(node))
290
291                 return self._useflags
292
293         def upstream(self):
294                 """Get upstream contact information.
295
296                 @rtype: list
297                 @return: a sequence of L{_Upstream} objects in document order.
298                 """
299
300                 if self._upstream is not None:
301                         return self._upstream
302
303                 self._upstream = []
304                 for node in self._xml_tree.findall('upstream'):
305                         self._upstream.append(_Upstream(node))
306
307                 return self._upstream
308
309 # vim: set ts=4 sw=4 tw=79: