Module pysdrlib.testing
Check that a driver is put together the way pysdrlib expects, without hardware or its vendor library
A driver is a Device subclass plus a package around it (see pysdrlib.contributing). Most mistakes in one show up
late and far from their cause: a gain stage whose hook is misspelled fails when a user sets that stage, a Setting
that names a method the class lacks fails when someone calls set_setting. check_driver() finds them from the class
alone:
from pysdrlib import testing
problems = testing.check_driver(MyRadio) # [Problem]; empty: nothing found
testing.assert_conforms(MyRadio) # raises AssertionError listing the errors (warnings are fine)
testing.check_package("my_radio") # the package around it: Device, find(), lib/, _ext/, README
tests/test_conformance.py runs both on every driver, so a new driver is checked as soon as it exists.
A Problem is an "error" (the driver is broken or incomplete) or a "warning" (it works, but something other drivers
provide is missing, or a declaration will be ignored).
Functions
def check_driver(cls) ‑> list-
Expand source code Browse git
def check_driver(cls) -> list: """[Problem] with the `Device` subclass `cls`. Reads class attributes and its CAPS; creates and opens nothing""" problems = [] name = getattr(cls, "__name__", str(cls)) def add(level, where, message): problems.append(Problem(level, f"{name}.{where}" if where else name, message)) if not (isinstance(cls, type) and issubclass(cls, Device)): add(ERROR, "", "is not a pysdrlib Device subclass") return problems if cls.NAME == Device.NAME: add(ERROR, "NAME", "is not set (it is what users and `caps.driver` call the device)") for hook in _REQUIRED_HOOKS: if not _overrides(cls, hook): add(ERROR, hook, "is not implemented (Device only raises NotImplementedError here)") if cls.CAN_TRANSMIT and cls.DUPLEX: # separate transmit synthesizer and clock: the receive hooks don't reach them for hook in ("_set_tx_freq", "_set_tx_sample_rate"): if not _overrides(cls, hook): add(ERROR, hook, "is not implemented (DUPLEX: the transmitter is tuned separately)") if issubclass(cls, NativeDevice) and not isinstance(cls.SAMPLE_FORMAT, convert.SampleFormat): add(ERROR, "SAMPLE_FORMAT", "must be a convert.SampleFormat describing the raw stream the extension delivers") caps = cls.CAPS if caps is None: add(ERROR, "CAPS", "is not set (a Capabilities with the device's limits and gain stages)") return problems _check_limits(caps, add) _check_gain(cls, caps, add) _check_settings(cls, add) _check_signatures(cls, add) if cls.CAN_TRANSMIT: if not _overrides(cls, "_tx_write"): add(WARNING, "CAN_TRANSMIT", "is True but _tx_write is not implemented, so write() raises NotSupported") if not caps.gains["tx"]: add(WARNING, "CAPS", "has no TX gain stages for a device that transmits") elif caps.gains["tx"]: add(WARNING, "CAPS", "declares TX gain stages but CAN_TRANSMIT is False: they are ignored") if cls.RETUNE_SETTLE < 0 or cls.RETUNE_SAMPLES < 0: add(ERROR, "RETUNE_SETTLE", "and RETUNE_SAMPLES cannot be negative") return problems[Problem] with the
Devicesubclasscls. Reads class attributes and its CAPS; creates and opens nothing def check_package(name) ‑> list-
Expand source code Browse git
def check_package(name) -> list: """[Problem] with the driver package `pysdrlib.<name>`: that it exposes `Device` and `find()`, and (for a driver on a compiled extension) has the `lib/` and `_ext/<name>.py` that load it. Also runs `check_driver` on its Device""" problems = [] def add(level, message): problems.append(Problem(level, f"{name} package", message)) try: module = devices.get(name) except Exception as exc: # a missing library is the machine's problem, not the driver's: say what happened add(ERROR, f"cannot be imported: {exc}") return problems device = getattr(module, "Device", None) if device is None: add(ERROR, "does not define `Device` in its __init__.py") return problems problems += check_driver(device) if not callable(getattr(module, "find", None)): add(WARNING, "has no find(): pysdrlib.find() cannot list its units") folder = Path(module.__file__).parent if not (folder / "README.md").is_file(): add(WARNING, "has no README.md (what it is, its specs, what to install)") if issubclass(device, NativeDevice): if importlib.util.find_spec(f"pysdrlib._ext.{name}") is None: add(ERROR, f"has no _ext/{name}.py (how its vendor library is found or built, and its extension)") else: ext = importlib.import_module(f"pysdrlib._ext.{name}") if getattr(ext, "LIB", None) is None: add(ERROR, f"_ext/{name}.py defines no LIB (a utils.VendorLibrary)") for needed in ("lib/__init__.py",): if not (folder / needed).is_file(): add(ERROR, f"has no {needed} (the compiled extension's import and its error classes)") return problems[Problem] with the driver package
pysdrlib.<name>: that it exposesDeviceandfind(), and (for a driver on a compiled extension) has thelib/and_ext/<name>.pythat load it. Also runscheck_driver()on its Device def errors(problems) ‑> list-
Expand source code Browse git
def errors(problems) -> list: return [p for p in problems if p.level == ERROR] def assert_conforms(driver)-
Expand source code Browse git
def assert_conforms(driver): """Raise AssertionError listing every error in `driver` (a Device class, or a driver package's name). Warnings pass""" problems = check_package(driver) if isinstance(driver, str) else check_driver(driver) bad = errors(problems) assert not bad, "driver does not conform:\n " + "\n ".join(map(str, bad))Raise AssertionError listing every error in
driver(a Device class, or a driver package's name). Warnings pass
Classes
class Problem (level: str, where: str, message: str)-
Expand source code Browse git
@dataclass(frozen=True) class Problem: level: str # ERROR or WARNING where: str # what it is about: "MyRadio.GAIN_RESERVE", "my_radio package"... message: str def __str__(self): return f"{self.level}: {self.where}: {self.message}"Problem(level: str, where: str, message: str)
Instance variables
var level : strvar where : strvar message : str