Skip to content

Commit ca596dc

Browse files
Merge pull request #324 from pellet/release/build-fixes
build: modernize Python support and decouple streaming env from PsychoPy & VR deps
2 parents e931507 + f7751c5 commit ca596dc

20 files changed

Lines changed: 556 additions & 160 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Set up conda env
2+
description: >
3+
Install Miniconda and create/activate an EEG-ExPy conda environment from
4+
the given env yml. Shared by the Test and Typecheck jobs so the two
5+
don't drift apart. Environment name is not set in the yml files so local
6+
installs can use any name they like.
7+
8+
inputs:
9+
environment-file:
10+
required: true
11+
description: Path to the conda environment yml file to install from.
12+
activate-environment:
13+
required: true
14+
description: Name to give the created environment.
15+
python-version:
16+
required: false
17+
description: >
18+
Python version to pin (e.g. '3.8'). Overrides the version conda would
19+
otherwise resolve from the environment file's constraints. When omitted,
20+
conda resolves freely within the environment file's range.
21+
22+
runs:
23+
using: composite
24+
steps:
25+
- uses: conda-incubator/setup-miniconda@v3
26+
with:
27+
environment-file: ${{ inputs.environment-file }}
28+
activate-environment: ${{ inputs.activate-environment }}
29+
python-version: ${{ inputs.python-version }}
30+
auto-activate-base: false
31+
channels: conda-forge
32+
miniconda-version: "latest"

.github/workflows/docs.yml

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,20 @@ on:
99
jobs:
1010
build:
1111
runs-on: ubuntu-22.04
12+
defaults:
13+
run:
14+
shell: bash -el {0}
1215
steps:
1316
- name: Checkout repo
1417
uses: actions/checkout@v3
1518
with:
16-
fetch-depth: 0
17-
18-
- name: Set up Python
19-
uses: actions/setup-python@v4
20-
with:
21-
python-version: 3.8
22-
23-
- name: Install dependencies
24-
run: |
25-
make install-deps-apt
26-
python -m pip install --upgrade pip wheel
27-
python -m pip install attrdict
28-
29-
make install-deps-wxpython
30-
31-
- name: Build project
32-
run: |
33-
make install-docs-build-dependencies
19+
fetch-depth: 0
3420

21+
- name: Set up conda env
22+
uses: ./.github/actions/setup-conda-env
23+
with:
24+
environment-file: environments/eeg-expy-docsbuild.yml
25+
activate-environment: eeg-expy-docsbuild
3526

3627
- name: Get list of changed files
3728
id: changes
@@ -40,21 +31,20 @@ jobs:
4031
git diff --name-only origin/master...HEAD > changed_files.txt
4132
cat changed_files.txt
4233
43-
4434
- name: Determine build mode
4535
id: mode
4636
run: |
4737
if grep -vqE '^examples/.*\.py$' changed_files.txt; then
4838
echo "FULL_BUILD=true" >> $GITHUB_ENV
4939
echo "Detected non-example file change. Full build triggered."
5040
else
51-
CHANGED_EXAMPLES=$(grep '^examples/.*\.py$' changed_files.txt | paste -sd '|' -)
41+
# || true prevents grep's exit code 1 (no matches) from aborting the step
42+
CHANGED_EXAMPLES=$(grep '^examples/.*\.py$' changed_files.txt | paste -sd '|' - || true)
5243
echo "FULL_BUILD=false" >> $GITHUB_ENV
5344
echo "CHANGED_EXAMPLES=$CHANGED_EXAMPLES" >> $GITHUB_ENV
5445
echo "Changed examples: $CHANGED_EXAMPLES"
5546
fi
5647
57-
5848
- name: Cache built documentation
5949
id: cache-docs
6050
uses: actions/cache@v4
@@ -65,12 +55,9 @@ jobs:
6555
restore-keys: |
6656
${{ runner.os }}-sphinx-
6757
68-
6958
- name: Build docs
70-
run: |
71-
make docs
59+
run: make docs
7260

73-
7461
- name: Deploy Docs
7562
uses: peaceiris/actions-gh-pages@v3
7663
if: github.ref == 'refs/heads/master' # TODO: Deploy seperate develop-version of docs?

.github/workflows/test.yml

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,38 +13,47 @@ jobs:
1313
defaults:
1414
run:
1515
shell: bash -el {0}
16+
continue-on-error: ${{ matrix.experimental == true }}
1617
strategy:
1718
fail-fast: false
1819
matrix:
19-
os: ['ubuntu-22.04', windows-latest, macOS-latest]
20-
python_version: ['3.8']
20+
os: [ubuntu-22.04, windows-latest, macOS-latest]
21+
python_version: ['3.10']
22+
env_file: [environments/eeg-expy-full.yml]
23+
env_name: [eeg-expy-full]
2124
include:
22-
# PsychoPy currently restricted to <= 3.10
25+
# Experimental Full Build: Catch regressions on 3.11 early
2326
- os: ubuntu-22.04
24-
python_version: '3.10'
27+
python_version: '3.11'
28+
experimental: true
29+
env_file: environments/eeg-expy-full.yml
30+
env_name: eeg-expy-full
31+
32+
# Experimental Streaming Builds: Verify acquisition/analysis on 3.12+
33+
- os: ubuntu-22.04
34+
python_version: '3.12'
35+
experimental: true
36+
env_file: environments/eeg-expy-streaming.yml
37+
env_name: eeg-expy-streaming
38+
- os: ubuntu-22.04
39+
python_version: '3.13'
40+
experimental: true
41+
env_file: environments/eeg-expy-streaming.yml
42+
env_name: eeg-expy-streaming
2543

2644
steps:
2745
- uses: actions/checkout@v2
2846
- name: Install APT dependencies
2947
if: "startsWith(runner.os, 'Linux')"
3048
run: |
3149
make install-deps-apt
32-
- name: Install conda
33-
uses: conda-incubator/setup-miniconda@v3
50+
- name: Set up conda env
51+
uses: ./.github/actions/setup-conda-env
3452
with:
35-
environment-file: environments/eeg-expy-full.yml
36-
auto-activate-base: false
53+
environment-file: ${{ matrix.env_file }}
54+
activate-environment: ${{ matrix.env_name }}
3755
python-version: ${{ matrix.python_version }}
38-
activate-environment: eeg-expy-full
39-
channels: conda-forge
40-
miniconda-version: "latest"
41-
42-
- name: Fix PsychXR numpy dependency DLL issues (Windows only)
43-
if: matrix.os == 'windows-latest'
44-
run: |
45-
conda install --force-reinstall numpy
46-
47-
- name: Run eegnb install test
56+
- name: Run eeg-expy install test
4857
run: |
4958
if [ "$RUNNER_OS" == "Linux" ]; then
5059
Xvfb :0 -screen 0 1024x768x24 -ac +extension GLX +render -noreset &> xvfb.log &
@@ -58,7 +67,7 @@ jobs:
5867
Xvfb :0 -screen 0 1024x768x24 -ac +extension GLX +render -noreset &> xvfb.log &
5968
export DISPLAY=:0
6069
fi
61-
make test PYTEST_ARGS="--ignore=tests/test_run_experiments.py"
70+
make test
6271
6372
6473
typecheck:
@@ -71,19 +80,16 @@ jobs:
7180
fail-fast: false
7281
matrix:
7382
os: ['ubuntu-22.04']
74-
python_version: [3.9]
83+
python_version: ['3.10']
7584

7685
steps:
7786
- uses: actions/checkout@v2
78-
- name: Install conda
79-
uses: conda-incubator/setup-miniconda@v3
87+
- name: Set up conda env
88+
uses: ./.github/actions/setup-conda-env
8089
with:
8190
environment-file: environments/eeg-expy-full.yml
82-
auto-activate-base: false
83-
python-version: ${{ matrix.python_version }}
8491
activate-environment: eeg-expy-full
85-
channels: conda-forge
86-
miniconda-version: "latest"
92+
python-version: ${{ matrix.python_version }}
8793
- name: Typecheck
8894
run: |
8995
make typecheck

conftest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import importlib.util
2+
3+
4+
def _is_available(module_name: str) -> bool:
5+
try:
6+
return importlib.util.find_spec(module_name) is not None
7+
except (ImportError, ValueError):
8+
return False
9+
10+
11+
collect_ignore: list[str] = []
12+
13+
if not _is_available("psychopy"):
14+
collect_ignore += [
15+
"eegnb/experiments",
16+
"eegnb/devices/vr.py",
17+
]
18+
elif not _is_available("psychxr"):
19+
collect_ignore += ["eegnb/devices/vr.py"]

eegnb/cli/introprompt.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from eegnb import generate_save_fn, DATA_DIR
66
from eegnb.devices.eeg import EEG
7-
from .utils import run_experiment, get_exp_desc, experiments
7+
from .utils import run_experiment, get_exp_desc, get_experiments
88

99
eegnb_sites = ['eegnb_examples', 'grifflab_dev', 'jadinlab_home']
1010

@@ -87,6 +87,7 @@ def device_prompt() -> EEG:
8787

8888

8989
def exp_prompt(runorzip:str='run') -> str:
90+
experiments = get_experiments()
9091
print("\nPlease select which experiment you would like to %s: \n" %runorzip)
9192
print(
9293
"\n".join(

eegnb/cli/utils.py

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,42 @@
11

2-
#change the pref libraty to PTB and set the latency mode to high precision
3-
from psychopy import prefs
4-
prefs.hardware['audioLib'] = 'PTB'
5-
prefs.hardware['audioLatencyMode'] = 3
2+
try:
3+
#change the pref libraty to PTB and set the latency mode to high precision
4+
from psychopy import prefs
5+
prefs.hardware['audioLib'] = 'PTB'
6+
prefs.hardware['audioLatencyMode'] = 3
7+
except ImportError:
8+
pass
69

710

811
from eegnb.devices.eeg import EEG
9-
10-
from eegnb.experiments import VisualN170, Experiment
11-
from eegnb.experiments import VisualP300
12-
from eegnb.experiments import VisualSSVEP
13-
from eegnb.experiments import AuditoryOddball
14-
from eegnb.experiments.visual_cueing import cueing
15-
from eegnb.experiments.visual_codeprose import codeprose
16-
from eegnb.experiments.auditory_oddball import diaconescu
17-
from eegnb.experiments.auditory_ssaep import ssaep, ssaep_onefreq
1812
from typing import Optional
1913

14+
def get_experiments():
15+
from eegnb.experiments import VisualN170, Experiment
16+
from eegnb.experiments import VisualP300
17+
from eegnb.experiments import VisualSSVEP
18+
from eegnb.experiments import AuditoryOddball
19+
from eegnb.experiments.visual_cueing import cueing
20+
from eegnb.experiments.visual_codeprose import codeprose
21+
from eegnb.experiments.auditory_oddball import diaconescu
22+
from eegnb.experiments.auditory_ssaep import ssaep, ssaep_onefreq
2023

21-
# New Experiment Class structure has a different initilization, to be noted
22-
experiments = {
23-
"visual-N170": VisualN170(),
24-
"visual-P300": VisualP300(),
25-
"visual-SSVEP": VisualSSVEP(),
26-
"visual-cue": cueing,
27-
"visual-codeprose": codeprose,
28-
"auditory-SSAEP orig": ssaep,
29-
"auditory-SSAEP onefreq": ssaep_onefreq,
30-
"auditory-oddball orig": AuditoryOddball(),
31-
"auditory-oddball diaconescu": diaconescu,
32-
}
24+
# New Experiment Class structure has a different initilization, to be noted
25+
return {
26+
"visual-N170": VisualN170,
27+
"visual-P300": VisualP300,
28+
"visual-SSVEP": VisualSSVEP,
29+
"visual-cue": cueing,
30+
"visual-codeprose": codeprose,
31+
"auditory-SSAEP orig": ssaep,
32+
"auditory-SSAEP onefreq": ssaep_onefreq,
33+
"auditory-oddball orig": AuditoryOddball,
34+
"auditory-oddball diaconescu": diaconescu,
35+
}
3336

3437

3538
def get_exp_desc(exp: str):
39+
experiments = get_experiments()
3640
if exp in experiments:
3741
module = experiments[exp]
3842
if hasattr(module, "__title__"):
@@ -43,17 +47,24 @@ def get_exp_desc(exp: str):
4347
def run_experiment(
4448
experiment: str, eeg_device: EEG, record_duration: Optional[float] = None, save_fn=None
4549
):
50+
experiments = get_experiments()
4651
if experiment in experiments:
47-
module = experiments[experiment]
52+
exp_item = experiments[experiment]
53+
54+
from eegnb.experiments import Experiment
4855

4956
# Condition added for different run types of old and new experiment class structure
50-
if isinstance(module, Experiment.BaseExperiment):
51-
module.duration = record_duration
52-
module.eeg = eeg_device
53-
module.save_fn = save_fn
54-
module.run()
57+
# If it's a class (BaseExperiment subclass), instantiate it
58+
if isinstance(exp_item, type) and issubclass(exp_item, Experiment.BaseExperiment):
59+
# Concrete subclasses supply defaults for BaseExperiment's required args; mypy can't see which subclass.
60+
exp_instance = exp_item() # type: ignore[call-arg]
61+
exp_instance.duration = record_duration
62+
exp_instance.eeg = eeg_device
63+
exp_instance.save_fn = save_fn
64+
exp_instance.run()
5565
else:
56-
module.present(duration=record_duration, eeg=eeg_device, save_fn=save_fn) # type: ignore
66+
# Otherwise it's an old-style module
67+
exp_item.present(duration=record_duration, eeg=eeg_device, save_fn=save_fn) # type: ignore
5768
else:
5869
print("\nError: Unknown experiment '{}'".format(experiment))
5970
print("\nExperiment can be one of:")

eegnb/devices/eeg.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,16 @@
2020

2121
from serial import Serial, EIGHTBITS, PARITY_NONE, STOPBITS_ONE
2222

23-
import pyxid2
23+
from eegnb.utils.missing import missing_module
24+
25+
try:
26+
import pyxid2
27+
except (ImportError, OSError):
28+
pyxid2 = missing_module(
29+
"pyxid2",
30+
"The Cedrus XID backend (NIRSport2 and other Cedrus stimulus-marker devices)",
31+
"xid",
32+
)
2433

2534
from eegnb.devices.utils import (
2635
get_openbci_usb,

0 commit comments

Comments
 (0)