diff --git a/src/installer/_core.py b/src/installer/_core.py index e23f06e..7889474 100644 --- a/src/installer/_core.py +++ b/src/installer/_core.py @@ -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: @@ -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. @@ -121,6 +123,18 @@ def install( source=source, root_scheme=root_scheme, ) + if scheme == "scripts" and destination_path in generated_script_paths: + warnings.warn( + ( + f"Skip installing script {path} from {source.distribution}" + f" because it conflicts with an entry point script named" + f" {destination_path!r}." + ), + RuntimeWarning, + stacklevel=2, + ) + continue + record = destination.write_file( scheme=scheme, path=destination_path, diff --git a/tests/test_core.py b/tests/test_core.py index 121b9d3..72c5d39 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -28,7 +28,10 @@ def custom_write_file(scheme, path, stream, is_executable): return (path, scheme, 0) def custom_write_script(name, module, attr, section): - return (name, module, attr, section) + assert module + assert attr + assert section in {"console", "gui"} + return RecordEntry(name, None, None) retval.write_file.side_effect = custom_write_file retval.write_script.side_effect = custom_write_script @@ -212,8 +215,8 @@ def main(): scheme="purelib", record_file_path="fancy-1.0.0.dist-info/RECORD", records=[ - ("scripts", ("fancy", "fancy", "main", "console")), - ("scripts", ("fancy-gui", "fancy", "main", "gui")), + ("scripts", RecordEntry("fancy", None, None)), + ("scripts", RecordEntry("fancy-gui", None, None)), ("purelib", ("fancy/__init__.py", "purelib", 0)), ("purelib", ("fancy/__main__.py", "purelib", 0)), ("purelib", ("fancy-1.0.0.dist-info/METADATA", "purelib", 0)), @@ -467,8 +470,8 @@ def main(): scheme="platlib", record_file_path="fancy-1.0.0.dist-info/RECORD", records=[ - ("scripts", ("fancy", "fancy", "main", "console")), - ("scripts", ("fancy-gui", "fancy", "main", "gui")), + ("scripts", RecordEntry("fancy", None, None)), + ("scripts", RecordEntry("fancy-gui", None, None)), ("platlib", ("fancy/__init__.py", "platlib", 0)), ("platlib", ("fancy/__main__.py", "platlib", 0)), ("platlib", ("fancy-1.0.0.dist-info/METADATA", "platlib", 0)), @@ -754,8 +757,8 @@ def test_handles_data_properly(self, mock_destination): scheme="purelib", record_file_path="fancy-1.0.0.dist-info/RECORD", records=[ - ("scripts", ("fancy", "fancy", "main", "console")), - ("scripts", ("fancy-gui", "fancy", "main", "gui")), + ("scripts", RecordEntry("fancy", None, None)), + ("scripts", RecordEntry("fancy-gui", None, None)), ("data", ("fancy/data.py", "data", 0)), ("headers", ("fancy/headers.py", "headers", 0)), ("platlib", ("fancy/platlib.py", "platlib", 0)), @@ -781,6 +784,54 @@ def test_handles_data_properly(self, mock_destination): ] ) + def test_skips_data_script_that_matches_entrypoint(self, mock_destination): + source = FakeWheelSource( + distribution="fancy", + version="1.0.0", + regular_files={ + "fancy/__init__.py": b"", + "fancy-1.0.0.data/scripts/fancy": b"""\ + #!/usr/bin/env python + print("legacy script") + """, + }, + dist_info_files={ + "top_level.txt": b"""\ + fancy + """, + "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="conflicts with an entry point script"): + install( + source=source, + destination=mock_destination, + additional_metadata={}, + ) + + assert not any( + call.kwargs["scheme"] == "scripts" and call.kwargs["path"] == "fancy" + for call in mock_destination.write_file.call_args_list + ) + records = mock_destination.finalize_installation.call_args.kwargs["records"] + script_records = [record for scheme, record in records if scheme == "scripts"] + assert script_records == [RecordEntry("fancy", None, None)] + def test_errors_out_when_given_invalid_scheme_in_data(self, mock_destination): # Create a fake wheel source = FakeWheelSource(