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
13 changes: 13 additions & 0 deletions src/installer/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def install(
# RECORD handling
record_file_path = posixpath.join(source.dist_info_dir, "RECORD")
written_records = []
generated_script_paths: set[str] = set()

# Write the entry_points based scripts.
if "entry_points.txt" in source.dist_info_filenames:
Expand All @@ -92,6 +93,7 @@ def install(
attr=attr,
section=section,
)
generated_script_paths.add(record.path)
written_records.append((Scheme("scripts"), record))

# Write all the files from the wheel.
Expand Down Expand Up @@ -121,6 +123,17 @@ def install(
source=source,
root_scheme=root_scheme,
)
if scheme == "scripts" and destination_path in generated_script_paths:
warnings.warn(
(
f"Skip installing {path} from {source.distribution}."
" It would overwrite a script generated from an entry point."
),
RuntimeWarning,
stacklevel=2,
)
continue

record = destination.write_file(
scheme=scheme,
path=destination_path,
Expand Down
109 changes: 108 additions & 1 deletion tests/test_core.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import hashlib
import textwrap
from io import BytesIO
from pathlib import Path
from unittest import mock

import pytest

from installer import install
from installer.destinations import SchemeDictionaryDestination
from installer.exceptions import InvalidWheelSource
from installer.records import RecordEntry
from installer.sources import WheelSource
from installer.utils import SCHEME_NAMES


# --------------------------------------------------------------------------------------
Expand All @@ -22,13 +25,18 @@ def hash_and_size(data):
def mock_destination():
retval = mock.Mock()

class MockScriptRecord(tuple):
@property
def path(self):
return self[0]

# A hacky approach to making sure we got the right objects going in.
def custom_write_file(scheme, path, stream, is_executable):
assert isinstance(stream, BytesIO)
return (path, scheme, 0)

def custom_write_script(name, module, attr, section):
return (name, module, attr, section)
return MockScriptRecord((name, module, attr, section))

retval.write_file.side_effect = custom_write_file
retval.write_script.side_effect = custom_write_script
Expand Down Expand Up @@ -987,3 +995,102 @@ def test_skips_pycache_and_warns(self, mock_destination):
assert sub_good_path in record_paths
assert top_pycache_path not in record_paths
assert sub_pycache_path not in record_paths

def test_skips_script_when_entrypoint_has_same_name(self, mock_destination):
source = FakeWheelSource(
distribution="fancy",
version="1.0.0",
regular_files={
"fancy-1.0.0.data/scripts/fancy": b"""\
#!/usr/bin/env python
print("script")
""",
"fancy-1.0.0.data/scripts/fancy-extra": b"""\
#!/usr/bin/env python
print("other script")
""",
},
dist_info_files={
"entry_points.txt": b"""\
[console_scripts]
fancy = fancy: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.warns(RuntimeWarning, match="generated from an entry point"):
install(
source=source,
destination=mock_destination,
additional_metadata={},
)

written_files = {
(kwargs["scheme"], kwargs["path"])
for __, kwargs in mock_destination.write_file.call_args_list
}
assert ("scripts", "fancy") not in written_files
assert ("scripts", "fancy-extra") in written_files

records = mock_destination.finalize_installation.call_args[1]["records"]
script_record_paths = [
rec.path if isinstance(rec, RecordEntry) else rec[0]
for scheme, rec in records
if scheme == "scripts"
]
assert script_record_paths == ["fancy", "fancy-extra"]

def test_script_entrypoint_conflict_does_not_overwrite_script(self, tmp_path):
source = FakeWheelSource(
distribution="fancy",
version="1.0.0",
regular_files={
"fancy-1.0.0.data/scripts/fancy": b"""\
#!/usr/bin/env python
print("wheel script")
""",
},
dist_info_files={
"entry_points.txt": b"""\
[console_scripts]
fancy = fancy: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
""",
},
)
scheme_dict = {}
for scheme in SCHEME_NAMES:
path = tmp_path / scheme
path.mkdir()
scheme_dict[scheme] = str(path)
destination = SchemeDictionaryDestination(scheme_dict, "/my/python", "posix")

with pytest.warns(RuntimeWarning, match="generated from an entry point"):
install(source=source, destination=destination, additional_metadata={})

script_path = Path(scheme_dict["scripts"]) / "fancy"
assert script_path.exists()
script_contents = script_path.read_bytes()
assert b"from fancy import main" in script_contents
assert b"wheel script" not in script_contents
Loading