Skip to content

The feature-check Python API

Commonly-used functions

feature_check.obtain_features(program, option=defs.DEFAULT_OPTION, prefix=defs.DEFAULT_PREFIX)

Execute the specified program and get its list of features.

The program is run with the specified query option (default: "--features") and its output is examined for a line starting with the specified prefix (default: "Features: "). The rest of the line is parsed as a whitespace-separated list of either feature names or "name=version" pairs. The function returns a dictionary of the features obtained with their versions (or "1.0" if only a feature name was found in the program's output).

import feature_check

data = feature_check.obtain_features("timelimit")
print(data.get("subsecond", "not supported"))

For programs that need a different command-line option to list features:

import feature_check

print("SSL" in feature_check.obtain_features("curl",
                                             option="--version"))
Source code in python/src/feature_check/obtain.py
def obtain_features(
    program: str,
    option: str = defs.DEFAULT_OPTION,
    prefix: str = defs.DEFAULT_PREFIX,
) -> dict[str, fver.Version]:
    """Execute the specified program and get its list of features.

    The program is run with the specified query option (default:
    "--features") and its output is examined for a line starting with
    the specified prefix (default: "Features: ").  The rest of the line
    is parsed as a whitespace-separated list of either feature names or
    "name=version" pairs.  The function returns a dictionary of the features
    obtained with their versions (or "1.0" if only a feature name was found
    in the program's output).

        import feature_check

        data = feature_check.obtain_features("timelimit")
        print(data.get("subsecond", "not supported"))

    For programs that need a different command-line option to list features:

        import feature_check

        print("SSL" in feature_check.obtain_features("curl",
                                                     option="--version"))
    """

    def try_run() -> list[str]:
        """Query a program about its features, return the output lines."""
        res: Final = subprocess.run(  # noqa: S603
            [program, option],
            capture_output=True,
            check=False,
            stdin=None,
        )
        if res.returncode != 0 or res.stderr.decode():
            # It does not support '--features', does it?
            raise ObtainNoFeaturesSupportError(program, option)

        return res.stdout.decode().split("\n")

    try:
        lines: Final = try_run()
    except ObtainExecError:
        raise
    # Yes, we do want to convert any error into an ObtainExecError one
    except Exception as exc:
        # Something went wrong in the --features processing
        raise ObtainExecError(str(exc)) from exc

    matching: Final = (
        [
            rest
            for line, rest in ((line, line.removeprefix(prefix)) for line in lines)
            if rest != line
        ]
        if prefix
        else [line for line in lines if line]
    )
    if len(matching) != 1:
        raise ObtainNoFeaturesError(program, option, prefix)

    return fparser.parse_features_line(matching[0])

feature_check.parse_expr(expr)

Parse a simple "feature-name op version" expression.

If the expression is valid, return an Expr object corresponding to the specified check. Use this object's evaluate() method and pass a features dictionary as returned by the obtain_features() function to get a Result object; for simple expressions it will be a ResultBool object with a boolean value member.

from feature_check import expr as fexpr
from feature_check import obtain as fobtain

data = fobtain.obtain_features("timelimit");
expr = fexpr.parse_simple("subsecond > 0")
print(expr.evaluate(data).value)
Source code in python/src/feature_check/parser.py
def parse_expr(expr: str) -> defs.Mode:
    """Parse a simple "feature-name op version" expression.

    If the expression is valid, return an `Expr` object corresponding
    to the specified check.  Use this object's `evaluate()` method and
    pass a features dictionary as returned by the `obtain_features()`
    function to get a `Result` object; for simple expressions it will be
    a `ResultBool` object with a boolean `value` member.

        from feature_check import expr as fexpr
        from feature_check import obtain as fobtain

        data = fobtain.obtain_features("timelimit");
        expr = fexpr.parse_simple("subsecond > 0")
        print(expr.evaluate(data).value)
    """
    res: Final = _p_expr_complete.parse_string(expr).as_list()
    match res:
        case [mode] if isinstance(mode, defs.Mode):
            return mode

    raise ParseError(2, f"Could not parse {expr!r} as an expression (results: {res!r})")

feature_check.parser.parse_features_line(features_line)

Parse the features list, default to version "1.0".

Source code in python/src/feature_check/parser.py
def parse_features_line(features_line: str) -> dict[str, fver.Version]:
    """Parse the features list, default to version "1.0"."""
    try:
        res: Final = _p_features_line_complete.parse_string(features_line).as_list()
    except pyp.exceptions.ParseBaseException as err:
        raise ParseError(2, f"Could not parse {features_line!r} as a features line: {err}") from err
    match res:
        case [features] if isinstance(features, dict):
            return features

    raise ParseError(
        2,
        f"Could not parse {features_line!r} as a features line (results: {res!r})",
    )

feature_check.parse_version(value)

Parse a version string into a Version object.

Source code in python/src/feature_check/parser.py
def parse_version(value: str) -> fver.Version:
    """Parse a version string into a `Version` object."""
    res: Final = _p_version_complete.parse_string(value).as_list()
    match res:
        case [ver] if isinstance(ver, fver.Version) and ver.value == value:
            return ver

        case [ver]:
            raise ParseError(
                2,
                f"Could not parse the whole of {value!r} as a version string "
                f"(parsed {ver!r} from {res!r})",
            )

    raise ParseError(2, f"Could not parse {value!r} as a version string (result: {res!r})")

feature_check.version.version_compare(ver_a, ver_b)

Compare two version strings.

Returns -1, 0, or 1 for the first version being less than, equal to, or greater than the second one.

Source code in python/src/feature_check/version.py
def version_compare(ver_a: Version, ver_b: Version) -> int:
    """Compare two version strings.

    Returns -1, 0, or 1 for the first version being less than, equal to,
    or greater than the second one.
    """
    return _version_compare_split(ver_a.components, ver_b.components)

Commonly-used functions

feature_check.obtain.obtain_features(program, option=defs.DEFAULT_OPTION, prefix=defs.DEFAULT_PREFIX)

Execute the specified program and get its list of features.

The program is run with the specified query option (default: "--features") and its output is examined for a line starting with the specified prefix (default: "Features: "). The rest of the line is parsed as a whitespace-separated list of either feature names or "name=version" pairs. The function returns a dictionary of the features obtained with their versions (or "1.0" if only a feature name was found in the program's output).

import feature_check

data = feature_check.obtain_features("timelimit")
print(data.get("subsecond", "not supported"))

For programs that need a different command-line option to list features:

import feature_check

print("SSL" in feature_check.obtain_features("curl",
                                             option="--version"))
Source code in python/src/feature_check/obtain.py
def obtain_features(
    program: str,
    option: str = defs.DEFAULT_OPTION,
    prefix: str = defs.DEFAULT_PREFIX,
) -> dict[str, fver.Version]:
    """Execute the specified program and get its list of features.

    The program is run with the specified query option (default:
    "--features") and its output is examined for a line starting with
    the specified prefix (default: "Features: ").  The rest of the line
    is parsed as a whitespace-separated list of either feature names or
    "name=version" pairs.  The function returns a dictionary of the features
    obtained with their versions (or "1.0" if only a feature name was found
    in the program's output).

        import feature_check

        data = feature_check.obtain_features("timelimit")
        print(data.get("subsecond", "not supported"))

    For programs that need a different command-line option to list features:

        import feature_check

        print("SSL" in feature_check.obtain_features("curl",
                                                     option="--version"))
    """

    def try_run() -> list[str]:
        """Query a program about its features, return the output lines."""
        res: Final = subprocess.run(  # noqa: S603
            [program, option],
            capture_output=True,
            check=False,
            stdin=None,
        )
        if res.returncode != 0 or res.stderr.decode():
            # It does not support '--features', does it?
            raise ObtainNoFeaturesSupportError(program, option)

        return res.stdout.decode().split("\n")

    try:
        lines: Final = try_run()
    except ObtainExecError:
        raise
    # Yes, we do want to convert any error into an ObtainExecError one
    except Exception as exc:
        # Something went wrong in the --features processing
        raise ObtainExecError(str(exc)) from exc

    matching: Final = (
        [
            rest
            for line, rest in ((line, line.removeprefix(prefix)) for line in lines)
            if rest != line
        ]
        if prefix
        else [line for line in lines if line]
    )
    if len(matching) != 1:
        raise ObtainNoFeaturesError(program, option, prefix)

    return fparser.parse_features_line(matching[0])

Defaults

feature_check.defs.DEFAULT_OPTION = '--features' module-attribute

The default command-line option to pass to a program to query for supported features.

feature_check.defs.DEFAULT_PREFIX = 'Features: ' module-attribute

The default prefix of the program's features output line.

feature_check.defs.DEFAULT_OUTPUT_FMT = 'tsv' module-attribute

The default output format for the feature-check command-line tool.

Version strings

feature_check.version.Version

Bases: NamedTuple

A version string: many components, possibly other attributes.

Source code in python/src/feature_check/version.py
class Version(NamedTuple):
    """A version string: many components, possibly other attributes."""

    value: str
    components: list[VersionComponent]

feature_check.version.VersionComponent

Bases: NamedTuple

Represent a single version component: a numerical part and a freeform string one.

Source code in python/src/feature_check/version.py
class VersionComponent(NamedTuple):
    """Represent a single version component: a numerical part and a freeform string one."""

    num: int | None
    rest: str

    def __str__(self) -> str:
        """Provide a string representation of the version component."""
        return (str(self.num) if self.num is not None else "") + self.rest

    def cmp(self, other: object) -> int:  # noqa: C901,PLR0911,PLR0912
        """Compare two components, return None if they are equal."""
        if not isinstance(other, VersionComponent):
            raise NotImplementedError(repr(other))

        if self.num is not None:
            if other.num is not None:
                if self.num < other.num:
                    return -1
                if self.num > other.num:
                    return 1

                if self.rest is not None:
                    if other.rest is not None:
                        if self.rest < other.rest:
                            return -1
                        if self.rest > other.rest:
                            return 1
                        return 0

                    return 1

                if other.rest is not None:
                    return -1

                return 0

            return 1

        if other.num is not None:
            return -1

        if self.rest is not None:
            if other.rest is not None:
                if self.rest < other.rest:
                    return -1
                if self.rest > other.rest:
                    return 1
                return 0

            return -1

        return 0

    def __lt__(self, other: object) -> bool:
        """Check whether this version component is less than the other one."""
        return self.cmp(other) < 0

    def __le__(self, other: object) -> bool:
        """Check whether this version component is less than or equal to the other one."""
        return self.cmp(other) <= 0

    def __eq__(self, other: object) -> bool:
        """Check whether this version component is equal to the other one."""
        return self.cmp(other) == 0

    def __ne__(self, other: object) -> bool:
        """Check whether this version component is not equal to the other one."""
        return self.cmp(other) != 0

    def __ge__(self, other: object) -> bool:
        """Check whether this version component is greater than or equal to the other one."""
        return self.cmp(other) >= 0

    def __gt__(self, other: object) -> bool:
        """Check whether this version component is greater than the other one."""
        return self.cmp(other) > 0

    def __hash__(self) -> int:
        """Return an integer key for hashable collections."""
        return hash((self.num, self.rest))

__eq__(other)

Check whether this version component is equal to the other one.

Source code in python/src/feature_check/version.py
def __eq__(self, other: object) -> bool:
    """Check whether this version component is equal to the other one."""
    return self.cmp(other) == 0

__ge__(other)

Check whether this version component is greater than or equal to the other one.

Source code in python/src/feature_check/version.py
def __ge__(self, other: object) -> bool:
    """Check whether this version component is greater than or equal to the other one."""
    return self.cmp(other) >= 0

__gt__(other)

Check whether this version component is greater than the other one.

Source code in python/src/feature_check/version.py
def __gt__(self, other: object) -> bool:
    """Check whether this version component is greater than the other one."""
    return self.cmp(other) > 0

__hash__()

Return an integer key for hashable collections.

Source code in python/src/feature_check/version.py
def __hash__(self) -> int:
    """Return an integer key for hashable collections."""
    return hash((self.num, self.rest))

__le__(other)

Check whether this version component is less than or equal to the other one.

Source code in python/src/feature_check/version.py
def __le__(self, other: object) -> bool:
    """Check whether this version component is less than or equal to the other one."""
    return self.cmp(other) <= 0

__lt__(other)

Check whether this version component is less than the other one.

Source code in python/src/feature_check/version.py
def __lt__(self, other: object) -> bool:
    """Check whether this version component is less than the other one."""
    return self.cmp(other) < 0

__ne__(other)

Check whether this version component is not equal to the other one.

Source code in python/src/feature_check/version.py
def __ne__(self, other: object) -> bool:
    """Check whether this version component is not equal to the other one."""
    return self.cmp(other) != 0

__str__()

Provide a string representation of the version component.

Source code in python/src/feature_check/version.py
def __str__(self) -> str:
    """Provide a string representation of the version component."""
    return (str(self.num) if self.num is not None else "") + self.rest

cmp(other)

Compare two components, return None if they are equal.

Source code in python/src/feature_check/version.py
def cmp(self, other: object) -> int:  # noqa: C901,PLR0911,PLR0912
    """Compare two components, return None if they are equal."""
    if not isinstance(other, VersionComponent):
        raise NotImplementedError(repr(other))

    if self.num is not None:
        if other.num is not None:
            if self.num < other.num:
                return -1
            if self.num > other.num:
                return 1

            if self.rest is not None:
                if other.rest is not None:
                    if self.rest < other.rest:
                        return -1
                    if self.rest > other.rest:
                        return 1
                    return 0

                return 1

            if other.rest is not None:
                return -1

            return 0

        return 1

    if other.num is not None:
        return -1

    if self.rest is not None:
        if other.rest is not None:
            if self.rest < other.rest:
                return -1
            if self.rest > other.rest:
                return 1
            return 0

        return -1

    return 0

Expressions

feature_check.defs.Expr dataclass

Bases: ABC

The (pretty much abstract) base class for an expression.

Source code in python/src/feature_check/defs.py
@dataclasses.dataclass(frozen=True)
class Expr(abc.ABC):
    """The (pretty much abstract) base class for an expression."""

    @abc.abstractmethod
    def evaluate(self, data: dict[str, fver.Version]) -> Result:
        """Evaluate the expression and return a Result object.

        Overridden in actual expression classes.
        """
        raise NotImplementedError(
            f"{type(self).__name__}.evaluate() must be overridden",  # noqa: EM102
        )

evaluate(data) abstractmethod

Evaluate the expression and return a Result object.

Overridden in actual expression classes.

Source code in python/src/feature_check/defs.py
@abc.abstractmethod
def evaluate(self, data: dict[str, fver.Version]) -> Result:
    """Evaluate the expression and return a Result object.

    Overridden in actual expression classes.
    """
    raise NotImplementedError(
        f"{type(self).__name__}.evaluate() must be overridden",  # noqa: EM102
    )

feature_check.expr.ExprFeature dataclass

Bases: Expr

An expression that returns a program feature name as a string.

Source code in python/src/feature_check/expr.py
@dataclasses.dataclass(frozen=True)
class ExprFeature(defs.Expr):
    """An expression that returns a program feature name as a string."""

    name: str

    def evaluate(self, data: dict[str, fver.Version]) -> ResultVersion:
        """Look up the feature, return the result in a ResultVersion object."""
        return ResultVersion(value=data[self.name])

evaluate(data)

Look up the feature, return the result in a ResultVersion object.

Source code in python/src/feature_check/expr.py
def evaluate(self, data: dict[str, fver.Version]) -> ResultVersion:
    """Look up the feature, return the result in a ResultVersion object."""
    return ResultVersion(value=data[self.name])

feature_check.expr.ExprVersion dataclass

Bases: Expr

An expression that returns a version number for a feature.

Source code in python/src/feature_check/expr.py
@dataclasses.dataclass(frozen=True)
class ExprVersion(defs.Expr):
    """An expression that returns a version number for a feature."""

    value: fver.Version

    def evaluate(self, _data: dict[str, fver.Version]) -> ResultVersion:
        """Return the version number as a ResultVersion object."""
        return ResultVersion(value=self.value)

evaluate(_data)

Return the version number as a ResultVersion object.

Source code in python/src/feature_check/expr.py
def evaluate(self, _data: dict[str, fver.Version]) -> ResultVersion:
    """Return the version number as a ResultVersion object."""
    return ResultVersion(value=self.value)

feature_check.expr.BoolOp dataclass

A two-argument boolean operation.

Source code in python/src/feature_check/expr.py
@dataclasses.dataclass(frozen=True)
class BoolOp:
    """A two-argument boolean operation."""

    args: list[type[defs.Result]]
    action: BoolOpFunction

feature_check.expr.ExprOp dataclass

Bases: Expr

A two-argument operation expression.

Source code in python/src/feature_check/expr.py
@dataclasses.dataclass(frozen=True)
class ExprOp(defs.Expr):
    """A two-argument operation expression."""

    op: BoolOp
    args: list[defs.Expr]

    def __post_init__(self) -> None:
        """Validate the passed arguments."""
        if len(self.args) != len(self.op.args):
            raise ValueError((self.args, self.op.args))

    def evaluate(self, data: dict[str, fver.Version]) -> ResultBool:
        """Evaluate the expression over the specified data."""
        args: Final = [expr.evaluate(data) for expr in self.args]

        for idx, value in enumerate(args):
            if not isinstance(value, self.op.args[idx]):
                raise TypeError((idx, self.op.args[idx], value))

        return ResultBool(value=self.op.action(args))

__post_init__()

Validate the passed arguments.

Source code in python/src/feature_check/expr.py
def __post_init__(self) -> None:
    """Validate the passed arguments."""
    if len(self.args) != len(self.op.args):
        raise ValueError((self.args, self.op.args))

evaluate(data)

Evaluate the expression over the specified data.

Source code in python/src/feature_check/expr.py
def evaluate(self, data: dict[str, fver.Version]) -> ResultBool:
    """Evaluate the expression over the specified data."""
    args: Final = [expr.evaluate(data) for expr in self.args]

    for idx, value in enumerate(args):
        if not isinstance(value, self.op.args[idx]):
            raise TypeError((idx, self.op.args[idx], value))

    return ResultBool(value=self.op.action(args))

feature_check.defs.Result dataclass

The base class for an expression result.

Source code in python/src/feature_check/defs.py
@dataclasses.dataclass(frozen=True)
class Result:
    """The base class for an expression result."""

feature_check.expr.ResultBool dataclass

Bases: Result

A boolean result of an expression; the "value" member is boolean.

Source code in python/src/feature_check/expr.py
@dataclasses.dataclass(frozen=True)
class ResultBool(defs.Result):
    """A boolean result of an expression; the "value" member is boolean."""

    value: bool

    def __str__(self) -> str:
        """Provide a human-readable representation of the calculation result."""
        return f"ResultBool: {self.value}"

__str__()

Provide a human-readable representation of the calculation result.

Source code in python/src/feature_check/expr.py
def __str__(self) -> str:
    """Provide a human-readable representation of the calculation result."""
    return f"ResultBool: {self.value}"

feature_check.expr.ResultVersion dataclass

Bases: Result

A version number as a result of an expression.

The "value" member is the version number string.

Source code in python/src/feature_check/expr.py
@dataclasses.dataclass(frozen=True)
class ResultVersion(defs.Result):
    """A version number as a result of an expression.

    The "value" member is the version number string.
    """

    value: fver.Version

    def __str__(self) -> str:
        """Provide a human-readable representation of the calculation result."""
        return f"ResultVersion: {self.value.value}"

__str__()

Provide a human-readable representation of the calculation result.

Source code in python/src/feature_check/expr.py
def __str__(self) -> str:
    """Provide a human-readable representation of the calculation result."""
    return f"ResultVersion: {self.value.value}"

Errors

feature_check.defs.FCError

Bases: Exception

A base class for errors in handling a feature-check request.

Source code in python/src/feature_check/defs.py
class FCError(Exception):
    """A base class for errors in handling a feature-check request."""

    def __init__(self, code: int, msg: str) -> None:
        """Initialize an error object."""
        super().__init__(msg)
        self._code = code
        self._msg = msg

    @property
    def code(self) -> int:
        """Return the numeric error code."""
        return self._code

    @property
    def message(self) -> str:
        """Return a human-readable error message."""
        return self._msg

code: int property

Return the numeric error code.

message: str property

Return a human-readable error message.

__init__(code, msg)

Initialize an error object.

Source code in python/src/feature_check/defs.py
def __init__(self, code: int, msg: str) -> None:
    """Initialize an error object."""
    super().__init__(msg)
    self._code = code
    self._msg = msg

feature_check.obtain.ObtainError

Bases: FCError

A base class for errors in obtaining the program's features.

Source code in python/src/feature_check/obtain.py
class ObtainError(defs.FCError):
    """A base class for errors in obtaining the program's features."""

feature_check.obtain.ObtainExecError

Bases: ObtainError

An error that occurred while executing the queried program.

Source code in python/src/feature_check/obtain.py
class ObtainExecError(ObtainError):
    """An error that occurred while executing the queried program."""

    def __init__(self, err: str) -> None:
        """Initialize an error object."""
        super().__init__(1, err)

__init__(err)

Initialize an error object.

Source code in python/src/feature_check/obtain.py
def __init__(self, err: str) -> None:
    """Initialize an error object."""
    super().__init__(1, err)

feature_check.obtain.ObtainNoFeaturesError

Bases: ObtainError

An error that occurred while looking for the features line.

Source code in python/src/feature_check/obtain.py
class ObtainNoFeaturesError(ObtainError):
    """An error that occurred while looking for the features line."""

    def __init__(self, program: str, option: str, prefix: str) -> None:
        """Initialize an error object."""
        super().__init__(
            2,
            f"The '{program} {option}' output did not contain a single '{prefix}' line",
        )

__init__(program, option, prefix)

Initialize an error object.

Source code in python/src/feature_check/obtain.py
def __init__(self, program: str, option: str, prefix: str) -> None:
    """Initialize an error object."""
    super().__init__(
        2,
        f"The '{program} {option}' output did not contain a single '{prefix}' line",
    )

feature_check.obtain.ObtainNoFeaturesSupportError

Bases: ObtainExecError

The program does not seem to support the "--features" option.

Source code in python/src/feature_check/obtain.py
class ObtainNoFeaturesSupportError(ObtainExecError):
    """The program does not seem to support the "--features" option."""

    def __init__(self, program: str, option: str) -> None:
        """Store the program name."""
        super().__init__(
            f"The {program} program does not seem to support "
            f"the {option} option for querying features",
        )

__init__(program, option)

Store the program name.

Source code in python/src/feature_check/obtain.py
def __init__(self, program: str, option: str) -> None:
    """Store the program name."""
    super().__init__(
        f"The {program} program does not seem to support "
        f"the {option} option for querying features",
    )

feature_check.parser.ParseError

Bases: FCError

An error that occurred while parsing the expression.

Source code in python/src/feature_check/parser.py
class ParseError(defs.FCError):
    """An error that occurred while parsing the expression."""

Main program operating modes

feature_check.defs.Mode dataclass

Base class for the feature-check operating modes.

Source code in python/src/feature_check/defs.py
@dataclasses.dataclass(frozen=True)
class Mode:
    """Base class for the feature-check operating modes."""

feature_check.defs.ModeList dataclass

Bases: Mode

List the features supported by the program.

Source code in python/src/feature_check/defs.py
@dataclasses.dataclass(frozen=True)
class ModeList(Mode):
    """List the features supported by the program."""

feature_check.defs.ModeSimple dataclass

Bases: Mode

Verify whether a simple 'feature op version' expression holds true.

Source code in python/src/feature_check/defs.py
@dataclasses.dataclass(frozen=True)
class ModeSimple(Mode):
    """Verify whether a simple 'feature op version' expression holds true."""

    ast: Expr

feature_check.defs.ModeSingle dataclass

Bases: Mode

Query for the presence or the version of a single feature.

Source code in python/src/feature_check/defs.py
@dataclasses.dataclass(frozen=True)
class ModeSingle(Mode):
    """Query for the presence or the version of a single feature."""

    feature: str
    ast: Expr

Miscellaneous

feature_check.defs.VERSION = '2.2.0' module-attribute

The feature-check library version, SemVer-style.