Skip to content

Commit

Permalink
Add script field to pex_binary for console scripts (#12849)
Browse files Browse the repository at this point in the history
Closes #11619.

This will benefit from #9561 to ensure both `script` and `entry_point` are not set at the same time. For now, `entry_point` wins out.

This means that we no longer make `entry_point` required because it's valid to set `script` instead. So, we deprecate `entry_point="<none>"` to indicate there is no entry point - now you leave off both the `script` and `entry_point` fields.

[ci skip-rust]
[ci skip-build-wheels]
  • Loading branch information
Eric-Arellano authored Sep 10, 2021
1 parent 2fd3bde commit add1a2d
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 32 deletions.
6 changes: 3 additions & 3 deletions src/python/pants/backend/python/goals/package_pex_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)
from pants.backend.python.target_types import PexPlatformsField as PythonPlatformsField
from pants.backend.python.target_types import (
PexScriptField,
PexShebangField,
PexStripEnvField,
PexZipSafeField,
Expand Down Expand Up @@ -54,6 +55,7 @@ class PexBinaryFieldSet(PackageFieldSet, RunFieldSet):
required_fields = (PexEntryPointField,)

entry_point: PexEntryPointField
script: PexScriptField

output_path: OutputPathField
always_write_cache: PexAlwaysWriteCacheField
Expand Down Expand Up @@ -126,9 +128,7 @@ async def package_pex_binary(
PexFromTargetsRequest(
addresses=[field_set.address],
internal_only=False,
# TODO(John Sirois): Support ConsoleScript in PexBinary targets:
# https://github.com/pantsbuild/pants/issues/11619
main=resolved_entry_point.val,
main=resolved_entry_point.val or field_set.script.value,
platforms=PexPlatforms.create_from_platforms_field(field_set.platforms),
resolve_and_lockfile=field_set.resolve.resolve_and_lockfile(python_setup),
output_filename=output_filename,
Expand Down
4 changes: 1 addition & 3 deletions src/python/pants/backend/python/goals/run_pex_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ async def create_pex_binary_run_request(
include_source_files=False,
# Note that the file for first-party entry points is not in the PEX itself. In that
# case, it's loaded by setting `PEX_EXTRA_SYS_PATH`.
# TODO(John Sirois): Support ConsoleScript in PexBinary targets:
# https://github.com/pantsbuild/pants/issues/11619
main=entry_point.val,
main=entry_point.val or field_set.script.value,
resolve_and_lockfile=field_set.resolve.resolve_and_lockfile(python_setup),
additional_args=(
*field_set.generate_additional_args(pex_binary_defaults),
Expand Down
56 changes: 46 additions & 10 deletions src/python/pants/backend/python/target_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,30 +253,44 @@ def spec(self) -> str:

class PexEntryPointField(AsyncFieldMixin, SecondaryOwnerMixin, Field):
alias = "entry_point"
default = None
help = (
"The entry point for the binary, i.e. what gets run when executing `./my_binary.pex`.\n\n"
"Set the entry point, i.e. what gets run when executing `./my_app.pex`, to a module.\n\n"
"You can specify a full module like 'path.to.module' and 'path.to.module:func', or use a "
"shorthand to specify a file name, using the same syntax as the `sources` field:\n\n 1) "
"'app.py', Pants will convert into the module `path.to.app`;\n 2) 'app.py:func', Pants "
"will convert into `path.to.app:func`.\n\nYou must use the file name shorthand for file "
"arguments to work with this target.\n\nTo leave off an entry point, set to `<none>`."
"shorthand to specify a file name, using the same syntax as the `sources` field:\n\n"
" 1) 'app.py', Pants will convert into the module `path.to.app`;\n"
" 2) 'app.py:func', Pants will convert into `path.to.app:func`.\n\n"
"You must use the file name shorthand for file arguments to work with this target.\n\n"
"You may either set this field or the `script` field, but not both. Leave off both fields "
"to have no entry point."
)
required = True
value: EntryPoint
value: EntryPoint | None

@classmethod
def compute_value(cls, raw_value: Optional[str], address: Address) -> EntryPoint:
def compute_value(cls, raw_value: Optional[str], address: Address) -> Optional[EntryPoint]:
value = super().compute_value(raw_value, address)
if value is None:
return None
if not isinstance(value, str):
raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
if value in {"<none>", "<None>"}:
warn_or_error(
"2.9.0.dev0",
"using `<none>` for the `entry_point` field",
(
"Rather than setting `entry_point='<none>' for the pex_binary target "
f"{address}, simply leave off the field."
),
)
return None
try:
return EntryPoint.parse(value, provenance=f"for {address}")
except ValueError as e:
raise InvalidFieldException(str(e))

@property
def filespec(self) -> Filespec:
if not self.value.module.endswith(".py"):
if self.value is None or not self.value.module.endswith(".py"):
return {"includes": []}
full_glob = os.path.join(self.address.spec_path, self.value.module)
return {"includes": [full_glob]}
Expand All @@ -285,7 +299,7 @@ def filespec(self) -> Filespec:
# See `target_types_rules.py` for the `ResolvePexEntryPointRequest -> ResolvedPexEntryPoint` rule.
@dataclass(frozen=True)
class ResolvedPexEntryPoint:
val: Optional[EntryPoint]
val: EntryPoint | None
file_name_used: bool


Expand All @@ -296,6 +310,27 @@ class ResolvePexEntryPointRequest:
entry_point_field: PexEntryPointField


class PexScriptField(Field):
alias = "script"
default = None
help = (
"Set the entry point, i.e. what gets run when executing `./my_app.pex`, to a script or "
"console_script as defined by any of the distributions in the PEX.\n\n"
"You may either set this field or the `entry_point` field, but not both. Leave off both "
"fields to have no entry point."
)
value: ConsoleScript | None

@classmethod
def compute_value(cls, raw_value: Optional[str], address: Address) -> Optional[ConsoleScript]:
value = super().compute_value(raw_value, address)
if value is None:
return None
if not isinstance(value, str):
raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
return ConsoleScript(value)


class PexPlatformsField(StringSequenceField):
alias = "platforms"
help = (
Expand Down Expand Up @@ -468,6 +503,7 @@ class PexBinary(Target):
PythonResolveField,
PexBinaryDependencies,
PexEntryPointField,
PexScriptField,
PexPlatformsField,
PexInheritPathField,
PexZipSafeField,
Expand Down
30 changes: 14 additions & 16 deletions src/python/pants/backend/python/target_types_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,18 @@
@rule(desc="Determining the entry point for a `pex_binary` target", level=LogLevel.DEBUG)
async def resolve_pex_entry_point(request: ResolvePexEntryPointRequest) -> ResolvedPexEntryPoint:
ep_val = request.entry_point_field.value
if ep_val is None:
return ResolvedPexEntryPoint(None, file_name_used=False)
address = request.entry_point_field.address

# We support several different schemes:
# 1) `<none>` or `<None>` => set to `None`.
# 2) `path.to.module` => preserve exactly.
# 3) `path.to.module:func` => preserve exactly.
# 4) `app.py` => convert into `path.to.app`.
# 5) `app.py:func` => convert into `path.to.app:func`.

# Case #1.
if ep_val.module in ("<none>", "<None>"):
return ResolvedPexEntryPoint(None, file_name_used=False)
# 1) `path.to.module` => preserve exactly.
# 2) `path.to.module:func` => preserve exactly.
# 3) `app.py` => convert into `path.to.app`.
# 4) `app.py:func` => convert into `path.to.app:func`.

# If it's already a module (cases #2 and #3), simply use that. Otherwise, convert the file name
# into a module path (cases #4 and #5).
# If it's already a module (cases #1 and #2), simply use that. Otherwise, convert the file name
# into a module path (cases #3 and #4).
if not ep_val.module.endswith(".py"):
return ResolvedPexEntryPoint(ep_val, file_name_used=False)

Expand Down Expand Up @@ -123,12 +120,13 @@ async def inject_pex_binary_entry_point_dependency(
if not python_infer_subsystem.entry_points:
return InjectedDependencies()
original_tgt = await Get(WrappedTarget, Address, request.dependencies_field.address)
entry_point_field = original_tgt.target[PexEntryPointField]
if entry_point_field.value is None:
return InjectedDependencies()

explicitly_provided_deps, entry_point = await MultiGet(
Get(ExplicitlyProvidedDependencies, DependenciesRequest(original_tgt.target[Dependencies])),
Get(
ResolvedPexEntryPoint,
ResolvePexEntryPointRequest(original_tgt.target[PexEntryPointField]),
),
Get(ResolvedPexEntryPoint, ResolvePexEntryPointRequest(entry_point_field)),
)
if entry_point.val is None:
return InjectedDependencies()
Expand All @@ -143,7 +141,7 @@ async def inject_pex_binary_entry_point_dependency(
import_reference="module",
context=(
f"The pex_binary target {address} has the field "
f"`entry_point={repr(original_tgt.target[PexEntryPointField].value.spec)}`, which "
f"`entry_point={repr(entry_point_field.value.spec)}`, which "
f"maps to the Python module `{entry_point.val.module}`"
),
)
Expand Down

0 comments on commit add1a2d

Please sign in to comment.