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 Device subclass cls. 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 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

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 : str
var where : str
var message : str