summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/conftest.py75
-rw-r--r--tests/requirements.txt3
-rw-r--r--tests/test_core.py905
-rw-r--r--tests/test_destinations.py173
-rw-r--r--tests/test_main.py69
-rw-r--r--tests/test_records.py275
-rw-r--r--tests/test_scripts.py70
-rw-r--r--tests/test_sources.py94
-rw-r--r--tests/test_utils.py232
9 files changed, 1896 insertions, 0 deletions
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..029cb8f
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,75 @@
+import textwrap
+import zipfile
+
+import pytest
+
+
+@pytest.fixture
+def fancy_wheel(tmp_path):
+ path = tmp_path / "fancy-1.0.0-py2.py3-none-any.whl"
+ files = {
+ "fancy/": b"""""",
+ "fancy/__init__.py": b"""\
+ def main():
+ print("I'm fancy.")
+ """,
+ "fancy/__main__.py": b"""\
+ if __name__ == "__main__":
+ from . import main
+ main()
+ """,
+ "fancy-1.0.0.data/data/fancy/": b"""""",
+ "fancy-1.0.0.data/data/fancy/data.py": b"""\
+ # put me in data
+ """,
+ "fancy-1.0.0.dist-info/": b"""""",
+ "fancy-1.0.0.dist-info/top_level.txt": b"""\
+ fancy
+ """,
+ "fancy-1.0.0.dist-info/entry_points.txt": b"""\
+ [console_scripts]
+ fancy = fancy:main
+
+ [gui_scripts]
+ fancy-gui = fancy:main
+ """,
+ "fancy-1.0.0.dist-info/WHEEL": b"""\
+ Wheel-Version: 1.0
+ Generator: magic (1.0.0)
+ Root-Is-Purelib: true
+ Tag: py3-none-any
+ """,
+ "fancy-1.0.0.dist-info/METADATA": b"""\
+ Metadata-Version: 2.1
+ Name: fancy
+ Version: 1.0.0
+ Summary: A fancy package
+ Author: Agendaless Consulting
+ Author-email: nobody@example.com
+ License: MIT
+ Keywords: fancy amazing
+ Platform: UNKNOWN
+ Classifier: Intended Audience :: Developers
+ """,
+ # The RECORD file is indirectly validated by the WheelFile, since it only
+ # provides the items that are a part of the wheel.
+ "fancy-1.0.0.dist-info/RECORD": b"""\
+ fancy/__init__.py,,
+ fancy/__main__.py,,
+ fancy-1.0.0.data/data/fancy/data.py,,
+ fancy-1.0.0.dist-info/top_level.txt,,
+ fancy-1.0.0.dist-info/entry_points.txt,,
+ fancy-1.0.0.dist-info/WHEEL,,
+ fancy-1.0.0.dist-info/METADATA,,
+ fancy-1.0.0.dist-info/RECORD,,
+ """,
+ }
+
+ with zipfile.ZipFile(path, "w") as archive:
+ for name, indented_content in files.items():
+ archive.writestr(
+ name,
+ textwrap.dedent(indented_content.decode("utf-8")).encode("utf-8"),
+ )
+
+ return path
diff --git a/tests/requirements.txt b/tests/requirements.txt
new file mode 100644
index 0000000..78b1a59
--- /dev/null
+++ b/tests/requirements.txt
@@ -0,0 +1,3 @@
+pytest
+pytest-cov
+pytest-xdist
diff --git a/tests/test_core.py b/tests/test_core.py
new file mode 100644
index 0000000..1f3a44e
--- /dev/null
+++ b/tests/test_core.py
@@ -0,0 +1,905 @@
+import hashlib
+import textwrap
+from io import BytesIO
+from unittest import mock
+
+import pytest
+
+from installer import install
+from installer.exceptions import InvalidWheelSource
+from installer.records import RecordEntry
+from installer.sources import WheelSource
+
+
+# --------------------------------------------------------------------------------------
+# Helpers
+# --------------------------------------------------------------------------------------
+def hash_and_size(data):
+ return hashlib.sha256(data).hexdigest(), len(data)
+
+
+@pytest.fixture
+def mock_destination():
+ retval = mock.Mock()
+
+ # 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)
+
+ retval.write_file.side_effect = custom_write_file
+ retval.write_script.side_effect = custom_write_script
+
+ return retval
+
+
+class FakeWheelSource(WheelSource):
+ def __init__(self, *, distribution, version, regular_files, dist_info_files):
+ super().__init__(distribution, version)
+
+ self.dist_info_files = {
+ file: textwrap.dedent(content.decode("utf-8"))
+ for file, content in dist_info_files.items()
+ }
+ self.regular_files = {
+ file: textwrap.dedent(content.decode("utf-8")).encode("utf-8")
+ for file, content in regular_files.items()
+ }
+
+ # Compute RECORD file.
+ _records = [record for record, _, _ in self.get_contents()]
+ self.dist_info_files["RECORD"] = "\n".join(
+ sorted(
+ ",".join([file, "sha256=" + hash_, str(size)])
+ for file, hash_, size in _records
+ )
+ )
+
+ @property
+ def dist_info_filenames(self):
+ return list(self.dist_info_files)
+
+ def read_dist_info(self, filename):
+ return self.dist_info_files[filename]
+
+ def get_contents(self):
+ # Sort for deterministic behaviour for Python versions that do not preserve
+ # insertion order for dictionaries.
+ for file, content in sorted(self.regular_files.items()):
+ hashed, size = hash_and_size(content)
+ record = (file, f"sha256={hashed}", str(size))
+ with BytesIO(content) as stream:
+ yield record, stream, False
+
+ # Sort for deterministic behaviour for Python versions that do not preserve
+ # insertion order for dictionaries.
+ for file, text in sorted(self.dist_info_files.items()):
+ content = text.encode("utf-8")
+ hashed, size = hash_and_size(content)
+ record = (
+ self.dist_info_dir + "/" + file,
+ f"sha256={hashed}",
+ str(size),
+ )
+ with BytesIO(content) as stream:
+ yield record, stream, False
+
+
+# --------------------------------------------------------------------------------------
+# Actual Tests
+# --------------------------------------------------------------------------------------
+class TestInstall:
+ def test_calls_destination_correctly(self, mock_destination):
+ # Create a fake wheel
+ source = FakeWheelSource(
+ distribution="fancy",
+ version="1.0.0",
+ regular_files={
+ "fancy/__init__.py": b"""\
+ def main():
+ print("I'm a fancy package")
+ """,
+ "fancy/__main__.py": b"""\
+ if __name__ == "__main__":
+ from . import main
+ main()
+ """,
+ },
+ dist_info_files={
+ "top_level.txt": b"""\
+ fancy
+ """,
+ "entry_points.txt": b"""\
+ [console_scripts]
+ fancy = fancy:main
+
+ [gui_scripts]
+ fancy-gui = 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
+ Summary: A fancy package
+ Author: Agendaless Consulting
+ Author-email: nobody@example.com
+ License: MIT
+ Keywords: fancy amazing
+ Platform: UNKNOWN
+ Classifier: Intended Audience :: Developers
+ """,
+ },
+ )
+
+ install(
+ source=source,
+ destination=mock_destination,
+ additional_metadata={
+ "fun_file.txt": b"this should be in dist-info!",
+ },
+ )
+
+ mock_destination.assert_has_calls(
+ [
+ mock.call.write_script(
+ name="fancy",
+ module="fancy",
+ attr="main",
+ section="console",
+ ),
+ mock.call.write_script(
+ name="fancy-gui",
+ module="fancy",
+ attr="main",
+ section="gui",
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy/__init__.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy/__main__.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/METADATA",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/WHEEL",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/entry_points.txt",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/top_level.txt",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/fun_file.txt",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.finalize_installation(
+ scheme="purelib",
+ record_file_path="fancy-1.0.0.dist-info/RECORD",
+ records=[
+ ("scripts", ("fancy", "fancy", "main", "console")),
+ ("scripts", ("fancy-gui", "fancy", "main", "gui")),
+ ("purelib", ("fancy/__init__.py", "purelib", 0)),
+ ("purelib", ("fancy/__main__.py", "purelib", 0)),
+ ("purelib", ("fancy-1.0.0.dist-info/METADATA", "purelib", 0)),
+ ("purelib", ("fancy-1.0.0.dist-info/WHEEL", "purelib", 0)),
+ (
+ "purelib",
+ ("fancy-1.0.0.dist-info/entry_points.txt", "purelib", 0),
+ ),
+ (
+ "purelib",
+ ("fancy-1.0.0.dist-info/top_level.txt", "purelib", 0),
+ ),
+ (
+ "purelib",
+ ("fancy-1.0.0.dist-info/fun_file.txt", "purelib", 0),
+ ),
+ (
+ "purelib",
+ RecordEntry("fancy-1.0.0.dist-info/RECORD", None, None),
+ ),
+ ],
+ ),
+ ]
+ )
+
+ def test_no_entrypoints_is_ok(self, mock_destination):
+ # Create a fake wheel
+ source = FakeWheelSource(
+ distribution="fancy",
+ version="1.0.0",
+ regular_files={
+ "fancy/__init__.py": b"""\
+ def main():
+ print("I'm a fancy package")
+ """,
+ "fancy/__main__.py": b"""\
+ if __name__ == "__main__":
+ from . import main
+ main()
+ """,
+ },
+ dist_info_files={
+ "top_level.txt": b"""\
+ fancy
+ """,
+ "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
+ Summary: A fancy package
+ Author: Agendaless Consulting
+ Author-email: nobody@example.com
+ License: MIT
+ Keywords: fancy amazing
+ Platform: UNKNOWN
+ Classifier: Intended Audience :: Developers
+ """,
+ },
+ )
+
+ install(
+ source=source,
+ destination=mock_destination,
+ additional_metadata={
+ "fun_file.txt": b"this should be in dist-info!",
+ },
+ )
+
+ mock_destination.assert_has_calls(
+ [
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy/__init__.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy/__main__.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/METADATA",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/WHEEL",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/top_level.txt",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/fun_file.txt",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.finalize_installation(
+ scheme="purelib",
+ record_file_path="fancy-1.0.0.dist-info/RECORD",
+ records=[
+ ("purelib", ("fancy/__init__.py", "purelib", 0)),
+ ("purelib", ("fancy/__main__.py", "purelib", 0)),
+ ("purelib", ("fancy-1.0.0.dist-info/METADATA", "purelib", 0)),
+ ("purelib", ("fancy-1.0.0.dist-info/WHEEL", "purelib", 0)),
+ (
+ "purelib",
+ ("fancy-1.0.0.dist-info/top_level.txt", "purelib", 0),
+ ),
+ (
+ "purelib",
+ ("fancy-1.0.0.dist-info/fun_file.txt", "purelib", 0),
+ ),
+ (
+ "purelib",
+ RecordEntry("fancy-1.0.0.dist-info/RECORD", None, None),
+ ),
+ ],
+ ),
+ ]
+ )
+
+ def test_handles_platlib(self, mock_destination):
+ # Create a fake wheel
+ source = FakeWheelSource(
+ distribution="fancy",
+ version="1.0.0",
+ regular_files={
+ "fancy/__init__.py": b"""\
+ def main():
+ print("I'm a fancy package")
+ """,
+ "fancy/__main__.py": b"""\
+ if __name__ == "__main__":
+ from . import main
+ main()
+ """,
+ },
+ dist_info_files={
+ "top_level.txt": b"""\
+ fancy
+ """,
+ "entry_points.txt": b"""\
+ [console_scripts]
+ fancy = fancy:main
+
+ [gui_scripts]
+ fancy-gui = fancy:main
+ """,
+ "WHEEL": b"""\
+ Wheel-Version: 1.0
+ Generator: magic (1.0.0)
+ Root-Is-Purelib: false
+ Tag: py3-none-any
+ """,
+ "METADATA": b"""\
+ Metadata-Version: 2.1
+ Name: fancy
+ Version: 1.0.0
+ Summary: A fancy package
+ Author: Agendaless Consulting
+ Author-email: nobody@example.com
+ License: MIT
+ Keywords: fancy amazing
+ Platform: UNKNOWN
+ Classifier: Intended Audience :: Developers
+ """,
+ },
+ )
+
+ install(
+ source=source,
+ destination=mock_destination,
+ additional_metadata={
+ "fun_file.txt": b"this should be in dist-info!",
+ },
+ )
+
+ mock_destination.assert_has_calls(
+ [
+ mock.call.write_script(
+ name="fancy",
+ module="fancy",
+ attr="main",
+ section="console",
+ ),
+ mock.call.write_script(
+ name="fancy-gui",
+ module="fancy",
+ attr="main",
+ section="gui",
+ ),
+ mock.call.write_file(
+ scheme="platlib",
+ path="fancy/__init__.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="platlib",
+ path="fancy/__main__.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="platlib",
+ path="fancy-1.0.0.dist-info/METADATA",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="platlib",
+ path="fancy-1.0.0.dist-info/WHEEL",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="platlib",
+ path="fancy-1.0.0.dist-info/entry_points.txt",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="platlib",
+ path="fancy-1.0.0.dist-info/top_level.txt",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="platlib",
+ path="fancy-1.0.0.dist-info/fun_file.txt",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.finalize_installation(
+ scheme="platlib",
+ record_file_path="fancy-1.0.0.dist-info/RECORD",
+ records=[
+ ("scripts", ("fancy", "fancy", "main", "console")),
+ ("scripts", ("fancy-gui", "fancy", "main", "gui")),
+ ("platlib", ("fancy/__init__.py", "platlib", 0)),
+ ("platlib", ("fancy/__main__.py", "platlib", 0)),
+ ("platlib", ("fancy-1.0.0.dist-info/METADATA", "platlib", 0)),
+ ("platlib", ("fancy-1.0.0.dist-info/WHEEL", "platlib", 0)),
+ (
+ "platlib",
+ ("fancy-1.0.0.dist-info/entry_points.txt", "platlib", 0),
+ ),
+ (
+ "platlib",
+ ("fancy-1.0.0.dist-info/top_level.txt", "platlib", 0),
+ ),
+ (
+ "platlib",
+ ("fancy-1.0.0.dist-info/fun_file.txt", "platlib", 0),
+ ),
+ (
+ "platlib",
+ RecordEntry("fancy-1.0.0.dist-info/RECORD", None, None),
+ ),
+ ],
+ ),
+ ]
+ )
+
+ def test_accepts_newer_minor_wheel_versions(self, mock_destination):
+ # Create a fake wheel
+ source = FakeWheelSource(
+ distribution="fancy",
+ version="1.0.0",
+ regular_files={
+ "fancy/__init__.py": b"""\
+ def main():
+ print("I'm a fancy package")
+ """,
+ "fancy/__main__.py": b"""\
+ if __name__ == "__main__":
+ from . import main
+ main()
+ """,
+ },
+ dist_info_files={
+ "top_level.txt": b"""\
+ fancy
+ """,
+ "entry_points.txt": b"""\
+ [console_scripts]
+ fancy = fancy:main
+
+ [gui_scripts]
+ fancy-gui = fancy:main
+ """,
+ "WHEEL": b"""\
+ Wheel-Version: 1.1
+ 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
+ Summary: A fancy package
+ Author: Agendaless Consulting
+ Author-email: nobody@example.com
+ License: MIT
+ Keywords: fancy amazing
+ Platform: UNKNOWN
+ Classifier: Intended Audience :: Developers
+ """,
+ },
+ )
+
+ install(
+ source=source,
+ destination=mock_destination,
+ additional_metadata={
+ "fun_file.txt": b"this should be in dist-info!",
+ },
+ )
+
+ # no assertions necessary, since we want to make sure this test didn't
+ # raises errors.
+ assert True
+
+ def test_rejects_newer_major_wheel_versions(self, mock_destination):
+ # Create a fake wheel
+ source = FakeWheelSource(
+ distribution="fancy",
+ version="1.0.0",
+ regular_files={
+ "fancy/__init__.py": b"""\
+ def main():
+ print("I'm a fancy package")
+ """,
+ "fancy/__main__.py": b"""\
+ if __name__ == "__main__":
+ from . import main
+ main()
+ """,
+ },
+ dist_info_files={
+ "top_level.txt": b"""\
+ fancy
+ """,
+ "entry_points.txt": b"""\
+ [console_scripts]
+ fancy = fancy:main
+
+ [gui_scripts]
+ fancy-gui = fancy:main
+ """,
+ "WHEEL": b"""\
+ Wheel-Version: 2.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
+ Summary: A fancy package
+ Author: Agendaless Consulting
+ Author-email: nobody@example.com
+ License: MIT
+ Keywords: fancy amazing
+ Platform: UNKNOWN
+ Classifier: Intended Audience :: Developers
+ """,
+ },
+ )
+
+ with pytest.raises(InvalidWheelSource) as ctx:
+ install(
+ source=source,
+ destination=mock_destination,
+ additional_metadata={
+ "fun_file.txt": b"this should be in dist-info!",
+ },
+ )
+
+ assert "Incompatible Wheel-Version" in str(ctx.value)
+
+ def test_handles_data_properly(self, mock_destination):
+ # Create a fake wheel
+ source = FakeWheelSource(
+ distribution="fancy",
+ version="1.0.0",
+ regular_files={
+ "fancy/__init__.py": b"""\
+ # put me in purelib
+ """,
+ "fancy-1.0.0.data/purelib/fancy/purelib.py": b"""\
+ # put me in purelib
+ """,
+ "fancy-1.0.0.data/platlib/fancy/platlib.py": b"""\
+ # put me in platlib
+ """,
+ "fancy-1.0.0.data/scripts/fancy/scripts.py": b"""\
+ # put me in scripts
+ """,
+ "fancy-1.0.0.data/headers/fancy/headers.py": b"""\
+ # put me in headers
+ """,
+ "fancy-1.0.0.data/data/fancy/data.py": b"""\
+ # put me in data
+ """,
+ },
+ dist_info_files={
+ "top_level.txt": b"""\
+ fancy
+ """,
+ "entry_points.txt": b"""\
+ [console_scripts]
+ fancy = fancy:main
+
+ [gui_scripts]
+ fancy-gui = 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
+ Summary: A fancy package
+ Author: Agendaless Consulting
+ Author-email: nobody@example.com
+ License: MIT
+ Keywords: fancy amazing
+ Platform: UNKNOWN
+ Classifier: Intended Audience :: Developers
+ """,
+ },
+ )
+
+ install(
+ source=source,
+ destination=mock_destination,
+ additional_metadata={},
+ )
+
+ mock_destination.assert_has_calls(
+ [
+ mock.call.write_script(
+ name="fancy",
+ module="fancy",
+ attr="main",
+ section="console",
+ ),
+ mock.call.write_script(
+ name="fancy-gui",
+ module="fancy",
+ attr="main",
+ section="gui",
+ ),
+ mock.call.write_file(
+ scheme="data",
+ path="fancy/data.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="headers",
+ path="fancy/headers.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="platlib",
+ path="fancy/platlib.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy/purelib.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="scripts",
+ path="fancy/scripts.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy/__init__.py",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/METADATA",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/WHEEL",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/entry_points.txt",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/top_level.txt",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ mock.call.finalize_installation(
+ scheme="purelib",
+ record_file_path="fancy-1.0.0.dist-info/RECORD",
+ records=[
+ ("scripts", ("fancy", "fancy", "main", "console")),
+ ("scripts", ("fancy-gui", "fancy", "main", "gui")),
+ ("data", ("fancy/data.py", "data", 0)),
+ ("headers", ("fancy/headers.py", "headers", 0)),
+ ("platlib", ("fancy/platlib.py", "platlib", 0)),
+ ("purelib", ("fancy/purelib.py", "purelib", 0)),
+ ("scripts", ("fancy/scripts.py", "scripts", 0)),
+ ("purelib", ("fancy/__init__.py", "purelib", 0)),
+ ("purelib", ("fancy-1.0.0.dist-info/METADATA", "purelib", 0)),
+ ("purelib", ("fancy-1.0.0.dist-info/WHEEL", "purelib", 0)),
+ (
+ "purelib",
+ ("fancy-1.0.0.dist-info/entry_points.txt", "purelib", 0),
+ ),
+ (
+ "purelib",
+ ("fancy-1.0.0.dist-info/top_level.txt", "purelib", 0),
+ ),
+ (
+ "purelib",
+ RecordEntry("fancy-1.0.0.dist-info/RECORD", None, None),
+ ),
+ ],
+ ),
+ ]
+ )
+
+ def test_errors_out_when_given_invalid_scheme_in_data(self, mock_destination):
+ # Create a fake wheel
+ source = FakeWheelSource(
+ distribution="fancy",
+ version="1.0.0",
+ regular_files={
+ "fancy/__init__.py": b"""\
+ # put me in purelib
+ """,
+ "fancy-1.0.0.data/purelib/fancy/purelib.py": b"""\
+ # put me in purelib
+ """,
+ "fancy-1.0.0.data/invalid/fancy/invalid.py": b"""\
+ # i am invalid
+ """,
+ },
+ dist_info_files={
+ "top_level.txt": b"""\
+ fancy
+ """,
+ "entry_points.txt": b"""\
+ [console_scripts]
+ fancy = fancy:main
+
+ [gui_scripts]
+ fancy-gui = 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
+ Summary: A fancy package
+ Author: Agendaless Consulting
+ Author-email: nobody@example.com
+ License: MIT
+ Keywords: fancy amazing
+ Platform: UNKNOWN
+ Classifier: Intended Audience :: Developers
+ """,
+ },
+ )
+
+ with pytest.raises(InvalidWheelSource) as ctx:
+ install(
+ source=source,
+ destination=mock_destination,
+ additional_metadata={},
+ )
+
+ assert "fancy-1.0.0.data/invalid/fancy/invalid.py" in str(ctx.value)
+
+ def test_ensure_non_executable_for_additional_metadata(self, mock_destination):
+ # Create a fake wheel
+ source = FakeWheelSource(
+ distribution="fancy",
+ version="1.0.0",
+ regular_files={
+ "fancy/__init__.py": b"""\
+ # put me in purelib
+ """,
+ },
+ dist_info_files={
+ "top_level.txt": b"""\
+ fancy
+ """,
+ "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
+ Summary: A fancy package
+ Author: Agendaless Consulting
+ Author-email: nobody@example.com
+ License: MIT
+ Keywords: fancy amazing
+ Platform: UNKNOWN
+ Classifier: Intended Audience :: Developers
+ """,
+ },
+ )
+ all_contents = list(source.get_contents())
+ source.get_contents = lambda: (
+ (*contents, True) for (*contents, _) in all_contents
+ )
+ install(
+ source=source,
+ destination=mock_destination,
+ additional_metadata={
+ "fun_file.txt": b"this should be in dist-info!",
+ },
+ )
+
+ mock_destination.assert_has_calls(
+ [
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy/__init__.py",
+ stream=mock.ANY,
+ is_executable=True,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/METADATA",
+ stream=mock.ANY,
+ is_executable=True,
+ ),
+ mock.call.write_file(
+ scheme="purelib",
+ path="fancy-1.0.0.dist-info/fun_file.txt",
+ stream=mock.ANY,
+ is_executable=False,
+ ),
+ ],
+ any_order=True,
+ )
diff --git a/tests/test_destinations.py b/tests/test_destinations.py
new file mode 100644
index 0000000..225b009
--- /dev/null
+++ b/tests/test_destinations.py
@@ -0,0 +1,173 @@
+import io
+import os.path
+
+import pytest
+
+from installer.destinations import SchemeDictionaryDestination, WheelDestination
+from installer.records import RecordEntry
+from installer.scripts import Script
+from installer.utils import SCHEME_NAMES
+
+
+class TestWheelDestination:
+ def test_takes_no_arguments(self):
+ WheelDestination()
+
+ def test_raises_not_implemented_error(self):
+ destination = WheelDestination()
+
+ with pytest.raises(NotImplementedError):
+ destination.write_script(name=None, module=None, attr=None, section=None)
+
+ with pytest.raises(NotImplementedError):
+ destination.write_file(
+ scheme=None, path=None, stream=None, is_executable=False
+ )
+
+ with pytest.raises(NotImplementedError):
+ destination.finalize_installation(
+ scheme=None,
+ record_file_path=None,
+ records=None,
+ )
+
+
+class TestSchemeDictionaryDestination:
+ @pytest.fixture()
+ def destination(self, tmp_path):
+ scheme_dict = {}
+ for scheme in SCHEME_NAMES:
+ full_path = tmp_path / scheme
+ if not full_path.exists():
+ full_path.mkdir()
+ scheme_dict[scheme] = str(full_path)
+ return SchemeDictionaryDestination(scheme_dict, "/my/python", "posix")
+
+ @pytest.mark.parametrize(
+ ("scheme", "path", "data", "expected"),
+ [
+ pytest.param(
+ "data", "my_data.bin", b"my data", b"my data", id="normal file"
+ ),
+ pytest.param(
+ "data",
+ "data_folder/my_data.bin",
+ b"my data",
+ b"my data",
+ id="normal file in subfolder",
+ ),
+ pytest.param(
+ "scripts",
+ "my_script.py",
+ b"#!python\nmy script",
+ b"#!/my/python\nmy script",
+ id="script file",
+ ),
+ pytest.param(
+ "scripts",
+ "script_folder/my_script.py",
+ b"#!python\nmy script",
+ b"#!/my/python\nmy script",
+ id="script file in subfolder",
+ ),
+ ],
+ )
+ def test_write_file(self, destination, scheme, path, data, expected):
+ record = destination.write_file(scheme, path, io.BytesIO(data), False)
+ file_path = os.path.join(destination.scheme_dict[scheme], path)
+ with open(file_path, "rb") as f:
+ file_data = f.read()
+
+ assert file_data == expected
+ assert record.path == path
+
+ def test_write_record_duplicate(self, destination):
+ destination.write_file("data", "my_data.bin", io.BytesIO(b"my data"), False)
+ with pytest.raises(FileExistsError):
+ destination.write_file("data", "my_data.bin", io.BytesIO(b"my data"), False)
+
+ def test_write_script(self, destination):
+ script_args = ("my_entrypoint", "my_module", "my_function", "console")
+ record = destination.write_script(*script_args)
+ file_path = os.path.join(destination.scheme_dict["scripts"], "my_entrypoint")
+
+ assert os.path.isfile(file_path)
+
+ with open(file_path, "rb") as f:
+ file_data = f.read()
+ name, expected_data = Script(*script_args).generate("/my/python", "posix")
+
+ assert file_data == expected_data
+ assert record.path == "my_entrypoint"
+
+ def test_finalize_write_record(self, destination):
+ records = [
+ (
+ "data",
+ destination.write_file(
+ "data",
+ "my_data1.bin",
+ io.BytesIO(b"my data 1"),
+ is_executable=False,
+ ),
+ ),
+ (
+ "data",
+ destination.write_file(
+ "data",
+ "my_data2.bin",
+ io.BytesIO(b"my data 2"),
+ is_executable=False,
+ ),
+ ),
+ (
+ "data",
+ destination.write_file(
+ "data",
+ "my_data3,my_data4.bin",
+ io.BytesIO(b"my data 3"),
+ is_executable=False,
+ ),
+ ),
+ (
+ "scripts",
+ destination.write_file(
+ "scripts",
+ "my_script",
+ io.BytesIO(b"my script"),
+ is_executable=True,
+ ),
+ ),
+ (
+ "scripts",
+ destination.write_file(
+ "scripts",
+ "my_script2",
+ io.BytesIO(b"#!python\nmy script"),
+ is_executable=False,
+ ),
+ ),
+ (
+ "scripts",
+ destination.write_script(
+ "my_entrypoint", "my_module", "my_function", "console"
+ ),
+ ),
+ ("purelib", RecordEntry("RECORD", None, None)),
+ ]
+
+ destination.finalize_installation("purelib", "RECORD", records)
+ file_path = os.path.join(destination.scheme_dict["purelib"], "RECORD")
+
+ with open(file_path, "rb") as f:
+ data = f.read()
+
+ assert data == (
+ b"../data/my_data1.bin,sha256=NV0A-M4OPuqTsHjeD6Wth_-UqrpAAAdyplcustFZ8s4,9\n"
+ b"../data/my_data2.bin,sha256=lP7V8oWLqgyXCbdASNiPdsUogzPUZhht_7F8T5bC3eQ,9\n"
+ b'"../data/my_data3,my_data4.bin",sha256=18krruu1gr01x-WM_9ChSASoHv0mfRAV6-B2bd9sxpo,9\n'
+ b"../scripts/my_script,sha256=M60fWvUSMJkPtw2apUvjWWwOcnRPcVy_zO4-4lpH08o,9\n"
+ b"../scripts/my_script2,sha256=k9_997kTbTYQm7EXFLclVZL1m2N98rU90QX46XeMvjY,22\n"
+ b"../scripts/my_entrypoint,sha256=_p_9nwmeIeoMBfQ0akhr1KbKn3laDydg0J7cy0Fs6JI,216\n"
+ b"RECORD,,\n"
+ )
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..391a13d
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,69 @@
+import os
+
+from installer.__main__ import _get_scheme_dict as get_scheme_dict
+from installer.__main__ import _main as main
+
+
+def test_get_scheme_dict():
+ d = get_scheme_dict(distribution_name="foo")
+ assert set(d.keys()) >= {"purelib", "platlib", "headers", "scripts", "data"}
+
+
+def test_get_scheme_dict_prefix():
+ d = get_scheme_dict(distribution_name="foo", prefix="/foo")
+ for key in ("purelib", "platlib", "headers", "scripts", "data"):
+ assert d[key].startswith(
+ f"{os.sep}foo"
+ ), f"{key} does not start with /foo: {d[key]}"
+
+
+def test_main(fancy_wheel, tmp_path):
+ destdir = tmp_path / "dest"
+
+ main([str(fancy_wheel), "-d", str(destdir)], "python -m installer")
+
+ installed_py_files = destdir.rglob("*.py")
+
+ assert {f.stem for f in installed_py_files} == {"__init__", "__main__", "data"}
+
+ installed_pyc_files = destdir.rglob("*.pyc")
+ assert {f.name.split(".")[0] for f in installed_pyc_files} == {
+ "__init__",
+ "__main__",
+ }
+
+
+def test_main_prefix(fancy_wheel, tmp_path):
+ destdir = tmp_path / "dest"
+
+ main([str(fancy_wheel), "-d", str(destdir), "-p", "/foo"], "python -m installer")
+
+ installed_py_files = list(destdir.rglob("*.py"))
+
+ for f in installed_py_files:
+ assert str(f.parent).startswith(
+ str(destdir / "foo")
+ ), f"path does not respect destdir+prefix: {f}"
+ assert {f.stem for f in installed_py_files} == {"__init__", "__main__", "data"}
+
+ installed_pyc_files = destdir.rglob("*.pyc")
+ assert {f.name.split(".")[0] for f in installed_pyc_files} == {
+ "__init__",
+ "__main__",
+ }
+
+
+def test_main_no_pyc(fancy_wheel, tmp_path):
+ destdir = tmp_path / "dest"
+
+ main(
+ [str(fancy_wheel), "-d", str(destdir), "--no-compile-bytecode"],
+ "python -m installer",
+ )
+
+ installed_py_files = destdir.rglob("*.py")
+
+ assert {f.stem for f in installed_py_files} == {"__init__", "__main__", "data"}
+
+ installed_pyc_files = destdir.rglob("*.pyc")
+ assert set(installed_pyc_files) == set()
diff --git a/tests/test_records.py b/tests/test_records.py
new file mode 100644
index 0000000..45427ae
--- /dev/null
+++ b/tests/test_records.py
@@ -0,0 +1,275 @@
+import pytest
+
+from installer.records import Hash, InvalidRecordEntry, RecordEntry, parse_record_file
+
+
+#
+# pytest fixture witchcraft
+#
+@pytest.fixture()
+def record_simple_list():
+ return [
+ "file.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144",
+ "distribution-1.0.dist-info/RECORD,,",
+ ]
+
+
+@pytest.fixture()
+def record_simple_iter(record_simple_list):
+ return iter(record_simple_list)
+
+
+@pytest.fixture()
+def record_simple_file(tmpdir, record_simple_list):
+ p = tmpdir.join("RECORD")
+ p.write("\n".join(record_simple_list))
+ with open(str(p)) as f:
+ yield f
+
+
+@pytest.fixture()
+def record_input(request):
+ return request.getfixturevalue(request.param)
+
+
+SAMPLE_RECORDS = [
+ (
+ "purelib",
+ ("test1.py", "sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4", 6),
+ b"test1\n",
+ True,
+ ),
+ (
+ "purelib",
+ ("test2.py", "sha256=fW_Xd08Nh2JNptzxbQ09EEwxkedx--LznIau1LK_Gg8", 6),
+ b"test2\n",
+ True,
+ ),
+ (
+ "purelib",
+ ("test3.py", "sha256=qwPDTx7OCCEf4qgDn9ZCQZmz9de1X_E7ETSzZHdsRcU", 6),
+ b"test3\n",
+ True,
+ ),
+ (
+ "purelib",
+ ("test4.py", "sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4", 7),
+ b"test1\n",
+ False,
+ ),
+ (
+ "purelib",
+ (
+ "test5.py",
+ "sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4",
+ None,
+ ),
+ b"test1\n",
+ True,
+ ),
+ ("purelib", ("test6.py", None, None), b"test1\n", True),
+]
+
+
+#
+# Actual Tests
+#
+class TestRecordEntry:
+ @pytest.mark.parametrize(
+ "path, hash_, size, caused_by",
+ [
+ ("", "", "", ["path"]),
+ ("", "", "non-int", ["path", "size"]),
+ ("a.py", "", "non-int", ["size"]),
+ # Notice that we're explicitly allowing non-compliant hash values
+ ("a.py", "some-random-value", "non-int", ["size"]),
+ ],
+ )
+ def test_invalid_elements(self, path, hash_, size, caused_by):
+ with pytest.raises(InvalidRecordEntry) as exc_info:
+ RecordEntry.from_elements(path, hash_, size)
+
+ assert exc_info.value.elements == (path, hash_, size)
+ for word in caused_by:
+ assert word in str(exc_info.value)
+
+ @pytest.mark.parametrize(
+ "path, hash_, size",
+ [
+ ("a.py", "", ""),
+ ("a.py", "", "3144"),
+ ("a.py", "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI", ""),
+ ("a.py", "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI", "3144"),
+ ],
+ )
+ def test_valid_elements(self, path, hash_, size):
+ RecordEntry.from_elements(path, hash_, size)
+
+ @pytest.mark.parametrize(
+ ("scheme", "elements", "data", "passes_validation"), SAMPLE_RECORDS
+ )
+ def test_populates_attributes_correctly(
+ self, scheme, elements, data, passes_validation
+ ):
+ path, hash_string, size = elements
+
+ record = RecordEntry.from_elements(path, hash_string, size)
+
+ assert record.path == path
+ assert record.size == size
+
+ if record.hash_ is not None:
+ assert isinstance(record.hash_, Hash)
+ assert record.hash_.name == "sha256"
+ assert record.hash_.value == hash_string[len("sha256=") :]
+
+ @pytest.mark.parametrize(
+ ("scheme", "elements", "data", "passes_validation"), SAMPLE_RECORDS
+ )
+ def test_validation(self, scheme, elements, data, passes_validation):
+ record = RecordEntry.from_elements(*elements)
+ assert record.validate(data) == passes_validation
+
+ @pytest.mark.parametrize(
+ ("scheme", "elements", "data", "passes_validation"), SAMPLE_RECORDS
+ )
+ def test_string_representation(self, scheme, elements, data, passes_validation):
+ record = RecordEntry.from_elements(*elements)
+
+ expected_row = tuple(
+ [(str(elem) if elem is not None else "") for elem in elements]
+ )
+ assert record.to_row() == expected_row
+
+ @pytest.mark.parametrize(
+ ("scheme", "elements", "data", "passes_validation"), SAMPLE_RECORDS
+ )
+ def test_string_representation_with_prefix(
+ self, scheme, elements, data, passes_validation
+ ):
+ record = RecordEntry.from_elements(*elements)
+
+ expected_row = tuple(
+ [
+ (str(elem) if elem is not None else "")
+ for elem in ("prefix/" + elements[0], elements[1], elements[2])
+ ]
+ )
+ assert record.to_row("prefix/") == expected_row
+
+ def test_equality(self):
+ record = RecordEntry.from_elements(
+ "file.py",
+ "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI",
+ "3144",
+ )
+ record_same = RecordEntry.from_elements(
+ "file.py",
+ "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI",
+ "3144",
+ )
+ record_different_name = RecordEntry.from_elements(
+ "file2.py",
+ "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI",
+ "3144",
+ )
+ record_different_hash_name = RecordEntry.from_elements(
+ "file.py",
+ "md5=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI",
+ "3144",
+ )
+ record_different_hash_value = RecordEntry.from_elements(
+ "file.py",
+ "sha256=qwertyuiodfdsflkgshdlkjghrefawrwerwffsdfflk29",
+ "3144",
+ )
+ record_different_size = RecordEntry.from_elements(
+ "file.py",
+ "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI",
+ "10",
+ )
+
+ assert record == record_same
+
+ assert record != "random string"
+ assert record != record_different_name
+ assert record != record_different_hash_name
+ assert record != record_different_hash_value
+ assert record != record_different_size
+
+ # Ensure equality is based on current state
+ record_same.hash_ = None
+ assert record != record_same
+
+
+class TestParseRecordFile:
+ def test_accepts_empty_iterable(self):
+ list(parse_record_file([]))
+
+ @pytest.mark.parametrize(
+ "record_input",
+ ["record_simple_list", "record_simple_iter", "record_simple_file"],
+ indirect=True,
+ )
+ def test_accepts_all_kinds_of_iterables(self, record_input):
+ """Should accepts any iterable, e.g. container, iterator, or file object."""
+ records = list(parse_record_file(record_input))
+ assert len(records) == 2
+
+ assert records == [
+ (
+ "file.py",
+ "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI",
+ "3144",
+ ),
+ ("distribution-1.0.dist-info/RECORD", "", ""),
+ ]
+
+ @pytest.mark.parametrize(
+ "line, element_count",
+ [
+ pytest.param(
+ "file.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144,",
+ 4,
+ id="four",
+ ),
+ pytest.param(
+ "distribution-1.0.dist-info/RECORD,,,,",
+ 5,
+ id="five",
+ ),
+ ],
+ )
+ def test_rejects_wrong_element_count(self, line, element_count):
+ with pytest.raises(InvalidRecordEntry) as exc_info:
+ list(parse_record_file([line]))
+
+ message = f"expected 3 elements, got {element_count}"
+ assert message in str(exc_info.value)
+
+ def test_shows_correct_row_number(self):
+ record_lines = [
+ "file1.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144",
+ "file2.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144",
+ "file3.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144",
+ "distribution-1.0.dist-info/RECORD,,,,",
+ ]
+ with pytest.raises(InvalidRecordEntry) as exc_info:
+ list(parse_record_file(record_lines))
+
+ assert "Row Index 3" in str(exc_info.value)
+
+ def test_parse_record_entry_with_comma(self):
+ record_lines = [
+ '"file1,file2.txt",sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI,3144',
+ "distribution-1.0.dist-info/RECORD,,",
+ ]
+ records = list(parse_record_file(record_lines))
+ assert records == [
+ (
+ "file1,file2.txt",
+ "sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\\_pNh2yI",
+ "3144",
+ ),
+ ("distribution-1.0.dist-info/RECORD", "", ""),
+ ]
diff --git a/tests/test_scripts.py b/tests/test_scripts.py
new file mode 100644
index 0000000..2da6577
--- /dev/null
+++ b/tests/test_scripts.py
@@ -0,0 +1,70 @@
+import io
+import os
+import zipfile
+
+import pytest
+
+from installer import _scripts
+from installer.scripts import InvalidScript, Script
+
+
+def test_script_generate_simple():
+ script = Script("foo", "foo.bar", "baz.qux", section="console")
+ name, data = script.generate("/path/to/my/python", kind="posix")
+
+ assert name == "foo"
+ assert data.startswith(b"#!/path/to/my/python\n")
+ assert b"\nfrom foo.bar import baz\n" in data
+ assert b"baz.qux()" in data
+
+
+def test_script_generate_space_in_executable():
+ script = Script("foo", "foo.bar", "baz.qux", section="console")
+ name, data = script.generate("/path to my/python", kind="posix")
+
+ assert name == "foo"
+ assert data.startswith(b"#!/bin/sh\n")
+ assert b" '/path to my/python'" in data
+ assert b"\nfrom foo.bar import baz\n" in data
+ assert b"baz.qux()" in data
+
+
+def _read_launcher_data(section, kind):
+ prefix = {"console": "t", "gui": "w"}[section]
+ suffix = {"win-ia32": "32", "win-amd64": "64", "win-arm": "_arm"}[kind]
+ filename = os.path.join(
+ os.path.dirname(os.path.abspath(_scripts.__file__)),
+ f"{prefix}{suffix}.exe",
+ )
+ with open(filename, "rb") as f:
+ return f.read()
+
+
+@pytest.mark.parametrize("section", ["console", "gui"])
+@pytest.mark.parametrize("kind", ["win-ia32", "win-amd64", "win-arm"])
+def test_script_generate_launcher(section, kind):
+ launcher_data = _read_launcher_data(section, kind)
+
+ script = Script("foo", "foo.bar", "baz.qux", section=section)
+ name, data = script.generate("#!C:\\path to my\\python.exe\n", kind=kind)
+
+ prefix_len = len(launcher_data) + len(b"#!C:\\path to my\\python.exe\n")
+ stream = io.BytesIO(data[prefix_len:])
+ with zipfile.ZipFile(stream) as zf:
+ code = zf.read("__main__.py")
+
+ assert name == "foo.exe"
+ assert data.startswith(launcher_data)
+ assert b"#!C:\\path to my\\python.exe\n" in data
+ assert b"\nfrom foo.bar import baz\n" in code
+ assert b"baz.qux()" in code
+
+
+@pytest.mark.parametrize(
+ "section, kind",
+ [("nonexist", "win-ia32"), ("console", "nonexist"), ("nonexist", "nonexist")],
+)
+def test_script_generate_launcher_error(section, kind):
+ script = Script("foo", "foo.bar", "baz.qux", section=section)
+ with pytest.raises(InvalidScript):
+ script.generate("#!C:\\path to my\\python.exe\n", kind=kind)
diff --git a/tests/test_sources.py b/tests/test_sources.py
new file mode 100644
index 0000000..a79cc24
--- /dev/null
+++ b/tests/test_sources.py
@@ -0,0 +1,94 @@
+import posixpath
+import zipfile
+
+import pytest
+
+from installer.records import parse_record_file
+from installer.sources import WheelFile, WheelSource
+
+
+class TestWheelSource:
+ def test_takes_two_arguments(self):
+ WheelSource("distribution", "version")
+ WheelSource(distribution="distribution", version="version")
+
+ def test_correctly_computes_properties(self):
+ source = WheelSource(distribution="distribution", version="version")
+
+ assert source.data_dir == "distribution-version.data"
+ assert source.dist_info_dir == "distribution-version.dist-info"
+
+ def test_raises_not_implemented_error(self):
+ source = WheelSource(distribution="distribution", version="version")
+
+ with pytest.raises(NotImplementedError):
+ source.dist_info_filenames
+
+ with pytest.raises(NotImplementedError):
+ source.read_dist_info("METADATA")
+
+ with pytest.raises(NotImplementedError):
+ source.get_contents()
+
+
+class TestWheelFile:
+ def test_rejects_not_okay_name(self, tmp_path):
+ # Create an empty zipfile
+ path = tmp_path / "not_a_valid_name.whl"
+ with zipfile.ZipFile(str(path), "w"):
+ pass
+
+ with pytest.raises(ValueError, match="Not a valid wheel filename: .+"):
+ with WheelFile.open(str(path)):
+ pass
+
+ def test_provides_correct_dist_info_filenames(self, fancy_wheel):
+ with WheelFile.open(fancy_wheel) as source:
+ assert sorted(source.dist_info_filenames) == [
+ "METADATA",
+ "RECORD",
+ "WHEEL",
+ "entry_points.txt",
+ "top_level.txt",
+ ]
+
+ def test_correctly_reads_from_dist_info_files(self, fancy_wheel):
+ files = {}
+ with zipfile.ZipFile(fancy_wheel) as archive:
+ for file in archive.namelist():
+ if ".dist-info" not in file:
+ continue
+ files[posixpath.basename(file)] = archive.read(file).decode("utf-8")
+
+ got_files = {}
+ with WheelFile.open(fancy_wheel) as source:
+ for file in files:
+ got_files[file] = source.read_dist_info(file)
+
+ assert got_files == files
+
+ def test_provides_correct_contents(self, fancy_wheel):
+ # Know the contents of the wheel
+ files = {}
+ with zipfile.ZipFile(fancy_wheel) as archive:
+ for file in archive.namelist():
+ if file[-1:] == "/":
+ continue
+ files[file] = archive.read(file)
+
+ expected_record_lines = (
+ files["fancy-1.0.0.dist-info/RECORD"].decode("utf-8").splitlines()
+ )
+ expected_records = list(parse_record_file(expected_record_lines))
+
+ # Check that the object's output is appropriate
+ got_records = []
+ got_files = {}
+ with WheelFile.open(fancy_wheel) as source:
+ for record_elements, stream, is_executable in source.get_contents():
+ got_records.append(record_elements)
+ got_files[record_elements[0]] = stream.read()
+ assert not is_executable
+
+ assert sorted(got_records) == sorted(expected_records)
+ assert got_files == files
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..bfcc089
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,232 @@
+"""Tests for installer.utils
+"""
+
+import base64
+import hashlib
+import textwrap
+from email.message import Message
+from io import BytesIO
+
+import pytest
+from test_records import SAMPLE_RECORDS
+
+from installer.records import RecordEntry
+from installer.utils import (
+ WheelFilename,
+ construct_record_file,
+ copyfileobj_with_hashing,
+ fix_shebang,
+ parse_entrypoints,
+ parse_metadata_file,
+ parse_wheel_filename,
+)
+
+
+class TestParseMetadata:
+ def test_basics(self):
+ result = parse_metadata_file(
+ textwrap.dedent(
+ """\
+ Name: package
+ Version: 1.0.0
+ Multi-Use-Field: 1
+ Multi-Use-Field: 2
+ Multi-Use-Field: 3
+ """
+ )
+ )
+ assert isinstance(result, Message)
+ assert result.get("Name") == "package"
+ assert result.get("version") == "1.0.0"
+ assert result.get_all("MULTI-USE-FIELD") == ["1", "2", "3"]
+
+
+class TestParseWheelFilename:
+ @pytest.mark.parametrize(
+ "string, expected",
+ [
+ # Crafted package name w/ a "complex" version and build tag
+ (
+ "package-1!1.0+abc.7-753-py3-none-any.whl",
+ WheelFilename("package", "1!1.0+abc.7", "753", "py3-none-any"),
+ ),
+ # Crafted package name w/ a "complex" version and no build tag
+ (
+ "package-1!1.0+abc.7-py3-none-any.whl",
+ WheelFilename("package", "1!1.0+abc.7", None, "py3-none-any"),
+ ),
+ # Use real tensorflow wheel names
+ (
+ "tensorflow-2.3.0-cp38-cp38-macosx_10_11_x86_64.whl",
+ WheelFilename(
+ "tensorflow", "2.3.0", None, "cp38-cp38-macosx_10_11_x86_64"
+ ),
+ ),
+ (
+ "tensorflow-2.3.0-cp38-cp38-manylinux2010_x86_64.whl",
+ WheelFilename(
+ "tensorflow", "2.3.0", None, "cp38-cp38-manylinux2010_x86_64"
+ ),
+ ),
+ (
+ "tensorflow-2.3.0-cp38-cp38-win_amd64.whl",
+ WheelFilename("tensorflow", "2.3.0", None, "cp38-cp38-win_amd64"),
+ ),
+ ],
+ )
+ def test_valid_cases(self, string, expected):
+ got = parse_wheel_filename(string)
+ assert expected == got, (expected, got)
+
+ @pytest.mark.parametrize(
+ "string",
+ [
+ # Not ".whl"
+ "pip-20.0.0-py2.py3-none-any.zip",
+ # No tag
+ "pip-20.0.0.whl",
+ # Empty tag
+ "pip-20.0.0---.whl",
+ ],
+ )
+ def test_invalid_cases(self, string):
+ with pytest.raises(ValueError):
+ parse_wheel_filename(string)
+
+
+class TestCopyFileObjWithHashing:
+ def test_basic_functionality(self):
+ data = b"input data is this"
+ hash_ = (
+ base64.urlsafe_b64encode(hashlib.sha256(data).digest())
+ .decode("ascii")
+ .rstrip("=")
+ )
+ size = len(data)
+
+ with BytesIO(data) as source:
+ with BytesIO() as dest:
+ result = copyfileobj_with_hashing(source, dest, hash_algorithm="sha256")
+ written_data = dest.getvalue()
+
+ assert result == (hash_, size)
+ assert written_data == data
+
+
+class TestScript:
+ @pytest.mark.parametrize(
+ ("data", "expected"),
+ [
+ pytest.param(
+ b"#!python\ntest",
+ b"#!/my/python\ntest",
+ id="python",
+ ),
+ pytest.param(
+ b"#!pythonw\ntest",
+ b"#!/my/python\ntest",
+ id="pythonw",
+ ),
+ pytest.param(
+ b"#!python something\ntest",
+ b"#!/my/python\ntest",
+ id="python-with-args",
+ ),
+ pytest.param(
+ b"#!python",
+ b"#!/my/python\n",
+ id="python-no-content",
+ ),
+ ],
+ )
+ def test_replace_shebang(self, data, expected):
+ with BytesIO(data) as source:
+ with fix_shebang(source, "/my/python") as stream:
+ result = stream.read()
+ assert result == expected
+
+ @pytest.mark.parametrize(
+ "data",
+ [
+ b"#!py\ntest",
+ b"#!something\ntest",
+ b"#something\ntest",
+ b"#something",
+ b"something",
+ ],
+ )
+ def test_keep_data(self, data):
+ with BytesIO(data) as source:
+ with fix_shebang(source, "/my/python") as stream:
+ result = stream.read()
+ assert result == data
+
+
+class TestConstructRecord:
+ def test_construct(self):
+ records = [
+ (scheme, RecordEntry.from_elements(*elements))
+ for scheme, elements, _, _ in SAMPLE_RECORDS
+ ]
+ assert construct_record_file(records).read() == (
+ b"test1.py,sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4,6\n"
+ b"test2.py,sha256=fW_Xd08Nh2JNptzxbQ09EEwxkedx--LznIau1LK_Gg8,6\n"
+ b"test3.py,sha256=qwPDTx7OCCEf4qgDn9ZCQZmz9de1X_E7ETSzZHdsRcU,6\n"
+ b"test4.py,sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4,7\n"
+ b"test5.py,sha256=Y0sCextp4SQtQNU-MSs7SsdxD1W-gfKJtUlEbvZ3i-4,\n"
+ b"test6.py,,\n"
+ )
+
+
+class TestParseEntryPoints:
+ @pytest.mark.parametrize(
+ ("script", "expected"),
+ [
+ pytest.param("", [], id="empty"),
+ pytest.param(
+ """
+ [foo]
+ foo = foo.bar
+ """,
+ [],
+ id="unrelated",
+ ),
+ pytest.param(
+ """
+ [console_scripts]
+ package = package.__main__:package
+ """,
+ [
+ ("package", "package.__main__", "package", "console"),
+ ],
+ id="cli",
+ ),
+ pytest.param(
+ """
+ [gui_scripts]
+ package = package.__main__:package
+ """,
+ [
+ ("package", "package.__main__", "package", "gui"),
+ ],
+ id="gui",
+ ),
+ pytest.param(
+ """
+ [console_scripts]
+ magic-cli = magic.cli:main
+
+ [gui_scripts]
+ magic-gui = magic.gui:main
+ """,
+ [
+ ("magic-cli", "magic.cli", "main", "console"),
+ ("magic-gui", "magic.gui", "main", "gui"),
+ ],
+ id="cli-and-gui",
+ ),
+ ],
+ )
+ def test_valid(self, script, expected):
+ iterable = parse_entrypoints(textwrap.dedent(script))
+ assert list(iterable) == expected, expected