From 1c36200cbc118caf11ba77812bd2304634e08cee Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Sat, 15 Jan 2011 16:14:10 +0000 Subject: [PATCH] Made a proper Config object This now holds the global options and lazily creates the various other required objects (iface_cache, etc). It is passed around as an argument rather than being a singleton, although there is a method to get a singleton to help with migration. TODO: - unittests - backwards compat --- tests/basetest.py | 2 +- zeroinstall/cmd/__init__.py | 19 +++- zeroinstall/cmd/add_feed.py | 7 +- zeroinstall/cmd/config.py | 182 ++++++++++++++++++++++-------------- zeroinstall/cmd/download.py | 7 +- zeroinstall/cmd/import.py | 15 +-- zeroinstall/cmd/list.py | 7 +- zeroinstall/cmd/remove_feed.py | 4 +- zeroinstall/cmd/run.py | 7 +- zeroinstall/cmd/select.py | 52 +++++------ zeroinstall/cmd/update.py | 6 +- zeroinstall/injector/cli.py | 9 +- zeroinstall/injector/iface_cache.py | 22 +++-- zeroinstall/injector/model.py | 10 +- zeroinstall/injector/policy.py | 171 +++++++++++++++++++++------------ zeroinstall/injector/reader.py | 9 +- zeroinstall/injector/run.py | 24 +++-- zeroinstall/injector/selections.py | 9 +- zeroinstall/injector/solver.py | 46 ++++----- zeroinstall/injector/writer.py | 1 - 20 files changed, 353 insertions(+), 256 deletions(-) rewrite zeroinstall/cmd/config.py (62%) diff --git a/tests/basetest.py b/tests/basetest.py index c91e01a..5de15ad 100755 --- a/tests/basetest.py +++ b/tests/basetest.py @@ -70,7 +70,7 @@ class BaseTest(unittest.TestCase): os.environ['XDG_CACHE_DIRS'] = self.cache_system reload(basedir) assert basedir.xdg_config_home == self.config_home - iface_cache.iface_cache.__init__() + #iface_cache.iface_cache.__init__() os.mkdir(self.config_home, 0700) os.mkdir(self.cache_home, 0700) diff --git a/zeroinstall/cmd/__init__.py b/zeroinstall/cmd/__init__.py index 0cb78ff..3dce6b8 100644 --- a/zeroinstall/cmd/__init__.py +++ b/zeroinstall/cmd/__init__.py @@ -12,7 +12,6 @@ import logging from zeroinstall import SafeException, NeedDownload from zeroinstall.injector import model, autopolicy, selections -from zeroinstall.injector.iface_cache import iface_cache valid_commands = ['select', 'download', 'run', 'update', 'config', 'import', 'list', 'add-feed', 'remove-feed'] @@ -50,13 +49,21 @@ def _no_command(command_args): parser.print_help() sys.exit(2) -def main(command_args): +def main(command_args, config = None): """Act as if 0install was run with the given arguments. @arg command_args: array of arguments (e.g. C{sys.argv[1:]}) @type command_args: [str] """ _ensure_standard_fds() + if config is None: + from zeroinstall.injector import policy, handler + if os.isatty(1): + h = handler.ConsoleHandler() + else: + h = handler.Handler() + config = policy.load_config(h) + # The first non-option argument is the command name (or "help" if none is found). command = None for arg in command_args: @@ -103,10 +110,12 @@ def main(command_args): if options.with_store: from zeroinstall import zerostore for x in options.with_store: - iface_cache.stores.stores.append(zerostore.Store(os.path.abspath(x))) - logging.info(_("Stores search path is now %s"), iface_cache.stores.stores) + config.stores.stores.append(zerostore.Store(os.path.abspath(x))) + logging.info(_("Stores search path is now %s"), config.stores.stores) + + h.dry_run = bool(options.dry_run) - cmd.handle(options, args) + cmd.handle(config, options, args) except UsageError: parser.print_help() sys.exit(1) diff --git a/zeroinstall/cmd/add_feed.py b/zeroinstall/cmd/add_feed.py index 00d69d1..3d086ea 100644 --- a/zeroinstall/cmd/add_feed.py +++ b/zeroinstall/cmd/add_feed.py @@ -12,14 +12,13 @@ from zeroinstall import cmd, SafeException, _ from zeroinstall.cmd import UsageError from zeroinstall.injector import model, writer, handler from zeroinstall.injector.policy import Policy -from zeroinstall.injector.iface_cache import iface_cache syntax = "NEW-FEED" def add_options(parser): parser.add_option("-o", "--offline", help=_("try to avoid using the network"), action='store_true') -def handle(options, args, add_ok = True, remove_ok = False): +def handle(config, options, args, add_ok = True, remove_ok = False): if not args: raise UsageError() if os.isatty(1): @@ -41,9 +40,9 @@ def handle(options, args, add_ok = True, remove_ok = False): if options.offline: policy.network_use = model.network_offline - feed = iface_cache.get_feed(x) + feed = config.iface_cache.get_feed(x) if policy.network_use != model.network_offline and policy.is_stale(feed): - blocker = policy.fetcher.download_and_import_feed(x, iface_cache) + blocker = policy.fetcher.download_and_import_feed(x, config.iface_cache) print _("Downloading feed; please wait...") h.wait_for_blocker(blocker) print _("Done") diff --git a/zeroinstall/cmd/config.py b/zeroinstall/cmd/config.py dissimilarity index 62% index 95a1e61..3a32adc 100644 --- a/zeroinstall/cmd/config.py +++ b/zeroinstall/cmd/config.py @@ -1,71 +1,111 @@ -""" -The B{0install config} command-line interface. -""" - -# Copyright (C) 2011, Thomas Leonard -# See the README file for details, or visit http://0install.net. - -import os, sys -import logging -import ConfigParser - -from zeroinstall import cmd, SafeException, _ -from zeroinstall.support import basedir -from zeroinstall.injector import policy, namespaces, model -from zeroinstall.cmd import UsageError - -syntax = "[NAME [VALUE]]" - -def add_options(parser): - pass - -def handle(options, args): - config = policy.load_config() - if len(args) == 0: - if options.gui is None and os.environ.get('DISPLAY', None): - options.gui = True - if options.gui: - from zeroinstall import helpers - return helpers.get_selections_gui(None, []) - else: - config.write(sys.stdout) - return - elif len(args) > 2: - raise UsageError() - - if '.' not in args[0]: - raise SafeException(_('Missing section name in "%s" (e.g. try "global.freshness")') % args[0]) - section, option = args[0].split('.', 1) - - if len(args) == 1: - try: - print config.get(section, option) - except ConfigParser.NoOptionError, ex: - raise SafeException(str(ex)) - except ConfigParser.NoSectionError, ex: - raise SafeException(str(ex)) - else: - if section != 'global': - raise SafeException(_('Unknown section "%s" (try "global")' % section)) - - value = args[1] - if option == 'freshness': - int(value) - elif option == 'help_with_testing': - if value.lower() == 'true': - value = 'True' - elif value.lower() == 'false': - value = 'False' - else: - raise SafeException(_('Must be True or False, not "%s"') % value) - elif option == 'network_use': - if value not in model.network_levels: - raise SafeException(_("Must be one of %s") % list(model.network_levels)) - else: - raise SafeException(_('Unknown option "%s"') % option) - - config.set(section, option, value) - path = basedir.save_config_path(namespaces.config_site, namespaces.config_prog) - path = os.path.join(path, 'global') - config.write(file(path + '.new', 'w')) - os.rename(path + '.new', path) +""" +The B{0install config} command-line interface. +""" + +# Copyright (C) 2011, Thomas Leonard +# See the README file for details, or visit http://0install.net. + +import os, sys +import logging +import ConfigParser + +from zeroinstall import cmd, SafeException, _ +from zeroinstall.support import basedir +from zeroinstall.injector import policy, namespaces, model +from zeroinstall.cmd import UsageError + +syntax = "[NAME [VALUE]]" + +def add_options(parser): + pass + +class String: + @staticmethod + def format(value): + return value + + @staticmethod + def parse(value): + return value + +class TimeInterval: + @staticmethod + def format(value): + value = float(value) + if value < 60: + return str(value) + "s" + value /= 60 + if value < 60: + return str(value) + "m" + value /= 60 + if value < 24: + return str(value) + "h" + value /= 24 + return str(value) + "d" + + @staticmethod + def parse(value): + v = float(value[:-1]) + unit = value[-1] + if unit == 's': + return int(v) + v *= 60 + if unit == 'm': + return int(v) + v *= 60 + if unit == 'h': + return int(v) + v *= 24 + if unit == 'd': + return int(v) + raise SafeException(_('Unknown unit "%s" - use e.g. 5d for 5 days') % unit) + +class Boolean: + @staticmethod + def format(value): + return value + + @staticmethod + def parse(value): + if value.lower() == 'true': + return True + elif value.lower() == 'false': + return False + else: + raise SafeException(_('Must be True or False, not "%s"') % value) + +settings = { + 'network_use': String, + 'freshness': TimeInterval, + 'help_with_testing': Boolean, +} + +def handle(config, options, args): + if len(args) == 0: + if options.gui is None and os.environ.get('DISPLAY', None): + options.gui = True + if options.gui: + from zeroinstall import helpers + return helpers.get_selections_gui(None, []) + else: + for key, setting_type in settings.iteritems(): + value = getattr(config, key) + print key, "=", setting_type.format(value) + return + elif len(args) > 2: + raise UsageError() + + option = args[0] + if option not in settings: + raise SafeException(_('Unknown option "%s"') % option) + + if len(args) == 1: + value = getattr(config, option) + print settings[option].format(value) + else: + value = settings[option].parse(args[1]) + if option == 'network_use' and value not in model.network_levels: + raise SafeException(_("Must be one of %s") % list(model.network_levels)) + setattr(config, option, value) + + config.save_globals() diff --git a/zeroinstall/cmd/download.py b/zeroinstall/cmd/download.py index 4685688..6aa8454 100644 --- a/zeroinstall/cmd/download.py +++ b/zeroinstall/cmd/download.py @@ -12,7 +12,6 @@ import logging from zeroinstall import cmd, SafeException, _ from zeroinstall.cmd import UsageError, select from zeroinstall.injector import model, autopolicy, selections, handler -from zeroinstall.injector.iface_cache import iface_cache syntax = "URI" @@ -20,12 +19,12 @@ def add_options(parser): select.add_options(parser) parser.add_option("", "--show", help=_("show where components are installed"), action='store_true') -def handle(options, args): +def handle(config, options, args): if len(args) != 1: raise UsageError() iface_uri = model.canonical_iface_uri(args[0]) - sels = select.get_selections(options, iface_uri, + sels = select.get_selections(config, options, iface_uri, select_only = False, download_only = True, test_callback = None) if not sels: sys.exit(1) # Aborted by user @@ -33,4 +32,4 @@ def handle(options, args): if options.xml: select.show_xml(sels) if options.show: - select.show_human(sels) + select.show_human(sels, config.stores) diff --git a/zeroinstall/cmd/import.py b/zeroinstall/cmd/import.py index d97f94f..9d2758f 100644 --- a/zeroinstall/cmd/import.py +++ b/zeroinstall/cmd/import.py @@ -10,8 +10,8 @@ import logging from zeroinstall import cmd, SafeException, _ from zeroinstall.cmd import UsageError -from zeroinstall.injector import model, autopolicy, selections, handler, gpg, fetch -from zeroinstall.injector.iface_cache import iface_cache, PendingFeed +from zeroinstall.injector import model, autopolicy, selections, gpg, fetch +from zeroinstall.injector.iface_cache import PendingFeed from zeroinstall.support import tasks from xml.dom import minidom @@ -20,8 +20,11 @@ syntax = "FEED" def add_options(parser): pass -def handle(options, args): - h = handler.Handler() +def handle(config, options, args): + if not args: + raise UsageError() + + h = config.handler for x in args: if not os.path.isfile(x): @@ -42,13 +45,13 @@ def handle(options, args): keys_downloaded = tasks.Task(pending.download_keys(h), "download keys") yield keys_downloaded.finished tasks.check(keys_downloaded.finished) - if not iface_cache.update_feed_if_trusted(uri, pending.sigs, pending.new_xml): + if not config.iface_cache.update_feed_if_trusted(uri, pending.sigs, pending.new_xml): fetcher = fetch.Fetcher(h) blocker = h.confirm_keys(pending, fetcher.fetch_key_info) if blocker: yield blocker tasks.check(blocker) - if not iface_cache.update_feed_if_trusted(uri, pending.sigs, pending.new_xml): + if not config.iface_cache.update_feed_if_trusted(uri, pending.sigs, pending.new_xml): raise SafeException(_("No signing keys trusted; not importing")) task = tasks.Task(run(), "import feed") diff --git a/zeroinstall/cmd/list.py b/zeroinstall/cmd/list.py index 2bfd3d9..6d3a970 100644 --- a/zeroinstall/cmd/list.py +++ b/zeroinstall/cmd/list.py @@ -6,19 +6,18 @@ The B{0install list} command-line interface. # See the README file for details, or visit http://0install.net. from zeroinstall.cmd import UsageError -from zeroinstall.injector.iface_cache import iface_cache syntax = "PATTERN" def add_options(parser): pass -def handle(options, args): +def handle(config, options, args): if len(args) == 0: - matches = iface_cache.list_all_interfaces() + matches = config.iface_cache.list_all_interfaces() elif len(args) == 1: match = args[0].lower() - matches = [i for i in iface_cache.list_all_interfaces() if match in i.lower()] + matches = [i for i in config.iface_cache.list_all_interfaces() if match in i.lower()] else: raise UsageError() diff --git a/zeroinstall/cmd/remove_feed.py b/zeroinstall/cmd/remove_feed.py index f36efa5..12b1c53 100644 --- a/zeroinstall/cmd/remove_feed.py +++ b/zeroinstall/cmd/remove_feed.py @@ -11,5 +11,5 @@ from zeroinstall.cmd import add_feed add_options = add_feed.add_options -def handle(options, args): - return add_feed.handle(options, args, add_ok = False, remove_ok = True) +def handle(config, options, args): + return add_feed.handle(config, options, args, add_ok = False, remove_ok = True) diff --git a/zeroinstall/cmd/run.py b/zeroinstall/cmd/run.py index 50bd7b0..20fc3ec 100644 --- a/zeroinstall/cmd/run.py +++ b/zeroinstall/cmd/run.py @@ -12,7 +12,6 @@ import logging from zeroinstall import cmd, SafeException, _ from zeroinstall.cmd import UsageError, select from zeroinstall.injector import model, autopolicy, selections, handler -from zeroinstall.injector.iface_cache import iface_cache syntax = "URI [ARGS]" @@ -22,7 +21,7 @@ def add_options(parser): parser.add_option("-w", "--wrapper", help=_("execute program using a debugger, etc"), metavar='COMMAND') parser.disable_interspersed_args() -def handle(options, args): +def handle(config, options, args): if len(args) < 1: raise UsageError() iface_uri = model.canonical_iface_uri(args[0]) @@ -34,11 +33,11 @@ def handle(options, args): False, # dry-run options.main) - sels = select.get_selections(options, iface_uri, + sels = select.get_selections(config, options, iface_uri, select_only = False, download_only = False, test_callback = test_callback) if not sels: sys.exit(1) # Aborted by user from zeroinstall.injector import run - run.execute_selections(sels, prog_args, dry_run = options.dry_run, main = options.main, wrapper = options.wrapper) + run.execute_selections(sels, prog_args, dry_run = options.dry_run, main = options.main, wrapper = options.wrapper, stores = config.stores) diff --git a/zeroinstall/cmd/select.py b/zeroinstall/cmd/select.py index 42e7e71..3b51f0a 100644 --- a/zeroinstall/cmd/select.py +++ b/zeroinstall/cmd/select.py @@ -11,8 +11,8 @@ import logging from zeroinstall import cmd, SafeException, _ from zeroinstall.cmd import UsageError -from zeroinstall.injector import model, autopolicy, selections, handler -from zeroinstall.injector.iface_cache import iface_cache +from zeroinstall.injector import model, selections, handler +from zeroinstall.injector.policy import Policy syntax = "URI" @@ -33,15 +33,7 @@ def add_options(parser): add_generic_select_options(parser) parser.add_option("", "--xml", help=_("write selected versions as XML"), action='store_true') -def _download_missing_selections(h, sels): - from zeroinstall.injector import fetch - fetcher = fetch.Fetcher(h) - blocker = sels.download_missing(iface_cache, fetcher) - if blocker: - logging.info(_("Waiting for selected implementations to be downloaded...")) - h.wait_for_blocker(blocker) - -def get_selections(options, iface_uri, select_only, download_only, test_callback): +def get_selections(config, options, iface_uri, select_only, download_only, test_callback): """Get selections for iface_uri, according to the options passed. Will switch to GUI mode if necessary. @param options: options from OptionParser @@ -51,32 +43,32 @@ def get_selections(options, iface_uri, select_only, download_only, test_callback @return: the selected versions, or None if the user cancels @rtype: L{selections.Selections} | None """ - if os.isatty(1): - h = handler.ConsoleHandler() - else: - h = handler.Handler() - h.dry_run = bool(options.dry_run) - # Try to load it as a feed. If it is a feed, it'll get cached. If not, it's a # selections document and we return immediately. - maybe_selections = iface_cache.get_feed(iface_uri, selections_ok = True) + maybe_selections = config.iface_cache.get_feed(iface_uri, selections_ok = True) if isinstance(maybe_selections, selections.Selections): if not select_only: + from zeroinstall.injector import fetch + fetcher = fetch.Fetcher(h) + blocker = maybe_selections.download_missing(config.iface_cache, fetcher) + if blocker: + logging.info(_("Waiting for selected implementations to be downloaded...")) + h.wait_for_blocker(blocker) _download_missing_selections(h, maybe_selections) return maybe_selections - root_iface = iface_cache.get_interface(iface_uri) + root_iface = config.iface_cache.get_interface(iface_uri) command_name = options.command if command_name is None: command_name = 'run' elif command_name == '': command_name = None - policy = autopolicy.AutoPolicy(iface_uri, - handler = h, - download_only = True, # unused? - src = options.source, - command = command_name) + policy = Policy(iface_uri, + handler = None, # (use config instead) + src = options.source, + command = command_name, + config = config) if options.before or options.not_before: policy.solver.extra_restrictions[root_iface] = [ @@ -104,7 +96,7 @@ def get_selections(options, iface_uri, select_only, download_only, test_callback stale_feeds = [feed for feed in policy.solver.feeds_used if not feed.startswith('distribution:') and # Ignore (memory-only) PackageKit feeds - policy.is_stale(iface_cache.get_feed(feed))] + policy.is_stale(config.iface_cache.get_feed(feed))] if download_only and stale_feeds: can_run_immediately = False @@ -180,12 +172,12 @@ def get_selections(options, iface_uri, select_only, download_only, test_callback return sels -def handle(options, args): +def handle(config, options, args): if len(args) != 1: raise UsageError() iface_uri = model.canonical_iface_uri(args[0]) - sels = get_selections(options, iface_uri, + sels = get_selections(config, options, iface_uri, select_only = True, download_only = False, test_callback = None) if not sels: sys.exit(1) # Aborted by user @@ -193,14 +185,14 @@ def handle(options, args): if options.xml: show_xml(sels) else: - show_human(sels) + show_human(sels, config.stores) def show_xml(sels): doc = sels.toDOM() doc.writexml(sys.stdout) sys.stdout.write('\n') -def show_human(sels): +def show_human(sels, stores): from zeroinstall import zerostore done = set() # detect cycles def print_node(uri, command, indent): @@ -214,7 +206,7 @@ def show_human(sels): if impl.id.startswith('package:'): path = "(" + impl.id + ")" else: - path = impl.local_path or iface_cache.stores.lookup_any(impl.digests) + path = impl.local_path or stores.lookup_any(impl.digests) except zerostore.NotStored: path = "(not cached)" print indent + " Path:", path diff --git a/zeroinstall/cmd/update.py b/zeroinstall/cmd/update.py index a5181c6..a0a3eaf 100644 --- a/zeroinstall/cmd/update.py +++ b/zeroinstall/cmd/update.py @@ -17,7 +17,7 @@ syntax = "URI" add_options = select.add_generic_select_options -def handle(options, args): +def handle(config, options, args): if len(args) != 1: raise UsageError() @@ -33,7 +33,7 @@ def handle(options, args): options.refresh = False try: - old_sels = select.get_selections(options, iface_uri, + old_sels = select.get_selections(config, options, iface_uri, select_only = True, download_only = False, test_callback = None) except SafeException, ex: old_selections = {} @@ -48,7 +48,7 @@ def handle(options, args): options.gui = old_gui options.refresh = True - sels = select.get_selections(options, iface_uri, + sels = select.get_selections(config, options, iface_uri, select_only = False, download_only = True, test_callback = None) if not sels: sys.exit(1) # Aborted by user diff --git a/zeroinstall/injector/cli.py b/zeroinstall/injector/cli.py index 37ed632..ba573b0 100644 --- a/zeroinstall/injector/cli.py +++ b/zeroinstall/injector/cli.py @@ -11,8 +11,7 @@ from optparse import OptionParser import logging from zeroinstall import SafeException, NeedDownload -from zeroinstall.injector import model, autopolicy, selections -from zeroinstall.injector.iface_cache import iface_cache +from zeroinstall.injector import model, policy, autopolicy, selections from zeroinstall.cmd import UsageError #def program_log(msg): os.access('MARK: 0launch: ' + msg, os.F_OK) @@ -81,11 +80,13 @@ def main(command_args): if options.select_only or options.show: options.download_only = True + config = policy.load_config() + if options.with_store: from zeroinstall import zerostore for x in options.with_store: - iface_cache.stores.stores.append(zerostore.Store(os.path.abspath(x))) - logging.info(_("Stores search path is now %s"), iface_cache.stores.stores) + config.stores.stores.append(zerostore.Store(os.path.abspath(x))) + logging.info(_("Stores search path is now %s"), config.stores.stores) if options.set_selections: args = [options.set_selections] + args diff --git a/zeroinstall/injector/iface_cache.py b/zeroinstall/injector/iface_cache.py index fbfb61a..b519bc8 100644 --- a/zeroinstall/injector/iface_cache.py +++ b/zeroinstall/injector/iface_cache.py @@ -180,7 +180,7 @@ class IfaceCache(object): @see: L{iface_cache} - the singleton IfaceCache instance. """ - __slots__ = ['_interfaces', 'stores', '_feeds', '_distro'] + __slots__ = ['_interfaces', '_feeds', '_distro', '_config'] def __init__(self, distro = None): """@param distro: distribution used to fetch "distribution:" feeds (since 0.49) @@ -189,18 +189,22 @@ class IfaceCache(object): """ self._interfaces = {} self._feeds = {} - - self.stores = zerostore.Stores() - self._distro = distro @property + def stores(self): + """deprecated""" + from zeroinstall.injector import policy + raise Exception("iface_cache.stores") + return policy.get_deprecated_singleton_config().stores + + @property def distro(self): if self._distro is None: from zeroinstall.injector.distro import get_host_distribution self._distro = get_host_distribution() return self._distro - + def update_interface_if_trusted(self, interface, sigs, xml): import warnings warnings.warn("Use update_feed_if_trusted instead", DeprecationWarning, stacklevel = 2) @@ -223,7 +227,7 @@ class IfaceCache(object): import trust updated = self._oldest_trusted(sigs, trust.domain_from_url(feed_url)) if updated is None: return False # None are trusted - + self.update_feed_from_network(feed_url, xml, updated) return True @@ -414,7 +418,7 @@ class IfaceCache(object): except SafeException, ex: debug(_("No signatures (old-style interface): %s") % ex) return None - + def _get_signature_date(self, uri): """Read the date-stamp from the signature of the cached interface. If the date-stamp is unavailable, returns None.""" @@ -422,7 +426,7 @@ class IfaceCache(object): sigs = self.get_cached_signatures(uri) if sigs: return self._oldest_trusted(sigs, trust.domain_from_url(uri)) - + def _oldest_trusted(self, sigs, domain): """Return the date of the oldest trusted signature in the list, or None if there are no trusted sigs in the list.""" @@ -495,4 +499,4 @@ class IfaceCache(object): impls += feed.implementations.values() return impls -iface_cache = IfaceCache() +#iface_cache = IfaceCache() diff --git a/zeroinstall/injector/model.py b/zeroinstall/injector/model.py index b5f4966..e096b58 100644 --- a/zeroinstall/injector/model.py +++ b/zeroinstall/injector/model.py @@ -635,10 +635,9 @@ class Interface(object): self.stability_policy = None def get_name(self): - from zeroinstall.injector.iface_cache import iface_cache - feed = iface_cache.get_feed(self.uri) - if feed: - return feed.get_name() + #feed = self._main_feed + #if feed: + # return feed.get_name() return '(' + os.path.basename(self.uri) + ')' def __repr__(self): @@ -664,7 +663,8 @@ class Interface(object): def _main_feed(self): #import warnings #warnings.warn("use the feed instead", DeprecationWarning, 3) - from zeroinstall.injector.iface_cache import iface_cache + from zeroinstall.injector import policy + iface_cache = policy.get_deprecated_singleton_config().iface_cache feed = iface_cache.get_feed(self.uri) if feed is None: return _dummy_feed diff --git a/zeroinstall/injector/policy.py b/zeroinstall/injector/policy.py index 55653c6..fcf5252 100644 --- a/zeroinstall/injector/policy.py +++ b/zeroinstall/injector/policy.py @@ -13,31 +13,83 @@ import os from logging import info, debug, warn import ConfigParser -from zeroinstall import SafeException +from zeroinstall import zerostore, SafeException from zeroinstall.injector import arch from zeroinstall.injector.model import Interface, Implementation, network_levels, network_offline, DistributionImplementation, network_full from zeroinstall.injector.namespaces import config_site, config_prog from zeroinstall.support import tasks, basedir -from zeroinstall.injector.iface_cache import iface_cache # If we started a check within this period, don't start another one: FAILED_CHECK_DELAY = 60 * 60 # 1 Hour -def load_config(): - config = ConfigParser.RawConfigParser() - config.add_section('global') - config.set('global', 'help_with_testing', 'False') - config.set('global', 'freshness', str(60 * 60 * 24 * 30)) # One month - config.set('global', 'network_use', 'full') +class Config(object): + """ + @ivar handler: handler for main-loop integration + @type handler: L{handler.Handler} + """ + + __slots__ = ['help_with_testing', 'freshness', 'network_use', '_fetcher', '_stores', '_iface_cache', 'handler'] + def __init__(self, handler): + self.help_with_testing = False + self.freshness = 60 * 60 * 24 * 30 + self.network_use = network_full + self.handler = handler + self._fetcher = self._stores = self._iface_cache = None + + @property + def stores(self): + if not self._stores: + self._stores = zerostore.Stores() + return self._stores + + @property + def iface_cache(self): + if not self._iface_cache: + from zeroinstall.injector import iface_cache + self._iface_cache = iface_cache.IfaceCache() + return self._iface_cache + + @property + def fetcher(self): + if not self._fetcher: + from zeroinstall.injector import fetch + self._fetcher = fetch.Fetcher(self.handler) + return self._fetcher + + def save_globals(self): + """Write global settings.""" + parser = ConfigParser.ConfigParser() + parser.add_section('global') + + parser.set('global', 'help_with_testing', self.help_with_testing) + parser.set('global', 'network_use', self.network_use) + parser.set('global', 'freshness', self.freshness) + + path = basedir.save_config_path(config_site, config_prog) + path = os.path.join(path, 'global') + parser.write(file(path + '.new', 'w')) + os.rename(path + '.new', path) + +def load_config(handler): + config = Config(handler) + parser = ConfigParser.RawConfigParser() + parser.add_section('global') + parser.set('global', 'help_with_testing', 'False') + parser.set('global', 'freshness', str(60 * 60 * 24 * 30)) # One month + parser.set('global', 'network_use', 'full') path = basedir.load_first_config(config_site, config_prog, 'global') if path: info("Loading configuration from %s", path) try: - config.read(path) + parser.read(path) except Exception, ex: warn(_("Error loading config: %s"), str(ex) or repr(ex)) + config.help_with_testing = parser.getboolean('global', 'help_with_testing') + config.network_use = parser.get('global', 'network_use') + config.freshness = int(parser.get('global', 'freshness')) + return config class Policy(object): @@ -60,25 +112,23 @@ class Policy(object): @ivar network_use: one of the model.network_* values @ivar freshness: seconds allowed since last update @type freshness: int - @ivar handler: handler for main-loop integration - @type handler: L{handler.Handler} @ivar src: whether we are looking for source code @type src: bool @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time @type stale_feeds: set """ __slots__ = ['root', 'watchers', 'command', 'config', - 'handler', '_warned_offline', + '_warned_offline', 'target_arch', 'src', 'stale_feeds', 'solver', '_fetcher'] - - help_with_testing = property(lambda self: self.config.getboolean('global', 'help_with_testing'), - lambda self, value: self.config.set('global', 'help_with_testing', bool(value))) - network_use = property(lambda self: self.config.get('global', 'network_use'), - lambda self, value: self.config.set('global', 'network_use', value)) + help_with_testing = property(lambda self: self.config.help_with_testing, + lambda self, value: setattr(self.config, 'help_with_testing', bool(value))) + + network_use = property(lambda self: self.config.network_use, + lambda self, value: setattr(self.config, 'network_use', value)) - freshness = property(lambda self: int(self.config.get('global', 'freshness')), - lambda self, value: self.config.set('global', 'freshness', str(value))) + freshness = property(lambda self: self.config.freshness, + lambda self, value: setattr(self.config, 'freshness', str(value))) implementation = property(lambda self: self.solver.selections) @@ -107,21 +157,19 @@ class Policy(object): self.command = command if config is None: - self.config = load_config() + self.config = load_config(handler) else: + assert handler is None, "can't pass a handler and a config" self.config = config from zeroinstall.injector.solver import DefaultSolver - self.solver = DefaultSolver(self.config, iface_cache, iface_cache.stores) + self.solver = DefaultSolver(self.config) # If we need to download something but can't because we are offline, # warn the user. But only the first time. self._warned_offline = False self._fetcher = None - # (allow self for backwards compat) - self.handler = handler or self - debug(_("Supported systems: '%s'"), arch.os_ranks) debug(_("Supported processors: '%s'"), arch.machine_ranks) @@ -132,11 +180,12 @@ class Policy(object): @property def fetcher(self): - if not self._fetcher: - import fetch - self._fetcher = fetch.Fetcher(self.handler) - return self._fetcher - + return self.config.fetcher + + @property + def handler(self): + return self.config.handler + def set_root(self, root): """Change the root interface URI.""" assert isinstance(root, (str, unicode)) @@ -144,12 +193,8 @@ class Policy(object): for w in self.watchers: w() def save_config(self): - """Write global settings.""" - path = basedir.save_config_path(config_site, config_prog) - path = os.path.join(path, 'global') - self.config.write(file(path + '.new', 'w')) - os.rename(path + '.new', path) - + self.config.save_globals() + def recalculate(self, fetch_stale_interfaces = True): """@deprecated: see L{solve_with_downloads} """ import warnings @@ -168,19 +213,19 @@ class Policy(object): blockers = [] for f in self.solver.feeds_used: if os.path.isabs(f): continue - feed = iface_cache.get_feed(f) + feed = self.config.iface_cache.get_feed(f) if feed is None or feed.last_modified is None: self.download_and_import_feed_if_online(f) # Will start a download elif self.is_stale(feed): debug(_("Adding %s to stale set"), f) - self.stale_feeds.add(iface_cache.get_interface(f)) # Legacy API + self.stale_feeds.add(self.config.iface_cache.get_interface(f)) # Legacy API if fetch_stale_interfaces: self.download_and_import_feed_if_online(f) # Will start a download for w in self.watchers: w() return blockers - + def usable_feeds(self, iface): """Generator for C{iface.feeds} that are valid for our architecture. @rtype: generator @@ -190,8 +235,8 @@ class Policy(object): machine_ranks = {'src': 1} else: machine_ranks = arch.machine_ranks - - for f in iface_cache.get_feed_imports(iface): + + for f in self.config.iface_cache.get_feed_imports(iface): if f.os in arch.os_ranks and f.machine in machine_ranks: yield f else: @@ -215,18 +260,18 @@ class Policy(object): if self.freshness <= 0 or staleness < self.freshness: return False # Fresh enough for us - last_check_attempt = iface_cache.get_last_check_attempt(feed.url) + last_check_attempt = self.config.iface_cache.get_last_check_attempt(feed.url) if last_check_attempt and last_check_attempt > now - FAILED_CHECK_DELAY: debug(_("Stale, but tried to check recently (%s) so not rechecking now."), time.ctime(last_check_attempt)) return False return True - + def download_and_import_feed_if_online(self, feed_url): """If we're online, call L{fetch.Fetcher.download_and_import_feed}. Otherwise, log a suitable warning.""" if self.network_use != network_offline: debug(_("Feed %s not cached and not off-line. Downloading..."), feed_url) - return self.fetcher.download_and_import_feed(feed_url, iface_cache) + return self.fetcher.download_and_import_feed(feed_url, self.config.iface_cache) else: if self._warned_offline: debug(_("Not downloading feed '%s' because we are off-line."), feed_url) @@ -239,7 +284,7 @@ class Policy(object): @rtype: str @raise zeroinstall.zerostore.NotStored: if it needs to be added to the cache first.""" assert isinstance(impl, Implementation) - return impl.local_path or iface_cache.stores.lookup_any(impl.digests) + return impl.local_path or self.config.stores.lookup_any(impl.digests) def get_implementation(self, interface): """Get the chosen implementation. @@ -271,7 +316,7 @@ class Policy(object): except: pass # OK return False - + def get_uncached_implementations(self): """List all chosen implementations which aren't yet available locally. @rtype: [(L{model.Interface}, L{model.Implementation})]""" @@ -282,19 +327,19 @@ class Policy(object): if not self.get_cached(impl): uncached.append((iface, impl)) return uncached - + def refresh_all(self, force = True): """Start downloading all feeds for all selected interfaces. @param force: Whether to restart existing downloads.""" return self.solve_with_downloads(force = True) - + def get_feed_targets(self, feed_iface_uri): """Return a list of Interfaces for which feed_iface can be a feed. This is used by B{0launch --feed}. @rtype: [model.Interface] @raise SafeException: If there are no known feeds.""" # TODO: what if it isn't cached yet? - feed_iface = iface_cache.get_interface(feed_iface_uri) + feed_iface = self.config.iface_cache.get_interface(feed_iface_uri) if not feed_iface.feed_for: if not feed_iface.name: raise SafeException(_("Can't get feed targets for '%s'; failed to load it.") % @@ -305,8 +350,8 @@ class Policy(object): debug(_("Feed targets: %s"), feed_targets) if not feed_iface.name: warn(_("Warning: unknown interface '%s'") % feed_iface_uri) - return [iface_cache.get_interface(uri) for uri in feed_targets] - + return [self.config.iface_cache.get_interface(uri) for uri in feed_targets] + @tasks.async def solve_with_downloads(self, force = False, update_local = False): """Run the solver, then download any feeds that are missing or @@ -314,7 +359,7 @@ class Policy(object): the cache, the solver is run again, possibly adding new downloads. @param force: whether to download even if we're already ready to run. @param update_local: fetch PackageKit feeds even if we're ready to run.""" - + downloads_finished = set() # Successful or otherwise downloads_in_progress = {} # URL -> Download @@ -349,14 +394,14 @@ class Policy(object): continue if os.path.isabs(f): if force: - iface_cache.get_feed(f, force = True) + self.config.iface_cache.get_feed(f, force = True) downloads_in_progress[f] = tasks.IdleBlocker('Refresh local feed') continue elif f.startswith('distribution:'): if force or update_local: - downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, iface_cache) + downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, self.config.iface_cache) elif force and self.network_use != network_offline: - downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, iface_cache) + downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, self.config.iface_cache) # Once we've starting downloading some things, # we might as well get them all. force = True @@ -415,17 +460,17 @@ class Policy(object): if not self.solver.ready: return True # Maybe a newer version will work? - + if self.get_uncached_implementations(): return True return False - + def download_uncached_implementations(self): """Download all implementations chosen by the solver that are missing from the cache.""" assert self.solver.ready, "Solver is not ready!\n%s" % self.solver.selections return self.fetcher.download_impls([impl for impl in self.solver.selections.values() if not self.get_cached(impl)], - iface_cache.stores) + self.config.stores) def download_icon(self, interface, force = False): """Download an icon for this interface and add it to the @@ -438,7 +483,7 @@ class Policy(object): modification_time = None - existing_icon = iface_cache.get_icon_path(interface) + existing_icon = self.config.iface_cache.get_icon_path(interface) if existing_icon: file_mtime = os.stat(existing_icon).st_mtime from email.utils import formatdate @@ -450,4 +495,12 @@ class Policy(object): """@deprecated: use L{iface_cache.IfaceCache.get_interface} instead""" import warnings warnings.warn("Policy.get_interface is deprecated!", DeprecationWarning, stacklevel = 2) - return iface_cache.get_interface(uri) + return self.config.iface_cache.get_interface(uri) + +_config = None +def get_deprecated_singleton_config(): + global _config + if _config is None: + from zeroinstall.injector import handler + _config = load_config(handler.Handler()) + return _config diff --git a/zeroinstall/injector/reader.py b/zeroinstall/injector/reader.py index 01c0af8..e009231 100644 --- a/zeroinstall/injector/reader.py +++ b/zeroinstall/injector/reader.py @@ -15,7 +15,7 @@ from zeroinstall.injector.namespaces import config_site, config_prog, XMLNS_IFAC from zeroinstall.injector.model import Interface, InvalidInterface, ZeroInstallFeed, escape, Feed, stability_levels from zeroinstall.injector import model -def update_from_cache(interface): +def update_from_cache(interface, iface_cache = None): """Read a cached interface and any native feeds or user overrides. @param interface: the interface object to update @type interface: L{model.Interface} @@ -25,7 +25,9 @@ def update_from_cache(interface): Internal: use L{iface_cache.IfaceCache.get_interface} instread. @rtype: bool""" interface.reset() - from zeroinstall.injector.iface_cache import iface_cache + if iface_cache is None: + from zeroinstall.injector import policy + iface_cache = policy.get_deprecated_singleton_config().iface_cache # Add the distribution package manager's version, if any path = basedir.load_first_data(config_site, 'native_feeds', model._pretty_escape(interface.uri)) @@ -182,7 +184,8 @@ def update(interface, source, local = False): {'feed_url': feed.url, 'interface_uri': interface.uri}) # Hack. - from zeroinstall.injector.iface_cache import iface_cache + from zeroinstall.injector import policy + iface_cache = policy.get_deprecated_singleton_config().iface_cache iface_cache._feeds[unicode(interface.uri)] = feed return feed diff --git a/zeroinstall/injector/run.py b/zeroinstall/injector/run.py index 22ffde4..d49a49e 100644 --- a/zeroinstall/injector/run.py +++ b/zeroinstall/injector/run.py @@ -12,7 +12,6 @@ from string import Template from zeroinstall.injector.model import SafeException, EnvironmentBinding, Command from zeroinstall.injector import namespaces, qdom -from zeroinstall.injector.iface_cache import iface_cache def do_env_binding(binding, path): """Update this process's environment by applying the binding. @@ -41,14 +40,6 @@ def execute(policy, prog_args, dry_run = False, main = None, wrapper = None): """ execute_selections(policy.solver.selections, prog_args, dry_run, main, wrapper) -def _do_bindings(impl, bindings): - for b in bindings: - if isinstance(b, EnvironmentBinding): - do_env_binding(b, _get_implementation_path(impl)) - -def _get_implementation_path(impl): - return impl.local_path or iface_cache.stores.lookup_any(impl.digests) - def test_selections(selections, prog_args, dry_run, main, wrapper = None): """Run the program in a child process, collecting stdout and stderr. @return: the output produced by the process @@ -94,7 +85,7 @@ def _process_args(args, element): if child.uri == namespaces.XMLNS_IFACE and child.name == 'arg': args.append(Template(child.content).substitute(os.environ)) -def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None): +def execute_selections(selections, prog_args, dry_run = False, main = None, wrapper = None, stores = None): """Execute program. On success, doesn't return. On failure, raises an Exception. Returns normally only for a successful dry run. @param selections: the selected versions @@ -110,6 +101,19 @@ def execute_selections(selections, prog_args, dry_run = False, main = None, wrap @since: 0.27 @precondition: All implementations are in the cache. """ + assert stores is not None + if stores is None: + from zeroinstall import zerostore + stores = zerostore.Stores() + + def _do_bindings(impl, bindings): + for b in bindings: + if isinstance(b, EnvironmentBinding): + do_env_binding(b, _get_implementation_path(impl)) + + def _get_implementation_path(impl): + return impl.local_path or stores.lookup_any(impl.digests) + commands = selections.commands sels = selections.selections for selection in sels.values(): diff --git a/zeroinstall/injector/selections.py b/zeroinstall/injector/selections.py index faeba77..e748cc8 100644 --- a/zeroinstall/injector/selections.py +++ b/zeroinstall/injector/selections.py @@ -303,19 +303,22 @@ class Selections(object): def iteritems(self): # Deprecated - from zeroinstall.injector.iface_cache import iface_cache + from zeroinstall.injector import policy + iface_cache = policy.get_deprecated_singleton_config().iface_cache for (uri, sel) in self.selections.iteritems(): yield (iface_cache.get_interface(uri), sel and sel.impl) def values(self): # Deprecated - from zeroinstall.injector.iface_cache import iface_cache + from zeroinstall.injector import policy + iface_cache = policy.get_deprecated_singleton_config().iface_cache for (uri, sel) in self.selections.iteritems(): yield sel and sel.impl def __iter__(self): # Deprecated - from zeroinstall.injector.iface_cache import iface_cache + from zeroinstall.injector import policy + iface_cache = policy.get_deprecated_singleton_config().iface_cache for (uri, sel) in self.selections.iteritems(): yield iface_cache.get_interface(uri) diff --git a/zeroinstall/injector/solver.py b/zeroinstall/injector/solver.py index 21d5369..ad58b8e 100644 --- a/zeroinstall/injector/solver.py +++ b/zeroinstall/injector/solver.py @@ -119,39 +119,27 @@ class Solver(object): raise NotImplementedError("Abstract") class SATSolver(Solver): - __slots__ = ['_failure_reason', 'config', 'iface_cache', 'stores', 'extra_restrictions', 'langs'] + __slots__ = ['_failure_reason', 'config', 'extra_restrictions', 'langs'] """Converts the problem to a set of pseudo-boolean constraints and uses a PB solver to solve them. @ivar langs: the preferred languages (e.g. ["es_ES", "en"]). Initialised to the current locale. @type langs: str""" - def __init__(self, config, iface_cache, stores, extra_restrictions = None): + def __init__(self, config, extra_restrictions = None): """ @param network_use: how much use to make of the network @type network_use: L{model.network_levels} - @param iface_cache: a cache of feeds containing information about available versions - @type iface_cache: L{iface_cache.IfaceCache} - @param stores: a cached of implementations (affects choice when offline or when minimising network use) - @type stores: L{zerostore.Stores} + @param config: policy preferences (e.g. stability), the iface_cache and the stores to use + @type config: L{policy.Config} @param extra_restrictions: extra restrictions on the chosen implementations @type extra_restrictions: {L{model.Interface}: [L{model.Restriction}]} """ Solver.__init__(self) assert not isinstance(config, str), "API change!" self.config = config - self.iface_cache = iface_cache - self.stores = stores self.extra_restrictions = extra_restrictions or {} self.langs = [locale.getlocale()[0] or 'en'] - @property - def network_use(self): - return self.config.get('global', 'network_use') - - @property - def help_with_testing(self): - return self.config.getboolean('global', 'help_with_testing') - def compare(self, interface, b, a, arch): """Compare a and b to see which would be chosen first. Does not consider whether the implementations are usable (check for that yourself first). @@ -174,8 +162,8 @@ class SATSolver(Solver): r = cmp(a_stab == model.preferred, b_stab == model.preferred) if r: return r - stores = self.stores - if self.network_use != model.network_full: + stores = self.config.stores + if self.config.network_use != model.network_full: r = cmp(_get_cached(stores, a), _get_cached(stores, b)) if r: return r @@ -186,7 +174,7 @@ class SATSolver(Solver): # Stability stab_policy = interface.stability_policy if not stab_policy: - if self.help_with_testing: stab_policy = model.testing + if self.config.help_with_testing: stab_policy = model.testing else: stab_policy = model.stable if a_stab >= stab_policy: a_stab = model.preferred @@ -223,7 +211,7 @@ class SATSolver(Solver): if r: return r # Slightly prefer cached versions - if self.network_use == model.network_full: + if self.config.network_use == model.network_full: r = cmp(_get_cached(stores, a), _get_cached(stores, b)) if r: return r @@ -239,6 +227,7 @@ class SATSolver(Solver): # this is probably too much. We could insert a dummy optimial # implementation in stale/uncached feeds and see whether it # selects that. + iface_cache = self.config.iface_cache feeds_added = set() problem = sat.SATProblem() @@ -277,7 +266,7 @@ class SATSolver(Solver): # matching 'dependency' must also be selected. # Must have already done add_iface on dependency.interface. def find_dependency_candidates(requiring_impl_var, dependency): - dep_iface = self.iface_cache.get_interface(dependency.interface) + dep_iface = iface_cache.get_interface(dependency.interface) dep_union = [sat.neg(requiring_impl_var)] # Either requiring_impl_var is False, or ... for candidate in impls_for_iface[dep_iface]: for r in dependency.restrictions: @@ -313,7 +302,7 @@ class SATSolver(Solver): stability = impl.get_stability() if stability <= model.buggy: return stability.name - if (self.network_use == model.network_offline or not impl.download_sources) and not _get_cached(self.stores, impl): + if (self.config.network_use == model.network_offline or not impl.download_sources) and not _get_cached(self.config.stores, impl): if not impl.download_sources: return _("No retrieval methods") return _("Not cached and we are off-line") @@ -330,7 +319,7 @@ class SATSolver(Solver): @rtype: generator(ZeroInstallFeed)""" yield iface.uri - for f in self.iface_cache.get_feed_imports(iface): + for f in iface_cache.get_feed_imports(iface): # Note: when searching for src, None is not in machine_ranks if f.os in arch.os_ranks and \ (f.machine is None or f.machine in arch.machine_ranks): @@ -345,7 +334,7 @@ class SATSolver(Solver): ifaces_processed.add(uri) iface_name = 'i%d' % len(ifaces_processed) - iface = self.iface_cache.get_interface(uri) + iface = iface_cache.get_interface(uri) impls = [] for f in usable_feeds(iface, arch): @@ -353,7 +342,7 @@ class SATSolver(Solver): debug(_("Processing feed %s"), f) try: - feed = self.iface_cache.get_feed(f) + feed = iface_cache.get_feed(f) if feed is None: continue #if feed.name and iface.uri != feed.url and iface.uri not in feed.feed_for: # info(_("Missing for '%(uri)s' in '%(feed)s'"), {'uri': iface.uri, 'feed': f}) @@ -364,11 +353,12 @@ class SATSolver(Solver): distro_feed_url = feed.get_distro_feed() if distro_feed_url: self.feeds_used.add(distro_feed_url) - distro_feed = self.iface_cache.get_feed(distro_feed_url) + distro_feed = iface_cache.get_feed(distro_feed_url) if distro_feed.implementations: impls.extend(distro_feed.implementations.values()) except Exception, ex: warn(_("Failed to load feed %(feed)s for %(interface)s: %(exception)s"), {'feed': f, 'interface': iface, 'exception': ex}) + raise impls.sort(lambda a, b: self.compare(iface, a, b, arch)) @@ -440,7 +430,7 @@ class SATSolver(Solver): # the commands too. add_iface(uri, arch) - iface = self.iface_cache.get_interface(uri) + iface = iface_cache.get_interface(uri) filtered_impls = impls_for_iface[iface] var_names = [] @@ -505,7 +495,7 @@ class SATSolver(Solver): # (note: might be because we haven't cached it yet) info("No %s in %s", command_name, root_interface) - impls = impls_for_iface[self.iface_cache.get_interface(root_interface)] + impls = impls_for_iface[iface_cache.get_interface(root_interface)] if impls == [] or (len(impls) == 1 and isinstance(impls[0], _DummyImpl)): # There were no candidates at all. self._failure_reason = _("Interface '%s' has no usable implementations") % root_interface diff --git a/zeroinstall/injector/writer.py b/zeroinstall/injector/writer.py index 30bfa73..0617a97 100644 --- a/zeroinstall/injector/writer.py +++ b/zeroinstall/injector/writer.py @@ -13,7 +13,6 @@ from zeroinstall.support import basedir from zeroinstall.injector import model from zeroinstall.injector.namespaces import config_site, config_prog, XMLNS_IFACE -from zeroinstall.injector.iface_cache import iface_cache def _atomic_save(doc, parent, uri): import tempfile -- 2.11.4.GIT