Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions src/installer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
cast,
)

from installer.exceptions import InstallerError

if TYPE_CHECKING:
from installer.records import RecordEntry
from installer.scripts import LauncherKind, ScriptSection
Expand All @@ -31,6 +33,7 @@

__all__ = [
"SCHEME_NAMES",
"InvalidEntryPoint",
"WheelFilename",
"construct_record_file",
"copyfileobj_with_hashing",
Expand Down Expand Up @@ -63,6 +66,21 @@
"WheelFilename", ["distribution", "version", "build_tag", "tag"]
)


class InvalidEntryPoint(ValueError, InstallerError):
"""Raised when an ``entry_points.txt`` entry is invalid."""

def __init__(self, *, section: str, name: str, value: str, reason: str) -> None:
"""Initialize with context about the invalid entry point."""
super().__init__(
f"Invalid entry point {name!r} in [{section}]: {value!r}. {reason}"
)
self.section = section
self.name = name
self.value = value
self.reason = reason


# Adapted from https://github.com/python/importlib_metadata/blob/v3.4.0/importlib_metadata/__init__.py#L90
_ENTRYPOINT_REGEX = re.compile(
r"""
Expand Down Expand Up @@ -230,6 +248,7 @@ def parse_entrypoints(text: str) -> Iterable[tuple[str, str, str, "ScriptSection
:param text: entire contents of the file
:return:
name of the script, module to use, attribute to call, kind of script (cli / gui)
:raises InvalidEntryPoint: if an entry point cannot be parsed.
"""
# Borrowed from https://github.com/python/importlib_metadata/blob/v3.4.0/importlib_metadata/__init__.py#L115
config = ConfigParser(delimiters="=")
Expand All @@ -243,14 +262,25 @@ def parse_entrypoints(text: str) -> Iterable[tuple[str, str, str, "ScriptSection
for name, value in config.items(section):
assert isinstance(name, str)
match = _ENTRYPOINT_REGEX.match(value)
assert match
if not match:
raise InvalidEntryPoint(
section=section,
name=name,
value=value,
reason="Expected a reference in the form module:attribute.",
)

module = match.group("module")
assert isinstance(module, str)

attrs = match.group("attrs")
# TODO: make this a proper error, which can be caught.
assert attrs is not None
if attrs is None: # pragma: no cover
raise InvalidEntryPoint(
section=section,
name=name,
value=value,
reason="Missing callable attribute.",
)
assert isinstance(attrs, str)

script_section = cast("ScriptSection", section[: -len("_scripts")])
Expand Down
39 changes: 39 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from installer.exceptions import InvalidWheelSource
from installer.records import RecordEntry
from installer.sources import WheelSource
from installer.utils import InvalidEntryPoint


# --------------------------------------------------------------------------------------
Expand Down Expand Up @@ -351,6 +352,44 @@ def main():
]
)

def test_invalid_entrypoint_errors_clearly(self, mock_destination):
source = FakeWheelSource(
distribution="fancy",
version="1.0.0",
regular_files={
"fancy/__init__.py": b"",
},
dist_info_files={
"entry_points.txt": b"""\
[console_scripts]
fancy = fancy-project:main
""",
"WHEEL": b"""\
Wheel-Version: 1.0
Generator: magic (1.0.0)
Root-Is-Purelib: true
Tag: py3-none-any
""",
"METADATA": b"""\
Metadata-Version: 2.1
Name: fancy
Version: 1.0.0
""",
},
)

with pytest.raises(InvalidEntryPoint) as ctx:
install(
source=source,
destination=mock_destination,
additional_metadata={},
)

assert ctx.value.section == "console_scripts"
assert ctx.value.name == "fancy"
assert ctx.value.value == "fancy-project:main"
assert "fancy-project:main" in str(ctx.value)

def test_handles_platlib(self, mock_destination):
# Create a fake wheel
source = FakeWheelSource(
Expand Down
34 changes: 34 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from installer.records import RecordEntry
from installer.utils import (
InvalidEntryPoint,
WheelFilename,
canonicalize_name,
construct_record_file,
Expand Down Expand Up @@ -264,3 +265,36 @@ class TestParseEntryPoints:
def test_valid(self, script, expected):
iterable = parse_entrypoints(textwrap.dedent(script))
assert list(iterable) == expected, expected

@pytest.mark.parametrize(
("script", "expected_name", "expected_value"),
[
pytest.param(
"""
[console_scripts]
package = package-main:main
""",
"package",
"package-main:main",
id="invalid-module",
),
pytest.param(
"""
[console_scripts]
package = package.__main__
""",
"package",
"package.__main__",
id="missing-attribute",
),
],
)
def test_invalid(self, script, expected_name, expected_value):
with pytest.raises(InvalidEntryPoint) as ctx:
list(parse_entrypoints(textwrap.dedent(script)))

assert ctx.value.section == "console_scripts"
assert ctx.value.name == expected_name
assert ctx.value.value == expected_value
assert expected_name in str(ctx.value)
assert expected_value in str(ctx.value)
Loading