···11"""Get the top-level packages of a Python project."""
22+# The good parts come from https://gist.github.com/pradyunsg/22ca089b48ca55d75ca843a5946b2691
33+# Licensed under the MIT license.
44+#
55+# Copyright (c) 2022 Pradyun Gedam <mail@pradyunsg.me>
66+#
77+# Permission is hereby granted, free of charge, to any person obtaining a copy
88+# of this software and associated documentation files (the “Software”), to deal
99+# in the Software without restriction, including without limitation the rights
1010+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1111+# copies of the Software, and to permit persons to whom the Software is
1212+# furnished to do so, subject to the following conditions:
1313+#
1414+# The above copyright notice and this permission notice shall be included in
1515+# all copies or substantial portions of the Software.
1616+#
1717+# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1818+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1919+# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
2020+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2121+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2222+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2323+# SOFTWARE.
22433-import csv
425import pathlib
526import tempfile
627import tomllib
2828+import typing as t
729import zipfile
830931from build import ProjectBuilder
1032from build.env import DefaultIsolatedEnv
3333+from installer.sources import WheelFile, WheelSource
3434+from installer.utils import parse_metadata_file
1135from pyproject_hooks import quiet_subprocess_runner
1236from validate_pyproject import api, errors
133714381515-def _simple_build_wheel(source_dir, out_dir, *, build_config_settings=None):
3939+def _simple_build_wheel(
4040+ source_dir, out_dir, *, build_config_settings=None
4141+) -> WheelFile:
1642 """Silently build wheel using build package."""
1743 with DefaultIsolatedEnv() as env:
1844 builder = ProjectBuilder.from_isolated_env(
···2450 return path_built_wheel
255126525353+def _find_importable_components_from_wheel_content_listing(
5454+ filepaths: t.Iterable[str], *, dist_info_dir: str, data_dir: str
5555+) -> t.Iterable[tuple[str, ...]]:
5656+ purelib_str = f"{data_dir}/purelib/"
5757+ platlib_str = f"{data_dir}/platlib/"
5858+ for path in filepaths:
5959+ if path.startswith(dist_info_dir):
6060+ # Nothing in dist-info is importable.
6161+ continue
6262+6363+ if path.startswith((platlib_str, purelib_str)):
6464+ # Remove the prefix from purelib and platlib files.
6565+ name = path[len(platlib_str) :]
6666+ elif path.startswith(data_dir):
6767+ # Nothing else in data is importable.
6868+ continue
6969+ else:
7070+ # Top level files end up in an importable location.
7171+ name = path
7272+7373+ if name.endswith(".py"):
7474+ yield tuple(name[: -len(".py")].split("/"))
7575+7676+7777+def _determine_major_import_names(
7878+ importable_components: t.Iterable[tuple[str, ...]]
7979+) -> set[str]:
8080+ return {components[0] for components in importable_components}
8181+8282+8383+def _get_importable_components_from_wheel(wheel: WheelSource) -> t.Iterable[str]:
8484+ metadata = parse_metadata_file(wheel.read_dist_info("WHEEL"))
8585+ if not (metadata["Wheel-Version"] and metadata["Wheel-Version"].startswith("1.")):
8686+ raise NotImplementedError("Only supports wheel 1.x")
8787+8888+ filepaths: t.Iterable[str] = (
8989+ record_elements[0] for record_elements, _, _ in wheel.get_contents()
9090+ )
9191+ importable_components = _find_importable_components_from_wheel_content_listing(
9292+ filepaths, dist_info_dir=wheel.dist_info_dir, data_dir=wheel.data_dir
9393+ )
9494+9595+ return importable_components
9696+9797+2798def get_packages_from_wheel(wheel_path: str | pathlib.Path):
2899 """Get the top-level packages of a Python project from a wheel file."""
29100 with zipfile.ZipFile(wheel_path, "r") as zf:
3030- record_fnames = [
3131- fname for fname in zf.namelist() if fname.endswith(".dist-info/RECORD")
3232- ]
3333- record_fname = min(record_fnames, key=len)
3434- with zf.open(record_fname) as fh:
3535- record_content = fh.read().decode()
101101+ wheel_file = WheelFile(zf)
102102+ importable_components = _get_importable_components_from_wheel(wheel_file)
103103+ top_packages = _determine_major_import_names(importable_components)
361043737- record_files = [
3838- pathlib.Path(row[0]) for row in csv.reader(record_content.splitlines())
3939- ]
4040-4141- # A directory is a top-level package if it contains a top-level __init__.py file
4242- top_packages = {
4343- record_file.parts[0]
4444- for record_file in record_files
4545- if len(record_file.parts) > 1 and record_file.parts[1] == "__init__.py"
4646- }
4747-4848- return list(top_packages)
105105+ return top_packages
491065010751108def get_packages(source_dir, *, build_config_settings=None):
5252- """Get the top-level packages of a Python project."""
109109+ """Get the top-level packages of a Python source tree."""
53110 pyproject_toml_path = pathlib.Path(source_dir) / "pyproject.toml"
54111 if not pyproject_toml_path.is_file():
55112 raise ValueError(
···76133 )
77134 top_packages = get_packages_from_wheel(wheel_path)
781357979- return list(top_packages)
136136+ return top_packages