Package zeroinstall :: Package injector :: Module model
[frames] | no frames]

Source Code for Module zeroinstall.injector.model

   1  """In-memory representation of interfaces and other data structures. 
   2   
   3  The objects in this module are used to build a representation of an XML interface 
   4  file in memory. 
   5   
   6  @see: L{reader} constructs these data-structures 
   7  @see: U{http://0install.net/interface-spec.html} description of the domain model 
   8   
   9  @var defaults: Default values for the 'default' attribute for <environment> bindings of 
  10  well-known variables. 
  11  """ 
  12   
  13  # Copyright (C) 2009, Thomas Leonard 
  14  # See the README file for details, or visit http://0install.net. 
  15   
  16  from zeroinstall import _, logger 
  17  import os, re, locale, sys 
  18  from zeroinstall import SafeException, version 
  19  from zeroinstall.injector.namespaces import XMLNS_IFACE 
  20  from zeroinstall.injector.versions import parse_version, format_version 
  21  from zeroinstall.injector import qdom, versions 
  22  from zeroinstall import support, zerostore 
  23  from zeroinstall.support import escaping 
  24   
  25  # Element names for bindings in feed files 
  26  binding_names = frozenset(['environment', 'overlay', 'executable-in-path', 'executable-in-var']) 
  27   
  28  _dependency_names = frozenset(['requires', 'restricts']) 
  29   
  30  network_offline = 'off-line' 
  31  network_minimal = 'minimal' 
  32  network_full = 'full' 
  33  network_levels = (network_offline, network_minimal, network_full) 
  34   
  35  stability_levels = {}   # Name -> Stability 
  36   
  37  defaults = { 
  38          'PATH': '/bin:/usr/bin', 
  39          'XDG_CONFIG_DIRS': '/etc/xdg', 
  40          'XDG_DATA_DIRS': '/usr/local/share:/usr/share', 
  41  } 
42 43 -class InvalidInterface(SafeException):
44 """Raised when parsing an invalid feed.""" 45 feed_url = None 46
47 - def __init__(self, message, ex = None):
48 """@type message: str""" 49 if ex: 50 try: 51 message += "\n\n(exact error: %s)" % ex 52 except: 53 # Some Python messages have type str but contain UTF-8 sequences. 54 # (e.g. IOException). Adding these to a Unicode 'message' (e.g. 55 # after gettext translation) will cause an error. 56 import codecs 57 decoder = codecs.lookup('utf-8') 58 decex = decoder.decode(str(ex), errors = 'replace')[0] 59 message += "\n\n(exact error: %s)" % decex 60 61 SafeException.__init__(self, message)
62
63 - def __unicode__(self):
64 """@rtype: str""" 65 if hasattr(SafeException, '__unicode__'): 66 # Python >= 2.6 67 if self.feed_url: 68 return _('%s [%s]') % (SafeException.__unicode__(self), self.feed_url) 69 return SafeException.__unicode__(self) 70 else: 71 return support.unicode(SafeException.__str__(self))
72
73 -def _split_arch(arch):
74 """Split an arch into an (os, machine) tuple. Either or both parts may be None. 75 @type arch: str""" 76 if not arch: 77 return None, None 78 elif '-' not in arch: 79 raise SafeException(_("Malformed arch '%s'") % arch) 80 else: 81 osys, machine = arch.split('-', 1) 82 if osys == '*': osys = None 83 if machine == '*': machine = None 84 return osys, machine
85
86 -def _join_arch(osys, machine):
87 """@type osys: str 88 @type machine: str 89 @rtype: str""" 90 if osys == machine == None: return None 91 return "%s-%s" % (osys or '*', machine or '*')
92
93 -def _best_language_match(options):
94 """@type options: {str: str} 95 @rtype: str""" 96 (language, encoding) = locale.getlocale() 97 98 if language: 99 # xml:lang uses '-', while LANG uses '_' 100 language = language.replace('_', '-') 101 else: 102 language = 'en-US' 103 104 return (options.get(language, None) or # Exact match (language+region) 105 options.get(language.split('-', 1)[0], None) or # Matching language 106 options.get('en', None)) # English
107
108 -class Stability(object):
109 """A stability rating. Each implementation has an upstream stability rating and, 110 optionally, a user-set rating.""" 111 __slots__ = ['level', 'name', 'description']
112 - def __init__(self, level, name, description):
113 """@type level: int 114 @type name: str 115 @type description: str""" 116 self.level = level 117 self.name = name 118 self.description = description 119 assert name not in stability_levels 120 stability_levels[name] = self
121
122 - def __cmp__(self, other):
123 """@type other: L{Stability} 124 @rtype: int""" 125 return cmp(self.level, other.level)
126
127 - def __lt__(self, other):
128 """@type other: L{Stability} 129 @rtype: bool""" 130 if isinstance(other, Stability): 131 return self.level < other.level 132 else: 133 return NotImplemented
134
135 - def __eq__(self, other):
136 """@type other: L{Stability} 137 @rtype: bool""" 138 if isinstance(other, Stability): 139 return self.level == other.level 140 else: 141 return NotImplemented
142
143 - def __str__(self):
144 """@rtype: str""" 145 return self.name
146
147 - def __repr__(self):
148 return _("<Stability: %s>") % self.description
149
150 -def process_binding(e):
151 """Internal 152 @type e: L{zeroinstall.injector.qdom.Element} 153 @rtype: L{Binding}""" 154 if e.name == 'environment': 155 mode = { 156 None: EnvironmentBinding.PREPEND, 157 'prepend': EnvironmentBinding.PREPEND, 158 'append': EnvironmentBinding.APPEND, 159 'replace': EnvironmentBinding.REPLACE, 160 }[e.getAttribute('mode')] 161 162 binding = EnvironmentBinding(e.getAttribute('name'), 163 insert = e.getAttribute('insert'), 164 default = e.getAttribute('default'), 165 value = e.getAttribute('value'), 166 mode = mode, 167 separator = e.getAttribute('separator')) 168 if not binding.name: raise InvalidInterface(_("Missing 'name' in binding")) 169 if binding.insert is None and binding.value is None: 170 raise InvalidInterface(_("Missing 'insert' or 'value' in binding")) 171 if binding.insert is not None and binding.value is not None: 172 raise InvalidInterface(_("Binding contains both 'insert' and 'value'")) 173 return binding 174 elif e.name == 'executable-in-path': 175 return ExecutableBinding(e, in_path = True) 176 elif e.name == 'executable-in-var': 177 return ExecutableBinding(e, in_path = False) 178 elif e.name == 'overlay': 179 return OverlayBinding(e.getAttribute('src'), e.getAttribute('mount-point')) 180 else: 181 raise Exception(_("Unknown binding type '%s'") % e.name)
182
183 -def process_depends(item, local_feed_dir):
184 """Internal 185 @type item: L{zeroinstall.injector.qdom.Element} 186 @type local_feed_dir: str 187 @rtype: L{Dependency}""" 188 # Note: also called from selections 189 # Note: used by 0compile 190 attrs = item.attrs 191 dep_iface = item.getAttribute('interface') 192 if not dep_iface: 193 raise InvalidInterface(_("Missing 'interface' on <%s>") % item.name) 194 if dep_iface.startswith('.'): 195 if local_feed_dir: 196 dep_iface = os.path.abspath(os.path.join(local_feed_dir, dep_iface)) 197 # (updates the element too, in case we write it out again) 198 attrs['interface'] = dep_iface 199 else: 200 raise InvalidInterface(_('Relative interface URI "%s" in non-local feed') % dep_iface) 201 202 if item.name == 'restricts': 203 dependency = InterfaceRestriction(dep_iface, element = item) 204 else: 205 dependency = InterfaceDependency(dep_iface, element = item) 206 207 version = item.getAttribute('version') 208 if version: 209 try: 210 r = VersionExpressionRestriction(version) 211 except SafeException as ex: 212 msg = "Can't parse version restriction '{version}': {error}".format(version = version, error = ex) 213 logger.warning(msg) 214 r = ImpossibleRestriction(msg) 215 dependency.restrictions.append(r) 216 217 distro = item.getAttribute('distribution') 218 if distro: 219 dependency.restrictions.append(DistributionRestriction(distro)) 220 221 for e in item.childNodes: 222 if e.uri != XMLNS_IFACE: continue 223 if e.name in binding_names: 224 dependency.bindings.append(process_binding(e)) 225 elif e.name == 'version': 226 dependency.restrictions.append( 227 VersionRangeRestriction(not_before = parse_version(e.getAttribute('not-before')), 228 before = parse_version(e.getAttribute('before')))) 229 return dependency
230
231 -def N_(message): return message
232 233 insecure = Stability(0, N_('insecure'), _('This is a security risk')) 234 buggy = Stability(5, N_('buggy'), _('Known to have serious bugs')) 235 developer = Stability(10, N_('developer'), _('Work-in-progress - bugs likely')) 236 testing = Stability(20, N_('testing'), _('Stability unknown - please test!')) 237 stable = Stability(30, N_('stable'), _('Tested - no serious problems found')) 238 packaged = Stability(35, N_('packaged'), _('Supplied by the local package manager')) 239 preferred = Stability(40, N_('preferred'), _('Best of all - must be set manually')) 240 241 del N_
242 243 -class Restriction(object):
244 """A Restriction limits the allowed implementations of an Interface.""" 245 __slots__ = [] 246 247 reason = _("Incompatible with user-specified requirements") 248
249 - def meets_restriction(self, impl):
250 """Called by the L{solver.Solver} to check whether a particular implementation is acceptable. 251 @return: False if this implementation is not a possibility 252 @rtype: bool""" 253 raise NotImplementedError(_("Abstract"))
254
255 - def __str__(self):
256 return "missing __str__ on %s" % type(self)
257
258 - def __repr__(self):
259 """@rtype: str""" 260 return "<restriction: %s>" % self
261
262 -class VersionRestriction(Restriction):
263 """Only select implementations with a particular version number. 264 @since: 0.40""" 265
266 - def __init__(self, version):
267 """@param version: the required version number 268 @see: L{parse_version}; use this to pre-process the version number""" 269 assert not isinstance(version, str), "Not parsed: " + version 270 self.version = version
271
272 - def meets_restriction(self, impl):
273 """@type impl: L{ZeroInstallImplementation} 274 @rtype: bool""" 275 return impl.version == self.version
276
277 - def __str__(self):
278 return _("version = %s") % format_version(self.version)
279
280 -class VersionRangeRestriction(Restriction):
281 """Only versions within the given range are acceptable""" 282 __slots__ = ['before', 'not_before'] 283
284 - def __init__(self, before, not_before):
285 """@param before: chosen versions must be earlier than this 286 @param not_before: versions must be at least this high 287 @see: L{parse_version}; use this to pre-process the versions""" 288 self.before = before 289 self.not_before = not_before
290
291 - def meets_restriction(self, impl):
292 """@type impl: L{Implementation} 293 @rtype: bool""" 294 if self.not_before and impl.version < self.not_before: 295 return False 296 if self.before and impl.version >= self.before: 297 return False 298 return True
299
300 - def __str__(self):
301 """@rtype: str""" 302 if self.not_before is not None or self.before is not None: 303 range = '' 304 if self.not_before is not None: 305 range += format_version(self.not_before) + ' <= ' 306 range += 'version' 307 if self.before is not None: 308 range += ' < ' + format_version(self.before) 309 else: 310 range = 'none' 311 return range
312
313 -class VersionExpressionRestriction(Restriction):
314 """Only versions for which the expression is true are acceptable. 315 @since: 1.13""" 316 __slots__ = ['expr', '_test_fn'] 317
318 - def __init__(self, expr):
319 """Constructor. 320 @param expr: the expression, in the form "2.6..!3 | 3.2.2.." 321 @type expr: str""" 322 self.expr = expr 323 self._test_fn = versions.parse_version_expression(expr)
324
325 - def meets_restriction(self, impl):
326 """@type impl: L{Implementation} 327 @rtype: bool""" 328 return self._test_fn(impl.version)
329
330 - def __str__(self):
331 """@rtype: str""" 332 return "version " + self.expr
333
334 -class ImpossibleRestriction(Restriction):
335 """A restriction that can never be met. 336 This is used when we can't understand some other restriction. 337 @since: 1.13""" 338
339 - def __init__(self, reason):
340 """@type reason: str""" 341 self.reason = reason
342
343 - def meets_restriction(self, impl):
344 """@type impl: L{Implementation} 345 @rtype: bool""" 346 return False
347
348 - def __str__(self):
349 """@rtype: str""" 350 return "<impossible: %s>" % self.reason
351
352 -class DistributionRestriction(Restriction):
353 """A restriction that can only be satisfied by an implementation 354 from the given distribution. 355 For example, a MacPorts Python library requires us to select the MacPorts 356 version of Python too. 357 @since: 1.15""" 358 distros = None 359
360 - def __init__(self, distros):
361 """@type distros: str""" 362 self.distros = frozenset(distros.split(' '))
363
364 - def meets_restriction(self, impl):
365 """@type impl: L{Implementation} 366 @rtype: bool""" 367 return impl.distro_name in self.distros
368
369 - def __str__(self):
370 """@rtype: str""" 371 return "distro " + '|'.join(sorted(self.distros))
372
373 -class Binding(object):
374 """Information about how the choice of a Dependency is made known 375 to the application being run.""" 376 377 @property
378 - def command(self):
379 """"Returns the name of the specific command needed by this binding, if any. 380 @since: 1.2""" 381 return None
382
383 -class EnvironmentBinding(Binding):
384 """Indicate the chosen implementation using an environment variable.""" 385 __slots__ = ['name', 'insert', 'default', 'mode', 'value'] 386 387 PREPEND = 'prepend' 388 APPEND = 'append' 389 REPLACE = 'replace' 390
391 - def __init__(self, name, insert, default = None, mode = PREPEND, value=None, separator=None):
392 """ 393 mode argument added in version 0.28 394 value argument added in version 0.52 395 """ 396 self.name = name 397 self.insert = insert 398 self.default = default 399 self.mode = mode 400 self.value = value 401 if separator is None: 402 self.separator = os.pathsep 403 else: 404 self.separator = separator
405 406
407 - def __str__(self):
408 return _("<environ %(name)s %(mode)s %(insert)s %(value)s>") % \ 409 {'name': self.name, 'mode': self.mode, 'insert': self.insert, 'value': self.value}
410 411 __repr__ = __str__ 412
413 - def get_value(self, path, old_value):
414 """Calculate the new value of the environment variable after applying this binding. 415 @param path: the path to the selected implementation 416 @param old_value: the current value of the environment variable 417 @return: the new value for the environment variable""" 418 419 if self.insert is not None: 420 extra = os.path.join(path, self.insert) 421 else: 422 assert self.value is not None 423 extra = self.value 424 425 if self.mode == EnvironmentBinding.REPLACE: 426 return extra 427 428 if old_value is None: 429 old_value = self.default or defaults.get(self.name, None) 430 if old_value is None: 431 return extra 432 if self.mode == EnvironmentBinding.PREPEND: 433 return extra + self.separator + old_value 434 else: 435 return old_value + self.separator + extra
436
437 - def _toxml(self, doc, prefixes):
438 """Create a DOM element for this binding. 439 @param doc: document to use to create the element 440 @return: the new element 441 """ 442 env_elem = doc.createElementNS(XMLNS_IFACE, 'environment') 443 env_elem.setAttributeNS(None, 'name', self.name) 444 if self.mode is not None: 445 env_elem.setAttributeNS(None, 'mode', self.mode) 446 if self.insert is not None: 447 env_elem.setAttributeNS(None, 'insert', self.insert) 448 else: 449 env_elem.setAttributeNS(None, 'value', self.value) 450 if self.default: 451 env_elem.setAttributeNS(None, 'default', self.default) 452 if self.separator: 453 env_elem.setAttributeNS(None, 'separator', self.separator) 454 return env_elem
455
456 -class ExecutableBinding(Binding):
457 """Make the chosen command available in $PATH. 458 @ivar in_path: True to add the named command to $PATH, False to store in named variable 459 @type in_path: bool 460 """ 461 __slots__ = ['qdom'] 462
463 - def __init__(self, qdom, in_path):
464 self.qdom = qdom 465 self.in_path = in_path
466
467 - def __str__(self):
468 return str(self.qdom)
469 470 __repr__ = __str__ 471
472 - def _toxml(self, doc, prefixes):
473 return self.qdom.toDOM(doc, prefixes)
474 475 @property
476 - def name(self):
477 return self.qdom.getAttribute('name')
478 479 @property
480 - def command(self):
481 return self.qdom.getAttribute("command") or 'run'
482
483 -class OverlayBinding(Binding):
484 """Make the chosen implementation available by overlaying it onto another part of the file-system. 485 This is to support legacy programs which use hard-coded paths.""" 486 __slots__ = ['src', 'mount_point'] 487
488 - def __init__(self, src, mount_point):
489 self.src = src 490 self.mount_point = mount_point
491
492 - def __str__(self):
493 return _("<overlay %(src)s on %(mount_point)s>") % {'src': self.src or '.', 'mount_point': self.mount_point or '/'}
494 495 __repr__ = __str__ 496
497 - def _toxml(self, doc, prefixes):
498 """Create a DOM element for this binding. 499 @param doc: document to use to create the element 500 @return: the new element 501 """ 502 env_elem = doc.createElementNS(XMLNS_IFACE, 'overlay') 503 if self.src is not None: 504 env_elem.setAttributeNS(None, 'src', self.src) 505 if self.mount_point is not None: 506 env_elem.setAttributeNS(None, 'mount-point', self.mount_point) 507 return env_elem
508
509 -class Feed(object):
510 """An interface's feeds are other interfaces whose implementations can also be 511 used as implementations of this interface.""" 512 __slots__ = ['uri', 'os', 'machine', 'user_override', 'langs', 'site_package']
513 - def __init__(self, uri, arch, user_override, langs = None, site_package = False):
514 self.uri = uri 515 # This indicates whether the feed comes from the user's overrides 516 # file. If true, writer.py will write it when saving. 517 self.user_override = user_override 518 self.os, self.machine = _split_arch(arch) 519 self.langs = langs 520 self.site_package = site_package
521
522 - def __str__(self):
523 return "<Feed from %s>" % self.uri
524 __repr__ = __str__ 525 526 arch = property(lambda self: _join_arch(self.os, self.machine))
527
528 -class Dependency(object):
529 """A Dependency indicates that an Implementation requires some additional 530 code to function. This is an abstract base class. 531 @ivar qdom: the XML element for this Dependency (since 0launch 0.51) 532 @type qdom: L{qdom.Element} 533 @ivar metadata: any extra attributes from the XML element 534 @type metadata: {str: str} 535 """ 536 __slots__ = ['qdom'] 537 538 Essential = "essential" # Must select a version of the dependency 539 Recommended = "recommended" # Prefer to select a version 540 Restricts = "restricts" # Just adds restrictions without expressing any opinion 541
542 - def __init__(self, element):
543 """@type element: L{zeroinstall.injector.qdom.Element}""" 544 assert isinstance(element, qdom.Element), type(element) # Use InterfaceDependency instead! 545 self.qdom = element
546 547 @property
548 - def metadata(self):
549 return self.qdom.attrs
550
551 - def get_required_commands(self):
552 """Return a list of command names needed by this dependency""" 553 return []
554
555 -class InterfaceRestriction(Dependency):
556 """A Dependency that restricts the possible choices of a Zero Install interface. 557 @ivar interface: the interface required by this dependency 558 @type interface: str 559 @ivar restrictions: a list of constraints on acceptable implementations 560 @type restrictions: [L{Restriction}] 561 @since: 1.10 562 """ 563 __slots__ = ['interface', 'restrictions'] 564
565 - def __init__(self, interface, restrictions = None, element = None):
566 """@type interface: str 567 @type element: L{zeroinstall.injector.qdom.Element} | None""" 568 Dependency.__init__(self, element) 569 assert isinstance(interface, (str, support.unicode)) 570 assert interface 571 self.interface = interface 572 if restrictions is None: 573 self.restrictions = [] 574 else: 575 self.restrictions = restrictions
576 577 importance = Dependency.Restricts 578 bindings = () 579
580 - def __str__(self):
581 return _("<Restriction on %(interface)s; %(restrictions)s>") % {'interface': self.interface, 'restrictions': self.restrictions}
582
583 -class InterfaceDependency(InterfaceRestriction):
584 """A Dependency on a Zero Install interface. 585 @ivar interface: the interface required by this dependency 586 @type interface: str 587 @ivar restrictions: a list of constraints on acceptable implementations 588 @type restrictions: [L{Restriction}] 589 @ivar bindings: how to make the choice of implementation known 590 @type bindings: [L{Binding}] 591 @since: 0.28 592 """ 593 __slots__ = ['bindings'] 594
595 - def __init__(self, interface, restrictions = None, element = None):
596 """@type interface: str 597 @type element: L{zeroinstall.injector.qdom.Element} | None""" 598 InterfaceRestriction.__init__(self, interface, restrictions, element) 599 self.bindings = []
600
601 - def __str__(self):
602 """@rtype: str""" 603 return _("<Dependency on %(interface)s; bindings: %(bindings)s%(restrictions)s>") % {'interface': self.interface, 'bindings': self.bindings, 'restrictions': self.restrictions}
604 605 @property
606 - def importance(self):
607 return self.qdom.getAttribute("importance") or Dependency.Essential
608
609 - def get_required_commands(self):
610 """Return a list of command names needed by this dependency""" 611 if self.qdom.name == 'runner': 612 commands = [self.qdom.getAttribute('command') or 'run'] 613 else: 614 commands = [] 615 for b in self.bindings: 616 c = b.command 617 if c is not None: 618 commands.append(c) 619 return commands
620 621 @property
622 - def command(self):
623 if self.qdom.name == 'runner': 624 return self.qdom.getAttribute('command') or 'run' 625 return None
626
627 -class RetrievalMethod(object):
628 """A RetrievalMethod provides a way to fetch an implementation.""" 629 __slots__ = []
630
631 -class DownloadSource(RetrievalMethod):
632 """A DownloadSource provides a way to fetch an implementation.""" 633 __slots__ = ['implementation', 'url', 'size', 'extract', 'start_offset', 'type', 'dest'] 634
635 - def __init__(self, implementation, url, size, extract, start_offset = 0, type = None, dest = None):
636 """@type implementation: L{ZeroInstallImplementation} 637 @type url: str 638 @type size: int 639 @type extract: str 640 @type start_offset: int 641 @type type: str | None 642 @type dest: str | None""" 643 self.implementation = implementation 644 self.url = url 645 self.size = size 646 self.extract = extract 647 self.dest = dest 648 self.start_offset = start_offset 649 self.type = type # MIME type - see unpack.py
650
651 -class RenameStep(RetrievalMethod):
652 """A Rename provides a way to rename / move a file within an implementation.""" 653 __slots__ = ['source', 'dest'] 654
655 - def __init__(self, source, dest):
656 """@type source: str 657 @type dest: str""" 658 self.source = source 659 self.dest = dest
660
661 -class FileSource(RetrievalMethod):
662 """A FileSource provides a way to fetch a single file.""" 663 __slots__ = ['url', 'dest', 'size'] 664
665 - def __init__(self, url, dest, size):
666 """@type url: str 667 @type dest: str 668 @type size: int""" 669 self.url = url 670 self.dest = dest 671 self.size = size
672
673 -class RemoveStep(RetrievalMethod):
674 """A RemoveStep provides a way to delete a path within an implementation.""" 675 __slots__ = ['path'] 676
677 - def __init__(self, path):
678 """@type path: str""" 679 self.path = path
680
681 -class Recipe(RetrievalMethod):
682 """Get an implementation by following a series of steps. 683 @ivar size: the combined download sizes from all the steps 684 @type size: int 685 @ivar steps: the sequence of steps which must be performed 686 @type steps: [L{RetrievalMethod}]""" 687 __slots__ = ['steps'] 688
689 - def __init__(self):
690 self.steps = []
691 692 size = property(lambda self: sum([x.size for x in self.steps if hasattr(x, 'size')]))
693
694 -class DistributionSource(RetrievalMethod):
695 """A package that is installed using the distribution's tools (including PackageKit). 696 @ivar install: a function to call to install this package 697 @type install: (L{handler.Handler}) -> L{tasks.Blocker} 698 @ivar package_id: the package name, in a form recognised by the distribution's tools 699 @type package_id: str 700 @ivar size: the download size in bytes 701 @type size: int 702 @ivar needs_confirmation: whether the user should be asked to confirm before calling install() 703 @type needs_confirmation: bool""" 704 705 __slots__ = ['package_id', 'size', 'install', 'needs_confirmation'] 706
707 - def __init__(self, package_id, size, install, needs_confirmation = True):
708 """@type package_id: str 709 @type size: int 710 @type needs_confirmation: bool""" 711 RetrievalMethod.__init__(self) 712 self.package_id = package_id 713 self.size = size 714 self.install = install 715 self.needs_confirmation = needs_confirmation
716
717 -class Command(object):
718 """A Command is a way of running an Implementation as a program.""" 719 720 __slots__ = ['qdom', '_depends', '_local_dir', '_runner', '_bindings'] 721
722 - def __init__(self, qdom, local_dir):
723 """@param qdom: the <command> element 724 @type qdom: L{zeroinstall.injector.qdom.Element} 725 @param local_dir: the directory containing the feed (for relative dependencies), or None if not local""" 726 assert qdom.name == 'command', 'not <command>: %s' % qdom 727 self.qdom = qdom 728 self._local_dir = local_dir 729 self._depends = None 730 self._bindings = None
731 732 path = property(lambda self: self.qdom.attrs.get("path", None)) 733
734 - def _toxml(self, doc, prefixes):
735 """@type prefixes: L{zeroinstall.injector.qdom.Prefixes}""" 736 return self.qdom.toDOM(doc, prefixes)
737 738 @property
739 - def requires(self):
740 if self._depends is None: 741 self._runner = None 742 depends = [] 743 for child in self.qdom.childNodes: 744 if child.uri != XMLNS_IFACE: continue 745 if child.name in _dependency_names: 746 dep = process_depends(child, self._local_dir) 747 depends.append(dep) 748 elif child.name == 'runner': 749 if self._runner: 750 raise InvalidInterface(_("Multiple <runner>s in <command>!")) 751 dep = process_depends(child, self._local_dir) 752 depends.append(dep) 753 self._runner = dep 754 self._depends = depends 755 return self._depends
756
757 - def get_runner(self):
758 """@rtype: L{InterfaceDependency}""" 759 self.requires # (sets _runner) 760 return self._runner
761
762 - def __str__(self):
763 return str(self.qdom)
764 765 @property
766 - def bindings(self):
767 """@since: 1.3""" 768 if self._bindings is None: 769 bindings = [] 770 for e in self.qdom.childNodes: 771 if e.uri != XMLNS_IFACE: continue 772 if e.name in binding_names: 773 bindings.append(process_binding(e)) 774 self._bindings = bindings 775 return self._bindings
776
777 -class Implementation(object):
778 """An Implementation is a package which implements an Interface. 779 @ivar download_sources: list of methods of getting this implementation 780 @type download_sources: [L{RetrievalMethod}] 781 @ivar feed: the feed owning this implementation (since 0.32) 782 @type feed: [L{ZeroInstallFeed}] 783 @ivar bindings: how to tell this component where it itself is located (since 0.31) 784 @type bindings: [Binding] 785 @ivar upstream_stability: the stability reported by the packager 786 @type upstream_stability: [insecure | buggy | developer | testing | stable | packaged] 787 @ivar user_stability: the stability as set by the user 788 @type upstream_stability: [insecure | buggy | developer | testing | stable | packaged | preferred] 789 @ivar langs: natural languages supported by this package 790 @type langs: str 791 @ivar requires: interfaces this package depends on 792 @type requires: [L{Dependency}] 793 @ivar commands: ways to execute as a program 794 @type commands: {str: Command} 795 @ivar metadata: extra metadata from the feed 796 @type metadata: {"[URI ]localName": str} 797 @ivar id: a unique identifier for this Implementation 798 @ivar version: a parsed version number 799 @ivar released: release date 800 @ivar local_path: the directory containing this local implementation, or None if it isn't local (id isn't a path) 801 @type local_path: str | None 802 @ivar requires_root_install: whether the user will need admin rights to use this 803 @type requires_root_install: bool 804 """ 805 806 # Note: user_stability shouldn't really be here 807 808 __slots__ = ['upstream_stability', 'user_stability', 'langs', 809 'requires', 'metadata', 'download_sources', 'commands', 810 'id', 'feed', 'version', 'released', 'bindings', 'machine'] 811
812 - def __init__(self, feed, id):
813 """@type feed: L{ZeroInstallFeed} 814 @type id: str""" 815 assert id 816 self.feed = feed 817 self.id = id 818 self.user_stability = None 819 self.upstream_stability = None 820 self.metadata = {} # [URI + " "] + localName -> value 821 self.requires = [] 822 self.version = None 823 self.released = None 824 self.download_sources = [] 825 self.langs = "" 826 self.machine = None 827 self.bindings = [] 828 self.commands = {}
829
830 - def get_stability(self):
831 """@rtype: L{Stability}""" 832 return self.user_stability or self.upstream_stability or testing
833
834 - def __str__(self):
835 """@rtype: str""" 836 return self.id
837
838 - def __repr__(self):
839 return "v%s (%s)" % (self.get_version(), self.id)
840
841 - def __cmp__(self, other):
842 """Newer versions come first 843 @type other: L{Implementation} 844 @rtype: int""" 845 d = cmp(other.version, self.version) 846 if d: return d 847 # If the version number is the same, just give a stable sort order, and 848 # ensure that two different implementations don't compare equal. 849 d = cmp(other.feed.url, self.feed.url) 850 if d: return d 851 return cmp(other.id, self.id)
852
853 - def __hash__(self):
854 """@rtype: int""" 855 return self.id.__hash__()
856
857 - def __eq__(self, other):
858 """@type other: L{Implementation} 859 @rtype: bool""" 860 return self is other
861
862 - def __le__(self, other):
863 if isinstance(other, Implementation): 864 if other.version < self.version: return True 865 elif other.version > self.version: return False 866 867 if other.feed.url < self.feed.url: return True 868 elif other.feed.url > self.feed.url: return False 869 870 return other.id <= self.id 871 else: 872 return NotImplemented
873
874 - def get_version(self):
875 """Return the version as a string. 876 @rtype: str 877 @see: L{format_version}""" 878 return format_version(self.version)
879 880 arch = property(lambda self: _join_arch(self.os, self.machine)) 881 882 os = None 883 local_path = None 884 digests = None 885 requires_root_install = False 886
887 - def _get_main(self):
888 """"@deprecated: use commands["run"] instead 889 @rtype: str""" 890 main = self.commands.get("run", None) 891 if main is not None: 892 return main.path 893 return None
894 - def _set_main(self, path):
895 """"@deprecated: use commands["run"] instead""" 896 if path is None: 897 if "run" in self.commands: 898 del self.commands["run"] 899 else: 900 self.commands["run"] = Command(qdom.Element(XMLNS_IFACE, 'command', {'path': path, 'name': 'run'}), None)
901 main = property(_get_main, _set_main) 902
903 - def is_available(self, stores):
904 """Is this Implementation available locally? 905 (a local implementation, an installed distribution package, or a cached ZeroInstallImplementation) 906 @rtype: bool 907 @since: 0.53""" 908 raise NotImplementedError("abstract")
909
910 -class DistributionImplementation(Implementation):
911 """An implementation provided by the distribution. Information such as the version 912 comes from the package manager. 913 @ivar package_implementation: the <package-implementation> element that generated this impl (since 1.7) 914 @type package_implementation: L{qdom.Element} 915 @since: 0.28""" 916 __slots__ = ['distro', 'installed', 'package_implementation', 'distro_name'] 917
918 - def __init__(self, feed, id, distro, package_implementation = None, distro_name = None):
919 """@type feed: L{ZeroInstallFeed} 920 @type id: str 921 @type distro: L{zeroinstall.injector.distro.Distribution} 922 @type package_implementation: L{zeroinstall.injector.qdom.Element} | None 923 @type distro_name: str | None""" 924 assert id.startswith('package:') 925 Implementation.__init__(self, feed, id) 926 self.distro = distro 927 self.installed = False 928 self.package_implementation = package_implementation 929 self.distro_name = distro_name or distro.name 930 931 if package_implementation: 932 for child in package_implementation.childNodes: 933 if child.uri != XMLNS_IFACE: continue 934 if child.name == 'command': 935 command_name = child.attrs.get('name', None) 936 if not command_name: 937 raise InvalidInterface('Missing name for <command>') 938 self.commands[command_name] = Command(child, local_dir = None)
939 940 @property
941 - def requires_root_install(self):
942 return not self.installed
943
944 - def is_available(self, stores):
945 """@type stores: L{zeroinstall.zerostore.Stores} 946 @rtype: bool""" 947 return self.installed
948
949 -class ZeroInstallImplementation(Implementation):
950 """An implementation where all the information comes from Zero Install. 951 @ivar digests: a list of "algorith=value" or "algorith_value" strings (since 0.45) 952 @type digests: [str] 953 @since: 0.28""" 954 __slots__ = ['os', 'size', 'digests', 'local_path'] 955 956 distro_name = '0install' 957
958 - def __init__(self, feed, id, local_path):
959 """id can be a local path (string starting with /) or a manifest hash (eg "sha1=XXX") 960 @type feed: L{ZeroInstallFeed} 961 @type id: str 962 @type local_path: str""" 963 assert not id.startswith('package:'), id 964 Implementation.__init__(self, feed, id) 965 self.size = None 966 self.os = None 967 self.digests = [] 968 self.local_path = local_path
969 970 # Deprecated 971 dependencies = property(lambda self: dict([(x.interface, x) for x in self.requires 972 if isinstance(x, InterfaceRestriction)])) 973
974 - def add_download_source(self, url, size, extract, start_offset = 0, type = None, dest = None):
975 """Add a download source. 976 @type url: str 977 @type size: int 978 @type extract: str 979 @type start_offset: int 980 @type type: str | None 981 @type dest: str | None""" 982 self.download_sources.append(DownloadSource(self, url, size, extract, start_offset, type, dest))
983
984 - def set_arch(self, arch):
985 """@type arch: str""" 986 self.os, self.machine = _split_arch(arch)
987 arch = property(lambda self: _join_arch(self.os, self.machine), set_arch) 988
989 - def is_available(self, stores):
990 """@type stores: L{zeroinstall.zerostore.Stores} 991 @rtype: bool""" 992 if self.local_path is not None: 993 return os.path.exists(self.local_path) 994 if self.digests: 995 path = stores.lookup_maybe(self.digests) 996 return path is not None 997 return False # (0compile creates fake entries with no digests)
998
999 -class Interface(object):
1000 """An Interface represents some contract of behaviour. 1001 @ivar uri: the URI for this interface. 1002 @ivar stability_policy: user's configured policy. 1003 Implementations at this level or higher are preferred. 1004 Lower levels are used only if there is no other choice. 1005 """ 1006 __slots__ = ['uri', 'stability_policy', 'extra_feeds'] 1007 1008 implementations = property(lambda self: self._main_feed.implementations) 1009 name = property(lambda self: self._main_feed.name) 1010 description = property(lambda self: self._main_feed.description) 1011 summary = property(lambda self: self._main_feed.summary) 1012 last_modified = property(lambda self: self._main_feed.last_modified) 1013 feeds = property(lambda self: self.extra_feeds + self._main_feed.feeds) 1014 metadata = property(lambda self: self._main_feed.metadata) 1015 1016 last_checked = property(lambda self: self._main_feed.last_checked) 1017
1018 - def __init__(self, uri):
1019 """@type uri: str""" 1020 assert uri 1021 if uri.startswith('http:') or uri.startswith('https:') or os.path.isabs(uri): 1022 self.uri = uri 1023 else: 1024 raise SafeException(_("Interface name '%s' doesn't start " 1025 "with 'http:' or 'https:'") % uri) 1026 self.reset()
1027
1028 - def _get_feed_for(self):
1029 retval = {} 1030 for key in self._main_feed.feed_for: 1031 retval[key] = True 1032 return retval
1033 feed_for = property(_get_feed_for) # Deprecated (used by 0publish) 1034
1035 - def reset(self):
1036 self.extra_feeds = [] 1037 self.stability_policy = None
1038
1039 - def get_name(self):
1040 """@rtype: str""" 1041 from zeroinstall.injector.iface_cache import iface_cache 1042 feed = iface_cache.get_feed(self.uri) 1043 if feed: 1044 return feed.get_name() 1045 return '(' + os.path.basename(self.uri) + ')'
1046
1047 - def __repr__(self):
1048 """@rtype: str""" 1049 return _("<Interface %s>") % self.uri
1050
1051 - def set_stability_policy(self, new):
1052 """@type new: L{Stability}""" 1053 assert new is None or isinstance(new, Stability) 1054 self.stability_policy = new
1055
1056 - def get_feed(self, url):
1057 #import warnings 1058 #warnings.warn("use iface_cache.get_feed instead", DeprecationWarning, 2) 1059 for x in self.extra_feeds: 1060 if x.uri == url: 1061 return x 1062 #return self._main_feed.get_feed(url) 1063 return None
1064
1065 - def get_metadata(self, uri, name):
1066 return self._main_feed.get_metadata(uri, name)
1067 1068 @property
1069 - def _main_feed(self):
1070 import warnings 1071 warnings.warn("use the feed instead", DeprecationWarning, 3) 1072 from zeroinstall.injector import policy 1073 iface_cache = policy.get_deprecated_singleton_config().iface_cache 1074 feed = iface_cache.get_feed(self.uri) 1075 if feed is None: 1076 return _dummy_feed 1077 return feed
1078
1079 -def _merge_attrs(attrs, item):
1080 """Add each attribute of item to a copy of attrs and return the copy. 1081 @type attrs: {str: str} 1082 @type item: L{qdom.Element} 1083 @rtype: {str: str}""" 1084 new = attrs.copy() 1085 for a in item.attrs: 1086 new[str(a)] = item.attrs[a] 1087 return new
1088
1089 -def _get_long(elem, attr_name):
1090 """@type elem: L{zeroinstall.injector.qdom.Element} 1091 @type attr_name: str 1092 @rtype: int""" 1093 val = elem.getAttribute(attr_name) 1094 if val is not None: 1095 try: 1096 val = int(val) 1097 except ValueError: 1098 raise SafeException(_("Invalid value for integer attribute '%(attribute_name)s': %(value)s") % {'attribute_name': attr_name, 'value': val}) 1099 return val
1100
1101 -class ZeroInstallFeed(object):
1102 """A feed lists available implementations of an interface. 1103 @ivar url: the URL for this feed 1104 @ivar implementations: Implementations in this feed, indexed by ID 1105 @type implementations: {str: L{Implementation}} 1106 @ivar name: human-friendly name 1107 @ivar summaries: short textual description (in various languages, since 0.49) 1108 @type summaries: {str: str} 1109 @ivar descriptions: long textual description (in various languages, since 0.49) 1110 @type descriptions: {str: str} 1111 @ivar last_modified: timestamp on signature 1112 @ivar last_checked: time feed was last successfully downloaded and updated 1113 @ivar local_path: the path of this local feed, or None if remote (since 1.7) 1114 @type local_path: str | None 1115 @ivar feeds: list of <feed> elements in this feed 1116 @type feeds: [L{Feed}] 1117 @ivar feed_for: interfaces for which this could be a feed 1118 @type feed_for: set(str) 1119 @ivar metadata: extra elements we didn't understand 1120 """ 1121 # _main is deprecated 1122 __slots__ = ['url', 'implementations', 'name', 'descriptions', 'first_description', 'summaries', 'first_summary', '_package_implementations', 1123 'last_checked', 'last_modified', 'feeds', 'feed_for', 'metadata', 'local_path'] 1124
1125 - def __init__(self, feed_element, local_path = None, distro = None):
1126 """Create a feed object from a DOM. 1127 @param feed_element: the root element of a feed file 1128 @type feed_element: L{qdom.Element} 1129 @param local_path: the pathname of this local feed, or None for remote feeds 1130 @type local_path: str | None""" 1131 self.local_path = local_path 1132 self.implementations = {} 1133 self.name = None 1134 self.summaries = {} # { lang: str } 1135 self.first_summary = None 1136 self.descriptions = {} # { lang: str } 1137 self.first_description = None 1138 self.last_modified = None 1139 self.feeds = [] 1140 self.feed_for = set() 1141 self.metadata = [] 1142 self.last_checked = None 1143 self._package_implementations = [] 1144 1145 if distro is not None: 1146 import warnings 1147 warnings.warn("distro argument is now ignored", DeprecationWarning, 2) 1148 1149 if feed_element is None: 1150 return # XXX subclass? 1151 1152 if feed_element.name not in ('interface', 'feed'): 1153 raise SafeException("Root element should be <interface>, not <%s>" % feed_element.name) 1154 assert feed_element.uri == XMLNS_IFACE, "Wrong namespace on root element: %s" % feed_element.uri 1155 1156 main = feed_element.getAttribute('main') 1157 #if main: warn("Setting 'main' on the root element is deprecated. Put it on a <group> instead") 1158 1159 if local_path: 1160 self.url = local_path 1161 local_dir = os.path.dirname(local_path) 1162 else: 1163 assert local_path is None 1164 self.url = feed_element.getAttribute('uri') 1165 if not self.url: 1166 raise InvalidInterface(_("<interface> uri attribute missing")) 1167 local_dir = None # Can't have relative paths 1168 1169 min_injector_version = feed_element.getAttribute('min-injector-version') 1170 if min_injector_version: 1171 if parse_version(min_injector_version) > parse_version(version): 1172 raise InvalidInterface(_("This feed requires version %(min_version)s or later of " 1173 "Zero Install, but I am only version %(version)s. " 1174 "You can get a newer version from http://0install.net") % 1175 {'min_version': min_injector_version, 'version': version}) 1176 1177 for x in feed_element.childNodes: 1178 if x.uri != XMLNS_IFACE: 1179 self.metadata.append(x) 1180 continue 1181 if x.name == 'name': 1182 self.name = x.content 1183 elif x.name == 'description': 1184 if self.first_description == None: 1185 self.first_description = x.content 1186 self.descriptions[x.attrs.get("http://www.w3.org/XML/1998/namespace lang", 'en')] = x.content 1187 elif x.name == 'summary': 1188 if self.first_summary == None: 1189 self.first_summary = x.content 1190 self.summaries[x.attrs.get("http://www.w3.org/XML/1998/namespace lang", 'en')] = x.content 1191 elif x.name == 'feed-for': 1192 feed_iface = x.getAttribute('interface') 1193 if not feed_iface: 1194 raise InvalidInterface(_('Missing "interface" attribute in <feed-for>')) 1195 self.feed_for.add(feed_iface) 1196 # Bug report from a Debian/stable user that --feed gets the wrong value. 1197 # Can't reproduce (even in a Debian/stable chroot), but add some logging here 1198 # in case it happens again. 1199 logger.debug(_("Is feed-for %s"), feed_iface) 1200 elif x.name == 'feed': 1201 feed_src = x.getAttribute('src') 1202 if not feed_src: 1203 raise InvalidInterface(_('Missing "src" attribute in <feed>')) 1204 if feed_src.startswith('http:') or feed_src.startswith('https:') or local_path: 1205 if feed_src.startswith('.'): 1206 feed_src = os.path.abspath(os.path.join(local_dir, feed_src)) 1207 1208 langs = x.getAttribute('langs') 1209 if langs: langs = langs.replace('_', '-') 1210 self.feeds.append(Feed(feed_src, x.getAttribute('arch'), False, langs = langs)) 1211 else: 1212 raise InvalidInterface(_("Invalid feed URL '%s'") % feed_src) 1213 else: 1214 self.metadata.append(x) 1215 1216 if not self.name: 1217 raise InvalidInterface(_("Missing <name> in feed")) 1218 if not self.summary: 1219 raise InvalidInterface(_("Missing <summary> in feed")) 1220 1221 def process_group(group, group_attrs, base_depends, base_bindings, base_commands): 1222 for item in group.childNodes: 1223 if item.uri != XMLNS_IFACE: continue 1224 1225 if item.name not in ('group', 'implementation', 'package-implementation'): 1226 continue 1227 1228 # We've found a group or implementation. Scan for dependencies, 1229 # bindings and commands. Doing this here means that: 1230 # - We can share the code for groups and implementations here. 1231 # - The order doesn't matter, because these get processed first. 1232 # A side-effect is that the document root cannot contain 1233 # these. 1234 1235 depends = base_depends[:] 1236 bindings = base_bindings[:] 1237 commands = base_commands.copy() 1238 1239 for attr, command in [('main', 'run'), 1240 ('self-test', 'test')]: 1241 value = item.attrs.get(attr, None) 1242 if value is not None: 1243 commands[command] = Command(qdom.Element(XMLNS_IFACE, 'command', {'name': command, 'path': value}), None) 1244 1245 for child in item.childNodes: 1246 if child.uri != XMLNS_IFACE: continue 1247 if child.name in _dependency_names: 1248 dep = process_depends(child, local_dir) 1249 depends.append(dep) 1250 elif child.name == 'command': 1251 command_name = child.attrs.get('name', None) 1252 if not command_name: 1253 raise InvalidInterface('Missing name for <command>') 1254 commands[command_name] = Command(child, local_dir) 1255 elif child.name in binding_names: 1256 bindings.append(process_binding(child)) 1257 1258 compile_command = item.attrs.get('http://zero-install.sourceforge.net/2006/namespaces/0compile command') 1259 if compile_command is not None: 1260 commands['compile'] = Command(qdom.Element(XMLNS_IFACE, 'command', {'name': 'compile', 'shell-command': compile_command}), None) 1261 1262 item_attrs = _merge_attrs(group_attrs, item) 1263 1264 if item.name == 'group': 1265 process_group(item, item_attrs, depends, bindings, commands) 1266 elif item.name == 'implementation': 1267 process_impl(item, item_attrs, depends, bindings, commands) 1268 elif item.name == 'package-implementation': 1269 self._package_implementations.append((item, item_attrs, depends)) 1270 else: 1271 assert 0
1272 1273 def process_impl(item, item_attrs, depends, bindings, commands): 1274 id = item.getAttribute('id') 1275 if id is None: 1276 raise InvalidInterface(_("Missing 'id' attribute on %s") % item) 1277 local_path = item_attrs.get('local-path') 1278 if local_dir and local_path: 1279 abs_local_path = os.path.abspath(os.path.join(local_dir, local_path)) 1280 impl = ZeroInstallImplementation(self, id, abs_local_path) 1281 elif local_dir and (id.startswith('/') or id.startswith('.')): 1282 # For old feeds 1283 id = os.path.abspath(os.path.join(local_dir, id)) 1284 impl = ZeroInstallImplementation(self, id, id) 1285 else: 1286 impl = ZeroInstallImplementation(self, id, None) 1287 if '=' in id: 1288 # In older feeds, the ID was the (single) digest 1289 impl.digests.append(id) 1290 if id in self.implementations: 1291 logger.warning(_("Duplicate ID '%(id)s' in feed '%(feed)s'"), {'id': id, 'feed': self}) 1292 self.implementations[id] = impl 1293 1294 impl.metadata = item_attrs 1295 try: 1296 version_mod = item_attrs.get('version-modifier', None) 1297 if version_mod: 1298 item_attrs['version'] += version_mod 1299 del item_attrs['version-modifier'] 1300 version = item_attrs['version'] 1301 except KeyError: 1302 raise InvalidInterface(_("Missing version attribute")) 1303 impl.version = parse_version(version) 1304 1305 impl.commands = commands 1306 1307 impl.released = item_attrs.get('released', None) 1308 impl.langs = item_attrs.get('langs', '').replace('_', '-') 1309 1310 size = item.getAttribute('size') 1311 if size: 1312 impl.size = int(size) 1313 impl.arch = item_attrs.get('arch', None) 1314 try: 1315 stability = stability_levels[str(item_attrs['stability'])] 1316 except KeyError: 1317 stab = str(item_attrs['stability']) 1318 if stab != stab.lower(): 1319 raise InvalidInterface(_('Stability "%s" invalid - use lower case!') % item_attrs.stability) 1320 raise InvalidInterface(_('Stability "%s" invalid') % item_attrs['stability']) 1321 if stability >= preferred: 1322 raise InvalidInterface(_("Upstream can't set stability to preferred!")) 1323 impl.upstream_stability = stability 1324 1325 impl.bindings = bindings 1326 impl.requires = depends 1327 1328 def extract_file_source(elem): 1329 url = elem.getAttribute('href') 1330 if not url: 1331 raise InvalidInterface(_("Missing href attribute on <file>")) 1332 dest = elem.getAttribute('dest') 1333 if not dest: 1334 raise InvalidInterface(_("Missing dest attribute on <file>")) 1335 size = elem.getAttribute('size') 1336 if not size: 1337 raise InvalidInterface(_("Missing size attribute on <file>")) 1338 return FileSource(url, dest, int(size))
1339 1340 for elem in item.childNodes: 1341 if elem.uri != XMLNS_IFACE: continue 1342 if elem.name == 'archive': 1343 url = elem.getAttribute('href') 1344 if not url: 1345 raise InvalidInterface(_("Missing href attribute on <archive>")) 1346 size = elem.getAttribute('size') 1347 if not size: 1348 raise InvalidInterface(_("Missing size attribute on <archive>")) 1349 impl.add_download_source(url = url, size = int(size), 1350 extract = elem.getAttribute('extract'), 1351 start_offset = _get_long(elem, 'start-offset'), 1352 type = elem.getAttribute('type'), 1353 dest = elem.getAttribute('dest')) 1354 elif elem.name == 'file': 1355 impl.download_sources.append(extract_file_source(elem)) 1356 1357 elif elem.name == 'manifest-digest': 1358 for aname, avalue in elem.attrs.items(): 1359 if ' ' not in aname: 1360 impl.digests.append(zerostore.format_algorithm_digest_pair(aname, avalue)) 1361 elif elem.name == 'recipe': 1362 recipe = Recipe() 1363 for recipe_step in elem.childNodes: 1364 if recipe_step.uri == XMLNS_IFACE and recipe_step.name == 'archive': 1365 url = recipe_step.getAttribute('href') 1366 if not url: 1367 raise InvalidInterface(_("Missing href attribute on <archive>")) 1368 size = recipe_step.getAttribute('size') 1369 if not size: 1370 raise InvalidInterface(_("Missing size attribute on <archive>")) 1371 recipe.steps.append(DownloadSource(None, url = url, size = int(size), 1372 extract = recipe_step.getAttribute('extract'), 1373 start_offset = _get_long(recipe_step, 'start-offset'), 1374 type = recipe_step.getAttribute('type'), 1375 dest = recipe_step.getAttribute('dest'))) 1376 elif recipe_step.name == 'file': 1377 recipe.steps.append(extract_file_source(recipe_step)) 1378 elif recipe_step.uri == XMLNS_IFACE and recipe_step.name == 'rename': 1379 source = recipe_step.getAttribute('source') 1380 if not source: 1381 raise InvalidInterface(_("Missing source attribute on <rename>")) 1382 dest = recipe_step.getAttribute('dest') 1383 if not dest: 1384 raise InvalidInterface(_("Missing dest attribute on <rename>")) 1385 recipe.steps.append(RenameStep(source=source, dest=dest)) 1386 elif recipe_step.uri == XMLNS_IFACE and recipe_step.name == 'remove': 1387 path = recipe_step.getAttribute('path') 1388 if not path: 1389 raise InvalidInterface(_("Missing path attribute on <remove>")) 1390 recipe.steps.append(RemoveStep(path=path)) 1391 else: 1392 logger.info(_("Unknown step '%s' in recipe; skipping recipe"), recipe_step.name) 1393 break 1394 else: 1395 impl.download_sources.append(recipe) 1396 1397 root_attrs = {'stability': 'testing'} 1398 root_commands = {} 1399 if main: 1400 logger.info("Note: @main on document element is deprecated in %s", self) 1401 root_commands['run'] = Command(qdom.Element(XMLNS_IFACE, 'command', {'path': main, 'name': 'run'}), None) 1402 process_group(feed_element, root_attrs, [], [], root_commands) 1403
1404 - def get_distro_feed(self):
1405 """Does this feed contain any <pacakge-implementation> elements? 1406 i.e. is it worth asking the package manager for more information? 1407 @return: the URL of the virtual feed, or None 1408 @rtype: str 1409 @since: 0.49""" 1410 if self._package_implementations: 1411 return "distribution:" + self.url 1412 return None
1413
1414 - def get_package_impls(self, distro):
1415 """Find the best <pacakge-implementation> element(s) for the given distribution. 1416 @param distro: the distribution to use to rate them 1417 @type distro: L{distro.Distribution} 1418 @return: a list of tuples for the best ranked elements 1419 @rtype: [str] 1420 @since: 0.49""" 1421 best_score = 0 1422 best_impls = [] 1423 1424 for item, item_attrs, depends in self._package_implementations: 1425 distro_names = item_attrs.get('distributions', '') 1426 score_this_item = max( 1427 distro.get_score(distro_name) if distro_name else 0.5 1428 for distro_name in distro_names.split(' ')) 1429 if score_this_item > best_score: 1430 best_score = score_this_item 1431 best_impls = [] 1432 if score_this_item == best_score: 1433 best_impls.append((item, item_attrs, depends)) 1434 return best_impls
1435
1436 - def get_name(self):
1437 """@rtype: str""" 1438 return self.name or '(' + os.path.basename(self.url) + ')'
1439
1440 - def __repr__(self):
1441 return _("<Feed %s>") % self.url
1442
1443 - def set_stability_policy(self, new):
1444 assert new is None or isinstance(new, Stability) 1445 self.stability_policy = new
1446
1447 - def get_feed(self, url):
1448 for x in self.feeds: 1449 if x.uri == url: 1450 return x 1451 return None
1452
1453 - def add_metadata(self, elem):
1454 self.metadata.append(elem)
1455
1456 - def get_metadata(self, uri, name):
1457 """Return a list of interface metadata elements with this name and namespace URI. 1458 @type uri: str 1459 @type name: str""" 1460 return [m for m in self.metadata if m.name == name and m.uri == uri]
1461 1462 @property
1463 - def summary(self):
1464 return _best_language_match(self.summaries) or self.first_summary
1465 1466 @property
1467 - def description(self):
1468 return _best_language_match(self.descriptions) or self.first_description
1469
1470 - def get_replaced_by(self):
1471 """Return the URI of the interface that replaced the one with the URI of this feed's URL. 1472 This is the value of the feed's <replaced-by interface'...'/> element. 1473 @return: the new URI, or None if it hasn't been replaced 1474 @rtype: str | None 1475 @since: 1.7""" 1476 for child in self.metadata: 1477 if child.uri == XMLNS_IFACE and child.name == 'replaced-by': 1478 new_uri = child.getAttribute('interface') 1479 if new_uri and (new_uri.startswith('http:') or new_uri.startswith('https:') or self.local_path): 1480 return new_uri 1481 return None
1482
1483 -class DummyFeed(object):
1484 """Temporary class used during API transition.""" 1485 last_modified = None 1486 name = '-' 1487 last_checked = property(lambda self: None) 1488 implementations = property(lambda self: {}) 1489 feeds = property(lambda self: []) 1490 summary = property(lambda self: '-') 1491 description = property(lambda self: '')
1492 - def get_name(self): return self.name
1493 - def get_feed(self, url): return None
1494 - def get_metadata(self, uri, name): return []
1495 _dummy_feed = DummyFeed() 1496 1497 if sys.version_info[0] > 2: 1498 # Python 3 1499 1500 from functools import total_ordering
1501 # (note: delete these two lines when generating epydoc) 1502 1503 # These could be replaced by urllib.parse.quote, except that 1504 # it uses upper-case escapes and we use lower-case ones... 1505 - def unescape(uri):
1506 """Convert each %20 to a space, etc. 1507 @type uri: str 1508 @rtype: str""" 1509 uri = uri.replace('#', '/') 1510 if '%' not in uri: return uri 1511 return re.sub(b'%[0-9a-fA-F][0-9a-fA-F]', 1512 lambda match: bytes([int(match.group(0)[1:], 16)]), 1513 uri.encode('ascii')).decode('utf-8')
1514
1515 - def escape(uri):
1516 """Convert each space to %20, etc 1517 @type uri: str 1518 @rtype: str""" 1519 return re.sub(b'[^-_.a-zA-Z0-9]', 1520 lambda match: ('%%%02x' % ord(match.group(0))).encode('ascii'), 1521 uri.encode('utf-8')).decode('ascii')
1522
1523 - def _pretty_escape(uri):
1524 """Convert each space to %20, etc 1525 : is preserved and / becomes #. This makes for nicer strings, 1526 and may replace L{escape} everywhere in future. 1527 @type uri: str 1528 @rtype: str""" 1529 if os.name == "posix": 1530 # Only preserve : on Posix systems 1531 preserveRegex = b'[^-_.a-zA-Z0-9:/]' 1532 else: 1533 # Other OSes may not allow the : character in file names 1534 preserveRegex = b'[^-_.a-zA-Z0-9/]' 1535 return re.sub(preserveRegex, 1536 lambda match: ('%%%02x' % ord(match.group(0))).encode('ascii'), 1537 uri.encode('utf-8')).decode('ascii').replace('/', '#')
1538 else:
1539 # Python 2 1540 1541 - def unescape(uri):
1542 """Convert each %20 to a space, etc. 1543 @type uri: str 1544 @rtype: str""" 1545 uri = uri.replace('#', '/') 1546 if '%' not in uri: return uri 1547 return re.sub('%[0-9a-fA-F][0-9a-fA-F]', 1548 lambda match: chr(int(match.group(0)[1:], 16)), 1549 uri).decode('utf-8')
1550
1551 - def escape(uri):
1552 """Convert each space to %20, etc 1553 @type uri: str 1554 @rtype: str""" 1555 return re.sub('[^-_.a-zA-Z0-9]', 1556 lambda match: '%%%02x' % ord(match.group(0)), 1557 uri.encode('utf-8'))
1558
1559 - def _pretty_escape(uri):
1560 """Convert each space to %20, etc 1561 : is preserved and / becomes #. This makes for nicer strings, 1562 and may replace L{escape} everywhere in future. 1563 @type uri: str 1564 @rtype: str""" 1565 if os.name == "posix": 1566 # Only preserve : on Posix systems 1567 preserveRegex = '[^-_.a-zA-Z0-9:/]' 1568 else: 1569 # Other OSes may not allow the : character in file names 1570 preserveRegex = '[^-_.a-zA-Z0-9/]' 1571 return re.sub(preserveRegex, 1572 lambda match: '%%%02x' % ord(match.group(0)), 1573 uri.encode('utf-8')).replace('/', '#')
1574
1575 -def escape_interface_uri(uri):
1576 """Convert an interface URI to a list of path components. 1577 e.g. "http://example.com/foo.xml" becomes ["http", "example.com", "foo.xml"], while 1578 "file:///root/feed.xml" becomes ["file", "root__feed.xml"] 1579 The number of components is determined by the scheme (three for http, two for file). 1580 Uses L{support.escaping.underscore_escape} to escape each component. 1581 @type uri: str 1582 @rtype: [str]""" 1583 if uri.startswith('http://') or uri.startswith('https://'): 1584 scheme, rest = uri.split('://', 1) 1585 parts = rest.split('/', 1) 1586 else: 1587 assert os.path.isabs(uri), uri 1588 scheme = 'file' 1589 parts = [uri[1:]] 1590 1591 return [scheme] + [escaping.underscore_escape(part) for part in parts]
1592
1593 -def canonical_iface_uri(uri):
1594 """If uri is a relative path, convert to an absolute one. 1595 A "file:///foo" URI is converted to "/foo". 1596 An "alias:prog" URI expands to the URI in the 0alias script 1597 Otherwise, return it unmodified. 1598 @type uri: str 1599 @rtype: str 1600 @raise SafeException: if uri isn't valid""" 1601 if uri.startswith('http://') or uri.startswith('https://'): 1602 if uri.count("/") < 3: 1603 raise SafeException(_("Missing / after hostname in URI '%s'") % uri) 1604 return uri 1605 elif uri.startswith('file:///'): 1606 path = uri[7:] 1607 elif uri.startswith('file:'): 1608 if uri[5] == '/': 1609 raise SafeException(_('Use file:///path for absolute paths, not {uri}').format(uri = uri)) 1610 path = os.path.abspath(uri[5:]) 1611 elif uri.startswith('alias:'): 1612 from zeroinstall import alias 1613 alias_prog = uri[6:] 1614 if not os.path.isabs(alias_prog): 1615 full_path = support.find_in_path(alias_prog) 1616 if not full_path: 1617 raise alias.NotAnAliasScript("Not found in $PATH: " + alias_prog) 1618 else: 1619 full_path = alias_prog 1620 return alias.parse_script(full_path).uri 1621 else: 1622 path = os.path.realpath(uri) 1623 1624 if os.path.isfile(path): 1625 return path 1626 1627 if '/' not in uri: 1628 alias_path = support.find_in_path(uri) 1629 if alias_path is not None: 1630 from zeroinstall import alias 1631 try: 1632 alias.parse_script(alias_path) 1633 except alias.NotAnAliasScript: 1634 pass 1635 else: 1636 raise SafeException(_("Bad interface name '{uri}'.\n" 1637 "(hint: try 'alias:{uri}' instead)".format(uri = uri))) 1638 1639 raise SafeException(_("Bad interface name '%(uri)s'.\n" 1640 "(doesn't start with 'http:', and " 1641 "doesn't exist as a local file '%(interface_uri)s' either)") % 1642 {'uri': uri, 'interface_uri': path})
1643