|
2 | 2 |
|
3 | 3 | import logging
|
4 | 4 | import sys
|
| 5 | +from abc import ABC, abstractmethod |
5 | 6 | from dataclasses import dataclass, field
|
6 | 7 | from enum import Enum
|
7 | 8 | from pathlib import Path
|
@@ -101,7 +102,22 @@ def is_used(self, imported_names: Iterable[str]) -> bool:
|
101 | 102 | return bool(self.import_names.intersection(imported_names))
|
102 | 103 |
|
103 | 104 |
|
104 |
| -class LocalPackageLookup: |
| 105 | +class BasePackageResolver(ABC): |
| 106 | + """Define the interface for doing package -> import names lookup.""" |
| 107 | + |
| 108 | + @abstractmethod |
| 109 | + def lookup_package(self, package_name: str) -> Optional[Package]: |
| 110 | + """Convert a package name into a Package object with available imports. |
| 111 | +
|
| 112 | + Return a Package object that encapsulates the package-name-to-import- |
| 113 | + names mapping for the given package name. |
| 114 | +
|
| 115 | + Return None if this PackageResolver is unable to resolve the package. |
| 116 | + """ |
| 117 | + raise NotImplementedError |
| 118 | + |
| 119 | + |
| 120 | +class LocalPackageResolver(BasePackageResolver): |
105 | 121 | """Lookup imports exposed by packages installed in a Python environment."""
|
106 | 122 |
|
107 | 123 | def __init__(self, pyenv_path: Optional[Path] = None) -> None:
|
@@ -194,32 +210,54 @@ def lookup_package(self, package_name: str) -> Optional[Package]:
|
194 | 210 | return self.packages.get(Package.normalize_name(package_name))
|
195 | 211 |
|
196 | 212 |
|
| 213 | +class IdentityMapping(BasePackageResolver): |
| 214 | + """An imperfect package resolver that assmues package name == import name. |
| 215 | +
|
| 216 | + This will resolve _any_ package name into a corresponding identical import |
| 217 | + name (modulo normalization, see Package.normalize_name() for details). |
| 218 | + """ |
| 219 | + |
| 220 | + def lookup_package(self, package_name: str) -> Optional[Package]: |
| 221 | + """Convert a package name into a Package with the same import name.""" |
| 222 | + ret = Package.identity_mapping(package_name) |
| 223 | + logger.info( |
| 224 | + f"Could not find {package_name!r} in the current environment. " |
| 225 | + f"Assuming it can be imported as {', '.join(sorted(ret.import_names))}" |
| 226 | + ) |
| 227 | + return ret |
| 228 | + |
| 229 | + |
197 | 230 | def resolve_dependencies(
|
198 | 231 | dep_names: Iterable[str], pyenv_path: Optional[Path] = None
|
199 | 232 | ) -> Dict[str, Package]:
|
200 | 233 | """Associate dependencies with corresponding Package objects.
|
201 | 234 |
|
202 |
| - Use LocalPackageLookup to find Package objects for each of the given |
| 235 | + Use LocalPackageResolver to find Package objects for each of the given |
203 | 236 | dependencies inside the virtualenv given by 'pyenv_path'. When 'pyenv_path'
|
204 | 237 | is None (the default), look for packages in the current Python environment
|
205 | 238 | (i.e. equivalent to sys.path).
|
206 | 239 |
|
207 |
| - For dependencies that cannot be found with LocalPackageLookup, |
208 |
| - fabricate an identity mapping (a pseudo-package making available an import |
209 |
| - of the same name as the package, modulo normalization). |
| 240 | + For dependencies that cannot be found with LocalPackageResolver, fall back |
| 241 | + an identity mapping (a pseudo-package making available an import of the same |
| 242 | + name as the package, modulo normalization). |
210 | 243 |
|
211 | 244 | Return a dict mapping dependency names to the resolved Package objects.
|
212 | 245 | """
|
| 246 | + # This defines the "stack" of resolvers that we will use to convert |
| 247 | + # dependencies into provided import names. We call .lookup() on each |
| 248 | + # resolver in order until one of them returns a Package object. At that |
| 249 | + # point we are happy, and do not consult any of the later resolvers. |
| 250 | + resolvers = [ |
| 251 | + LocalPackageResolver(pyenv_path), |
| 252 | + IdentityMapping(), |
| 253 | + ] |
213 | 254 | ret = {}
|
214 |
| - local_packages = LocalPackageLookup(pyenv_path) |
215 | 255 | for name in dep_names:
|
216 | 256 | if name not in ret:
|
217 |
| - package = local_packages.lookup_package(name) |
218 |
| - if package is None: # fall back to identity mapping |
219 |
| - package = Package.identity_mapping(name) |
220 |
| - logger.info( |
221 |
| - f"Could not find {name!r} in the current environment. Assuming " |
222 |
| - f"it can be imported as {', '.join(sorted(package.import_names))}" |
223 |
| - ) |
224 |
| - ret[name] = package |
| 257 | + for resolver in resolvers: |
| 258 | + package = resolver.lookup_package(name) |
| 259 | + if package is None: # skip to next resolver |
| 260 | + continue |
| 261 | + ret[name] = package |
| 262 | + break # skip to next dependency name |
225 | 263 | return ret
|
0 commit comments