Coverage for src/dependenpy/finder.py: 61.86%
72 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-09-04 11:35 +0200
« prev ^ index » next coverage.py v6.4.1, created at 2022-09-04 11:35 +0200
1"""dependenpy finder module."""
3from __future__ import annotations
5from importlib.util import find_spec
6from os.path import basename, exists, isdir, isfile, join, splitext
7from typing import Any, List, Type
10class PackageSpec(object):
11 """Holder for a package specification (given as argument to DSM)."""
13 def __init__(self, name, path, limit_to=None):
14 """
15 Initialization method.
17 Args:
18 name (str): name of the package.
19 path (str): path to the package.
20 limit_to (list of str): limitations.
21 """
22 self.name = name
23 self.path = path
24 self.limit_to = limit_to or []
26 def __hash__(self):
27 return hash((self.name, self.path))
29 @property
30 def ismodule(self) -> bool:
31 """
32 Property to tell if the package is in fact a module (a file).
34 Returns:
35 Whether this package is in fact a module.
36 """
37 return self.path.endswith(".py")
39 def add(self, spec: PackageSpec) -> None:
40 """
41 Add limitations of given spec to self's.
43 Args:
44 spec: Another spec.
45 """
46 for limit in spec.limit_to:
47 if limit not in self.limit_to:
48 self.limit_to.append(limit)
50 @staticmethod # noqa: WPS602
51 def combine(specs: list[PackageSpec]) -> list[PackageSpec]: # noqa: WPS602
52 """
53 Combine package specifications' limitations.
55 Args:
56 specs: The package specifications.
58 Returns:
59 The new, merged list of PackageSpec.
60 """
61 new_specs: dict[PackageSpec, PackageSpec] = {}
62 for spec in specs:
63 if new_specs.get(spec, None) is None: 63 ↛ 66line 63 didn't jump to line 66, because the condition on line 63 was never false
64 new_specs[spec] = spec
65 else:
66 new_specs[spec].add(spec)
67 return list(new_specs.values())
70class PackageFinder(object):
71 """Abstract package finder class."""
73 def find(self, package: str, **kwargs: Any) -> PackageSpec | None:
74 """
75 Find method.
77 Args:
78 package: package to find.
79 **kwargs: additional keyword arguments.
81 Returns:
82 Package spec or None.
83 """ # noqa: DAR202,DAR401
84 raise NotImplementedError
87class LocalPackageFinder(PackageFinder):
88 """Finder to find local packages (directories on the disk)."""
90 def find(self, package: str, **kwargs: Any) -> PackageSpec | None:
91 """
92 Find method.
94 Args:
95 package: package to find.
96 **kwargs: additional keyword arguments.
98 Returns:
99 Package spec or None.
100 """
101 if not exists(package): 101 ↛ 103line 101 didn't jump to line 103, because the condition on line 101 was never false
102 return None
103 name, path = None, None
104 enforce_init = kwargs.pop("enforce_init", True)
105 if isdir(package):
106 if isfile(join(package, "__init__.py")) or not enforce_init:
107 name, path = basename(package), package
108 elif isfile(package) and package.endswith(".py"):
109 name, path = splitext(basename(package))[0], package
110 if name and path:
111 return PackageSpec(name, path)
112 return None
115class InstalledPackageFinder(PackageFinder):
116 """Finder to find installed Python packages using importlib."""
118 def find(self, package: str, **kwargs: Any) -> PackageSpec | None:
119 """
120 Find method.
122 Args:
123 package: package to find.
124 **kwargs: additional keyword arguments.
126 Returns:
127 Package spec or None.
128 """
129 spec = find_spec(package)
130 if spec is None:
131 return None
132 if "." in package: 132 ↛ 133line 132 didn't jump to line 133, because the condition on line 132 was never true
133 package, rest = package.split(".", 1)
134 limit = [rest]
135 spec = find_spec(package)
136 else:
137 limit = []
138 if spec is not None: 138 ↛ 146line 138 didn't jump to line 146, because the condition on line 138 was never false
139 if spec.submodule_search_locations: 139 ↛ 141line 139 didn't jump to line 141, because the condition on line 139 was never false
140 path = spec.submodule_search_locations[0]
141 elif spec.origin and spec.origin != "built-in":
142 path = spec.origin
143 else:
144 return None
145 return PackageSpec(spec.name, path, limit)
146 return None
149class Finder(object):
150 """
151 Main package finder class.
153 Initialize it with a list of package finder classes (not instances).
154 """
156 def __init__(self, finders: List[Type] = None):
157 """
158 Initialization method.
160 Args:
161 finders: list of package finder classes (not instances) in a specific
162 order. Default: [LocalPackageFinder, InstalledPackageFinder].
163 """
164 if finders is None: 164 ↛ 167line 164 didn't jump to line 167, because the condition on line 164 was never false
165 self.finders = [LocalPackageFinder(), InstalledPackageFinder()]
166 else:
167 self.finders = [finder() for finder in finders]
169 def find(self, package: str, **kwargs: Any) -> PackageSpec | None:
170 """
171 Find a package using package finders.
173 Return the first package found.
175 Args:
176 package: package to find.
177 **kwargs: additional keyword arguments used by finders.
179 Returns:
180 Package spec or None.
181 """
182 for finder in self.finders:
183 package_spec = finder.find(package, **kwargs)
184 if package_spec:
185 return package_spec
186 return None