def main(self) -> MainSpecification: is_default_console_script = self.options.is_default("console_script") is_default_entry_point = self.options.is_default("entry_point") if not is_default_console_script and not is_default_entry_point: raise OptionsError( f"Both [{self.scope}].console-script={self.options.console_script} and " f"[{self.scope}].entry-point={self.options.entry_point} are configured but these " f"options are mutually exclusive. Please pick one.") if not is_default_console_script: return ConsoleScript(cast(str, self.options.console_script)) if not is_default_entry_point: return EntryPoint.parse(cast(str, self.options.entry_point)) return self.default_main
def test_resolve_pex_binary_entry_point() -> None: rule_runner = RuleRunner( rules=[ resolve_pex_entry_point, QueryRule(ResolvedPexEntryPoint, [ResolvePexEntryPointRequest]), ] ) def assert_resolved( *, entry_point: str | None, expected: EntryPoint | None, is_file: bool ) -> None: addr = Address("src/python/project") rule_runner.write_files( { "src/python/project/app.py": "", "src/python/project/f2.py": "", } ) ep_field = PexEntryPointField(entry_point, addr) result = rule_runner.request(ResolvedPexEntryPoint, [ResolvePexEntryPointRequest(ep_field)]) assert result.val == expected assert result.file_name_used == is_file # Full module provided. assert_resolved( entry_point="custom.entry_point", expected=EntryPoint("custom.entry_point"), is_file=False ) assert_resolved( entry_point="custom.entry_point:func", expected=EntryPoint.parse("custom.entry_point:func"), is_file=False, ) # File names are expanded into the full module path. assert_resolved(entry_point="app.py", expected=EntryPoint(module="project.app"), is_file=True) assert_resolved( entry_point="app.py:func", expected=EntryPoint(module="project.app", function="func"), is_file=True, ) with pytest.raises(ExecutionError): assert_resolved( entry_point="doesnt_exist.py", expected=EntryPoint("doesnt matter"), is_file=True ) # Resolving >1 file is an error. with pytest.raises(ExecutionError): assert_resolved(entry_point="*.py", expected=EntryPoint("doesnt matter"), is_file=True)
def main(self) -> MainSpecification: is_default_console_script = self.options.is_default("console_script") is_default_entry_point = self.options.is_default("entry_point") if not is_default_console_script and not is_default_entry_point: raise OptionsError( softwrap(f""" Both [{self.options_scope}].console-script={self.console_script} and [{self.options_scope}].entry-point={self.entry_point} are configured but these options are mutually exclusive. Please pick one. """)) if not is_default_console_script: assert self.console_script is not None return ConsoleScript(self.console_script) if not is_default_entry_point: assert self.entry_point is not None return EntryPoint.parse(self.entry_point) return self.default_main
def test_resolve_pex_binary_entry_point() -> None: rule_runner = RuleRunner(rules=[ resolve_pex_entry_point, QueryRule(ResolvedPexEntryPoint, [ResolvePexEntryPointRequest]), ]) def assert_resolved(*, entry_point: Optional[str], expected: Optional[EntryPoint]) -> None: addr = Address("src/python/project") rule_runner.create_file("src/python/project/app.py") rule_runner.create_file("src/python/project/f2.py") ep_field = PexEntryPointField(entry_point, address=addr) result = rule_runner.request(ResolvedPexEntryPoint, [ResolvePexEntryPointRequest(ep_field)]) assert result.val == expected # Full module provided. assert_resolved(entry_point="custom.entry_point", expected=EntryPoint("custom.entry_point")) assert_resolved(entry_point="custom.entry_point:func", expected=EntryPoint.parse("custom.entry_point:func")) # File names are expanded into the full module path. assert_resolved(entry_point="app.py", expected=EntryPoint(module="project.app")) assert_resolved(entry_point="app.py:func", expected=EntryPoint(module="project.app", function="func")) # We special case the strings `<none>` and `<None>`. assert_resolved(entry_point="<none>", expected=None) assert_resolved(entry_point="<None>", expected=None) with pytest.raises(ExecutionError): assert_resolved(entry_point="doesnt_exist.py", expected=EntryPoint("doesnt matter")) # Resolving >1 file is an error. with pytest.raises(ExecutionError): assert_resolved(entry_point="*.py", expected=EntryPoint("doesnt matter"))
async def resolve_python_distribution_entry_points( request: ResolvePythonDistributionEntryPointsRequest, ) -> ResolvedPythonDistributionEntryPoints: if request.entry_points_field: if request.entry_points_field.value is None: return ResolvedPythonDistributionEntryPoints() address = request.entry_points_field.address all_entry_points = cast(_EntryPointsDictType, request.entry_points_field.value) elif request.provides_field: address = request.provides_field.address provides_field_value = cast( _EntryPointsDictType, request.provides_field.value.kwargs.get("entry_points") or {} ) if provides_field_value: all_entry_points = provides_field_value else: return ResolvedPythonDistributionEntryPoints() else: return ResolvedPythonDistributionEntryPoints() classified_entry_points = list(_classify_entry_points(all_entry_points)) # Pick out all target addresses up front, so we can use MultiGet later. # # This calls for a bit of trickery however (using the "y_by_x" mapping dicts), so we keep track # of which address belongs to which entry point. I.e. the `address_by_ref` and # `binary_entry_point_by_address` variables. target_refs = [ entry_point_str for is_target, _, _, entry_point_str in classified_entry_points if is_target ] # Intermediate step, as Get(Targets) returns a deduplicated set.. which breaks in case of # multiple input refs that maps to the same target. target_addresses = await Get( Addresses, UnparsedAddressInputs( target_refs, owning_address=address, description_of_origin="TODO(#14468)", ), ) address_by_ref = dict(zip(target_refs, target_addresses)) targets = await Get(Targets, Addresses, target_addresses) # Check that we only have targets with a pex entry_point field. for target in targets: if not target.has_field(PexEntryPointField): raise InvalidEntryPoint( softwrap( f""" All target addresses in the entry_points field must be for pex_binary targets, but the target {address} includes the value {target.address}, which has the target type {target.alias}. Alternatively, you can use a module like "project.app:main". See {doc_url('python-distributions')}. """ ) ) binary_entry_points = await MultiGet( Get( ResolvedPexEntryPoint, ResolvePexEntryPointRequest(target[PexEntryPointField]), ) for target in targets ) binary_entry_point_by_address = { target.address: entry_point for target, entry_point in zip(targets, binary_entry_points) } entry_points: DefaultDict[str, Dict[str, PythonDistributionEntryPoint]] = defaultdict(dict) # Parse refs/replace with resolved pex entry point, and validate console entry points have function. for is_target, category, name, ref in classified_entry_points: owner: Optional[Address] = None if is_target: owner = address_by_ref[ref] entry_point = binary_entry_point_by_address[owner].val if entry_point is None: logger.warning( softwrap( f""" The entry point {name} in {category} references a pex_binary target {ref} which does not set `entry_point`. Skipping. """ ) ) continue else: entry_point = EntryPoint.parse(ref, f"{name} for {address} {category}") if category in ["console_scripts", "gui_scripts"] and not entry_point.function: url = "https://python-packaging.readthedocs.io/en/latest/command-line-scripts.html#the-console-scripts-entry-point" raise InvalidEntryPoint( dedent( f"""\ Every entry point in `{category}` for {address} must end in the format `:my_func`, but {name} set it to {entry_point.spec!r}. For example, set `entry_points={{"{category}": {{"{name}": "{entry_point.module}:main}} }}`. See {url}. """ ) ) entry_points[category][name] = PythonDistributionEntryPoint(entry_point, owner) return ResolvedPythonDistributionEntryPoints( FrozenDict( {category: FrozenDict(entry_points) for category, entry_points in entry_points.items()} ) )