diff --git a/src/python/pants/build_graph/target_filter_subsystem.py b/src/python/pants/build_graph/target_filter_subsystem.py new file mode 100644 index 00000000000..af6d4b766fa --- /dev/null +++ b/src/python/pants/build_graph/target_filter_subsystem.py @@ -0,0 +1,45 @@ +# coding=utf-8 +# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import absolute_import, division, print_function, unicode_literals + +import logging +from builtins import object, set + +from pants.subsystem.subsystem import Subsystem + + +logger = logging.getLogger(__name__) + + +class TargetFilter(Subsystem): + """Filter targets matching configured criteria. + + :API: public + """ + + options_scope = 'target-filter' + + @classmethod + def register_options(cls, register): + super(TargetFilter, cls).register_options(register) + + register('--exclude-tags', type=list, + default=[], fingerprint=True, + help='Skip targets with given tag(s).') + + def apply(self, targets): + exclude_tags = set(self.get_options().exclude_tags) + return TargetFiltering(targets, exclude_tags).apply_tag_blacklist() + + +class TargetFiltering(object): + """Apply filtering logic against targets.""" + + def __init__(self, targets, exclude_tags): + self.targets = targets + self.exclude_tags = exclude_tags + + def apply_tag_blacklist(self): + return [t for t in self.targets if not self.exclude_tags.intersection(t.tags)] diff --git a/src/python/pants/task/task.py b/src/python/pants/task/task.py index d7b5e28dc90..037e917a2e3 100644 --- a/src/python/pants/task/task.py +++ b/src/python/pants/task/task.py @@ -7,7 +7,7 @@ import os import sys from abc import abstractmethod -from builtins import filter, map, object, str, zip +from builtins import filter, map, object, set, str, zip from contextlib import contextmanager from hashlib import sha1 from itertools import repeat @@ -16,6 +16,7 @@ from pants.base.exceptions import TaskError from pants.base.worker_pool import Work +from pants.build_graph.target_filter_subsystem import TargetFilter from pants.cache.artifact_cache import UnreadableArtifact, call_insert, call_use_cached_files from pants.cache.cache_setup import CacheSetup from pants.invalidation.build_invalidator import (BuildInvalidator, CacheKeyGenerator, @@ -96,7 +97,7 @@ def _compute_stable_name(cls): @classmethod def subsystem_dependencies(cls): return (super(TaskBase, cls).subsystem_dependencies() + - (CacheSetup.scoped(cls), BuildInvalidator.Factory, SourceRootConfig)) + (CacheSetup.scoped(cls), TargetFilter.scoped(cls), BuildInvalidator.Factory, SourceRootConfig)) @classmethod def product_types(cls): @@ -237,8 +238,18 @@ def get_targets(self, predicate=None): :API: public """ - return (self.context.targets(predicate) if self.act_transitively - else list(filter(predicate, self.context.target_roots))) + initial_targets = (self.context.targets(predicate) if self.act_transitively + else list(filter(predicate, self.context.target_roots))) + + included_targets = TargetFilter.scoped_instance(self).apply(initial_targets) + excluded_targets = set(initial_targets).difference(included_targets) + + if excluded_targets: + self.context.log.info("{} target(s) excluded".format(len(excluded_targets))) + for target in excluded_targets: + self.context.log.debug("{} excluded".format(target.address.spec)) + + return included_targets @memoized_property def workdir(self): diff --git a/tests/python/pants_test/build_graph/BUILD b/tests/python/pants_test/build_graph/BUILD index 534c499179f..e8ab092819e 100644 --- a/tests/python/pants_test/build_graph/BUILD +++ b/tests/python/pants_test/build_graph/BUILD @@ -146,3 +146,14 @@ python_tests( 'tests/python/pants_test:test_base', ] ) + +python_tests( + name = 'target_filter_subsystem', + sources = ['test_target_filter_subsystem.py'], + dependencies = [ + '3rdparty/python:future', + 'src/python/pants/build_graph', + 'src/python/pants/task', + 'tests/python/pants_test:task_test_base', + ] +) diff --git a/tests/python/pants_test/build_graph/test_target_filter_subsystem.py b/tests/python/pants_test/build_graph/test_target_filter_subsystem.py new file mode 100644 index 00000000000..3c9fd41c0d1 --- /dev/null +++ b/tests/python/pants_test/build_graph/test_target_filter_subsystem.py @@ -0,0 +1,61 @@ +# coding=utf-8 +# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import absolute_import, division, print_function, unicode_literals + +from builtins import set + +from pants.build_graph.target_filter_subsystem import TargetFilter, TargetFiltering +from pants.task.task import Task +from pants_test.task_test_base import TaskTestBase + + +class TestTargetFilter(TaskTestBase): + + class DummyTask(Task): + options_scope = 'dummy' + + def execute(self): + self.context.products.safe_create_data('task_targets', self.get_targets) + + @classmethod + def task_type(cls): + return cls.DummyTask + + def test_task_execution_with_filter(self): + a = self.make_target('a', tags=['skip-me']) + b = self.make_target('b', dependencies=[a], tags=[]) + + context = self.context(for_task_types=[self.DummyTask], for_subsystems=[TargetFilter], target_roots=[b], options={ + TargetFilter.options_scope: { + 'exclude_tags': ['skip-me'] + } + }) + + self.create_task(context).execute() + self.assertEqual([b], context.products.get_data('task_targets')) + + def test_filtering_single_tag(self): + a = self.make_target('a', tags=[]) + b = self.make_target('b', tags=['skip-me']) + c = self.make_target('c', tags=['tag1', 'skip-me']) + + filtered_targets = TargetFiltering([a, b, c], {'skip-me'}).apply_tag_blacklist() + self.assertEqual([a], filtered_targets) + + def test_filtering_multiple_tags(self): + a = self.make_target('a', tags=['tag1', 'skip-me']) + b = self.make_target('b', tags=['tag1', 'tag2', 'skip-me']) + c = self.make_target('c', tags=['tag2']) + + filtered_targets = TargetFiltering([a, b, c], {'skip-me', 'tag2'}).apply_tag_blacklist() + self.assertEqual([], filtered_targets) + + def test_filtering_no_tags(self): + a = self.make_target('a', tags=['tag1']) + b = self.make_target('b', tags=['tag1', 'tag2']) + c = self.make_target('c', tags=['tag2']) + + filtered_targets = TargetFiltering([a, b, c], set()).apply_tag_blacklist() + self.assertEqual([a, b, c], filtered_targets)