Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support setting the pythonpath in configuration YAMLs #1454

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ New Features in 24.0
- The pyproject.toml gained a config for `ruff <https://github.com/astral-sh/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
Expand Down
2 changes: 2 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3555,6 +3555,8 @@ The skeleton for an environment consists of:
imports:
- <import.py>
- <python module>
pythonpath:
- <absolute or relative path>

If you have a single target in your environment, name it "main", as the
``get_target`` function defaults to "main".
Expand Down
8 changes: 8 additions & 0 deletions labgrid/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
This class encapsulates access functions to the environment configuration

"""
from typing import List
import os
from yaml import YAMLError
import attr

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))
Expand Down Expand Up @@ -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

Expand Down
14 changes: 9 additions & 5 deletions labgrid/environment.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions man/labgrid-device-config.5
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions man/labgrid-device-config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions tests/test_environment.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
import pytest
import warnings

Expand Down Expand Up @@ -81,6 +82,91 @@ class Test:
t = myimport.Test()
assert (isinstance(t, myimport.Test))

def test_env_pythonpath_yaml_module(self, tmp_path: Path):
config = tmp_path / "configs" / "config.yaml"
config.parent.mkdir(parents=True, exist_ok=True)
config.write_text(f"""---
pythonpath:
- ../modules
imports:
- ../sample.py
""")
sample_py = tmp_path / "sample.py"
sample_py.write_text("""
from some_module import Foo
from other_module import Bar

Foo()
Bar()
""")
modules_dir = tmp_path / "modules"
modules_dir.mkdir(parents=True, exist_ok=True)

some_module = tmp_path / "modules" / "some_module.py"
some_module.write_text("""
class Foo:
pass
""")
other_module = tmp_path / "modules" / "other_module.py"
other_module.write_text("""
class Bar:
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_env_pythonpath_yaml_package(self, tmp_path: Path):
config = tmp_path / "configs" / "config.yaml"
config.parent.mkdir(parents=True, exist_ok=True)
config.write_text(f"""---
pythonpath:
- ../modules
imports:
- ../sample.py
""")
sample_py = tmp_path / "sample.py"
sample_py.write_text("""
from mine import Foo
from mine import Bar

Foo()
Bar()
""")
modules_dir = tmp_path / "modules"
modules_dir.mkdir(parents=True, exist_ok=True)

mine_dir = modules_dir / "mine"
mine_dir.mkdir(parents=True, exist_ok=True)

mine_init = mine_dir / "__init__.py"
mine_init.write_text("""
from .some_module import Foo
from .other_module import Bar
""")

some_module = tmp_path / "modules" / "mine" / "some_module.py"
some_module.write_text("""
class Foo:
pass
""")
other_module = tmp_path / "modules" / "mine" / "other_module.py"
other_module.write_text("""
class Bar:
pass
""")
e = Environment(str(config))
assert isinstance(e, Environment)
import sys
assert 'mine' in sys.modules
from mine import Foo
f = Foo()
assert isinstance(f, Foo)

def test_create_named_resources(self, tmpdir):
p = tmpdir.join("config.yaml")
p.write(
Expand Down
Loading