Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/pylsl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
from .util import library_version as library_version
from .util import library_info as library_info
from .util import local_clock as local_clock
from .util import set_config_filename as set_config_filename
from .util import set_config_content as set_config_content
from .lib import cf_int8 as cf_int8
from .lib import cf_int16 as cf_int16
from .lib import cf_int32 as cf_int32
Expand Down
9 changes: 9 additions & 0 deletions src/pylsl/lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,15 @@ def find_liblsl_libraries(verbose=False):
lib.lsl_create_continuous_resolver_byprop.restype = ctypes.c_void_p
except Exception:
print("pylsl: ContinuousResolver not (fully) available in your liblsl version.")
# noinspection PyBroadException
try:
lib.lsl_set_config_filename.argtypes = [ctypes.c_char_p]
lib.lsl_set_config_filename.restype = None
lib.lsl_set_config_content.argtypes = [ctypes.c_char_p]
lib.lsl_set_config_content.restype = None
except Exception:
# Available in liblsl >= 1.17.7; older versions don't expose these.
pass


# int64 support on windows and 32bit OSes isn't there yet
Expand Down
30 changes: 30 additions & 0 deletions src/pylsl/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,36 @@ def library_info():
return lib.lsl_library_info().decode("utf-8")


def set_config_filename(filename: str) -> None:
"""Set the path of the configuration file to be used by liblsl.

Must be called before any other LSL function; the configuration is loaded
lazily on first use, so calling this afterwards has no effect.
"""
if not hasattr(lib, "lsl_set_config_filename"):
raise NotImplementedError(
"lsl_set_config_filename is not available in your liblsl version "
"(requires liblsl >= 1.17.7)."
)
lib.lsl_set_config_filename(filename.encode("utf-8"))


def set_config_content(content: str) -> None:
"""Set the configuration content (as an INI string) to be used by liblsl.

Must be called before any other LSL function; the configuration is loaded
lazily on first use, so calling this afterwards has no effect. When set,
this content takes precedence over any configuration file. The content is
discarded after liblsl has initialized.
"""
if not hasattr(lib, "lsl_set_config_content"):
raise NotImplementedError(
"lsl_set_config_content is not available in your liblsl version "
"(requires liblsl >= 1.17.7)."
)
lib.lsl_set_config_content(content.encode("utf-8"))


def local_clock():
"""Obtain a local system time stamp in seconds.

Expand Down
72 changes: 72 additions & 0 deletions test/test_set_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Tests for set_config_filename and set_config_content.

Both functions must run before liblsl's config is loaded (on first use), so
the effect-verifying tests run in a fresh Python subprocess. In-process smoke
tests only verify that the call path works — they may be no-ops if liblsl has
already initialized in the current process.
"""

import subprocess
import sys
import textwrap

import pytest

import pylsl


def test_set_config_content_accepts_string():
# Safe to call even after init — content is simply ignored once liblsl has loaded.
pylsl.set_config_content("[lab]\nSessionID = ignored_after_init\n")


def test_set_config_filename_accepts_string():
pylsl.set_config_filename("/nonexistent/path/to/lsl.cfg")


def test_set_config_content_rejects_non_string():
with pytest.raises(AttributeError):
pylsl.set_config_content(b"not a str")


def test_set_config_filename_rejects_non_string():
with pytest.raises(AttributeError):
pylsl.set_config_filename(12345)


def _run_in_subprocess(script: str) -> str:
result = subprocess.run(
[sys.executable, "-c", script],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()


def test_set_config_content_applies_session_id():
"""Setting content before any LSL call should update the session id that
outlets inherit from the library config."""
session_id = "pytest_content_session"
script = textwrap.dedent(f"""
import pylsl
pylsl.set_config_content('[lab]\\nSessionID = {session_id}\\n')
info = pylsl.StreamInfo('T', 'EEG', 1, 100, 'float32', 'src')
outlet = pylsl.StreamOutlet(info)
print(outlet.get_info().session_id())
""")
assert _run_in_subprocess(script) == session_id


def test_set_config_filename_applies_session_id(tmp_path):
session_id = "pytest_filename_session"
cfg = tmp_path / "lsl.cfg"
cfg.write_text(f"[lab]\nSessionID = {session_id}\n")
script = textwrap.dedent(f"""
import pylsl
pylsl.set_config_filename({str(cfg)!r})
info = pylsl.StreamInfo('T', 'EEG', 1, 100, 'float32', 'src')
outlet = pylsl.StreamOutlet(info)
print(outlet.get_info().session_id())
""")
assert _run_in_subprocess(script) == session_id
Loading