Coverage for src/refcount/putils.py: 88.00%

68 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-14 17:01 +1100

1"""Platform specific helpers to manage locating native dynamic libraries. 

2 

3This module hosts features similar to https://github.com/rdotnet/dynamic-interop-dll/blob/main/DynamicInterop/PlatformUtility.cs 

4 

5""" 

6 

7import os 

8import sys 

9from ctypes.util import find_library as ctypes_find_library 

10from glob import glob 

11from typing import List, Optional, Union 

12 

13 

14def library_short_filename(library_name: Optional[str], platform: Optional[str] = None) -> str: 

15 """Based on the library name, return the platform-specific expected library short file name. 

16 

17 Args: 

18 library_name (str): name of the library, for instance 'R', which results out of this 

19 function as 'libR.so' on Linux and 'R.dll' on Windows 

20 

21 Raises: 

22 ValueError: invalid argument 

23 

24 Returns: 

25 str: expected short file name for the library, for this platform 

26 """ 

27 if platform is None: 

28 platform = sys.platform 

29 if library_name is None: 

30 raise ValueError("library_name cannot be None") 

31 if platform == "win32": 

32 return f"{library_name}.dll" 

33 if platform == "linux": 

34 return f"lib{library_name}.so" 

35 if platform == "darwin": 

36 return f"lib{library_name}.dylib" 

37 raise NotImplementedError(f"Platform '{platform}' is not (yet) supported") 

38 

39 

40def find_full_path(name: str, prefix: Optional[str] = None) -> Union[str, None]: 

41 """Find the full path of a library in under the python. 

42 

43 installation directory, or as devised by ctypes.find_library 

44 

45 Args: 

46 name (str): Library name, e.g. 'R' for the R programming language. 

47 

48 Returns: 

49 Union[str, None]: First suitable library full file name. 

50 

51 Examples: 

52 >>> from refcount.putils import * 

53 >>> find_full_path("gfortran") 

54 '/home/xxxyyy/anaconda3/envs/wqml/lib/libgfortran.so' 

55 >>> find_full_path("R") 

56 'libR.so' 

57 """ 

58 full_libpath = None 

59 if prefix is None: 59 ↛ 61line 59 didn't jump to line 61 because the condition on line 59 was always true

60 prefix = sys.prefix 

61 if name is None: 

62 return None 

63 lib_short_fname = library_short_filename(name) 

64 prefixed_lib_pat = os.path.join(prefix, "lib*", lib_short_fname) 

65 prefixed_libs = glob(prefixed_lib_pat) 

66 if prefixed_libs: 66 ↛ 67line 66 didn't jump to line 67 because the condition on line 66 was never true

67 full_libpath = prefixed_libs[0] 

68 if not full_libpath: 68 ↛ 70line 68 didn't jump to line 70 because the condition on line 68 was always true

69 full_libpath = ctypes_find_library(name) 

70 return full_libpath 

71 

72 

73# def find_full_paths(dll_short_name: str, directories: List[str] = None) -> List[str]: 

74# """Find the full paths to library files, if they exist 

75 

76# Args: 

77# dll_short_name (str): Short file name of the libary to search for, e.g. 'libgfortran.so' 

78# directories (List[str], optional): directories under which to look for this file. Defaults to None. 

79 

80# Returns: 

81# List[str]: zero or more matches, full paths to candidate files 

82# """ 

83# if directories is None: 

84# directories = [] 

85# full_paths = [os.path.join(d, dll_short_name) for d in directories] 

86# return [x for x in full_paths if os.path.exists(x)] 

87 

88 

89# def find_full_paths_env_var( 

90# dll_short_name: str, env_var_name: str = "PATH" 

91# ) -> List[str]: 

92# """Find the full paths to library files, if they exist 

93 

94# Args: 

95# dll_short_name (str): Short file name of the libary to search for, e.g. 'libgfortran.so' 

96# env_var_name (str, optional): [description]. Environment variable with paths to search under. Defaults to "PATH". 

97 

98# Returns: 

99# List[str]: zero or more matches, full paths to candidate files 

100# """ 

101# x = os.environ.get(env_var_name) 

102# if x is not None: 

103# search_paths = x.split(os.pathsep) 

104# else: 

105# search_paths = [""] 

106# return find_full_paths(dll_short_name, search_paths) 

107 

108 

109def augment_path_env( 

110 added_paths: Union[str, List[str]], 

111 subfolder: Optional[str] = None, 

112 to_env: str = "PATH", 

113 prepend: bool = False, 

114) -> str: 

115 """Build a new list of directory paths, prepending prior to an existing env var with paths. 

116 

117 New paths are prepended only if they do already exist. 

118 

119 Args: 

120 added_paths (Union[str,List[str]]): paths prepended 

121 subfolder (str, optional): Optional subfolder name to append to each in path prepended. Useful for 64/32 bits variations. Defaults to None. 

122 to_env (str, optional): Environment variable with existing Paths to start with. Defaults to 'PATH'. 

123 

124 Returns: 

125 str: Content (set of paths), typically for a updating/setting an environment variable 

126 """ 

127 path_sep = os.pathsep 

128 if isinstance(added_paths, str): 

129 added_paths = [added_paths] 

130 prior_path_env = os.environ.get(to_env) 

131 prior_paths = prior_path_env.split(path_sep) if prior_path_env is not None else [] 

132 

133 def _my_path_join(x: str, subfolder: str): # avoid trailing path separator # noqa: ANN202 

134 if subfolder is not None and subfolder != "": 

135 return os.path.join(x, subfolder) 

136 return x 

137 

138 if subfolder is not None: 138 ↛ 140line 138 didn't jump to line 140 because the condition on line 138 was always true

139 added_paths = [_my_path_join(x, subfolder) for x in added_paths] 

140 added_paths = [x for x in added_paths if os.path.exists(x)] 

141 new_paths = (added_paths + prior_paths) if prepend else (prior_paths + added_paths) 

142 # TODO: check for duplicate folders, perhaps. 

143 return path_sep.join(new_paths) 

144 

145 

146# TODO: is that of any use still?? refactored out from uchronia and co. , but appears unused. 

147# def find_first_full_path(native_lib_file_name, readable_lib_name = "native library", env_var_name = ""): 

148# if os.path.isabs(native_lib_file_name): 

149# if (not os.path.exists(native_lib_file_name)): 

150# raise FileNotFoundError("Could not find specified file {0} to load for {1}".format(native_lib_file_name, readable_lib_name)) 

151# return native_lib_file_name 

152# if (native_lib_file_name is None or native_lib_file_name == ''): 

153# raise FileNotFoundError("Invalid empty file name to load for {0}".format(readable_lib_name)) 

154# native_lib_file_name = _find_first_full_path(native_lib_file_name, env_var_name) 

155# return native_lib_file_name 

156 

157# def _find_first_full_path(short_file_name, env_var_name = ""): 

158# if (none_or_empty(short_file_name)): 

159# raise Exception("short_file_name") 

160# lib_search_path_env_var = env_var_name 

161# if (none_or_empty(lib_search_path_env_var)): 

162# if(sys.platform == 'win32'): 

163# lib_search_path_env_var = "PATH" 

164# else: 

165# lib_search_path_env_var = "LD_LIBRARY_PATH" 

166# candidates = find_full_path_env_var(short_file_name, lib_search_path_env_var) 

167# if ((len(candidates) == 0) and (sys.platform == 'win32')): 

168# if (os.path.exists(short_file_name)): 

169# candidates = [short_file_name] 

170# if (len(candidates) == 0): 

171# raise FileNotFoundError("Could not find native library named '{0}' within the directories specified in the '{1}' environment variable".format(short_file_name, lib_search_path_env_var)) 

172# else: 

173# return candidates[0] 

174 

175# def find_full_path_env_var(dll_short_name, env_var_name="PATH"): 

176# x = os.environ.get(env_var_name) 

177# if x is not None: 

178# search_paths = x.split(os.pathsep) 

179# else: 

180# search_pathsPathUpdater = [""] 

181# return find_full_paths(dll_short_name, search_paths) 

182 

183# def find_full_paths(dll_short_name, directories = []): 

184# full_paths = [os.path.join(d, dll_short_name) for d in directories] 

185# return [x for x in full_paths if os.path.exists(x)] 

186 

187# def none_or_empty(x): 

188# return (x is None or x == '') 

189 

190 

191# # The following is useful, but idiosyncratic. Consider and rethink. 

192def _win_architecture(platform: Optional[str] = None) -> str: 

193 platform = sys.platform if platform is None else platform 

194 if platform == "win32": 194 ↛ 195line 194 didn't jump to line 195 because the condition on line 194 was never true

195 arch = os.environ["PROCESSOR_ARCHITECTURE"] 

196 return "64" if arch == "AMD64" else "32" 

197 return "" 

198 

199 

200def build_new_path_env( 

201 from_env: str = "LIBRARY_PATH", 

202 to_env: str = "PATH", 

203 platform: Optional[str] = None, 

204) -> str: 

205 """Propose an update to an existing environment variable, based on the path(s) specified in another environment variable. This function is effectively meant to be useful on Windows only. 

206 

207 Args: 

208 from_env (str, optional): name of the source environment variable specifying the location(s) of custom libraries to load. Defaults to 'LIBRARY_PATH'. 

209 to_env (str, optional): environment variable to update, most likely the Windows PATH env var. Defaults to 'PATH'. 

210 

211 Returns: 

212 str: the proposed updated content for the 'to_env' environment variable. 

213 """ 

214 platform = sys.platform if platform is None else platform 

215 path_sep = os.pathsep 

216 shared_lib_paths = os.environ.get(from_env) 

217 if shared_lib_paths is not None: 

218 # We could consider a call to a logger info here 

219 subfolder = _win_architecture() 

220 shared_lib_paths_vec = shared_lib_paths.split(path_sep) 

221 return augment_path_env(shared_lib_paths_vec, subfolder, to_env=to_env) 

222 print( # noqa: T201 

223 f"WARNING: a function was called to look for environment variable '{from_env}' to update the environment variable '{to_env}', but was not found. This may be fine, but if the package fails to load because a native library is not found, this is a likely cause.", 

224 ) 

225 prior_path_env = os.environ.get(to_env) 

226 if prior_path_env is not None: 

227 return prior_path_env 

228 return "" 

229 

230 

231def update_path_windows(from_env: str = "LIBRARY_PATH", to_env: str = "PATH") -> None: 

232 """If called on Windows, append an environment variable, based on the path(s) specified in another environment variable. This function is effectively meant to be useful on Windows only. 

233 

234 Args: 

235 from_env (str, optional): name of the source environment variable specifying the location(s) of custom libraries to load. Defaults to 'LIBRARY_PATH'. 

236 to_env (str, optional): environment variable to update, most likely the Windows PATH env var. Defaults to 'PATH'. 

237 

238 Returns: 

239 None 

240 """ 

241 if sys.platform == "win32": 

242 os.environ[to_env] = build_new_path_env(from_env, to_env, sys.platform)