diff --git a/src/passa/cli/_base.py b/src/passa/cli/_base.py index 69e2683..04ef5eb 100644 --- a/src/passa/cli/_base.py +++ b/src/passa/cli/_base.py @@ -10,10 +10,16 @@ def build_project(root): # This is imported lazily to reduce import overhead. Not evey command # needs the project instance. - from passa.internals.projects import Project + from passa.projects import Project return Project(os.path.abspath(root)) +def configure_reporter(): + # This is imported lazily to reduce import overhead. + from passa.reporters import configure_reporter + configure_reporter("stdout") + + class BaseCommand(object): """A CLI command. """ @@ -51,4 +57,5 @@ def add_arguments(self): def main(self, options): # This __dict__ access is needed for Python 2 to prevent Python from # wrapping parsed_main into an unbounded method. + configure_reporter() return type(self).__dict__["parsed_main"](options) diff --git a/src/passa/cli/lock.py b/src/passa/cli/lock.py index 67b1d11..7961a5c 100644 --- a/src/passa/cli/lock.py +++ b/src/passa/cli/lock.py @@ -15,15 +15,27 @@ def main(options): if not success: return - project._l.write() - print("Written to project at", project.root) + if options.dry_run: + print(project._l.dumps()) + else: + project._l.write() + print("Written to project at", project.root) class Command(BaseCommand): + name = "lock" description = "Generate Pipfile.lock." parsed_main = main + def add_arguments(self): + super(Command, self).add_arguments() + self.parser.add_argument( + "--dry-run", + action="store_true", default=False, + help="run locking on Pipfile, but do not write to Pipfile.lock", + ) + if __name__ == "__main__": Command.run_current_module() diff --git a/src/passa/internals/lockers.py b/src/passa/internals/lockers.py index 4f7a722..b1b842f 100644 --- a/src/passa/internals/lockers.py +++ b/src/passa/internals/lockers.py @@ -9,11 +9,12 @@ import resolvelib import vistir +from passa import reporters + from .caches import HashCache from .hashes import get_hashes from .metadata import set_metadata from .providers import BasicProvider, EagerUpgradeProvider, PinReuseProvider -from .reporters import StdOutReporter from .traces import trace_graph from .utils import identify_requirment @@ -100,10 +101,6 @@ def __repr__(self): def get_provider(self): raise NotImplementedError - def get_reporter(self): - # TODO: Build SpinnerReporter, and use this only in verbose mode. - return StdOutReporter(self.requirements) - def lock(self): """Lock specified (abstract) requirements into (concrete) candidates. @@ -116,14 +113,20 @@ def lock(self): * Populate markers based on dependency specifications of each candidate, and the dependency graph. """ + reporters.report("lock-starting", {"requirements": self.requirements}) + provider = self.get_provider() - reporter = self.get_reporter() - resolver = resolvelib.Resolver(provider, reporter) + resolver = resolvelib.Resolver( + provider, reporters.get_reporter().build_for_resolvelib(), + ) with vistir.cd(self.project.root): state = resolver.resolve(self.requirements) traces = trace_graph(state.graph) + reporters.report("lock-trace-ended", { + "state": state, "traces": traces, + }) hash_cache = HashCache() for r in state.mapping.values(): diff --git a/src/passa/internals/reporters.py b/src/passa/internals/reporters.py deleted file mode 100644 index 4fe6c0b..0000000 --- a/src/passa/internals/reporters.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding=utf-8 -*- - -from __future__ import absolute_import, print_function, unicode_literals - -import resolvelib - -from .traces import trace_graph - - -def print_title(text): - print('\n{:=^84}\n'.format(text)) - - -def print_requirement(r, end='\n'): - print('{:>40}'.format(r.as_line(include_hashes=False)), end=end) - - -def print_dependency(state, key): - print_requirement(state.mapping[key], end='') - parents = sorted( - state.graph.iter_parents(key), - key=lambda n: (-1, '') if n is None else (ord(n[0].lower()), n), - ) - for i, p in enumerate(parents): - if p is None: - line = '(user)' - else: - line = state.mapping[p].as_line(include_hashes=False) - if i == 0: - padding = ' <= ' - else: - padding = ' ' * 44 - print('{pad}{line}'.format(pad=padding, line=line)) - - -class StdOutReporter(resolvelib.BaseReporter): - """Simple reporter that prints things to stdout. - """ - def __init__(self, requirements): - super(StdOutReporter, self).__init__() - self.requirements = requirements - - def starting(self): - self._prev = None - print_title(' User requirements ') - for r in self.requirements: - print_requirement(r) - - def ending_round(self, index, state): - print_title(' Round {} '.format(index)) - mapping = state.mapping - if self._prev is None: - difference = set(mapping.keys()) - changed = set() - else: - difference = set(mapping.keys()) - set(self._prev.keys()) - changed = set( - k for k, v in mapping.items() - if k in self._prev and self._prev[k] != v - ) - self._prev = mapping - - if difference: - print('New pins: ') - for k in difference: - print_dependency(state, k) - print() - - if changed: - print('Changed pins:') - for k in changed: - print_dependency(state, k) - print() - - def ending(self, state): - print_title(" STABLE PINS ") - path_lists = trace_graph(state.graph) - for k in sorted(state.mapping): - print(state.mapping[k].as_line(include_hashes=False)) - paths = path_lists[k] - for path in paths: - if path == [None]: - print(' User requirement') - continue - print(' ', end='') - for v in reversed(path[1:]): - line = state.mapping[v].as_line(include_hashes=False) - print(' <=', line, end='') - print() - print() diff --git a/src/passa/operations/lock.py b/src/passa/operations/lock.py index 200735a..d2e3d84 100644 --- a/src/passa/operations/lock.py +++ b/src/passa/operations/lock.py @@ -4,25 +4,15 @@ from resolvelib import NoVersionsAvailable, ResolutionImpossible -from passa.internals.reporters import print_requirement +from passa import reporters def lock(locker): success = False try: locker.lock() - except NoVersionsAvailable as e: - print("\nCANNOT RESOLVE. NO CANDIDATES FOUND FOR:") - print("{:>40}".format(e.requirement.as_line(include_hashes=False))) - if e.parent: - line = e.parent.as_line(include_hashes=False) - print("{:>41}".format("(from {})".format(line))) - else: - print("{:>41}".format("(user)")) - except ResolutionImpossible as e: - print("\nCANNOT RESOLVE.\nOFFENDING REQUIREMENTS:") - for r in e.requirements: - print_requirement(r) + except (NoVersionsAvailable, ResolutionImpossible) as e: + reporters.report("lock-failed", {"exception": e}) else: success = True return success diff --git a/src/passa/internals/projects.py b/src/passa/projects.py similarity index 100% rename from src/passa/internals/projects.py rename to src/passa/projects.py diff --git a/src/passa/reporters/__init__.py b/src/passa/reporters/__init__.py new file mode 100644 index 0000000..dffe532 --- /dev/null +++ b/src/passa/reporters/__init__.py @@ -0,0 +1,31 @@ +# -*- coding=utf-8 -*- + +from __future__ import absolute_import, print_function, unicode_literals + +from .base import BaseReporter + + +_REPORTER = BaseReporter() + + +def _get_stdout_reporter(): + from .stdout import Reporter + return Reporter() + + +def configure_reporter(name): + global _REPORTER + _REPORTER = { + None: BaseReporter, + "stdout": _get_stdout_reporter, + }[name]() + + +def get_reporter(): + return _REPORTER + + +def report(event, context=None): + if context is None: + context = {} + _REPORTER.report(event, context) diff --git a/src/passa/reporters/base.py b/src/passa/reporters/base.py new file mode 100644 index 0000000..66a432c --- /dev/null +++ b/src/passa/reporters/base.py @@ -0,0 +1,52 @@ +# -*- coding=utf-8 -*- + +from __future__ import absolute_import, print_function, unicode_literals + +import resolvelib + + +class ResolveLibReporter(resolvelib.BaseReporter): + """Implementation of a ResolveLib reporter that bridge messages. + """ + def __init__(self, parent): + super(ResolveLibReporter, self).__init__() + self.parent = parent + + def starting(self): + self.parent.report("resolvelib-starting", {"child": self}) + + def ending_round(self, index, state): + self.parent.report("resolvelib-ending-round", { + "child": self, "index": index, "state": state, + }) + + def ending(self, state): + self.parent.report("resolvelib-ending", { + "child": self, "state": state, + }) + + +class BaseReporter(object): + """Basic reporter that does nothing. + """ + def build_for_resolvelib(self): + """Build a reporter for ResolveLib. + """ + return ResolveLibReporter(self) + + def report(self, event, context): + """Report an event. + + The default behavior is to look for a "handle_EVENT" method on the + class to execute, or do nothing if there is no such method. + + :param event: A string to indicate the event. + :param context: A mapping containing appropriate data for the handling + function. + """ + handler_name = "handle_{}".format(event.replace("-", "_")) + try: + handler = getattr(self, handler_name) + except AttributeError: + return + handler(context or {}) diff --git a/src/passa/reporters/stdout.py b/src/passa/reporters/stdout.py new file mode 100644 index 0000000..2042337 --- /dev/null +++ b/src/passa/reporters/stdout.py @@ -0,0 +1,106 @@ +# -*- coding=utf-8 -*- + +from __future__ import absolute_import, print_function, unicode_literals + +from resolvelib import NoVersionsAvailable, ResolutionImpossible + +from .base import BaseReporter + + +def _print_title(text): + print('\n{:=^84}\n'.format(text)) + + +def _print_requirement(r, end='\n'): + print('{:>40}'.format(r.as_line(include_hashes=False)), end=end) + + +def _print_dependency(state, key): + _print_requirement(state.mapping[key], end='') + parents = sorted( + state.graph.iter_parents(key), + key=lambda n: (-1, '') if n is None else (ord(n[0].lower()), n), + ) + for i, p in enumerate(parents): + if p is None: + line = '(user)' + else: + line = state.mapping[p].as_line(include_hashes=False) + if i == 0: + padding = ' <= ' + else: + padding = ' ' * 44 + print('{pad}{line}'.format(pad=padding, line=line)) + + +class Reporter(BaseReporter): + """A reporter implementation that prints messages to stdout. + """ + def handle_resolvelib_starting(self, context): + context["child"]._prev_mapping = None + + def handle_resolvelib_ending_round(self, context): + _print_title(' Round {} '.format(context["index"])) + mapping = context["state"].mapping + if context["child"]._prev_mapping is None: + difference = set(mapping.keys()) + changed = set() + else: + prev = context["child"]._prev_mapping + difference = set(mapping.keys()) - set(prev.keys()) + changed = set( + k for k, v in mapping.items() + if k in prev and prev[k] != v + ) + context["child"]._prev_mapping = mapping + + if difference: + print('New pins: ') + for k in difference: + _print_dependency(context["state"], k) + print() + + if changed: + print('Changed pins:') + for k in changed: + _print_dependency(context["state"], k) + print() + + def handle_lock_starting(self, context): + _print_title(' User requirements ') + for r in context["requirements"]: + _print_requirement(r) + + def handle_lock_trace_ended(self, context): + _print_title(" STABLE PINS ") + mapping = context["state"].mapping + for k in sorted(mapping): + print(mapping[k].as_line(include_hashes=False)) + paths = context["traces"][k] + for path in paths: + if path == [None]: + print(' User requirement') + continue + print(' ', end='') + for v in reversed(path[1:]): + line = mapping[v].as_line(include_hashes=False) + print(' <=', line, end='') + print() + print() + + def handle_lock_failed(self, context): + e = context["exception"] + if isinstance(e, ResolutionImpossible): + print("\nCANNOT RESOLVE.\nOFFENDING REQUIREMENTS:") + for r in e.requirements: + _print_requirement(r) + elif isinstance(e, NoVersionsAvailable): + print("\nCANNOT RESOLVE. NO CANDIDATES FOUND FOR:") + print("{:>40}".format(e.requirement.as_line(include_hashes=False))) + if e.parent: + line = e.parent.as_line(include_hashes=False) + print("{:>41}".format("(from {})".format(line))) + else: + print("{:>41}".format("(user)")) + else: + raise