|
4 | 4 |
|
5 | 5 | import ctypes |
6 | 6 | import sys |
| 7 | +from dataclasses import dataclass |
| 8 | +from functools import cached_property |
7 | 9 | from importlib.metadata import version |
8 | 10 | from pathlib import Path |
9 | 11 |
|
10 | | -__all__ = ["__version__", "get_library_version"] |
| 12 | +__all__ = ["__version__", "HighsExtrasFeatureInfo", "library"] |
11 | 13 |
|
12 | 14 | __version__ = version("highspy-extras") |
13 | | - |
14 | 15 | _PACKAGE_DIR = Path(__file__).resolve().parent |
15 | | -_library_handle: ctypes.CDLL | None = None |
16 | 16 |
|
17 | 17 |
|
18 | | -def _load_library() -> ctypes.CDLL: |
19 | | - global _library_handle |
| 18 | +class _HighsExtrasFeatureInfoRaw(ctypes.Structure): |
| 19 | + """Raw feature info layout exposed by the shared library.""" |
| 20 | + |
| 21 | + _fields_ = [ |
| 22 | + ("provider", ctypes.c_char_p), |
| 23 | + ("version", ctypes.c_char_p), |
| 24 | + ("license", ctypes.c_char_p), |
| 25 | + ("enabled", ctypes.c_bool), |
| 26 | + ] |
| 27 | + |
20 | 28 |
|
21 | | - if _library_handle is not None: |
22 | | - return _library_handle |
| 29 | +@dataclass(frozen=True) |
| 30 | +class HighsExtrasFeatureInfo: |
| 31 | + """Feature metadata for an external dependency.""" |
23 | 32 |
|
24 | | - if sys.platform == "win32": |
25 | | - library_name = "highs_extras.dll" |
26 | | - elif sys.platform == "darwin": |
27 | | - library_name = "libhighs_extras.dylib" |
28 | | - else: |
29 | | - library_name = "libhighs_extras.so" |
| 33 | + provider: str |
| 34 | + version: str |
| 35 | + license: str |
| 36 | + enabled: bool |
30 | 37 |
|
31 | | - library_path = _PACKAGE_DIR / library_name |
32 | | - if not library_path.is_file(): |
33 | | - raise FileNotFoundError( |
34 | | - f"Could not find the highs_extras shared library at {library_path}" |
| 38 | + @classmethod |
| 39 | + def from_raw(cls, raw: _HighsExtrasFeatureInfoRaw) -> HighsExtrasFeatureInfo: |
| 40 | + return cls( |
| 41 | + provider=raw.provider.decode("utf-8"), |
| 42 | + version=raw.version.decode("utf-8"), |
| 43 | + license=raw.license.decode("utf-8"), |
| 44 | + enabled=bool(raw.enabled), |
35 | 45 | ) |
36 | 46 |
|
37 | | - _library_handle = ctypes.CDLL(str(library_path)) |
38 | | - return _library_handle |
39 | 47 |
|
| 48 | +class HighsExtrasLibrary: |
| 49 | + """Wrapper around the highs_extras shared library.""" |
| 50 | + |
| 51 | + @cached_property |
| 52 | + def handle(self) -> ctypes.CDLL: |
| 53 | + if sys.platform == "win32": |
| 54 | + library_name = "highs_extras.dll" |
| 55 | + elif sys.platform == "darwin": |
| 56 | + library_name = "libhighs_extras.dylib" |
| 57 | + else: |
| 58 | + library_name = "libhighs_extras.so" |
| 59 | + |
| 60 | + library_path = _PACKAGE_DIR / library_name |
| 61 | + if not library_path.is_file(): |
| 62 | + raise FileNotFoundError(f"Could not find the shared library at {library_path}") |
| 63 | + |
| 64 | + handle = ctypes.CDLL(str(library_path)) |
| 65 | + |
| 66 | + handle.HighsExtras_getVersion.argtypes = [] |
| 67 | + handle.HighsExtras_getVersion.restype = ctypes.c_char_p |
| 68 | + |
| 69 | + handle.HighsExtras_getFeatureCount.argtypes = [] |
| 70 | + handle.HighsExtras_getFeatureCount.restype = ctypes.c_size_t |
| 71 | + |
| 72 | + handle.HighsExtras_getFeatureName.argtypes = [ctypes.c_size_t] |
| 73 | + handle.HighsExtras_getFeatureName.restype = ctypes.c_char_p |
| 74 | + |
| 75 | + handle.HighsExtras_getFeatureInfo.argtypes = [] |
| 76 | + handle.HighsExtras_getFeatureInfo.restype = ctypes.POINTER(_HighsExtrasFeatureInfoRaw) |
| 77 | + |
| 78 | + return handle |
| 79 | + |
| 80 | + @property |
| 81 | + def version(self) -> str: |
| 82 | + version_bytes = self.handle.HighsExtras_getVersion() |
| 83 | + if version_bytes is None: |
| 84 | + raise RuntimeError("HighsExtras_getVersion() returned NULL") |
| 85 | + return version_bytes.decode("utf-8") |
| 86 | + |
| 87 | + def _feature_name(self, index: int) -> str: |
| 88 | + name_bytes = self.handle.HighsExtras_getFeatureName(index) |
| 89 | + if name_bytes is None: |
| 90 | + raise RuntimeError(f"HighsExtras_getFeatureName({index}) returned NULL") |
| 91 | + return name_bytes.decode("utf-8") |
| 92 | + |
| 93 | + @cached_property |
| 94 | + def features(self) -> dict[str, HighsExtrasFeatureInfo]: |
| 95 | + info_ptr = self.handle.HighsExtras_getFeatureInfo() |
| 96 | + if not info_ptr: |
| 97 | + raise RuntimeError("HighsExtras_getFeatureInfo() returned NULL") |
| 98 | + |
| 99 | + count = int(self.handle.HighsExtras_getFeatureCount()) |
| 100 | + return {self._feature_name(index): HighsExtrasFeatureInfo.from_raw(info_ptr[index]) for index in range(count)} |
| 101 | + |
| 102 | + def __getitem__(self, name: str) -> HighsExtrasFeatureInfo: |
| 103 | + return self.features[name] |
| 104 | + |
| 105 | + @cached_property |
| 106 | + def feature_table(self) -> str: |
| 107 | + """Return a human-readable table describing the external dependency features.""" |
| 108 | + |
| 109 | + headers = ("key", "name", "version", "license", "enabled") |
| 110 | + rows = [ |
| 111 | + ( |
| 112 | + name, |
| 113 | + info.provider, |
| 114 | + info.version, |
| 115 | + info.license, |
| 116 | + "yes" if info.enabled else "no", |
| 117 | + ) |
| 118 | + for name, info in self.features.items() |
| 119 | + ] |
| 120 | + |
| 121 | + widths = [max(len(headers[i]), *(len(row[i]) for row in rows)) if rows else len(headers[i]) for i in range(len(headers))] |
40 | 122 |
|
41 | | -def get_library_version() -> str: |
42 | | - """Return the ABI version string exported by the highs_extras library.""" |
| 123 | + def _fmt(row: tuple[str, ...]) -> str: |
| 124 | + return " ".join(cell.ljust(widths[i]) for i, cell in enumerate(row)) |
43 | 125 |
|
44 | | - get_version = _load_library().highs_extras_get_version |
45 | | - get_version.argtypes = [] |
46 | | - get_version.restype = ctypes.c_char_p |
| 126 | + separator = " ".join("-" * w for w in widths) |
| 127 | + lines = [_fmt(headers), separator] |
| 128 | + lines.extend(_fmt(row) for row in rows) |
| 129 | + return "\n".join(lines) |
47 | 130 |
|
48 | | - version_bytes = get_version() |
49 | | - if version_bytes is None: |
50 | | - raise RuntimeError("highs_extras_get_version() returned NULL") |
| 131 | + def __str__(self) -> str: |
| 132 | + return self.feature_table |
51 | 133 |
|
52 | | - return version_bytes.decode("utf-8") |
53 | 134 |
|
| 135 | +library = HighsExtrasLibrary() |
0 commit comments