pybinaryguard 1.0.0__py3-none-any.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.
Files changed (91) hide show
  1. pybinaryguard/__init__.py +78 -0
  2. pybinaryguard/__main__.py +7 -0
  3. pybinaryguard/_compat/__init__.py +0 -0
  4. pybinaryguard/agent/__init__.py +63 -0
  5. pybinaryguard/agent/guard.py +232 -0
  6. pybinaryguard/agent/recommender.py +283 -0
  7. pybinaryguard/agent/schema.py +200 -0
  8. pybinaryguard/agent/simulator.py +430 -0
  9. pybinaryguard/agent/tool_interface.py +474 -0
  10. pybinaryguard/analyzers/__init__.py +209 -0
  11. pybinaryguard/analyzers/base.py +40 -0
  12. pybinaryguard/analyzers/dependency_analyzer.py +336 -0
  13. pybinaryguard/analyzers/elf_analyzer.py +754 -0
  14. pybinaryguard/analyzers/symbol_analyzer.py +280 -0
  15. pybinaryguard/analyzers/wheel_analyzer.py +308 -0
  16. pybinaryguard/cli/__init__.py +5 -0
  17. pybinaryguard/cli/commands.py +414 -0
  18. pybinaryguard/cli/formatters.py +720 -0
  19. pybinaryguard/cli/main.py +250 -0
  20. pybinaryguard/diagnostics/__init__.py +13 -0
  21. pybinaryguard/diagnostics/explainer.py +356 -0
  22. pybinaryguard/diagnostics/findings.py +146 -0
  23. pybinaryguard/diagnostics/suggestions.py +508 -0
  24. pybinaryguard/frameworks/__init__.py +20 -0
  25. pybinaryguard/frameworks/onnxruntime.py +214 -0
  26. pybinaryguard/frameworks/pytorch.py +223 -0
  27. pybinaryguard/frameworks/tensorflow.py +266 -0
  28. pybinaryguard/frameworks/tensorrt.py +189 -0
  29. pybinaryguard/models/__init__.py +19 -0
  30. pybinaryguard/models/enums.py +102 -0
  31. pybinaryguard/models/finding.py +109 -0
  32. pybinaryguard/models/package.py +121 -0
  33. pybinaryguard/models/system.py +118 -0
  34. pybinaryguard/plugins/__init__.py +19 -0
  35. pybinaryguard/plugins/contrib/__init__.py +7 -0
  36. pybinaryguard/plugins/contrib/gstreamer.py +109 -0
  37. pybinaryguard/plugins/contrib/jetson.py +385 -0
  38. pybinaryguard/plugins/contrib/opencv.py +306 -0
  39. pybinaryguard/plugins/contrib/tensorrt.py +426 -0
  40. pybinaryguard/plugins/hooks.py +273 -0
  41. pybinaryguard/plugins/loader.py +190 -0
  42. pybinaryguard/predictor/__init__.py +23 -0
  43. pybinaryguard/predictor/dependency_graph.py +226 -0
  44. pybinaryguard/predictor/linker_simulator.py +161 -0
  45. pybinaryguard/predictor/predictor.py +144 -0
  46. pybinaryguard/predictor/resolver.py +282 -0
  47. pybinaryguard/probes/__init__.py +73 -0
  48. pybinaryguard/probes/base.py +45 -0
  49. pybinaryguard/probes/board_probe.py +248 -0
  50. pybinaryguard/probes/cpu_probe.py +176 -0
  51. pybinaryguard/probes/glibc_probe.py +215 -0
  52. pybinaryguard/probes/gpu_probe.py +430 -0
  53. pybinaryguard/probes/library_probe.py +132 -0
  54. pybinaryguard/probes/os_probe.py +227 -0
  55. pybinaryguard/probes/python_probe.py +135 -0
  56. pybinaryguard/probes/toolchain_probe.py +89 -0
  57. pybinaryguard/probes/venv_probe.py +111 -0
  58. pybinaryguard/profiles/__init__.py +12 -0
  59. pybinaryguard/profiles/engine.py +343 -0
  60. pybinaryguard/rules/__init__.py +24 -0
  61. pybinaryguard/rules/base.py +57 -0
  62. pybinaryguard/rules/builtin/__init__.py +136 -0
  63. pybinaryguard/rules/builtin/arch_rules.py +86 -0
  64. pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
  65. pybinaryguard/rules/builtin/container_rules.py +170 -0
  66. pybinaryguard/rules/builtin/cpu_rules.py +204 -0
  67. pybinaryguard/rules/builtin/cuda_rules.py +760 -0
  68. pybinaryguard/rules/builtin/dependency_rules.py +278 -0
  69. pybinaryguard/rules/builtin/framework_rules.py +252 -0
  70. pybinaryguard/rules/builtin/glibc_rules.py +320 -0
  71. pybinaryguard/rules/builtin/numpy_rules.py +110 -0
  72. pybinaryguard/rules/builtin/predictive_rules.py +165 -0
  73. pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
  74. pybinaryguard/rules/builtin/source_build_rules.py +150 -0
  75. pybinaryguard/rules/builtin/venv_rules.py +159 -0
  76. pybinaryguard/rules/engine.py +123 -0
  77. pybinaryguard/scanner.py +904 -0
  78. pybinaryguard/scoring/__init__.py +19 -0
  79. pybinaryguard/scoring/engine.py +416 -0
  80. pybinaryguard/snapshot/__init__.py +20 -0
  81. pybinaryguard/snapshot/generator.py +168 -0
  82. pybinaryguard/snapshot/lockfile.py +152 -0
  83. pybinaryguard/snapshot/verifier.py +315 -0
  84. pybinaryguard/validators/__init__.py +7 -0
  85. pybinaryguard/validators/import_validator.py +231 -0
  86. pybinaryguard-1.0.0.dist-info/METADATA +876 -0
  87. pybinaryguard-1.0.0.dist-info/RECORD +91 -0
  88. pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
  89. pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
  90. pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
  91. pybinaryguard-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,144 @@
1
+ """Main predictor module for runtime failure prediction.
2
+
3
+ Combines symbol resolution, dependency analysis, and linker simulation
4
+ to predict ImportErrors before they happen.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import List, Optional
11
+
12
+ from pybinaryguard.models.package import PackageBinaryInfo
13
+ from pybinaryguard.models.system import SystemProfile
14
+ from pybinaryguard.predictor.linker_simulator import LinkerSimulator
15
+
16
+
17
+ @dataclass
18
+ class PredictedFailure:
19
+ """A predicted runtime failure."""
20
+
21
+ package: str
22
+ module_path: str
23
+ error_type: str # "ImportError", "SymbolError", "LibraryMissing"
24
+ error_message: str
25
+ missing_symbol: Optional[str] = None
26
+ missing_library: Optional[str] = None
27
+ confidence: float = 0.0
28
+
29
+
30
+ def predict_import_failures(
31
+ package: PackageBinaryInfo,
32
+ profile: SystemProfile
33
+ ) -> List[PredictedFailure]:
34
+ """Predict import failures for a package.
35
+
36
+ Args:
37
+ package: Package binary information
38
+ profile: System profile
39
+
40
+ Returns:
41
+ List of predicted failures
42
+ """
43
+ failures: List[PredictedFailure] = []
44
+
45
+ # Get library paths from system
46
+ library_paths = profile.library_search_paths or None
47
+
48
+ simulator = LinkerSimulator(library_paths)
49
+
50
+ # Simulate loading each shared object
51
+ for so in package.shared_objects:
52
+ if not so.path:
53
+ continue
54
+
55
+ # Predict import error
56
+ error_msg = simulator.predict_import_error(so.path, f"{package.name}._C")
57
+
58
+ if error_msg:
59
+ # Parse error type
60
+ if "undefined symbol" in error_msg:
61
+ error_type = "SymbolError"
62
+ # Extract symbol name
63
+ if "undefined symbol:" in error_msg:
64
+ parts = error_msg.split("undefined symbol:")
65
+ if len(parts) > 1:
66
+ missing_symbol = parts[1].strip()
67
+ else:
68
+ missing_symbol = "unknown"
69
+ else:
70
+ missing_symbol = "unknown"
71
+
72
+ failures.append(
73
+ PredictedFailure(
74
+ package=package.name,
75
+ module_path=so.path,
76
+ error_type=error_type,
77
+ error_message=error_msg,
78
+ missing_symbol=missing_symbol,
79
+ confidence=0.95,
80
+ )
81
+ )
82
+
83
+ elif "cannot open shared object file" in error_msg:
84
+ error_type = "LibraryMissing"
85
+ # Extract library name
86
+ if "file:" in error_msg:
87
+ parts = error_msg.split("file:")
88
+ if len(parts) > 1:
89
+ lib_part = parts[1].split(":")[0].strip()
90
+ missing_library = lib_part
91
+ else:
92
+ missing_library = "unknown"
93
+ else:
94
+ missing_library = "unknown"
95
+
96
+ failures.append(
97
+ PredictedFailure(
98
+ package=package.name,
99
+ module_path=so.path,
100
+ error_type=error_type,
101
+ error_message=error_msg,
102
+ missing_library=missing_library,
103
+ confidence=0.9,
104
+ )
105
+ )
106
+
107
+ else:
108
+ # Generic import error
109
+ failures.append(
110
+ PredictedFailure(
111
+ package=package.name,
112
+ module_path=so.path,
113
+ error_type="ImportError",
114
+ error_message=error_msg,
115
+ confidence=0.8,
116
+ )
117
+ )
118
+
119
+ return failures
120
+
121
+
122
+ def format_predicted_failure(failure: PredictedFailure) -> str:
123
+ """Format a predicted failure as a human-readable message.
124
+
125
+ Args:
126
+ failure: PredictedFailure to format
127
+
128
+ Returns:
129
+ Formatted message string
130
+ """
131
+ lines = [
132
+ f"Predicted {failure.error_type} in {failure.package}:",
133
+ f" {failure.error_message}",
134
+ ]
135
+
136
+ if failure.missing_symbol:
137
+ lines.append(f" Missing symbol: {failure.missing_symbol}")
138
+
139
+ if failure.missing_library:
140
+ lines.append(f" Missing library: {failure.missing_library}")
141
+
142
+ lines.append(f" Confidence: {failure.confidence * 100:.0f}%")
143
+
144
+ return "\n".join(lines)
@@ -0,0 +1,282 @@
1
+ """Symbol resolution for ELF binaries.
2
+
3
+ Resolves undefined symbols against available shared libraries,
4
+ simulating the behavior of the dynamic linker.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Dict, List, Optional, Set
13
+
14
+ from pybinaryguard.analyzers.elf_analyzer import MinimalELFParser
15
+
16
+
17
+ @dataclass
18
+ class Symbol:
19
+ """Represents a symbol from an ELF binary."""
20
+
21
+ name: str
22
+ library: Optional[str] = None # Which library provides this symbol
23
+ version: Optional[str] = None # Symbol version (e.g., GLIBC_2.34)
24
+ weak: bool = False # Whether this is a weak symbol
25
+ undefined: bool = False # Whether this symbol needs to be resolved
26
+
27
+
28
+ @dataclass
29
+ class LibrarySymbols:
30
+ """Symbol table for a shared library."""
31
+
32
+ path: str
33
+ provided_symbols: Set[str] = field(default_factory=set)
34
+ required_symbols: Set[str] = field(default_factory=set)
35
+ versioned_symbols: Dict[str, str] = field(default_factory=dict) # symbol -> version
36
+
37
+
38
+ class SymbolResolver:
39
+ """Resolves symbols from ELF binaries against system libraries."""
40
+
41
+ def __init__(self, library_paths: Optional[List[str]] = None):
42
+ """Initialize the symbol resolver.
43
+
44
+ Args:
45
+ library_paths: List of directories to search for libraries.
46
+ If None, uses standard system paths.
47
+ """
48
+ if library_paths is None:
49
+ library_paths = self._get_default_library_paths()
50
+
51
+ self.library_paths = library_paths
52
+ self._symbol_cache: Dict[str, LibrarySymbols] = {}
53
+
54
+ def _get_default_library_paths(self) -> List[str]:
55
+ """Get default library search paths."""
56
+ paths = [
57
+ "/lib",
58
+ "/lib64",
59
+ "/usr/lib",
60
+ "/usr/lib64",
61
+ "/usr/local/lib",
62
+ "/usr/local/lib64",
63
+ ]
64
+
65
+ # Add LD_LIBRARY_PATH if set
66
+ ld_lib_path = os.environ.get("LD_LIBRARY_PATH", "")
67
+ if ld_lib_path:
68
+ paths.extend(ld_lib_path.split(":"))
69
+
70
+ # Filter to existing directories
71
+ return [p for p in paths if os.path.isdir(p)]
72
+
73
+ def resolve_undefined_symbols(
74
+ self,
75
+ binary_path: str,
76
+ needed_libraries: List[str]
77
+ ) -> Dict[str, Optional[str]]:
78
+ """Resolve undefined symbols in a binary.
79
+
80
+ Args:
81
+ binary_path: Path to the ELF binary
82
+ needed_libraries: List of DT_NEEDED libraries (from ELF)
83
+
84
+ Returns:
85
+ Dictionary mapping symbol name to providing library (or None if unresolved)
86
+ """
87
+ # Get undefined symbols from the binary
88
+ undefined_symbols = self._extract_undefined_symbols(binary_path)
89
+
90
+ # Build symbol index from needed libraries
91
+ symbol_index: Dict[str, str] = {}
92
+ for lib_name in needed_libraries:
93
+ lib_path = self._find_library(lib_name)
94
+ if lib_path:
95
+ lib_symbols = self._get_library_symbols(lib_path)
96
+ for sym in lib_symbols.provided_symbols:
97
+ if sym not in symbol_index:
98
+ symbol_index[sym] = lib_name
99
+
100
+ # Resolve each undefined symbol
101
+ resolution: Dict[str, Optional[str]] = {}
102
+ for sym in undefined_symbols:
103
+ resolution[sym] = symbol_index.get(sym)
104
+
105
+ return resolution
106
+
107
+ def _extract_undefined_symbols(self, binary_path: str) -> Set[str]:
108
+ """Extract undefined symbols from an ELF binary.
109
+
110
+ Args:
111
+ binary_path: Path to the ELF binary
112
+
113
+ Returns:
114
+ Set of undefined symbol names
115
+ """
116
+ # This is a simplified implementation
117
+ # A full implementation would parse the .dynsym section
118
+ # For now, we return an empty set as a placeholder
119
+ undefined = set()
120
+
121
+ try:
122
+ # Parse ELF to get dynamic symbols
123
+ parser = MinimalELFParser(binary_path)
124
+ # TODO: Extend MinimalELFParser to extract symbol table
125
+ # For now, we can extract from version requirements
126
+ for version in parser.get_version_requirements():
127
+ # Version strings like "GLIBC_2.34" indicate required symbols
128
+ undefined.add(version)
129
+ except Exception:
130
+ pass
131
+
132
+ return undefined
133
+
134
+ def _find_library(self, lib_name: str) -> Optional[str]:
135
+ """Find a library by name in the search paths.
136
+
137
+ Args:
138
+ lib_name: Library name (e.g., "libc.so.6")
139
+
140
+ Returns:
141
+ Full path to library or None if not found
142
+ """
143
+ for search_path in self.library_paths:
144
+ lib_path = os.path.join(search_path, lib_name)
145
+ if os.path.exists(lib_path):
146
+ return lib_path
147
+
148
+ # Also try without directory component if lib_name is absolute
149
+ if lib_name.startswith("/") and os.path.exists(lib_name):
150
+ return lib_name
151
+
152
+ return None
153
+
154
+ def _get_library_symbols(self, lib_path: str) -> LibrarySymbols:
155
+ """Get symbols provided by a library.
156
+
157
+ Args:
158
+ lib_path: Path to the library
159
+
160
+ Returns:
161
+ LibrarySymbols object with provided/required symbols
162
+ """
163
+ # Check cache first
164
+ if lib_path in self._symbol_cache:
165
+ return self._symbol_cache[lib_path]
166
+
167
+ lib_symbols = LibrarySymbols(path=lib_path)
168
+
169
+ try:
170
+ parser = MinimalELFParser(lib_path)
171
+
172
+ # Get version requirements (symbols this library requires)
173
+ for version in parser.get_version_requirements():
174
+ lib_symbols.required_symbols.add(version)
175
+ lib_symbols.versioned_symbols[version] = version
176
+
177
+ # For provided symbols, we'd need to parse .dynsym
178
+ # This is a simplified version
179
+ # In a full implementation, we'd extract exported symbols
180
+ # For now, mark GLIBC versions as "provided" if this is libc
181
+ if "libc.so" in lib_path:
182
+ for version in parser.get_version_requirements():
183
+ lib_symbols.provided_symbols.add(version)
184
+
185
+ except Exception:
186
+ pass
187
+
188
+ # Cache the result
189
+ self._symbol_cache[lib_path] = lib_symbols
190
+
191
+ return lib_symbols
192
+
193
+ def check_symbol_availability(
194
+ self,
195
+ symbol_name: str,
196
+ version_requirement: Optional[str] = None
197
+ ) -> bool:
198
+ """Check if a symbol is available in system libraries.
199
+
200
+ Args:
201
+ symbol_name: Name of the symbol
202
+ version_requirement: Optional version requirement (e.g., "GLIBC_2.34")
203
+
204
+ Returns:
205
+ True if symbol is available, False otherwise
206
+ """
207
+ # Search all system libraries
208
+ for search_path in self.library_paths:
209
+ if not os.path.isdir(search_path):
210
+ continue
211
+
212
+ try:
213
+ for entry in os.listdir(search_path):
214
+ if entry.endswith(".so") or ".so." in entry:
215
+ lib_path = os.path.join(search_path, entry)
216
+ if os.path.isfile(lib_path):
217
+ lib_symbols = self._get_library_symbols(lib_path)
218
+ if symbol_name in lib_symbols.provided_symbols:
219
+ if version_requirement:
220
+ # Check version compatibility
221
+ sym_version = lib_symbols.versioned_symbols.get(symbol_name)
222
+ if sym_version and self._version_gte(sym_version, version_requirement):
223
+ return True
224
+ else:
225
+ return True
226
+ except (OSError, PermissionError):
227
+ continue
228
+
229
+ return False
230
+
231
+ @staticmethod
232
+ def _version_gte(v1: str, v2: str) -> bool:
233
+ """Compare version strings (simple comparison).
234
+
235
+ Args:
236
+ v1: First version (e.g., "GLIBC_2.35")
237
+ v2: Second version (e.g., "GLIBC_2.34")
238
+
239
+ Returns:
240
+ True if v1 >= v2
241
+ """
242
+ try:
243
+ # Extract numeric parts (e.g., "GLIBC_2.34" -> [2, 34])
244
+ parts1 = [int(x) for x in v1.split("_")[-1].split(".")]
245
+ parts2 = [int(x) for x in v2.split("_")[-1].split(".")]
246
+ return parts1 >= parts2
247
+ except (ValueError, IndexError):
248
+ return False
249
+
250
+
251
+ def resolve_symbol(
252
+ symbol_name: str,
253
+ library_paths: Optional[List[str]] = None
254
+ ) -> Optional[str]:
255
+ """Convenience function to resolve a single symbol.
256
+
257
+ Args:
258
+ symbol_name: Name of the symbol to resolve
259
+ library_paths: Optional list of library search paths
260
+
261
+ Returns:
262
+ Path to library providing the symbol, or None if not found
263
+ """
264
+ resolver = SymbolResolver(library_paths)
265
+
266
+ # Search for the symbol
267
+ for search_path in resolver.library_paths:
268
+ if not os.path.isdir(search_path):
269
+ continue
270
+
271
+ try:
272
+ for entry in os.listdir(search_path):
273
+ if entry.endswith(".so") or ".so." in entry:
274
+ lib_path = os.path.join(search_path, entry)
275
+ if os.path.isfile(lib_path):
276
+ lib_symbols = resolver._get_library_symbols(lib_path)
277
+ if symbol_name in lib_symbols.provided_symbols:
278
+ return lib_path
279
+ except (OSError, PermissionError):
280
+ continue
281
+
282
+ return None
@@ -0,0 +1,73 @@
1
+ """System probes for PyBinaryGuard.
2
+
3
+ This package contains all probes that collect information about the
4
+ current system's environment. Each probe is a subclass of
5
+ :class:`ProbeBase` and implements a ``collect()`` method that returns
6
+ a dictionary whose keys correspond to :class:`SystemProfile` fields.
7
+
8
+ Usage::
9
+
10
+ from pybinaryguard.probes import get_all_probes
11
+
12
+ for probe in get_all_probes():
13
+ if probe.is_applicable():
14
+ data = probe.collect()
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import List
20
+
21
+ from .base import ProbeBase
22
+ from .board_probe import BoardProbe
23
+ from .cpu_probe import CpuProbe
24
+ from .glibc_probe import GlibcProbe
25
+ from .gpu_probe import GpuProbe
26
+ from .library_probe import LibraryProbe
27
+ from .os_probe import OsProbe
28
+ from .python_probe import PythonProbe
29
+ from .toolchain_probe import ToolchainProbe
30
+ from .venv_probe import VenvProbe
31
+
32
+ __all__ = [
33
+ "ProbeBase",
34
+ "BoardProbe",
35
+ "CpuProbe",
36
+ "GlibcProbe",
37
+ "GpuProbe",
38
+ "LibraryProbe",
39
+ "OsProbe",
40
+ "PythonProbe",
41
+ "ToolchainProbe",
42
+ "VenvProbe",
43
+ "get_all_probes",
44
+ ]
45
+
46
+
47
+ def get_all_probes() -> List[ProbeBase]:
48
+ """Return an instance of every available probe.
49
+
50
+ The probes are returned in a deterministic order chosen so that
51
+ cheaper / more fundamental probes run first:
52
+
53
+ 1. **PythonProbe** -- interpreter info (fast, no I/O beyond stdlib)
54
+ 2. **VenvProbe** -- virtual environment detection (fast)
55
+ 3. **CpuProbe** -- CPU architecture and flags (reads /proc/cpuinfo)
56
+ 4. **OsProbe** -- OS and container detection
57
+ 5. **GlibcProbe** -- C library version
58
+ 6. **ToolchainProbe** -- build toolchain (gcc, cmake, etc.)
59
+ 7. **LibraryProbe** -- shared library paths and ldconfig cache
60
+ 8. **BoardProbe** -- embedded board detection
61
+ 9. **GpuProbe** -- GPU / CUDA detection (most expensive)
62
+ """
63
+ return [
64
+ PythonProbe(),
65
+ VenvProbe(),
66
+ CpuProbe(),
67
+ OsProbe(),
68
+ GlibcProbe(),
69
+ ToolchainProbe(),
70
+ LibraryProbe(),
71
+ BoardProbe(),
72
+ GpuProbe(),
73
+ ]
@@ -0,0 +1,45 @@
1
+ """Abstract base class for all system probes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any, Dict
7
+
8
+
9
+ class ProbeBase(ABC):
10
+ """Base class for system probes that collect environment information.
11
+
12
+ Every probe must:
13
+ - Have a unique ``name`` attribute used for identification and logging.
14
+ - Implement ``collect()`` which returns a dict whose keys correspond to
15
+ field names on ``SystemProfile``.
16
+ - Be completely read-only -- probes must never modify system state.
17
+ - Handle all internal errors gracefully and return partial data rather
18
+ than raising.
19
+ """
20
+
21
+ name: str # Probe identifier -- must be set by subclasses
22
+
23
+ @abstractmethod
24
+ def collect(self) -> Dict[str, Any]:
25
+ """Collect system information.
26
+
27
+ Returns
28
+ -------
29
+ Dict[str, Any]
30
+ A dictionary whose keys are ``SystemProfile`` field names and
31
+ whose values are the detected values for those fields. If a
32
+ value cannot be determined, it should be omitted from the dict
33
+ rather than set to ``None`` (unless ``None`` is the intended
34
+ sentinel).
35
+ """
36
+ ...
37
+
38
+ def is_applicable(self) -> bool:
39
+ """Return whether this probe can run on the current system.
40
+
41
+ The default implementation returns ``True``. Subclasses may
42
+ override to skip probing when the current platform is
43
+ incompatible (e.g. GPU probe on a system with no GPU devices).
44
+ """
45
+ return True