Skip to content

Commit

Permalink
[plugin-api] Add default_glob_match_error_behavior to `SourcesField…
Browse files Browse the repository at this point in the history
…`. (pantsbuild#13578)

This optional field allows specifying that a default glob value for a source field is optional (i.e. should not warn or err in case the glob does not match any files).

fixes pantsbuild#13567

There was a bug in the `SingleSourceField.path_globs()` that was also fixed in this PR.
If the field has a default value then that would use the wrong glob expansion conjunction when your provided source value would have the same set of characters as the default value.
For example, a default value of `"file"` and a provided value of `"life"` would treat this as if it was the default value was provided.
Note however, that with a single glob, the conjunction used does not matter (but when using the new default glob error override introduced in this PR, this distinction does matter).
  • Loading branch information
kaos authored Nov 12, 2021
1 parent dbf6789 commit 176f142
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 3 deletions.
24 changes: 22 additions & 2 deletions src/python/pants/engine/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -1502,13 +1502,19 @@ class SourcesField(AsyncFieldMixin, Field):
files. The default is no limit on the number of source files.
- `uses_source_roots` -- Whether the concept of "source root" pertains to the source files
referenced by this field.
- `default` -- A default value for this field.
- `default_glob_match_error_behavior` -- Advanced option, should very rarely be used. Override
glob match error behavior when using the default value. If setting this to
`GlobMatchErrorBehavior.ignore`, make sure you have other validation in place in case the
default glob doesn't match any files if required, to alert the user appropriately.
"""

expected_file_extensions: ClassVar[tuple[str, ...] | None] = None
expected_num_files: ClassVar[int | range | None] = None
uses_source_roots: ClassVar[bool] = True

default: ClassVar[ImmutableValue] = None
default_glob_match_error_behavior: ClassVar[GlobMatchErrorBehavior | None] = None

@property
def globs(self) -> tuple[str, ...]:
Expand Down Expand Up @@ -1612,12 +1618,26 @@ def can_generate(
def path_globs(self, files_not_found_behavior: FilesNotFoundBehavior) -> PathGlobs:
if not self.globs:
return PathGlobs([])
error_behavior = files_not_found_behavior.to_glob_match_error_behavior()

# SingleSourceField has str as default type.
default_globs = (
[self.default] if self.default and isinstance(self.default, str) else self.default
)

# Match any if we use default globs, else match all.
conjunction = (
GlobExpansionConjunction.all_match
if not self.default or (set(self.globs) != set(self.default))
if not default_globs or (set(self.globs) != set(default_globs))
else GlobExpansionConjunction.any_match
)
# Use fields default error behavior if defined, if we use default globs else the provided
# error behavior.
error_behavior = (
files_not_found_behavior.to_glob_match_error_behavior()
if conjunction == GlobExpansionConjunction.all_match
or self.default_glob_match_error_behavior is None
else self.default_glob_match_error_behavior
)
return PathGlobs(
(self._prefix_glob_with_address(glob) for glob in self.globs),
conjunction=conjunction,
Expand Down
121 changes: 120 additions & 1 deletion src/python/pants/engine/target_test.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from collections import namedtuple
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple

import pytest

from pants.engine.addresses import Address
from pants.engine.fs import Paths
from pants.engine.fs import GlobExpansionConjunction, GlobMatchErrorBehavior, Paths
from pants.engine.target import (
AsyncFieldMixin,
BoolField,
Expand Down Expand Up @@ -1008,6 +1009,124 @@ class GenSources(GenerateSourcesRequest):
assert set(result) == {tgt2}


SKIP = object()
expected_path_globs = namedtuple(
"expected_path_globs",
["globs", "glob_match_error_behavior", "conjunction", "description_of_origin"],
defaults=(SKIP, SKIP, SKIP, SKIP),
)


@pytest.mark.parametrize(
"default_value, field_value, expected",
[
pytest.param(
None,
None,
expected_path_globs(globs=()),
id="empty",
),
pytest.param(
["*"],
None,
expected_path_globs(
globs=("test/*",),
glob_match_error_behavior=GlobMatchErrorBehavior.ignore,
conjunction=GlobExpansionConjunction.any_match,
description_of_origin=None,
),
id="default ignores glob match error",
),
pytest.param(
["*"],
["a", "b"],
expected_path_globs(
globs=(
"test/a",
"test/b",
),
glob_match_error_behavior=GlobMatchErrorBehavior.warn,
conjunction=GlobExpansionConjunction.all_match,
description_of_origin="test:test's `sources` field",
),
id="provided value warns on glob match error",
),
],
)
def test_multiple_sources_path_globs(
default_value: Any, field_value: Any, expected: expected_path_globs
) -> None:
class TestMultipleSourcesField(MultipleSourcesField):
default = default_value
default_glob_match_error_behavior = GlobMatchErrorBehavior.ignore

sources = TestMultipleSourcesField(field_value, Address("test"))
actual = sources.path_globs(FilesNotFoundBehavior.warn)
for attr, expect in zip(expected._fields, expected):
if expect is not SKIP:
assert getattr(actual, attr) == expect


@pytest.mark.parametrize(
"default_value, field_value, expected",
[
pytest.param(
None,
None,
expected_path_globs(globs=()),
id="empty",
),
pytest.param(
"file",
None,
expected_path_globs(
globs=("test/file",),
glob_match_error_behavior=GlobMatchErrorBehavior.ignore,
conjunction=GlobExpansionConjunction.any_match,
description_of_origin=None,
),
id="default ignores glob match error",
),
pytest.param(
"default_file",
"other_file",
expected_path_globs(
globs=("test/other_file",),
glob_match_error_behavior=GlobMatchErrorBehavior.warn,
conjunction=GlobExpansionConjunction.all_match,
description_of_origin="test:test's `source` field",
),
id="provided value warns on glob match error",
),
pytest.param(
"file",
"life",
expected_path_globs(
globs=("test/life",),
glob_match_error_behavior=GlobMatchErrorBehavior.warn,
conjunction=GlobExpansionConjunction.all_match,
description_of_origin="test:test's `source` field",
),
id="default glob conjunction",
),
],
)
def test_single_source_path_globs(
default_value: Any, field_value: Any, expected: expected_path_globs
) -> None:
class TestSingleSourceField(SingleSourceField):
default = default_value
default_glob_match_error_behavior = GlobMatchErrorBehavior.ignore
required = False

sources = TestSingleSourceField(field_value, Address("test"))

actual = sources.path_globs(FilesNotFoundBehavior.warn)
for attr, expect in zip(expected._fields, expected):
if expect is not SKIP:
assert getattr(actual, attr) == expect


# -----------------------------------------------------------------------------------------------
# Test `ExplicitlyProvidedDependencies` helper functions
# -----------------------------------------------------------------------------------------------
Expand Down

0 comments on commit 176f142

Please sign in to comment.