22
33from __future__ import annotations
44
5+ import re
6+ import shutil
7+ import subprocess
8+ import sys
9+ import tempfile
510from contextlib import suppress
611from datetime import datetime , timezone
12+ from importlib .util import find_spec
713from pathlib import Path
814from typing import TYPE_CHECKING , ClassVar , cast
915
@@ -925,20 +931,23 @@ def load_git(
925931
926932
927933def load_pypi (
928- package : str , # noqa: ARG001
929- distribution : str , # noqa: ARG001
930- version_spec : str , # noqa: ARG001
934+ package : str ,
935+ distribution : str ,
936+ version_spec : str ,
931937 * ,
932- submodules : bool = True , # noqa: ARG001
933- extensions : Extensions | None = None , # noqa: ARG001
934- search_paths : Sequence [str | Path ] | None = None , # noqa: ARG001
935- docstring_parser : DocstringStyle | Parser | None = None , # noqa: ARG001
936- docstring_options : DocstringOptions | None = None , # noqa: ARG001
937- lines_collection : LinesCollection | None = None , # noqa: ARG001
938- modules_collection : ModulesCollection | None = None , # noqa: ARG001
939- allow_inspection : bool = True , # noqa: ARG001
940- force_inspection : bool = False , # noqa: ARG001
941- find_stubs_package : bool = False , # noqa: ARG001
938+ submodules : bool = True ,
939+ extensions : Extensions | None = None ,
940+ search_paths : Sequence [str | Path ] | None = None ,
941+ docstring_parser : DocstringStyle | Parser | None = None ,
942+ docstring_options : DocstringOptions | None = None ,
943+ lines_collection : LinesCollection | None = None ,
944+ modules_collection : ModulesCollection | None = None ,
945+ allow_inspection : bool = True ,
946+ force_inspection : bool = False ,
947+ find_stubs_package : bool = False ,
948+ resolve_aliases : bool = False ,
949+ resolve_external : bool | None = None ,
950+ resolve_implicit : bool = False ,
942951) -> Object | Alias :
943952 """Load and return a module from a specific package version downloaded using pip.
944953
@@ -962,5 +971,88 @@ def load_pypi(
962971 find_stubs_package: Whether to search for stubs-only package.
963972 If both the package and its stubs are found, they'll be merged together.
964973 If only the stubs are found, they'll be used as the package itself.
974+ resolve_aliases: Whether to resolve aliases.
975+ resolve_external: Whether to try to load unspecified modules to resolve aliases.
976+ Default value (`None`) means to load external modules only if they are the private sibling
977+ or the origin module (for example when `ast` imports from `_ast`).
978+ resolve_implicit: When false, only try to resolve an alias if it is explicitly exported.
965979 """
966- raise ValueError ("Not available in non-Insiders versions of Griffe" )
980+ if not all (find_spec (pkg ) for pkg in ("pip" , "wheel" , "platformdirs" )):
981+ raise RuntimeError ("Please install Griffe with the 'pypi' extra to use this feature." )
982+
983+ import platformdirs # noqa: PLC0415
984+
985+ pypi_cache_dir = Path (platformdirs .user_cache_dir ("griffe" ))
986+ install_dir = pypi_cache_dir / f"{ distribution } { version_spec } "
987+ if install_dir .exists ():
988+ logger .debug ("Using cached %s%s" , distribution , version_spec )
989+ else :
990+ with tempfile .TemporaryDirectory (dir = pypi_cache_dir ) as tmpdir :
991+ install_dir = Path (tmpdir ) / distribution
992+ logger .debug ("Downloading %s%s" , distribution , version_spec )
993+ process = subprocess .run ( # noqa: S603
994+ [
995+ sys .executable ,
996+ "-mpip" ,
997+ "install" ,
998+ "--no-deps" ,
999+ "--no-compile" ,
1000+ "--no-warn-script-location" ,
1001+ "--no-input" ,
1002+ "--disable-pip-version-check" ,
1003+ "--no-python-version-warning" ,
1004+ "-t" ,
1005+ str (install_dir ),
1006+ f"{ distribution } { version_spec } " ,
1007+ ],
1008+ text = True ,
1009+ stdout = subprocess .PIPE ,
1010+ stderr = subprocess .STDOUT ,
1011+ check = False ,
1012+ )
1013+ if process .returncode :
1014+ logger .error (process .stdout )
1015+ raise RuntimeError (f"Could not pip install { distribution } { version_spec } " )
1016+ logger .debug (process .stdout )
1017+ shutil .rmtree (install_dir / "bin" , ignore_errors = True )
1018+ re_dist = re .sub ("[._-]" , "[._-]" , distribution )
1019+ version = next (
1020+ match .group (1 )
1021+ for file in install_dir .iterdir ()
1022+ if (match := re .match (rf"{ re_dist } -(.+)\.dist-info" , file .name , re .IGNORECASE ))
1023+ )
1024+ dest_dir = pypi_cache_dir / f"{ distribution } =={ version } "
1025+ if not dest_dir .exists ():
1026+ install_dir .rename (dest_dir )
1027+ install_dir = dest_dir
1028+
1029+ if not package :
1030+ files = sorted ((file .name .lower () for file in install_dir .iterdir ()), reverse = True )
1031+ name = distribution .lower ().replace ("-" , "_" )
1032+ if name in files or f"{ name } .py" in files :
1033+ package = name
1034+ elif len (files ) == 1 :
1035+ raise RuntimeError (f"No package found in { distribution } =={ version } " )
1036+ else :
1037+ try :
1038+ package = next (file .split ("." , 1 )[0 ] for file in files if not file .endswith (".dist-info" ))
1039+ except StopIteration :
1040+ raise RuntimeError (f"Could not guess package name for { distribution } =={ version } (files; { files } )" ) # noqa: B904
1041+
1042+ return load (
1043+ package ,
1044+ submodules = submodules ,
1045+ try_relative_path = False ,
1046+ extensions = extensions ,
1047+ search_paths = [install_dir , * (search_paths or ())],
1048+ docstring_parser = docstring_parser ,
1049+ docstring_options = docstring_options ,
1050+ lines_collection = lines_collection ,
1051+ modules_collection = modules_collection ,
1052+ allow_inspection = allow_inspection ,
1053+ force_inspection = force_inspection ,
1054+ find_stubs_package = find_stubs_package ,
1055+ resolve_aliases = resolve_aliases ,
1056+ resolve_external = resolve_external ,
1057+ resolve_implicit = resolve_implicit ,
1058+ )
0 commit comments