Python functions to get top-level importables names
0

Configure Feed

Select the types of activity you want to include in your feed.

Improve implementation

Good parts taken from https://gist.github.com/pradyunsg/22ca089b48ca55d75ca843a5946b2691.

Juan Luis Cano Rodríguez (Dec 4, 2023, 9:28 PM +0100) 75d28430 8272d96b

+79 -22
+79 -22
src/pygetpackages/__init__.py
··· 1 1 """Get the top-level packages of a Python project.""" 2 + # The good parts come from https://gist.github.com/pradyunsg/22ca089b48ca55d75ca843a5946b2691 3 + # Licensed under the MIT license. 4 + # 5 + # Copyright (c) 2022 Pradyun Gedam <mail@pradyunsg.me> 6 + # 7 + # Permission is hereby granted, free of charge, to any person obtaining a copy 8 + # of this software and associated documentation files (the “Software”), to deal 9 + # in the Software without restriction, including without limitation the rights 10 + # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 + # copies of the Software, and to permit persons to whom the Software is 12 + # furnished to do so, subject to the following conditions: 13 + # 14 + # The above copyright notice and this permission notice shall be included in 15 + # all copies or substantial portions of the Software. 16 + # 17 + # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 + # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 + # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 20 + # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 + # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 + # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 + # SOFTWARE. 2 24 3 - import csv 4 25 import pathlib 5 26 import tempfile 6 27 import tomllib 28 + import typing as t 7 29 import zipfile 8 30 9 31 from build import ProjectBuilder 10 32 from build.env import DefaultIsolatedEnv 33 + from installer.sources import WheelFile, WheelSource 34 + from installer.utils import parse_metadata_file 11 35 from pyproject_hooks import quiet_subprocess_runner 12 36 from validate_pyproject import api, errors 13 37 14 38 15 - def _simple_build_wheel(source_dir, out_dir, *, build_config_settings=None): 39 + def _simple_build_wheel( 40 + source_dir, out_dir, *, build_config_settings=None 41 + ) -> WheelFile: 16 42 """Silently build wheel using build package.""" 17 43 with DefaultIsolatedEnv() as env: 18 44 builder = ProjectBuilder.from_isolated_env( ··· 24 50 return path_built_wheel 25 51 26 52 53 + def _find_importable_components_from_wheel_content_listing( 54 + filepaths: t.Iterable[str], *, dist_info_dir: str, data_dir: str 55 + ) -> t.Iterable[tuple[str, ...]]: 56 + purelib_str = f"{data_dir}/purelib/" 57 + platlib_str = f"{data_dir}/platlib/" 58 + for path in filepaths: 59 + if path.startswith(dist_info_dir): 60 + # Nothing in dist-info is importable. 61 + continue 62 + 63 + if path.startswith((platlib_str, purelib_str)): 64 + # Remove the prefix from purelib and platlib files. 65 + name = path[len(platlib_str) :] 66 + elif path.startswith(data_dir): 67 + # Nothing else in data is importable. 68 + continue 69 + else: 70 + # Top level files end up in an importable location. 71 + name = path 72 + 73 + if name.endswith(".py"): 74 + yield tuple(name[: -len(".py")].split("/")) 75 + 76 + 77 + def _determine_major_import_names( 78 + importable_components: t.Iterable[tuple[str, ...]] 79 + ) -> set[str]: 80 + return {components[0] for components in importable_components} 81 + 82 + 83 + def _get_importable_components_from_wheel(wheel: WheelSource) -> t.Iterable[str]: 84 + metadata = parse_metadata_file(wheel.read_dist_info("WHEEL")) 85 + if not (metadata["Wheel-Version"] and metadata["Wheel-Version"].startswith("1.")): 86 + raise NotImplementedError("Only supports wheel 1.x") 87 + 88 + filepaths: t.Iterable[str] = ( 89 + record_elements[0] for record_elements, _, _ in wheel.get_contents() 90 + ) 91 + importable_components = _find_importable_components_from_wheel_content_listing( 92 + filepaths, dist_info_dir=wheel.dist_info_dir, data_dir=wheel.data_dir 93 + ) 94 + 95 + return importable_components 96 + 97 + 27 98 def get_packages_from_wheel(wheel_path: str | pathlib.Path): 28 99 """Get the top-level packages of a Python project from a wheel file.""" 29 100 with zipfile.ZipFile(wheel_path, "r") as zf: 30 - record_fnames = [ 31 - fname for fname in zf.namelist() if fname.endswith(".dist-info/RECORD") 32 - ] 33 - record_fname = min(record_fnames, key=len) 34 - with zf.open(record_fname) as fh: 35 - record_content = fh.read().decode() 101 + wheel_file = WheelFile(zf) 102 + importable_components = _get_importable_components_from_wheel(wheel_file) 103 + top_packages = _determine_major_import_names(importable_components) 36 104 37 - record_files = [ 38 - pathlib.Path(row[0]) for row in csv.reader(record_content.splitlines()) 39 - ] 40 - 41 - # A directory is a top-level package if it contains a top-level __init__.py file 42 - top_packages = { 43 - record_file.parts[0] 44 - for record_file in record_files 45 - if len(record_file.parts) > 1 and record_file.parts[1] == "__init__.py" 46 - } 47 - 48 - return list(top_packages) 105 + return top_packages 49 106 50 107 51 108 def get_packages(source_dir, *, build_config_settings=None): 52 - """Get the top-level packages of a Python project.""" 109 + """Get the top-level packages of a Python source tree.""" 53 110 pyproject_toml_path = pathlib.Path(source_dir) / "pyproject.toml" 54 111 if not pyproject_toml_path.is_file(): 55 112 raise ValueError( ··· 76 133 ) 77 134 top_packages = get_packages_from_wheel(wheel_path) 78 135 79 - return list(top_packages) 136 + return top_packages