Skip to content
2 changes: 1 addition & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ New Features in 25.0
~~~~~~~~~~~~~~~~~~~~
- All components can be installed into the same virtualenv again.
- Add support for Python 3.13.
- The ``pytest``-specific parts of the labgrid framework now have typing hints.
- The `QEMUDriver` now supports setting the ``display`` option to
``qemu-default``, which will neither set the QEMU ``-display`` option
or pass along ``-nographic``.
Expand All @@ -56,7 +57,6 @@ New Features in 25.0
statistics.
- Add support for LogiLink UA0379 / Microdia cameras to the ``USBVideoDriver``.
- Add more backends to the ``NetworkPowerDriver``:

- Gude 87-1210-18
- Digital Loggers PDUs via the REST API
- Ubiquity mFi mPower power strips
Expand Down
4 changes: 2 additions & 2 deletions labgrid/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ def get_imports(self):

return imports

def get_paths(self):
def get_paths(self) -> dict[str, str]:
"""Helper function that returns the subdict of all paths

Returns:
Expand All @@ -275,7 +275,7 @@ def get_paths(self):

return paths

def get_images(self):
def get_images(self) -> dict[str, str]:
"""Helper function that returns the subdict of all images

Returns:
Expand Down
6 changes: 4 additions & 2 deletions labgrid/environment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
from collections.abc import Callable
from typing import Optional

import attr

from .target import Target
Expand All @@ -9,10 +11,10 @@
@attr.s(eq=False)
class Environment:
"""An environment encapsulates targets."""
config_file = attr.ib(
config_file: str = attr.ib(
default="config.yaml", validator=attr.validators.instance_of(str)
)
interact = attr.ib(default=input, repr=False)
interact: Callable[[str], str] = attr.ib(default=input, repr=False)

def __attrs_post_init__(self):
self.targets = {}
Expand Down
14 changes: 7 additions & 7 deletions labgrid/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ def basicConfig(**kwargs):
root.handlers[0].setFormatter(StepFormatter(indent=indent, parent=parent))


logging.CONSOLE = logging.INFO - 5
assert(logging.CONSOLE > logging.DEBUG)
logging.addLevelName(logging.CONSOLE, "CONSOLE")
CONSOLE = logging.INFO - 5
assert(CONSOLE > logging.DEBUG)
logging.addLevelName(CONSOLE, "CONSOLE")

# Use composition instead of inheritance
class StepFormatter:
class StepFormatter(logging.Formatter):
def __init__(self, *args, indent=True, color=None, parent=None, **kwargs):
self.formatter = parent or logging.Formatter(*args, **kwargs)
self.indent = indent
Expand Down Expand Up @@ -106,11 +106,11 @@ def notify(self, event):

for part in parts:
data = self.vt100_replace_cr_nl(part)
logger.log(logging.CONSOLE, self._create_message(event, data), extra=extra)
logger.log(CONSOLE, self._create_message(event, data), extra=extra)

elif state == "start" and step.args and "data" in step.args:
data = self.vt100_replace_cr_nl(step.args["data"])
logger.log(logging.CONSOLE, self._create_message(event, data), extra=extra)
logger.log(CONSOLE, self._create_message(event, data), extra=extra)

def flush(self):
if self.lastevent is None:
Expand All @@ -122,7 +122,7 @@ def flush(self):
for source, logger in self.loggers.items():
data = self.vt100_replace_cr_nl(self.bufs[source])
if data:
logger.log(logging.CONSOLE, self._create_message(self.lastevent, data), extra=extra)
logger.log(CONSOLE, self._create_message(self.lastevent, data), extra=extra)
self.bufs[source] = b""


Expand Down
1 change: 1 addition & 0 deletions labgrid/py.typed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
partial
11 changes: 11 additions & 0 deletions labgrid/pytestplugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
from .fixtures import pytest_addoption, env, target, strategy
from .hooks import pytest_configure, pytest_collection_modifyitems, pytest_cmdline_main, pytest_runtest_setup

__all__ = [
"pytest_addoption",
"env",
"target",
"strategy",
"pytest_configure",
"pytest_collection_modifyitems",
"pytest_cmdline_main",
"pytest_runtest_setup",
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in a separate commit?

21 changes: 14 additions & 7 deletions labgrid/pytestplugin/fixtures.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import os
import subprocess
from typing import Iterator

import pytest

from .. import Environment, Target
from ..exceptions import NoResourceFoundError, NoDriverFoundError
from ..strategy import Strategy
from ..remote.client import UserError
from ..resource.remote import RemotePlace
from ..util.ssh import sshmanager
Expand All @@ -12,7 +16,9 @@
# pylint: disable=redefined-outer-name


def pytest_addoption(parser):
def pytest_addoption(parser: pytest.Parser, pluginmanager: pytest.PytestPluginManager) -> None:
del pluginmanager # unused

group = parser.getgroup('labgrid')
group.addoption(
'--lg-env',
Expand Down Expand Up @@ -52,7 +58,7 @@ def pytest_addoption(parser):


@pytest.fixture(scope="session")
def env(request, record_testsuite_property):
def env(request: pytest.FixtureRequest, record_testsuite_property) -> Iterator[Environment]:
"""Return the environment configured in the supplied configuration file.
It contains the targets contained in the configuration file.
"""
Expand All @@ -69,7 +75,8 @@ def env(request, record_testsuite_property):
try:
target = env.get_target(target_name)
except UserError as e:
pytest.exit(e)
pytest.exit(str(e))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems to be unrelated to typing? Or does it fix a bug? If so, best to put the bug-fixes first, in one or more separate commits.

Copy link
Contributor Author

@rpoisel rpoisel Aug 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this fixes that the wrong type of the reason parameter is passed to the pytest.exit() function: pytest.exit.

assert target, "could not get target from environment"
try:
remote_place = target.get_resource(RemotePlace, wait_avail=False)
remote_name = remote_place.name
Expand Down Expand Up @@ -112,7 +119,7 @@ def env(request, record_testsuite_property):


@pytest.fixture(scope="session")
def target(env):
def target(env: Environment) -> Target:
"""Return the default target "main" configured in the supplied
configuration file."""
target = env.get_target()
Expand All @@ -123,13 +130,13 @@ def target(env):


@pytest.fixture(scope="session")
def strategy(request, target):
def strategy(request: pytest.FixtureRequest, target: Target) -> Strategy:
"""Return the Strategy of the default target "main" configured in the
supplied configuration file."""
try:
strategy = target.get_driver("Strategy")
strategy: Strategy = target.get_driver("Strategy")
except NoDriverFoundError as e:
pytest.exit(e)
pytest.exit(str(e))

state = request.config.option.lg_initial_state
if state is not None:
Expand Down
44 changes: 30 additions & 14 deletions labgrid/pytestplugin/hooks.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import os
import logging
from typing import List, Optional

import pytest
from _pytest.logging import LoggingPlugin

from .. import Environment
from ..consoleloggingreporter import ConsoleLoggingReporter
from ..util.helper import processwrapper
from ..logging import StepFormatter, StepLogger
from ..logging import CONSOLE, StepFormatter, StepLogger
from ..exceptions import NoStrategyFoundError

LABGRID_ENV_KEY = pytest.StashKey[Environment]()
LABGRID_ENV_KEY = pytest.StashKey[Optional[Environment]]()


@pytest.hookimpl(tryfirst=True)
def pytest_cmdline_main(config):
def set_cli_log_level(level):
def pytest_cmdline_main(config: pytest.Config) -> None:
def set_cli_log_level(level: int) -> None:
nonlocal config

try:
Expand All @@ -23,28 +26,38 @@ def set_cli_log_level(level):
print(f"current_level: {current_level}")

if isinstance(current_level, str):
s = current_level.strip()
try:
current_level = int(logging.getLevelName(current_level))
current_level_val: Optional[int] = int(s)
except ValueError:
current_level = None
v = logging.getLevelName(s.upper())
current_level_val = v if isinstance(v, int) else None
elif isinstance(current_level, int):
current_level_val = current_level
else:
current_level_val = None

assert current_level_val is None or isinstance(current_level_val, int), \
"unexpected type of current log level"

# If no level was set previously (via ini or cli) or current_level is
# less verbose than level, set to new level.
if current_level is None or level < current_level:
if current_level_val is None or level < current_level_val:
config.option.log_cli_level = str(level)

verbosity = config.getoption("verbose")
assert isinstance(verbosity, int), "unexpected verbosity option type"
if verbosity > 3: # enable with -vvvv
set_cli_log_level(logging.DEBUG)
elif verbosity > 2: # enable with -vvv
set_cli_log_level(logging.CONSOLE)
set_cli_log_level(CONSOLE)
elif verbosity > 1: # enable with -vv
set_cli_log_level(logging.INFO)


def configure_pytest_logging(config, plugin):
if hasattr(plugin.log_cli_handler.formatter, "add_color_level"):
plugin.log_cli_handler.formatter.add_color_level(logging.CONSOLE, "blue")
def configure_pytest_logging(config: pytest.Config, plugin: LoggingPlugin) -> None:
if (add_color_level := getattr(plugin.log_cli_handler.formatter, "add_color_level", None)) is not None:
add_color_level(CONSOLE, "blue")
plugin.log_cli_handler.setFormatter(StepFormatter(
color=config.option.lg_colored_steps,
parent=plugin.log_cli_handler.formatter,
Expand All @@ -62,12 +75,13 @@ def configure_pytest_logging(config, plugin):
plugin.report_handler.setFormatter(StepFormatter(parent=caplog_formatter))

@pytest.hookimpl(trylast=True)
def pytest_configure(config):
def pytest_configure(config: pytest.Config) -> None:
StepLogger.start()
config.add_cleanup(StepLogger.stop)

logging_plugin = config.pluginmanager.getplugin('logging-plugin')
if logging_plugin:
assert isinstance(logging_plugin, LoggingPlugin), "unexpected type of logging-plugin"
configure_pytest_logging(config, logging_plugin)

config.addinivalue_line("markers",
Expand All @@ -90,9 +104,11 @@ def pytest_configure(config):
processwrapper.enable_logging()

@pytest.hookimpl()
def pytest_collection_modifyitems(config, items):
def pytest_collection_modifyitems(session: pytest.Session, config: pytest.Config, items: List[pytest.Item]) -> None:
"""This function matches function feature flags with those found in the
environment and disables the item if no match is found"""
del session # unused

env = config.stash[LABGRID_ENV_KEY]

if not env:
Expand Down Expand Up @@ -124,7 +140,7 @@ def pytest_collection_modifyitems(config, items):
item.add_marker(skip)

@pytest.hookimpl(tryfirst=True)
def pytest_runtest_setup(item):
def pytest_runtest_setup(item) -> None:
"""
Skip test if one of the targets uses a strategy considered broken.
"""
Expand Down
Empty file added labgrid/pytestplugin/py.typed
Empty file.
4 changes: 2 additions & 2 deletions labgrid/remote/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from ..util.proxy import proxymanager
from ..util.helper import processwrapper
from ..driver import Mode, ExecutionError
from ..logging import basicConfig, StepLogger
from ..logging import basicConfig, CONSOLE, StepLogger

# This is a workround for the gRPC issue
# https://github.com/grpc/grpc/issues/38679.
Expand Down Expand Up @@ -2102,7 +2102,7 @@ def main():
if args.verbose:
logging.getLogger().setLevel(logging.INFO)
if args.verbose > 1:
logging.getLogger().setLevel(logging.CONSOLE)
logging.getLogger().setLevel(CONSOLE)
if args.debug or args.verbose > 2:
logging.getLogger().setLevel(logging.DEBUG)

Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

psutil = pytest.importorskip("psutil")

pytest_plugins = ["pytester"]

@pytest.fixture(scope="session")
def curses_init():
""" curses only reads the terminfo DB once on the first import, so make
Expand Down
Loading