def parse_spec(self, spec: str) -> AddressSpec | FilesystemSpec: """Parse the given spec into an `AddressSpec` or `FilesystemSpec` object. :raises: CmdLineSpecParser.BadSpecError if the address selector could not be parsed. """ if spec.endswith("::"): spec_path = spec[:-len("::")] return DescendantAddresses( directory=self._normalize_spec_path(spec_path)) if spec.endswith(":"): spec_path = spec[:-len(":")] return SiblingAddresses( directory=self._normalize_spec_path(spec_path)) if ":" in spec or "#" in spec: tgt_parts = spec.split(":", maxsplit=1) path_component = tgt_parts[0] if len(tgt_parts) == 1: target_component = None generated_parts = path_component.split("#", maxsplit=1) if len(generated_parts) == 1: generated_component = None else: path_component, generated_component = generated_parts else: generated_parts = tgt_parts[1].split("#", maxsplit=1) if len(generated_parts) == 1: target_component = generated_parts[0] generated_component = None else: target_component, generated_component = generated_parts return AddressLiteralSpec( path_component=self._normalize_spec_path(path_component), target_component=target_component, generated_component=generated_component, ) if spec.startswith("!"): return FileIgnoreSpec(spec[1:]) if "*" in spec: return FileGlobSpec(spec) if PurePath(spec).suffix: return FileLiteralSpec(self._normalize_spec_path(spec)) spec_path = self._normalize_spec_path(spec) if spec_path == ".": return DirLiteralSpec("") # Some paths that look like dirs can actually be files without extensions. if Path(self._root_dir, spec_path).is_file(): return FileLiteralSpec(spec_path) return DirLiteralSpec(spec_path)
def test_raw_specs_with_only_file_owners_glob(rule_runner: RuleRunner) -> None: rule_runner.write_files({ "demo/f1.txt": "", "demo/f2.txt": "", "demo/BUILD": dedent("""\ file_generator(name='generator', sources=['*.txt']) nonfile_generator(name='nonfile') target(name='not-generator', sources=['*.txt']) target(name='skip-me', sources=['*.txt']) target(name='bad-tag', sources=['*.txt'], tags=['skip']) """), }) rule_runner.set_options(["--tag=-skip", "--exclude-target-regexp=skip-me"]) all_unskipped_addresses = [ Address("demo", target_name="not-generator"), Address("demo", target_name="generator", relative_file_path="f1.txt"), Address("demo", target_name="generator", relative_file_path="f2.txt"), ] assert (resolve_raw_specs_with_only_file_owners( rule_runner, [FileGlobSpec("demo/*.txt")]) == all_unskipped_addresses) # We should deduplicate between glob and literal specs. assert (resolve_raw_specs_with_only_file_owners( rule_runner, [FileGlobSpec("demo/*.txt"), FileLiteralSpec("demo/f1.txt")], ) == all_unskipped_addresses)
def parse_spec(self, spec: str) -> tuple[Spec, bool]: """Parse the given spec string and also return `true` if it's an ignore. :raises: CmdLineSpecParser.BadSpecError if the address selector could not be parsed. """ is_ignore = False if spec.startswith("-"): is_ignore = True spec = spec[1:] ( ( path_component, target_component, generated_component, parameters, ), wildcard, ) = native_engine.address_spec_parse(spec) if wildcard == "::": return RecursiveGlobSpec( directory=self._normalize_spec_path(path_component)), is_ignore if wildcard == ":": return DirGlobSpec( directory=self._normalize_spec_path(path_component)), is_ignore if target_component or generated_component or parameters: return ( AddressLiteralSpec( path_component=self._normalize_spec_path(path_component), target_component=target_component, generated_component=generated_component, parameters=FrozenDict(sorted(parameters)), ), is_ignore, ) if "*" in path_component: return FileGlobSpec(spec), is_ignore if PurePath(spec).suffix: return FileLiteralSpec(self._normalize_spec_path(spec)), is_ignore spec_path = self._normalize_spec_path(spec) if spec_path == ".": return DirLiteralSpec(""), is_ignore # Some paths that look like dirs can actually be files without extensions. if Path(self._root_dir, spec_path).is_file(): return FileLiteralSpec(spec_path), is_ignore return DirLiteralSpec(spec_path), is_ignore
def test_raw_specs_with_only_file_owners_nonexistent_file( rule_runner: RuleRunner) -> None: spec = FileLiteralSpec("demo/fake.txt") with engine_error(contains='Unmatched glob from tests: "demo/fake.txt"'): resolve_raw_specs_with_only_file_owners(rule_runner, [spec]) assert not resolve_raw_specs_with_only_file_owners(rule_runner, [spec], ignore_nonexistent=True)
def test_raw_specs_with_only_file_owners_no_owner( rule_runner: RuleRunner) -> None: rule_runner.set_options(["--owners-not-found-behavior=error"]) rule_runner.write_files({"no_owners/f.txt": ""}) # Error for literal specs. with pytest.raises(ExecutionError) as exc: resolve_raw_specs_with_only_file_owners( rule_runner, [FileLiteralSpec("no_owners/f.txt")]) assert "No owning targets could be found for the file `no_owners/f.txt`" in str( exc.value) # Do not error for glob specs. assert not resolve_raw_specs_with_only_file_owners( rule_runner, [FileGlobSpec("no_owners/*.txt")])
def test_raw_specs_with_only_file_owners_literal_file( rule_runner: RuleRunner) -> None: rule_runner.write_files({ "demo/f1.txt": "", "demo/f2.txt": "", "demo/BUILD": dedent("""\ file_generator(name='generator', sources=['*.txt']) nonfile_generator(name='nonfile') target(name='not-generator', sources=['*.txt']) """), }) assert resolve_raw_specs_with_only_file_owners( rule_runner, [FileLiteralSpec("demo/f1.txt")]) == [ Address("demo", target_name="not-generator"), Address("demo", target_name="generator", relative_file_path="f1.txt"), ]
def test_specs_to_dirs() -> None: assert specs_to_dirs(RawSpecs(description_of_origin="tests")) == ("", ) assert specs_to_dirs( RawSpecs(address_literals=(AddressLiteralSpec("src/python/foo"), ), description_of_origin="tests")) == ("src/python/foo", ) assert specs_to_dirs( RawSpecs(dir_literals=(DirLiteralSpec("src/python/foo"), ), description_of_origin="tests")) == ("src/python/foo", ) assert specs_to_dirs( RawSpecs( address_literals=( AddressLiteralSpec("src/python/foo"), AddressLiteralSpec("src/python/bar"), ), description_of_origin="tests", )) == ("src/python/foo", "src/python/bar") with pytest.raises(ValueError): specs_to_dirs( RawSpecs(file_literals=(FileLiteralSpec("src/python/foo.py"), ), description_of_origin="tests")) with pytest.raises(ValueError): specs_to_dirs( RawSpecs( address_literals=(AddressLiteralSpec("src/python/bar", "tgt"), ), description_of_origin="tests", )) with pytest.raises(ValueError): specs_to_dirs( RawSpecs( address_literals=(AddressLiteralSpec( "src/python/bar", target_component=None, generated_component="gen"), ), description_of_origin="tests", ))
def test_specs_to_dirs() -> None: assert specs_to_dirs(Specs(AddressSpecs([]), FilesystemSpecs([]))) == ("", ) assert specs_to_dirs( Specs(AddressSpecs([AddressLiteralSpec("src/python/foo")]), FilesystemSpecs([]))) == ("src/python/foo", ) assert specs_to_dirs( Specs(AddressSpecs([]), FilesystemSpecs([DirLiteralSpec("src/python/foo") ]))) == ("src/python/foo", ) assert specs_to_dirs( Specs( AddressSpecs([ AddressLiteralSpec("src/python/foo"), AddressLiteralSpec("src/python/bar") ]), FilesystemSpecs([]), )) == ("src/python/foo", "src/python/bar") with pytest.raises(ValueError): specs_to_dirs( Specs(AddressSpecs([]), FilesystemSpecs([FileLiteralSpec("src/python/foo.py")]))) with pytest.raises(ValueError): specs_to_dirs( Specs(AddressSpecs([AddressLiteralSpec("src/python/bar", "tgt")]), FilesystemSpecs([]))) with pytest.raises(ValueError): specs_to_dirs( Specs( AddressSpecs([ AddressLiteralSpec("src/python/bar", target_component=None, generated_component="gen") ]), FilesystemSpecs([]), ))
def calculate_specs( options_bootstrapper: OptionsBootstrapper, options: Options, session: SchedulerSession, ) -> Specs: """Determine the specs for a given Pants run.""" global_options = options.for_global_scope() unmatched_cli_globs = global_options.unmatched_cli_globs.to_glob_match_error_behavior( ) convert_dir_literal_to_address_literal = ( global_options.use_deprecated_directory_cli_args_semantics) if global_options.is_default( "use_deprecated_directory_cli_args_semantics"): warn_or_error( "2.14.0.dev0", "`use_deprecated_directory_cli_args_semantics` defaulting to True", softwrap(f""" Currently, a directory argument like `{bin_name()} test dir` is shorthand for the target `dir:dir`, i.e. the target that leaves off `name=`. In Pants 2.14, by default, a directory argument will instead match all targets/files in the directory. To opt into the new and more intuitive semantics early, set `use_deprecated_directory_cli_args_semantics = false` in the `[GLOBAL]` section in `pants.toml`. Otherwise, set to `true` to silence this warning. """), ) specs = SpecsParser().parse_specs( options.specs, description_of_origin="CLI arguments", unmatched_glob_behavior=unmatched_cli_globs, convert_dir_literal_to_address_literal= convert_dir_literal_to_address_literal, ) changed_options = ChangedOptions.from_options(options.for_scope("changed")) logger.debug("specs are: %s", specs) logger.debug("changed_options are: %s", changed_options) if specs and changed_options.provided: changed_name = "--changed-since" if changed_options.since else "--changed-diffspec" specs_description = specs.arguments_provided_description() assert specs_description is not None raise InvalidSpecConstraint( f"You used `{changed_name}` at the same time as using {specs_description}. You can " f"only use `{changed_name}` or use normal arguments.") if not changed_options.provided: return specs (git_binary, ) = session.product_request(GitBinary, [Params(GitBinaryRequest())]) (maybe_git_worktree, ) = session.product_request( MaybeGitWorktree, [Params(GitWorktreeRequest(), git_binary)]) if not maybe_git_worktree.git_worktree: raise InvalidSpecConstraint( "The `--changed-*` options are only available if Git is used for the repository." ) changed_files = tuple( changed_options.changed_files(maybe_git_worktree.git_worktree)) file_literal_specs = tuple(FileLiteralSpec(f) for f in changed_files) changed_request = ChangedRequest(changed_files, changed_options.dependees) (changed_addresses, ) = session.product_request( ChangedAddresses, [Params(changed_request, options_bootstrapper)]) logger.debug("changed addresses: %s", changed_addresses) address_literal_specs = [] for address in cast(ChangedAddresses, changed_addresses): address_input = AddressInput.parse( address.spec, description_of_origin="`--changed-since`") address_literal_specs.append( AddressLiteralSpec( path_component=address_input.path_component, target_component=address_input.target_component, generated_component=address_input.generated_component, parameters=address_input.parameters, )) return Specs( includes=RawSpecs( # We need both address_literals and file_literals to cover all our edge cases, including # target-aware vs. target-less goals, e.g. `list` vs `count-loc`. address_literals=tuple(address_literal_specs), file_literals=file_literal_specs, unmatched_glob_behavior=unmatched_cli_globs, filter_by_global_options=True, from_change_detection=True, description_of_origin="`--changed-since`", ), ignores=RawSpecs(description_of_origin="`--changed-since`"), )
def test_no_applicable_targets_exception() -> None: # Check that we correctly render the error message. class Tgt1(Target): alias = "tgt1" core_fields = () class Tgt2(Target): alias = "tgt2" core_fields = (MultipleSourcesField, ) class Tgt3(Target): alias = "tgt3" core_fields = () # No targets/files specified. Because none of the relevant targets have a sources field, we do # not give the filedeps command. exc = NoApplicableTargetsException( [], Specs.empty(), UnionMembership({}), applicable_target_types=[Tgt1], goal_description="the `foo` goal", ) remedy = ( "Please specify relevant file and/or target arguments. Run `./pants " "--filter-target-type=tgt1 list ::` to find all applicable targets in your project." ) assert (dedent(f"""\ No files or targets specified. The `foo` goal works with these target types: * tgt1 {remedy}""") in str(exc)) invalid_tgt = Tgt3({}, Address("blah")) exc = NoApplicableTargetsException( [invalid_tgt], Specs( includes=RawSpecs(file_literals=(FileLiteralSpec("foo.ext"), ), description_of_origin="tests"), ignores=RawSpecs(description_of_origin="tests"), ), UnionMembership({}), applicable_target_types=[Tgt1, Tgt2], goal_description="the `foo` goal", ) remedy = ( "Please specify relevant file and/or target arguments. Run `./pants " "--filter-target-type=tgt1,tgt2 list ::` to find all applicable targets in your project, " "or run `./pants --filter-target-type=tgt1,tgt2 filedeps ::` to find all " "applicable files.") assert (dedent(f"""\ No applicable files or targets matched. The `foo` goal works with these target types: * tgt1 * tgt2 However, you only specified file arguments with these target types: * tgt3 {remedy}""") in str(exc)) # Test handling of `Specs`. exc = NoApplicableTargetsException( [invalid_tgt], Specs( includes=RawSpecs(address_literals=(AddressLiteralSpec( "foo", "bar"), ), description_of_origin="tests"), ignores=RawSpecs(description_of_origin="tests"), ), UnionMembership({}), applicable_target_types=[Tgt1], goal_description="the `foo` goal", ) assert "However, you only specified target arguments with these target types:" in str( exc) exc = NoApplicableTargetsException( [invalid_tgt], Specs( includes=RawSpecs( address_literals=(AddressLiteralSpec("foo", "bar"), ), file_literals=(FileLiteralSpec("foo.ext"), ), description_of_origin="tests", ), ignores=RawSpecs(description_of_origin="tests"), ), UnionMembership({}), applicable_target_types=[Tgt1], goal_description="the `foo` goal", ) assert "However, you only specified target and file arguments with these target types:" in str( exc)
def file_literal(file: str) -> FileLiteralSpec: return FileLiteralSpec(file)