libembedding-ng 1.12.0__py3-none-macosx_15_0_universal2.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- libembedding/__init__.py +212 -0
- libembedding/_binding.py +179 -0
- libembedding/_cdefs.h +970 -0
- libembedding/_status.py +44 -0
- libembedding/autotune.py +383 -0
- libembedding/backend.py +63 -0
- libembedding/benchmark.py +303 -0
- libembedding/cache.py +164 -0
- libembedding/exceptions.py +55 -0
- libembedding/gguf_inspect.py +180 -0
- libembedding/image_embedding.py +255 -0
- libembedding/libembedding.dylib +0 -0
- libembedding/libonnxruntime.dylib +4 -0
- libembedding/models.py +200 -0
- libembedding/pool.py +147 -0
- libembedding/py.typed +0 -0
- libembedding/reranker.py +606 -0
- libembedding/sampling.py +66 -0
- libembedding/similarity.py +44 -0
- libembedding/sparse_text_embedding.py +524 -0
- libembedding/text_embedding.py +607 -0
- libembedding/types.py +157 -0
- libembedding_ng-1.12.0.dist-info/METADATA +463 -0
- libembedding_ng-1.12.0.dist-info/RECORD +27 -0
- libembedding_ng-1.12.0.dist-info/WHEEL +5 -0
- libembedding_ng-1.12.0.dist-info/licenses/LICENSE +21 -0
- libembedding_ng-1.12.0.dist-info/top_level.txt +1 -0
libembedding/__init__.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""libembedding — Fast ONNX-based text, image, and sparse embeddings for Python."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version("libembedding-ng")
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
__version__ = "0.0.0"
|
|
10
|
+
|
|
11
|
+
from ._binding import ffi, lib
|
|
12
|
+
from .autotune import (
|
|
13
|
+
auto_select_model,
|
|
14
|
+
autotune,
|
|
15
|
+
autotune_unified,
|
|
16
|
+
cleanup_model_cache,
|
|
17
|
+
cleanup_model_cache_except,
|
|
18
|
+
clear_autotune_cache,
|
|
19
|
+
clear_autotune_cache_unified,
|
|
20
|
+
)
|
|
21
|
+
from .backend import detect_backend
|
|
22
|
+
from .benchmark import (
|
|
23
|
+
Benchmark,
|
|
24
|
+
BenchmarkResult,
|
|
25
|
+
ComparisonResult,
|
|
26
|
+
CorpusType,
|
|
27
|
+
HardwareInfo,
|
|
28
|
+
Metrics,
|
|
29
|
+
Objective,
|
|
30
|
+
cache_path,
|
|
31
|
+
clear_cache,
|
|
32
|
+
detect_hardware,
|
|
33
|
+
)
|
|
34
|
+
from .cache import EmbeddingCache, cache_config_default
|
|
35
|
+
from .exceptions import LembedError, LlamaError
|
|
36
|
+
from .gguf_inspect import (
|
|
37
|
+
SPARSE_FORMULA_SCALAR,
|
|
38
|
+
SPARSE_FORMULA_SPLADE,
|
|
39
|
+
SPARSE_FORMULA_UNKNOWN,
|
|
40
|
+
GgufCapabilities,
|
|
41
|
+
GgufDesc,
|
|
42
|
+
inspect_gguf,
|
|
43
|
+
)
|
|
44
|
+
from .image_embedding import ImageEmbedding, image_autotune
|
|
45
|
+
from .models import (
|
|
46
|
+
list_image_models,
|
|
47
|
+
list_reranker_models,
|
|
48
|
+
list_sparse_models,
|
|
49
|
+
list_text_models,
|
|
50
|
+
)
|
|
51
|
+
from .reranker import (
|
|
52
|
+
Reranker,
|
|
53
|
+
clear_reranker_autotune_cache,
|
|
54
|
+
reranker_auto_config,
|
|
55
|
+
reranker_auto_config_profile,
|
|
56
|
+
reranker_autotune,
|
|
57
|
+
reranker_autotune_constrained,
|
|
58
|
+
reranker_autotune_custom,
|
|
59
|
+
)
|
|
60
|
+
from .similarity import cosine_similarity, dot_product, euclidean_distance
|
|
61
|
+
from .sparse_text_embedding import (
|
|
62
|
+
SparseTextEmbedding,
|
|
63
|
+
sparse_autotune,
|
|
64
|
+
sparse_best_config,
|
|
65
|
+
)
|
|
66
|
+
from .text_embedding import ( # pyright: ignore[reportPrivateImportUsage]
|
|
67
|
+
TextEmbedding,
|
|
68
|
+
TextEmbeddingPool, # pyright: ignore[reportPrivateImportUsage]
|
|
69
|
+
)
|
|
70
|
+
from .types import (
|
|
71
|
+
ImageTuningResult,
|
|
72
|
+
ModelDesc,
|
|
73
|
+
ModelInfo,
|
|
74
|
+
ModelSelectionResult,
|
|
75
|
+
RerankerTuningResult,
|
|
76
|
+
RerankResult,
|
|
77
|
+
SparseEmbedding,
|
|
78
|
+
SparseTuningResult,
|
|
79
|
+
Stats,
|
|
80
|
+
TuningResult,
|
|
81
|
+
UnifiedTuningResult,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
_logger = logging.getLogger(__name__)
|
|
85
|
+
|
|
86
|
+
# Version from C API (runtime)
|
|
87
|
+
try:
|
|
88
|
+
__version__ = lib.lembed_version().decode("utf-8", errors="replace")
|
|
89
|
+
except (AttributeError, OSError) as e:
|
|
90
|
+
_logger.debug("Could not get version from C API: %s", e)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def ggml_cpu_features() -> list[str]:
|
|
94
|
+
"""Instruction sets of the ggml CPU backend that runs GGUF models.
|
|
95
|
+
|
|
96
|
+
The wheel ships one ggml CPU library per instruction-set level and ggml
|
|
97
|
+
loads the best match for this machine; this names the one it picked, e.g.
|
|
98
|
+
``["SSE3", ..., "AVX512", "AVX512_VNNI", "LLAMAFILE", "REPACK"]``. Record
|
|
99
|
+
it with any GGUF benchmark: AVX2 and AVX-512 VNNI kernels are not
|
|
100
|
+
comparable. Loads the backend on first call. Empty when no CPU backend
|
|
101
|
+
could be loaded (a broken install).
|
|
102
|
+
"""
|
|
103
|
+
return ffi.string(lib.lembed_ggml_cpu_features()).decode("ascii").split()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Autotune modes (re-exported for convenience)
|
|
107
|
+
LEMBED_AUTOTUNE_QUICK = lib.LEMBED_AUTOTUNE_QUICK
|
|
108
|
+
LEMBED_AUTOTUNE_FULL = lib.LEMBED_AUTOTUNE_FULL
|
|
109
|
+
|
|
110
|
+
# Optimization objectives
|
|
111
|
+
LEMBED_OBJECTIVE_LATENCY = lib.LEMBED_OBJECTIVE_LATENCY
|
|
112
|
+
LEMBED_OBJECTIVE_THROUGHPUT = lib.LEMBED_OBJECTIVE_THROUGHPUT
|
|
113
|
+
LEMBED_OBJECTIVE_BALANCED = lib.LEMBED_OBJECTIVE_BALANCED
|
|
114
|
+
LEMBED_OBJECTIVE_MEMORY = lib.LEMBED_OBJECTIVE_MEMORY
|
|
115
|
+
|
|
116
|
+
# Task types for unified auto-tuner
|
|
117
|
+
LEMBED_TASK_EMBEDDING = lib.LEMBED_TASK_EMBEDDING
|
|
118
|
+
LEMBED_TASK_RERANKING = lib.LEMBED_TASK_RERANKING
|
|
119
|
+
LEMBED_TASK_IMAGE = lib.LEMBED_TASK_IMAGE
|
|
120
|
+
LEMBED_TASK_SPARSE = lib.LEMBED_TASK_SPARSE
|
|
121
|
+
|
|
122
|
+
# Reranker profiles
|
|
123
|
+
LEMBED_PROFILE_INTERACTIVE = lib.LEMBED_PROFILE_INTERACTIVE
|
|
124
|
+
LEMBED_PROFILE_BALANCED = lib.LEMBED_PROFILE_BALANCED
|
|
125
|
+
LEMBED_PROFILE_QUALITY = lib.LEMBED_PROFILE_QUALITY
|
|
126
|
+
|
|
127
|
+
# Sparse embedding storage formats
|
|
128
|
+
LEMBED_SPARSE_FORMAT_DICT = lib.LEMBED_SPARSE_FORMAT_DICT
|
|
129
|
+
LEMBED_SPARSE_FORMAT_INDEX_ORDER = lib.LEMBED_SPARSE_FORMAT_INDEX_ORDER
|
|
130
|
+
LEMBED_SPARSE_FORMAT_CSR = lib.LEMBED_SPARSE_FORMAT_CSR
|
|
131
|
+
LEMBED_SPARSE_FORMAT_NUMPY = lib.LEMBED_SPARSE_FORMAT_NUMPY
|
|
132
|
+
|
|
133
|
+
__all__ = [
|
|
134
|
+
"LEMBED_AUTOTUNE_FULL",
|
|
135
|
+
"LEMBED_AUTOTUNE_QUICK",
|
|
136
|
+
"LEMBED_OBJECTIVE_BALANCED",
|
|
137
|
+
"LEMBED_OBJECTIVE_LATENCY",
|
|
138
|
+
"LEMBED_OBJECTIVE_MEMORY",
|
|
139
|
+
"LEMBED_OBJECTIVE_THROUGHPUT",
|
|
140
|
+
"LEMBED_PROFILE_BALANCED",
|
|
141
|
+
"LEMBED_PROFILE_INTERACTIVE",
|
|
142
|
+
"LEMBED_PROFILE_QUALITY",
|
|
143
|
+
"LEMBED_SPARSE_FORMAT_CSR",
|
|
144
|
+
"LEMBED_SPARSE_FORMAT_DICT",
|
|
145
|
+
"LEMBED_SPARSE_FORMAT_INDEX_ORDER",
|
|
146
|
+
"LEMBED_SPARSE_FORMAT_NUMPY",
|
|
147
|
+
"LEMBED_TASK_EMBEDDING",
|
|
148
|
+
"LEMBED_TASK_IMAGE",
|
|
149
|
+
"LEMBED_TASK_RERANKING",
|
|
150
|
+
"LEMBED_TASK_SPARSE",
|
|
151
|
+
"SPARSE_FORMULA_SCALAR",
|
|
152
|
+
"SPARSE_FORMULA_SPLADE",
|
|
153
|
+
"SPARSE_FORMULA_UNKNOWN",
|
|
154
|
+
"Benchmark",
|
|
155
|
+
"BenchmarkResult",
|
|
156
|
+
"ComparisonResult",
|
|
157
|
+
"CorpusType",
|
|
158
|
+
"EmbeddingCache",
|
|
159
|
+
"GgufCapabilities",
|
|
160
|
+
"GgufDesc",
|
|
161
|
+
"HardwareInfo",
|
|
162
|
+
"ImageEmbedding",
|
|
163
|
+
"ImageTuningResult",
|
|
164
|
+
"LembedError",
|
|
165
|
+
"LlamaError",
|
|
166
|
+
"Metrics",
|
|
167
|
+
"ModelDesc",
|
|
168
|
+
"ModelInfo",
|
|
169
|
+
"ModelSelectionResult",
|
|
170
|
+
"Objective",
|
|
171
|
+
"RerankResult",
|
|
172
|
+
"Reranker",
|
|
173
|
+
"RerankerTuningResult",
|
|
174
|
+
"SparseEmbedding",
|
|
175
|
+
"SparseTextEmbedding",
|
|
176
|
+
"SparseTuningResult",
|
|
177
|
+
"Stats",
|
|
178
|
+
"TextEmbedding",
|
|
179
|
+
"TextEmbeddingPool",
|
|
180
|
+
"TuningResult",
|
|
181
|
+
"UnifiedTuningResult",
|
|
182
|
+
"auto_select_model",
|
|
183
|
+
"autotune",
|
|
184
|
+
"autotune_unified",
|
|
185
|
+
"cache_config_default",
|
|
186
|
+
"cache_path",
|
|
187
|
+
"cleanup_model_cache",
|
|
188
|
+
"cleanup_model_cache_except",
|
|
189
|
+
"clear_autotune_cache",
|
|
190
|
+
"clear_autotune_cache_unified",
|
|
191
|
+
"clear_cache",
|
|
192
|
+
"clear_reranker_autotune_cache",
|
|
193
|
+
"cosine_similarity",
|
|
194
|
+
"detect_backend",
|
|
195
|
+
"detect_hardware",
|
|
196
|
+
"dot_product",
|
|
197
|
+
"euclidean_distance",
|
|
198
|
+
"ggml_cpu_features",
|
|
199
|
+
"image_autotune",
|
|
200
|
+
"inspect_gguf",
|
|
201
|
+
"list_image_models",
|
|
202
|
+
"list_reranker_models",
|
|
203
|
+
"list_sparse_models",
|
|
204
|
+
"list_text_models",
|
|
205
|
+
"reranker_auto_config",
|
|
206
|
+
"reranker_auto_config_profile",
|
|
207
|
+
"reranker_autotune",
|
|
208
|
+
"reranker_autotune_constrained",
|
|
209
|
+
"reranker_autotune_custom",
|
|
210
|
+
"sparse_autotune",
|
|
211
|
+
"sparse_best_config",
|
|
212
|
+
]
|
libembedding/_binding.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Low-level cffi binding layer. Loads the shared library and exposes ffi/lib.
|
|
2
|
+
|
|
3
|
+
Auteur: David Orel
|
|
4
|
+
Version: 1.12.0
|
|
5
|
+
|
|
6
|
+
SPDX-License-Identifier: MIT
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
import warnings
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import cffi
|
|
17
|
+
|
|
18
|
+
ffi = cffi.FFI()
|
|
19
|
+
|
|
20
|
+
_cdefs_path = Path(__file__).with_name("_cdefs.h")
|
|
21
|
+
ffi.cdef(_cdefs_path.read_text(encoding="utf-8"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _library_candidates(pkg_dir: Path, root: Path, ext: str) -> tuple[list[Path], int]:
|
|
25
|
+
"""Candidate library paths in lookup order, and how many are package copies.
|
|
26
|
+
|
|
27
|
+
The package copies come first: they are what a wheel install ships. The
|
|
28
|
+
build-tree paths only exist in a source checkout.
|
|
29
|
+
"""
|
|
30
|
+
packaged = [pkg_dir / f"libembedding{ext}", pkg_dir / "lib" / f"libembedding{ext}"]
|
|
31
|
+
build_tree = [
|
|
32
|
+
root / "build" / "Debug" / f"libembedding{ext}",
|
|
33
|
+
root / "build" / "Release" / f"libembedding{ext}",
|
|
34
|
+
root / "build" / "python" / f"libembedding{ext}",
|
|
35
|
+
root / "build" / f"libembedding{ext}",
|
|
36
|
+
root / "build3" / "Release" / f"libembedding{ext}",
|
|
37
|
+
root / "build3" / f"libembedding{ext}",
|
|
38
|
+
root / "build_test" / "Debug" / f"libembedding{ext}",
|
|
39
|
+
root / "build_test" / "Release" / f"libembedding{ext}",
|
|
40
|
+
]
|
|
41
|
+
return packaged + build_tree, len(packaged)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _pick_library(candidates: list[Path], packaged_count: int) -> Path | None:
|
|
45
|
+
"""First existing candidate; warns when it is a package copy older than a build.
|
|
46
|
+
|
|
47
|
+
In a source checkout the package directory is the wheel staging area: a copy
|
|
48
|
+
left there goes stale as soon as the tree is rebuilt, and every test or
|
|
49
|
+
benchmark run without LIBEMBEDDING_LIBRARY then silently exercises old code.
|
|
50
|
+
"""
|
|
51
|
+
existing = [c for c in candidates if c.is_file()]
|
|
52
|
+
if not existing:
|
|
53
|
+
return None
|
|
54
|
+
chosen = existing[0]
|
|
55
|
+
if chosen in candidates[:packaged_count]:
|
|
56
|
+
newer = [
|
|
57
|
+
c for c in existing
|
|
58
|
+
if c in candidates[packaged_count:]
|
|
59
|
+
and c.stat().st_mtime > chosen.stat().st_mtime
|
|
60
|
+
]
|
|
61
|
+
if newer:
|
|
62
|
+
warnings.warn(
|
|
63
|
+
f"Loading {chosen}, which is older than the build output {newer[0]}. "
|
|
64
|
+
"Set LIBEMBEDDING_LIBRARY to the library to test, or remove the "
|
|
65
|
+
"stale copy.",
|
|
66
|
+
stacklevel=3,
|
|
67
|
+
)
|
|
68
|
+
return chosen
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _find_library() -> str:
|
|
72
|
+
# An explicit path wins: it is the only unambiguous choice in a source
|
|
73
|
+
# checkout that holds several builds.
|
|
74
|
+
override = os.environ.get("LIBEMBEDDING_LIBRARY")
|
|
75
|
+
if override:
|
|
76
|
+
if not Path(override).is_file():
|
|
77
|
+
raise OSError(f"LIBEMBEDDING_LIBRARY points to a missing file: {override}")
|
|
78
|
+
return str(Path(override).resolve())
|
|
79
|
+
|
|
80
|
+
ext = {"Darwin": ".dylib", "Linux": ".so", "Windows": ".dll"}.get(
|
|
81
|
+
platform.system(), ".so"
|
|
82
|
+
)
|
|
83
|
+
pkg_dir = Path(__file__).parent
|
|
84
|
+
root = pkg_dir.parent.parent.parent # project root (libembedding/)
|
|
85
|
+
chosen = _pick_library(*_library_candidates(pkg_dir, root, ext))
|
|
86
|
+
return str(chosen.resolve()) if chosen else f"libembedding{ext}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Runtime dependencies that must be discoverable before libembedding is loaded.
|
|
90
|
+
# On Windows the loader resolves a DLL's imports from the executable directory,
|
|
91
|
+
# the directories registered with SetDllDirectoryW, and PATH. The libcurl build
|
|
92
|
+
# that ships inside the package lives in the package directory, which is not the
|
|
93
|
+
# same directory as libembedding.dll in a source checkout, so both are registered
|
|
94
|
+
# (see _runtime_dirs for the bundled third_party/curl/bin location).
|
|
95
|
+
# ggml's CPU backend is not listed: with CPU dispatch it is one of several
|
|
96
|
+
# ggml-cpu-<variant> modules that ggml scores and loads itself, from the
|
|
97
|
+
# library's directory; preloading one here would pick the variant for it.
|
|
98
|
+
_DEPENDENT_DLLS = (
|
|
99
|
+
"onnxruntime.dll",
|
|
100
|
+
"onnxruntime_providers_shared.dll",
|
|
101
|
+
"ggml.dll",
|
|
102
|
+
"ggml-base.dll",
|
|
103
|
+
"llama.dll",
|
|
104
|
+
"libcurl-x64.dll",
|
|
105
|
+
"libcurl.dll",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _runtime_dirs(lib_path: Path) -> list[Path]:
|
|
110
|
+
"""Directories that may contain dependent shared libraries, in priority order.
|
|
111
|
+
|
|
112
|
+
The library's own directory comes first: a build tree places the matching
|
|
113
|
+
ggml, llama and ONNX Runtime copies next to libembedding. Other build
|
|
114
|
+
directories (build/bin/Debug, build/bin/Release) are deliberately absent:
|
|
115
|
+
taking a dependency from there mixes Debug with Release, or a stale build
|
|
116
|
+
with a fresh one, in the same process.
|
|
117
|
+
"""
|
|
118
|
+
root = Path(__file__).parent.parent.parent.parent
|
|
119
|
+
dirs = [
|
|
120
|
+
lib_path.parent,
|
|
121
|
+
# Wheel layout, and the MSVC runtime a wheel bundles.
|
|
122
|
+
Path(__file__).parent,
|
|
123
|
+
# Source-checkout libcurl installed by scripts/fetch_windows_libcurl.ps1;
|
|
124
|
+
# absent from a wheel install, hence the is_dir() filter below.
|
|
125
|
+
root / "third_party" / "curl" / "bin",
|
|
126
|
+
]
|
|
127
|
+
seen: set[Path] = set()
|
|
128
|
+
ordered: list[Path] = []
|
|
129
|
+
for d in dirs:
|
|
130
|
+
if d.is_dir() and d not in seen:
|
|
131
|
+
seen.add(d)
|
|
132
|
+
ordered.append(d)
|
|
133
|
+
return ordered
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _select_dependencies(dirs: list[Path]) -> list[Path]:
|
|
137
|
+
"""One path per known dependency: the copy in the first directory holding it.
|
|
138
|
+
|
|
139
|
+
Windows resolves an import by module name to an already loaded DLL, so
|
|
140
|
+
whichever copy is preloaded first is the one libembedding binds to. Loading
|
|
141
|
+
a single copy per name makes that choice explicit and keeps every other copy
|
|
142
|
+
out of the process.
|
|
143
|
+
"""
|
|
144
|
+
selected: list[Path] = []
|
|
145
|
+
for name in _DEPENDENT_DLLS:
|
|
146
|
+
for d in dirs:
|
|
147
|
+
if (d / name).is_file():
|
|
148
|
+
selected.append(d / name)
|
|
149
|
+
break
|
|
150
|
+
return selected
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if platform.system() == "Windows":
|
|
154
|
+
import ctypes
|
|
155
|
+
|
|
156
|
+
_lib_path_str = _find_library()
|
|
157
|
+
_lib_path = Path(_lib_path_str)
|
|
158
|
+
if _lib_path.exists():
|
|
159
|
+
_dirs = _runtime_dirs(_lib_path)
|
|
160
|
+
for _dep_path in _select_dependencies(_dirs):
|
|
161
|
+
try:
|
|
162
|
+
ctypes.CDLL(str(_dep_path))
|
|
163
|
+
except OSError:
|
|
164
|
+
pass
|
|
165
|
+
for _dir in reversed(_dirs):
|
|
166
|
+
ctypes.windll.kernel32.AddDllDirectory(str(_dir))
|
|
167
|
+
os.environ["PATH"] = str(_dir) + os.pathsep + os.environ.get("PATH", "")
|
|
168
|
+
# SetDllDirectoryW keeps a single directory: the library's own.
|
|
169
|
+
ctypes.windll.kernel32.SetDllDirectoryW(str(_dirs[0]))
|
|
170
|
+
lib = ffi.dlopen(_lib_path_str)
|
|
171
|
+
else:
|
|
172
|
+
_lib_path_str = _find_library()
|
|
173
|
+
_lib_path = Path(_lib_path_str)
|
|
174
|
+
if _lib_path.exists():
|
|
175
|
+
_dirs = [str(d) for d in _runtime_dirs(_lib_path)]
|
|
176
|
+
_env_var = "DYLD_LIBRARY_PATH" if platform.system() == "Darwin" else "LD_LIBRARY_PATH"
|
|
177
|
+
_existing = os.environ.get(_env_var, "")
|
|
178
|
+
os.environ[_env_var] = os.pathsep.join([*_dirs, _existing]) if _existing else os.pathsep.join(_dirs)
|
|
179
|
+
lib = ffi.dlopen(_lib_path_str)
|