diff --git a/build.py b/build.py index 468494c7d..6d69f158b 100755 --- a/build.py +++ b/build.py @@ -78,7 +78,7 @@ ] url = "http://pybuilder.github.io" license = "Apache License" -version = "0.11.14" +version = "0.11.15" requires_python = ">=2.6,!=3.0,!=3.1,!=3.2,<3.7" @@ -100,6 +100,7 @@ def initialize(project): project.depends_on("pip", ">=7.1") project.depends_on("setuptools", "~=36.0") project.depends_on("wheel", "~=0.29.0") + project.depends_on("tailer") project.set_property("verbose", True) diff --git a/src/integrationtest/python/should_raise_exception_when_detect_task_cycle_tests.py b/src/integrationtest/python/should_raise_exception_when_detect_task_cycle_tests.py new file mode 100644 index 000000000..594340473 --- /dev/null +++ b/src/integrationtest/python/should_raise_exception_when_detect_task_cycle_tests.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# +# This file is part of PyBuilder +# +# Copyright 2011-2015 PyBuilder Team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from integrationtest_support import IntegrationTestSupport +from pybuilder.errors import CircularTaskDependencyException + + +class Test(IntegrationTestSupport): + def test(self): + self.write_build_file(""" +from pybuilder.core import task, depends, dependents + +@task +@depends("task_c") +def task_a(project): + project.set_property("a", False) + +@task +@depends("task_a") +def task_b(project): + project.set_property("a", True) + +@task +@depends("task_a", "task_b") +def task_c(project): + project.set_property("c", True) + """) + reactor = self.prepare_reactor() + self.assertRaises(CircularTaskDependencyException, reactor.build, ["task_c", "task_a", "task_b"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/main/python/pybuilder/cli.py b/src/main/python/pybuilder/cli.py index 44223239a..65def2f6c 100644 --- a/src/main/python/pybuilder/cli.py +++ b/src/main/python/pybuilder/cli.py @@ -416,10 +416,10 @@ def main(*args): raise PyBuilderException("Build aborted") except (Exception, SystemExit) as e: + successful = False failure_message = str(e) if options.debug: traceback.print_exc(file=sys.stderr) - successful = False finally: end = datetime.datetime.now() diff --git a/src/main/python/pybuilder/errors.py b/src/main/python/pybuilder/errors.py index ec2154311..24c258f00 100644 --- a/src/main/python/pybuilder/errors.py +++ b/src/main/python/pybuilder/errors.py @@ -21,6 +21,8 @@ Defines all possible errors that can arise during the execution of PyBuilder. """ +from pprint import pformat + class PyBuilderException(Exception): def __init__(self, message, *arguments): @@ -47,14 +49,15 @@ def __init__(self, name): class CircularTaskDependencyException(PyBuilderException): - def __init__(self, first, second=None, message=None): - if message: - super(CircularTaskDependencyException, self).__init__(message) - elif second: - super(CircularTaskDependencyException, self).__init__("Circular task dependency detected between %s and %s", - first, second) - self.first = first - self.second = second + def __init__(self, message, *args): + if isinstance(message, (list, tuple)): + cycles = message + super(CircularTaskDependencyException, self).__init__("Circular task dependencies detected:\n%s", + "\n".join("\t" + pformat(cycle) for cycle in cycles) + ) + else: + super(CircularTaskDependencyException, self).__init__(message, + *args) class MissingPrerequisiteException(PyBuilderException): diff --git a/src/main/python/pybuilder/execution.py b/src/main/python/pybuilder/execution.py index 8a3070dfd..22946a743 100644 --- a/src/main/python/pybuilder/execution.py +++ b/src/main/python/pybuilder/execution.py @@ -37,7 +37,7 @@ MissingActionDependencyException, NoSuchTaskException, RequiredTaskExclusionException) -from pybuilder.graph_utils import Graph, GraphHasCycles +from pybuilder.graph_utils import Graph from pybuilder.utils import as_list, Timer, odict if sys.version_info[0] < 3: # if major is less than 3 @@ -391,10 +391,10 @@ def build_execution_plan(self, task_names): dependency_edges = {} for task in self.collect_all_transitive_tasks(as_list(task_names)): dependency_edges[task.name] = [dependency.name for dependency in task.dependencies] - try: - Graph(dependency_edges).assert_no_cycles_present() - except GraphHasCycles as cycles: - raise CircularTaskDependencyException(str(cycles)) + + cycles = Graph(dependency_edges).assert_no_cycles_present() + if cycles: + raise CircularTaskDependencyException(cycles) for task_name in as_list(task_names): self._enqueue_task(execution_plan, task_name) @@ -417,8 +417,8 @@ def build_shortest_execution_plan(self, task_names): if self._current_task and self._current_task in shortest_plan: raise CircularTaskDependencyException("Task '%s' attempted to invoke tasks %s, " - "resulting in plan %s, creating circular dependency" % - (self._current_task, task_names, shortest_plan)) + "resulting in plan %s, creating circular dependency", + self._current_task, task_names, shortest_plan) return shortest_plan def _enqueue_task(self, execution_plan, task_name): diff --git a/src/main/python/pybuilder/graph_utils.py b/src/main/python/pybuilder/graph_utils.py index 32c419945..1dcc273f8 100644 --- a/src/main/python/pybuilder/graph_utils.py +++ b/src/main/python/pybuilder/graph_utils.py @@ -20,13 +20,6 @@ """ -class GraphHasCycles(Exception): - """ - To be raised when a graph has one or more cycles - """ - pass - - class Graph(object): """ A graph using an edge dictionary as an internal representation. @@ -45,10 +38,10 @@ def assert_no_cycles_present(self, include_trivial_cycles=True): # contains at least one directed cycle, so len()>1 is a showstopper if cycles: - raise self.error_with_cycles(cycles) + return cycles if include_trivial_cycles: - self.assert_no_trivial_cycles_present() + return self.assert_no_trivial_cycles_present() def assert_no_trivial_cycles_present(self): trivial_cycles = [] @@ -57,13 +50,7 @@ def assert_no_trivial_cycles_present(self): trivial_cycles.append((source, source)) if trivial_cycles: - raise self.error_with_cycles(trivial_cycles) - - def error_with_cycles(self, cycles): - error_message = "Found cycle(s):\n" - for cycle in cycles: - error_message += "\tThese nodes form a cycle : " + str(cycle) + "\n" - return GraphHasCycles(error_message) + return trivial_cycles def tarjan_scc(graph): diff --git a/src/main/python/pybuilder/plugins/python/distutils_plugin.py b/src/main/python/pybuilder/plugins/python/distutils_plugin.py index c40c8ed65..67dfa61db 100644 --- a/src/main/python/pybuilder/plugins/python/distutils_plugin.py +++ b/src/main/python/pybuilder/plugins/python/distutils_plugin.py @@ -143,8 +143,8 @@ def set_description(project, logger): try: assert_can_execute(["pandoc", "--version"], "pandoc", "distutils") doc_convert(project, logger) - except MissingPrerequisiteException: - logger.warn("Was unable to find pandoc and did not convert the documentation") + except (MissingPrerequisiteException, ImportError): + logger.warn("Was unable to find pandoc or pypandoc and did not convert the documentation") @after("package") diff --git a/src/main/python/pybuilder/plugins/python/sphinx_plugin.py b/src/main/python/pybuilder/plugins/python/sphinx_plugin.py index db7b765d9..8d51fa4bc 100644 --- a/src/main/python/pybuilder/plugins/python/sphinx_plugin.py +++ b/src/main/python/pybuilder/plugins/python/sphinx_plugin.py @@ -66,7 +66,11 @@ def initialize_sphinx_plugin(project): default_project_name = project.name default_doc_author = ", ".join([author.name for author in project.authors]) - project.plugin_depends_on("sphinx") + if sys.version_info[:2] in ((2, 6), (3, 3)): + project.plugin_depends_on("sphinx", "~=1.4.0") + else: + project.plugin_depends_on("sphinx") + project.set_property_if_unset( "sphinx_source_dir", SCAFFOLDING.DEFAULT_DOCS_DIRECTORY) project.set_property_if_unset( diff --git a/src/unittest/python/graph_utils_tests.py b/src/unittest/python/graph_utils_tests.py index 0bbdbe64c..c4f129f8c 100644 --- a/src/unittest/python/graph_utils_tests.py +++ b/src/unittest/python/graph_utils_tests.py @@ -17,22 +17,22 @@ # limitations under the License. from unittest import TestCase -from pybuilder.graph_utils import Graph, GraphHasCycles +from pybuilder.graph_utils import Graph class GraphUtilsTests(TestCase): def test_should_find_trivial_cycle_in_graph_when_there_is_one(self): graph_with_trivial_cycle = Graph({"a": "a"}) - self.assertRaises(GraphHasCycles, graph_with_trivial_cycle.assert_no_trivial_cycles_present) + self.assertTrue(graph_with_trivial_cycle.assert_no_trivial_cycles_present() is not None) def test_should_find_trivial_cycle_in_graph_when_there_are_two(self): graph_with_trivial_cycles = Graph({"a": "a", "b": "b"}) - self.assertRaises(GraphHasCycles, graph_with_trivial_cycles.assert_no_trivial_cycles_present) + self.assertTrue(graph_with_trivial_cycles.assert_no_trivial_cycles_present() is not None) def test_should_find_trivial_cycle_in_graph_when_searching_for_cycles(self): graph_with_trivial_cycle = Graph({"a": "a"}) - self.assertRaises(GraphHasCycles, graph_with_trivial_cycle.assert_no_cycles_present) + self.assertTrue(graph_with_trivial_cycle.assert_no_cycles_present() is not None) def test_should_not_find_trivial_cycles_in_graph_when_there_are_none(self): graph_without_trivial_cycle = Graph({"a": "b", "b": "c", "d": "e"}) @@ -44,12 +44,12 @@ def test_should_not_find_cycles_in_graph_when_there_are_none(self): def test_should_find_simple_nontrivial_cycle_in_graph_when_there_is_one(self): graph_with_simple_cycle = Graph({"a": "b", "b": "a"}) - self.assertRaises(GraphHasCycles, graph_with_simple_cycle.assert_no_cycles_present) + self.assertTrue(graph_with_simple_cycle.assert_no_cycles_present() is not None) def test_should_find_long_nontrivial_cycle_in_graph_when_there_is_one(self): graph_with_long_cycle = Graph({"a": "b", "b": "c", "c": "d", "d": "b"}) - self.assertRaises(GraphHasCycles, graph_with_long_cycle.assert_no_cycles_present) + self.assertTrue(graph_with_long_cycle.assert_no_cycles_present() is not None) def test_should_find_long_nontrivial_cycle_in_graph_when_there_are_two(self): graph_with_long_cycle = Graph({"a": "b", "b": "c", "c": "a", "d": "e", "e": "f", "f": "d"}) - self.assertRaises(GraphHasCycles, graph_with_long_cycle.assert_no_cycles_present) + self.assertTrue(graph_with_long_cycle.assert_no_cycles_present() is not None)