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

1"""dependenpy finder module.""" 

2 

3from __future__ import annotations 

4 

5from importlib.util import find_spec 

6from os.path import basename, exists, isdir, isfile, join, splitext 

7from typing import Any, List, Type 

8 

9 

10class PackageSpec(object): 

11 """Holder for a package specification (given as argument to DSM).""" 

12 

13 def __init__(self, name, path, limit_to=None): 

14 """ 

15 Initialization method. 

16 

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 [] 

25 

26 def __hash__(self): 

27 return hash((self.name, self.path)) 

28 

29 @property 

30 def ismodule(self) -> bool: 

31 """ 

32 Property to tell if the package is in fact a module (a file). 

33 

34 Returns: 

35 Whether this package is in fact a module. 

36 """ 

37 return self.path.endswith(".py") 

38 

39 def add(self, spec: PackageSpec) -> None: 

40 """ 

41 Add limitations of given spec to self's. 

42 

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) 

49 

50 @staticmethod # noqa: WPS602 

51 def combine(specs: list[PackageSpec]) -> list[PackageSpec]: # noqa: WPS602 

52 """ 

53 Combine package specifications' limitations. 

54 

55 Args: 

56 specs: The package specifications. 

57 

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()) 

68 

69 

70class PackageFinder(object): 

71 """Abstract package finder class.""" 

72 

73 def find(self, package: str, **kwargs: Any) -> PackageSpec | None: 

74 """ 

75 Find method. 

76 

77 Args: 

78 package: package to find. 

79 **kwargs: additional keyword arguments. 

80 

81 Returns: 

82 Package spec or None. 

83 """ # noqa: DAR202,DAR401 

84 raise NotImplementedError 

85 

86 

87class LocalPackageFinder(PackageFinder): 

88 """Finder to find local packages (directories on the disk).""" 

89 

90 def find(self, package: str, **kwargs: Any) -> PackageSpec | None: 

91 """ 

92 Find method. 

93 

94 Args: 

95 package: package to find. 

96 **kwargs: additional keyword arguments. 

97 

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 

113 

114 

115class InstalledPackageFinder(PackageFinder): 

116 """Finder to find installed Python packages using importlib.""" 

117 

118 def find(self, package: str, **kwargs: Any) -> PackageSpec | None: 

119 """ 

120 Find method. 

121 

122 Args: 

123 package: package to find. 

124 **kwargs: additional keyword arguments. 

125 

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 

147 

148 

149class Finder(object): 

150 """ 

151 Main package finder class. 

152 

153 Initialize it with a list of package finder classes (not instances). 

154 """ 

155 

156 def __init__(self, finders: List[Type] = None): 

157 """ 

158 Initialization method. 

159 

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] 

168 

169 def find(self, package: str, **kwargs: Any) -> PackageSpec | None: 

170 """ 

171 Find a package using package finders. 

172 

173 Return the first package found. 

174 

175 Args: 

176 package: package to find. 

177 **kwargs: additional keyword arguments used by finders. 

178 

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