Skip to content

Commit

Permalink
Merge pull request #575 from arcivanov/issue_498_0.11
Browse files Browse the repository at this point in the history
 Fix silent failure in loop-detection dependency
  • Loading branch information
arcivanov committed Mar 25, 2018
2 parents bbde073 + 01f128d commit 6e37caf
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 51 deletions.
3 changes: 2 additions & 1 deletion build.py
Expand Up @@ -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"

Expand All @@ -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)

Expand Down
@@ -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()
2 changes: 1 addition & 1 deletion src/main/python/pybuilder/cli.py
Expand Up @@ -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()
Expand Down
25 changes: 17 additions & 8 deletions src/main/python/pybuilder/errors.py
Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -91,6 +94,12 @@ def __init__(self, plugin, message=""):
"Missing plugin '%s': %s", plugin, message)


class MissingPluginDependencyException(PyBuilderException):
def __init__(self, dependency, message=""):
super(MissingPluginDependencyException, self).__init__(
"Missing plugin dependency '%s': %s", dependency, message)


class UnspecifiedPluginNameException(PyBuilderException):
def __init__(self, plugin):
super(UnspecifiedPluginNameException, self).__init__(
Expand Down
14 changes: 7 additions & 7 deletions src/main/python/pybuilder/execution.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down
19 changes: 3 additions & 16 deletions src/main/python/pybuilder/graph_utils.py
Expand Up @@ -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.
Expand All @@ -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 = []
Expand All @@ -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):
Expand Down
26 changes: 19 additions & 7 deletions src/main/python/pybuilder/plugins/core_plugin.py
Expand Up @@ -20,10 +20,11 @@
import shutil
from os.path import join

from pybuilder.core import init, task, description, depends, optional
from pybuilder.utils import safe_log_file_name
# Plugin install_dependencies_plugin can reload pip_common and pip_utils. Do not use from ... import ...
from pybuilder import pip_utils
from pybuilder.core import init, task, description, depends, optional
from pybuilder.errors import MissingPluginDependencyException
from pybuilder.utils import safe_log_file_name, tail


@init
Expand Down Expand Up @@ -63,21 +64,32 @@ def prepare(project, logger):
plugin_dependency_versions = pip_utils.get_package_version(project.plugin_dependencies, logger)
for plugin_dependency in project.plugin_dependencies:
logger.debug("Processing plugin dependency %s" % plugin_dependency)
if plugin_dependency.name.lower() not in plugin_dependency_versions \
or not pip_utils.version_satisfies_spec(plugin_dependency.version,
plugin_dependency_versions[plugin_dependency.name.lower()]):
dependency_name = plugin_dependency.name.lower()
no_version_installed = dependency_name not in plugin_dependency_versions
if not no_version_installed:
version_satisfies = pip_utils.version_satisfies_spec(plugin_dependency.version,
plugin_dependency_versions[dependency_name])
if no_version_installed or not version_satisfies:
logger.info("Installing plugin dependency %s" % plugin_dependency)
log_file = project.expand_path("$dir_reports",
safe_log_file_name("dependency_%s_install.log" % plugin_dependency))
pip_utils.pip_install(
install_targets=pip_utils.as_pip_install_target(plugin_dependency),
install_targets = pip_utils.as_pip_install_target(plugin_dependency)
result = pip_utils.pip_install(
install_targets=install_targets,
index_url=project.get_property("install_dependencies_index_url"),
extra_index_url=project.get_property("install_dependencies_extra_index_url"),
verbose=project.get_property("verbose"),
logger=logger,
force_reinstall=plugin_dependency.url is not None,
outfile_name=log_file,
error_file_name=log_file)
if result:
log_lines = list(tail(log_file))
log = log_lines[-1] if not project.get_property("debug") else "\n".join(log_lines)
if no_version_installed:
raise MissingPluginDependencyException(",".join(install_targets), log)
else:
logger.error("Failed to install '%s':\n%s", plugin_dependency, log)


@task
Expand Down
4 changes: 2 additions & 2 deletions src/main/python/pybuilder/plugins/python/distutils_plugin.py
Expand Up @@ -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")
Expand Down
6 changes: 5 additions & 1 deletion src/main/python/pybuilder/plugins/python/sphinx_plugin.py
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/main/python/pybuilder/utils.py
Expand Up @@ -204,7 +204,7 @@ def tail(file_path, lines=20):
try:
import tailer
except ImportError:
return read_file(file_path)
return (l[:len(l)] for l in read_file(file_path))

with open(file_path) as f:
return tailer.tail(f, lines)
Expand Down
14 changes: 7 additions & 7 deletions src/unittest/python/graph_utils_tests.py
Expand Up @@ -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"})
Expand All @@ -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)

0 comments on commit 6e37caf

Please sign in to comment.