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
« 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.
3This module hosts features similar to https://github.com/rdotnet/dynamic-interop-dll/blob/main/DynamicInterop/PlatformUtility.cs
5"""
7import os
8import sys
9from ctypes.util import find_library as ctypes_find_library
10from glob import glob
11from typing import List, Optional, Union
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.
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
21 Raises:
22 ValueError: invalid argument
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")
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.
43 installation directory, or as devised by ctypes.find_library
45 Args:
46 name (str): Library name, e.g. 'R' for the R programming language.
48 Returns:
49 Union[str, None]: First suitable library full file name.
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
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
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.
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)]
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
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".
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)
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.
117 New paths are prepended only if they do already exist.
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'.
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 []
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
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)
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
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]
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)
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)]
187# def none_or_empty(x):
188# return (x is None or x == '')
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 ""
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.
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'.
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 ""
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.
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'.
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)