diff --git a/CHANGES.rst b/CHANGES.rst index c8a4d9545..e346bc2cd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -48,6 +48,9 @@ New Features in 24.0 - The pyproject.toml gained a config for `ruff `_. - ``setuptools_scm`` is now used to generate a version file. - labgrid-client console will fallback to telnet if microcom is not available. +- The ``pythonpath`` key allows specifying additional directories for the + Python search path (sys.path). This helps include modules/packages not + in the standard library or current directory. Bug fixes in 24.0 diff --git a/doc/configuration.rst b/doc/configuration.rst index 3697dc744..ecc69d73d 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -3555,6 +3555,8 @@ The skeleton for an environment consists of: imports: - - + pythonpath: + - If you have a single target in your environment, name it "main", as the ``get_target`` function defaults to "main". diff --git a/labgrid/config.py b/labgrid/config.py index 7801e05e1..3d7424927 100644 --- a/labgrid/config.py +++ b/labgrid/config.py @@ -3,6 +3,7 @@ This class encapsulates access functions to the environment configuration """ +from typing import List import os from yaml import YAMLError import attr @@ -10,6 +11,7 @@ from .exceptions import NoConfigFoundError, InvalidConfigError from .util.yaml import load, resolve_templates + @attr.s(eq=False) class Config: filename = attr.ib(validator=attr.validators.instance_of(str)) @@ -258,6 +260,12 @@ def get_imports(self): return imports + def get_pythonpath(self) -> List[str]: + configured_pythonpath = self.data.get('pythonpath', []) + if not isinstance(configured_pythonpath, list): + raise KeyError("pythonpath needs to be a list") + return [self.resolve_path(p) for p in configured_pythonpath] + def get_paths(self): """Helper function that returns the subdict of all paths diff --git a/labgrid/environment.py b/labgrid/environment.py index abff96ce1..20c8e32b8 100644 --- a/labgrid/environment.py +++ b/labgrid/environment.py @@ -1,5 +1,8 @@ -import os from typing import Optional +from importlib.machinery import SourceFileLoader +import importlib.util +import os +import sys import attr from .target import Target @@ -19,11 +22,12 @@ def __attrs_post_init__(self): self.config = Config(self.config_file) - for user_import in self.config.get_imports(): - import importlib.util - from importlib.machinery import SourceFileLoader - import sys + # we want configured paths to appear at the beginning of the + # PYTHONPATH, so they need to be inserted in reverse order + for user_pythonpath in reversed(self.config.get_pythonpath()): + sys.path.insert(0, user_pythonpath) + for user_import in self.config.get_imports(): if user_import.endswith('.py'): module_name = os.path.basename(user_import)[:-3] loader = SourceFileLoader(module_name, user_import) diff --git a/man/labgrid-device-config.5 b/man/labgrid-device-config.5 index 13943611c..971bf0aca 100644 --- a/man/labgrid-device-config.5 +++ b/man/labgrid-device-config.5 @@ -204,6 +204,29 @@ imports: .fi .UNINDENT .UNINDENT +.SH PYTHONPATH +.sp +The \fBpythonpath\fP key allows specifying additional directories to be added +to the Python search path (sys.path). This is particularly useful for +including directories that contain Python modules and packages which are +not installed in the standard library or not in the current directory. +Relative paths are resolved relative to the configuration file\(aqs location. +Specifying this option enables the environment to locate and import modules +that are not in the default Python path. +.SS PYTHONPATH EXAMPLE +.sp +Extend the Python search path to include the \fIlibs\fP directory: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +pythonpath: + \- libs +.ft P +.fi +.UNINDENT +.UNINDENT .SH EXAMPLES .sp A sample configuration with one \fImain\fP target, accessible via SerialPort diff --git a/man/labgrid-device-config.rst b/man/labgrid-device-config.rst index 216a657bd..be1fd1ab8 100644 --- a/man/labgrid-device-config.rst +++ b/man/labgrid-device-config.rst @@ -192,6 +192,25 @@ Import a local `myfunctions.py` file: imports: - myfunctions.py +PYTHONPATH +---------- +The ``pythonpath`` key allows specifying additional directories to be added +to the Python search path (sys.path). This is particularly useful for +including directories that contain Python modules and packages which are +not installed in the standard library or not in the current directory. +Relative paths are resolved relative to the configuration file's location. +Specifying this option enables the environment to locate and import modules +that are not in the default Python path. + +PYTHONPATH EXAMPLE +~~~~~~~~~~~~~~~~~~ +Extend the Python search path to include the `libs` directory: + +:: + + pythonpath: + - libs + EXAMPLES -------- A sample configuration with one `main` target, accessible via SerialPort diff --git a/tests/test_environment.py b/tests/test_environment.py index e8138d429..8d4ec1967 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -1,3 +1,4 @@ +from pathlib import Path import pytest import warnings @@ -81,6 +82,36 @@ class Test: t = myimport.Test() assert (isinstance(t, myimport.Test)) + def test_env_pythonpath_yaml(self, tmp_path: Path): + config = tmp_path / "configs" / "config.yaml" + config.parent.mkdir(parents=True, exist_ok=True) + config.write_text(f"""--- +target: + main: + drivers: {{}} +pythonpath: + - ../modules +imports: + - ../sample.py +""") + sample_py = tmp_path / "sample.py" + sample_py.write_text(""" +from some_module import Foo +""") + some_module = tmp_path / "modules" / "some_module.py" + some_module.parent.mkdir(parents=True, exist_ok=True) + some_module.write_text(""" +class Foo: + pass +""") + e = Environment(str(config)) + assert isinstance(e, Environment) + import sys + assert 'some_module' in sys.modules + import some_module + f = some_module.Foo() + assert isinstance(f, some_module.Foo) + def test_create_named_resources(self, tmpdir): p = tmpdir.join("config.yaml") p.write(