psychopy 2025.1.0__py3-none-any.whl → 2025.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of psychopy might be problematic. Click here for more details.
- psychopy/VERSION +1 -1
- psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
- psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
- psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
- psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
- psychopy/app/Resources/classic/add_many.png +0 -0
- psychopy/app/Resources/classic/add_many@2x.png +0 -0
- psychopy/app/Resources/classic/devices.png +0 -0
- psychopy/app/Resources/classic/devices@2x.png +0 -0
- psychopy/app/Resources/classic/photometer.png +0 -0
- psychopy/app/Resources/classic/photometer@2x.png +0 -0
- psychopy/app/Resources/dark/add_many.png +0 -0
- psychopy/app/Resources/dark/add_many@2x.png +0 -0
- psychopy/app/Resources/dark/devices.png +0 -0
- psychopy/app/Resources/dark/devices@2x.png +0 -0
- psychopy/app/Resources/dark/photometer.png +0 -0
- psychopy/app/Resources/dark/photometer@2x.png +0 -0
- psychopy/app/Resources/light/add_many.png +0 -0
- psychopy/app/Resources/light/add_many@2x.png +0 -0
- psychopy/app/Resources/light/devices.png +0 -0
- psychopy/app/Resources/light/devices@2x.png +0 -0
- psychopy/app/Resources/light/photometer.png +0 -0
- psychopy/app/Resources/light/photometer@2x.png +0 -0
- psychopy/app/_psychopyApp.py +35 -13
- psychopy/app/builder/builder.py +88 -35
- psychopy/app/builder/dialogs/__init__.py +69 -220
- psychopy/app/builder/dialogs/dlgsCode.py +29 -8
- psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
- psychopy/app/builder/validators.py +25 -17
- psychopy/app/coder/coder.py +12 -1
- psychopy/app/coder/repl.py +5 -2
- psychopy/app/colorpicker/__init__.py +1 -1
- psychopy/app/deviceManager/__init__.py +1 -0
- psychopy/app/deviceManager/addDialog.py +218 -0
- psychopy/app/deviceManager/dialog.py +185 -0
- psychopy/app/deviceManager/panel.py +191 -0
- psychopy/app/deviceManager/utils.py +60 -0
- psychopy/app/idle.py +7 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
- psychopy/app/plugin_manager/dialog.py +12 -3
- psychopy/app/plugin_manager/packageIndex.py +303 -0
- psychopy/app/plugin_manager/packages.py +203 -63
- psychopy/app/plugin_manager/plugins.py +120 -240
- psychopy/app/preferencesDlg.py +6 -1
- psychopy/app/psychopyApp.py +16 -4
- psychopy/app/runner/runner.py +10 -2
- psychopy/app/runner/scriptProcess.py +8 -3
- psychopy/app/stdout/stdOutRich.py +11 -4
- psychopy/app/themes/icons.py +3 -0
- psychopy/app/utils.py +61 -0
- psychopy/colors.py +10 -5
- psychopy/data/experiment.py +133 -23
- psychopy/data/routine.py +12 -0
- psychopy/data/staircase.py +42 -20
- psychopy/data/trial.py +20 -12
- psychopy/data/utils.py +43 -3
- psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
- psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
- psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
- psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
- psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
- psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
- psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
- psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
- psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
- psychopy/event.py +20 -15
- psychopy/experiment/_experiment.py +86 -10
- psychopy/experiment/components/__init__.py +3 -10
- psychopy/experiment/components/_base.py +9 -20
- psychopy/experiment/components/button/__init__.py +1 -1
- psychopy/experiment/components/buttonBox/__init__.py +50 -54
- psychopy/experiment/components/camera/__init__.py +137 -359
- psychopy/experiment/components/keyboard/__init__.py +17 -24
- psychopy/experiment/components/microphone/__init__.py +61 -110
- psychopy/experiment/components/movie/__init__.py +2 -3
- psychopy/experiment/components/serialOut/__init__.py +192 -93
- psychopy/experiment/components/settings/__init__.py +45 -27
- psychopy/experiment/components/sound/__init__.py +82 -73
- psychopy/experiment/components/soundsensor/__init__.py +43 -80
- psychopy/experiment/devices.py +303 -0
- psychopy/experiment/exports.py +20 -18
- psychopy/experiment/flow.py +7 -0
- psychopy/experiment/loops.py +47 -29
- psychopy/experiment/monitor.py +74 -0
- psychopy/experiment/params.py +48 -10
- psychopy/experiment/plugins.py +28 -108
- psychopy/experiment/py2js_transpiler.py +1 -1
- psychopy/experiment/routines/__init__.py +1 -1
- psychopy/experiment/routines/_base.py +59 -24
- psychopy/experiment/routines/audioValidator/__init__.py +19 -155
- psychopy/experiment/routines/visualValidator/__init__.py +25 -25
- psychopy/hardware/__init__.py +20 -57
- psychopy/hardware/button.py +15 -2
- psychopy/hardware/camera/__init__.py +2237 -1394
- psychopy/hardware/joystick/__init__.py +1 -1
- psychopy/hardware/keyboard.py +5 -8
- psychopy/hardware/listener.py +4 -1
- psychopy/hardware/manager.py +75 -35
- psychopy/hardware/microphone.py +53 -7
- psychopy/hardware/monitor.py +144 -0
- psychopy/hardware/photometer/__init__.py +156 -117
- psychopy/hardware/serialdevice.py +16 -2
- psychopy/hardware/soundsensor.py +4 -1
- psychopy/iohub/devices/deviceConfigValidation.py +2 -1
- psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +2 -2
- psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/__init__.py +1 -0
- psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/eyetracker.py +10 -0
- psychopy/iohub/devices/keyboard/darwin.py +8 -5
- psychopy/iohub/util/__init__.py +7 -8
- psychopy/localization/generateTranslationTemplate.py +208 -116
- psychopy/localization/messages.pot +4305 -3502
- psychopy/monitors/MonitorCenter.py +174 -74
- psychopy/plugins/__init__.py +6 -4
- psychopy/preferences/devices.py +80 -0
- psychopy/preferences/generateHints.py +2 -1
- psychopy/preferences/preferences.py +35 -11
- psychopy/scripts/psychopy-pkgutil.py +969 -0
- psychopy/scripts/psyexpCompile.py +1 -1
- psychopy/session.py +34 -38
- psychopy/sound/__init__.py +6 -260
- psychopy/sound/audioclip.py +164 -0
- psychopy/sound/backend_ptb.py +8 -0
- psychopy/sound/backend_pygame.py +10 -0
- psychopy/sound/backend_pysound.py +9 -0
- psychopy/sound/backends/__init__.py +0 -0
- psychopy/sound/microphone.py +3 -0
- psychopy/sound/sound.py +58 -0
- psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
- psychopy/tests/data/duplicateHeaders.csv +2 -0
- psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
- psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
- psychopy/tests/test_data/test_utils.py +5 -1
- psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
- psychopy/tests/test_hardware/test_ports.py +0 -12
- psychopy/tests/test_tools/test_stringtools.py +1 -1
- psychopy/tools/attributetools.py +12 -5
- psychopy/tools/fontmanager.py +17 -14
- psychopy/tools/gltools.py +4 -2
- psychopy/tools/movietools.py +43 -2
- psychopy/tools/stringtools.py +33 -8
- psychopy/tools/versionchooser.py +1 -1
- psychopy/validation/audio.py +5 -1
- psychopy/validation/visual.py +5 -1
- psychopy/visual/basevisual.py +8 -7
- psychopy/visual/circle.py +2 -2
- psychopy/visual/helpers.py +3 -1
- psychopy/visual/image.py +29 -109
- psychopy/visual/movies/__init__.py +1800 -313
- psychopy/visual/polygon.py +4 -0
- psychopy/visual/shape.py +2 -2
- psychopy/visual/window.py +35 -12
- psychopy/voicekey/__init__.py +41 -669
- psychopy/voicekey/labjack_vks.py +7 -48
- psychopy/voicekey/parallel_vks.py +7 -42
- psychopy/voicekey/vk_tools.py +114 -263
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/METADATA +20 -13
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/RECORD +222 -190
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
- psychopy/visual/movies/players/__init__.py +0 -62
- psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
- psychopy/voicekey/demo_vks.py +0 -12
- psychopy/voicekey/signal.py +0 -42
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
- {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
psychopy/sound/sound.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Sound:
|
|
5
|
+
"""
|
|
6
|
+
Class for playing a sound in PsychoPy. See specific sound backends for details and methods for
|
|
7
|
+
implementations of Sound.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# name of the backend to use for Sound objects
|
|
11
|
+
backend = "ptb"
|
|
12
|
+
|
|
13
|
+
def __new__(cls, *args, **kwargs):
|
|
14
|
+
# get backends
|
|
15
|
+
backends = cls.getBackends()
|
|
16
|
+
# if not present, error
|
|
17
|
+
if cls.backend not in backends:
|
|
18
|
+
raise ModuleNotFoundError(f"Invalid value '{cls.backend}' for Sound.backend, known backends are: {list(backends)}")
|
|
19
|
+
# import backend
|
|
20
|
+
backend = backends[cls.backend].load()
|
|
21
|
+
return backend.Sound(*args, **kwargs)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def getBackends(cls):
|
|
25
|
+
"""
|
|
26
|
+
Get all available Sound backends (by name)
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
dict[str:importlib.metadata.EntryPoint]
|
|
31
|
+
Dict mapping backend names to backend entry points - call `.load` on an entry point to
|
|
32
|
+
import the relevant module.
|
|
33
|
+
"""
|
|
34
|
+
# start off with builtin backends
|
|
35
|
+
backends = {
|
|
36
|
+
ep.name: ep for ep in [
|
|
37
|
+
importlib.metadata.EntryPoint(
|
|
38
|
+
name="ptb",
|
|
39
|
+
value="psychopy.sound.backend_ptb",
|
|
40
|
+
group="psychopy.sound.backends"
|
|
41
|
+
),
|
|
42
|
+
importlib.metadata.EntryPoint(
|
|
43
|
+
name="pygame",
|
|
44
|
+
value="psychopy.sound.backend_pygame",
|
|
45
|
+
group="psychopy.sound.backends"
|
|
46
|
+
),
|
|
47
|
+
importlib.metadata.EntryPoint(
|
|
48
|
+
name="pysound",
|
|
49
|
+
value="psychopy.sound.backend_pysound",
|
|
50
|
+
group="psychopy.sound.backends"
|
|
51
|
+
)
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
# get others from plugins
|
|
55
|
+
for ep in importlib.metadata.entry_points(group="psychopy.sound.backends"):
|
|
56
|
+
backends[ep.name] = ep
|
|
57
|
+
|
|
58
|
+
return backends
|
|
@@ -110,7 +110,7 @@ for thisComponent in trialComponents:
|
|
|
110
110
|
while continueRoutine and routineTimer.getTime() > 0:
|
|
111
111
|
# get current time
|
|
112
112
|
t = trialClock.getTime()
|
|
113
|
-
frameN
|
|
113
|
+
frameN += 1 # number of completed frames (so 0 is the first frame)
|
|
114
114
|
# update/draw components on each frame
|
|
115
115
|
|
|
116
116
|
# *noise* updates
|
|
@@ -122,8 +122,8 @@ class Test_BuilderFrame():
|
|
|
122
122
|
|
|
123
123
|
# Define 'tykes' - combinations of values likely to cause an error if certain features aren't working
|
|
124
124
|
tykes = [
|
|
125
|
-
{'fieldName': "brokenCode", 'param': Param(val="for + :", valType="code"), 'msg': "Python syntax error in field `
|
|
126
|
-
{'fieldName': "variableDef", 'param': Param(val="visual = 1", valType="
|
|
125
|
+
{'fieldName': "brokenCode", 'param': Param(val="for + :", valType="code"), 'msg': "Python syntax error in field `brokenCode`: "}, # Make sure it's picking up clearly broken code
|
|
126
|
+
{'fieldName': "variableDef", 'param': Param(val="visual = 1", valType="extendedCode"), 'msg': "Setting the variable `visual` will overwrite an existing variable (Psychopy module)"},
|
|
127
127
|
{'fieldName': "correctAns", 'param': Param(val="'space'", valType="code"), 'msg': ""}, # Single-element lists should not cause warning
|
|
128
128
|
]
|
|
129
129
|
for case in tykes:
|
|
@@ -140,12 +140,27 @@ class Test_BuilderFrame():
|
|
|
140
140
|
timeout=500)
|
|
141
141
|
# Does the message delivered by the validator match what is expected?
|
|
142
142
|
for case in tykes:
|
|
143
|
+
# get warning for the relevant ctrl
|
|
144
|
+
warning = dlg.paramCtrls[case['fieldName']].valueCtrl.getWarning()
|
|
145
|
+
# make sure warning is correct
|
|
143
146
|
if case['msg']:
|
|
144
|
-
assert case['msg']
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"but instead
|
|
147
|
+
assert case['msg'] in warning.msg, (
|
|
148
|
+
"Param {fieldName} with value `{param}` should raise a warning:\n"
|
|
149
|
+
"{msg}\n"
|
|
150
|
+
"but instead raised:\n"
|
|
148
151
|
"{actual}\n"
|
|
149
|
-
).format(
|
|
152
|
+
).format(
|
|
153
|
+
**case,
|
|
154
|
+
actual=warning.msg
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
assert warning is None, (
|
|
158
|
+
"Param {fieldName} with value `{param}` should not raise a warning, but "
|
|
159
|
+
"raised:\n"
|
|
160
|
+
"{actual}"
|
|
161
|
+
).format(
|
|
162
|
+
**case,
|
|
163
|
+
actual=warning.msg
|
|
164
|
+
)
|
|
150
165
|
# Cleanup
|
|
151
166
|
dlg.Destroy()
|
|
@@ -65,7 +65,6 @@ class Test_PsychoJS_from_Builder():
|
|
|
65
65
|
exp = experiment.Experiment()
|
|
66
66
|
exp.loadFromXML(demosDir/'builder'/'Experiments'/'stroop'/'stroop.psyexp')
|
|
67
67
|
# try once packaging up the js libs
|
|
68
|
-
exp.settings.params['JS libs'].val = 'remote'
|
|
69
68
|
outFolder = self.temp_dir/'stroopJS_remote/html'
|
|
70
69
|
os.makedirs(outFolder)
|
|
71
70
|
self.writeScript(exp, outFolder)
|
|
@@ -75,7 +74,6 @@ class Test_PsychoJS_from_Builder():
|
|
|
75
74
|
exp = experiment.Experiment()
|
|
76
75
|
exp.loadFromXML(demosDir/'builder'/'Design Templates'/'randomisedBlocks'/'randomisedBlocks.psyexp')
|
|
77
76
|
# try once packaging up the js libs
|
|
78
|
-
exp.settings.params['JS libs'].val = 'packaged'
|
|
79
77
|
outFolder = self.temp_dir/'blocked_packaged/html'
|
|
80
78
|
os.makedirs(outFolder)
|
|
81
79
|
self.writeScript(exp, outFolder)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
|
|
4
4
|
import os
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
import pytest
|
|
6
7
|
import numpy as np
|
|
7
8
|
|
|
@@ -57,7 +58,10 @@ class Test_utilsClass:
|
|
|
57
58
|
with pytest.raises(exceptions.ConditionsImportError) as errMsg:
|
|
58
59
|
utils.importConditions(fileName_docx)
|
|
59
60
|
assert ('Your conditions file should be an ''xlsx, csv, dlm, tsv or pkl file') == str(errMsg.value)
|
|
60
|
-
|
|
61
|
+
# test that duplicate headers are caught
|
|
62
|
+
with pytest.raises(exceptions.ConditionsImportError) as err:
|
|
63
|
+
utils.importConditions(str(Path(fixturesPath) / "duplicateHeaders.csv"))
|
|
64
|
+
assert "'dupe'" in str(err.value)
|
|
61
65
|
# test random selection of conditions
|
|
62
66
|
all_conditions = utils.importConditions(standard_files[0])
|
|
63
67
|
assert len(all_conditions) == 6
|
|
@@ -13,11 +13,26 @@ from psychopy.experiment.components.buttonBox import ButtonBoxComponent
|
|
|
13
13
|
from psychopy.experiment.components.code import CodeComponent
|
|
14
14
|
from psychopy.tests.test_experiment.test_components.test_base_components import BaseComponentTests
|
|
15
15
|
from psychopy.hardware.button import ButtonBox
|
|
16
|
+
from psychopy.tests import utils
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class TestButtonBoxComponent(BaseComponentTests):
|
|
19
20
|
comp = ButtonBoxComponent
|
|
20
21
|
libraryClass = ButtonBox
|
|
22
|
+
|
|
23
|
+
def setup_class(cls):
|
|
24
|
+
"""
|
|
25
|
+
Setup a keyboard response box for this test in prefs
|
|
26
|
+
"""
|
|
27
|
+
from psychopy.preferences import prefs
|
|
28
|
+
from psychopy.experiment.components.buttonBox import KeyboardButtonBoxDeviceBackend
|
|
29
|
+
from psychopy.hardware import DeviceManager
|
|
30
|
+
|
|
31
|
+
for profile in DeviceManager.getAvailableDevices("psychopy.hardware.button.KeyboardButtonBox"):
|
|
32
|
+
device = KeyboardButtonBoxDeviceBackend(profile)
|
|
33
|
+
device.params['deviceLabel'].val = "testButtonBox"
|
|
34
|
+
prefs.devices['testButtonBox'] = device
|
|
35
|
+
break
|
|
21
36
|
|
|
22
37
|
def test_values(self):
|
|
23
38
|
"""
|
|
@@ -60,6 +75,8 @@ class TestButtonBoxComponent(BaseComponentTests):
|
|
|
60
75
|
cases.append(thisCase)
|
|
61
76
|
# make minimal experiment just for this test
|
|
62
77
|
comp, rt, exp = self.make_minimal_experiment()
|
|
78
|
+
# link to device
|
|
79
|
+
comp.params['deviceLabel'].val = "testButtonBox"
|
|
63
80
|
# configure experiment
|
|
64
81
|
exp.requireImport("ButtonResponse", importFrom="psychopy.hardware.button")
|
|
65
82
|
exp.settings.params['Full-screen window'].val = False
|
|
@@ -114,10 +131,13 @@ class TestButtonBoxComponent(BaseComponentTests):
|
|
|
114
131
|
check=True,
|
|
115
132
|
)
|
|
116
133
|
except subprocess.CalledProcessError as err:
|
|
134
|
+
# save experiment file to fails folder
|
|
135
|
+
failScript = Path(utils.TESTS_FAILS_PATH) / tmpPy.name
|
|
136
|
+
failScript.write_text(script, encoding="utf-8")
|
|
117
137
|
# if we get any errors, check their line number against error ranges
|
|
118
138
|
matches = re.findall(
|
|
119
139
|
pattern=r"testButtonBox.py\", line (\d*),",
|
|
120
|
-
string=err.stderr
|
|
140
|
+
string=err.stderr.decode("utf-8")
|
|
121
141
|
)
|
|
122
142
|
# if no matches, raise error as is
|
|
123
143
|
if not matches:
|
|
@@ -136,7 +156,7 @@ class TestButtonBoxComponent(BaseComponentTests):
|
|
|
136
156
|
f"Error in Routine with following params:\n"
|
|
137
157
|
f"{lastCase}\n"
|
|
138
158
|
f"Original traceback:\n"
|
|
139
|
-
f"{err.
|
|
159
|
+
f"{err.stderr.decode('utf-8')}"
|
|
140
160
|
)
|
|
141
161
|
raise ValueError(msg)
|
|
142
162
|
|
|
@@ -125,17 +125,5 @@ def test_findPhotometer():
|
|
|
125
125
|
assert (hw.findPhotometer(device=[]) is None)
|
|
126
126
|
# even when both are empty
|
|
127
127
|
assert (hw.findPhotometer(device=[],ports=[]) is None)
|
|
128
|
-
|
|
129
128
|
# non-existent photometers return None, for now
|
|
130
129
|
assert (hw.findPhotometer(device="thisIsNotAPhotometer!") is None)
|
|
131
|
-
|
|
132
|
-
# if the photometer raises an exception don't crash, return None
|
|
133
|
-
assert (hw.findPhotometer(device=[_exceptionRaisingPhotometer],ports="foobar") is None)
|
|
134
|
-
|
|
135
|
-
# specifying a photometer should work
|
|
136
|
-
assert hw.findPhotometer(device=[_workingPhotometer],ports="foobar") == _MockPhotometer
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
# one broken, one working
|
|
140
|
-
device = [_exceptionRaisingPhotometer,_workingPhotometer]
|
|
141
|
-
assert hw.findPhotometer(device=device,ports="foobar") == _MockPhotometer
|
|
@@ -30,7 +30,7 @@ def test_get_variables():
|
|
|
30
30
|
{"code": "x=\"(1, 2)\"\ny=\"(3, 4)\"", "ans": {'x': "(1, 2)", 'y': "(3, 4)"}}, # string representation of array (double)
|
|
31
31
|
]
|
|
32
32
|
for case in exemplars + tykes:
|
|
33
|
-
assert tools.
|
|
33
|
+
assert tools.getVariableDefs(case['code']) == case['ans']
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
@pytest.mark.stringtools
|
psychopy/tools/attributetools.py
CHANGED
|
@@ -58,12 +58,19 @@ class attributeSetter:
|
|
|
58
58
|
"""Makes functions appear as attributes. Takes care of autologging.
|
|
59
59
|
"""
|
|
60
60
|
|
|
61
|
-
def __init__(self, func
|
|
61
|
+
def __init__(self, func):
|
|
62
62
|
self.func = func
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
self.__doc__ = func.__doc__
|
|
64
|
+
|
|
65
|
+
def __set_name__(self, owner: type, name: str):
|
|
66
|
+
# if we already have docs, no further action needed
|
|
67
|
+
if self.__doc__ is not None:
|
|
68
|
+
return
|
|
69
|
+
# inherit docs from first base class which has any for this method
|
|
70
|
+
for base in owner.__bases__:
|
|
71
|
+
if hasattr(base, name) and getattr(base, name).__doc__ is not None:
|
|
72
|
+
self.__doc__ = getattr(base, name).__doc__
|
|
73
|
+
break
|
|
67
74
|
|
|
68
75
|
def __set__(self, obj, value):
|
|
69
76
|
newValue = self.func(obj, value)
|
psychopy/tools/fontmanager.py
CHANGED
|
@@ -641,13 +641,17 @@ class TextureGlyph:
|
|
|
641
641
|
return 0
|
|
642
642
|
|
|
643
643
|
|
|
644
|
-
def findFontFiles(folders=(), recursive=True):
|
|
644
|
+
def findFontFiles(folders=(), recursive=True, currentDir=Path(".")):
|
|
645
645
|
"""Search for font files in the folder (or system folders)
|
|
646
646
|
|
|
647
647
|
Parameters
|
|
648
648
|
----------
|
|
649
649
|
folders: iterable
|
|
650
650
|
folders to search. If empty then search typical system folders
|
|
651
|
+
recursive : bool
|
|
652
|
+
If True, recursively search within subfolders
|
|
653
|
+
currentDir : pathlib.Path
|
|
654
|
+
Path to use as current directory when making paths absolute
|
|
651
655
|
|
|
652
656
|
Returns
|
|
653
657
|
-------
|
|
@@ -665,14 +669,13 @@ def findFontFiles(folders=(), recursive=True):
|
|
|
665
669
|
# make sure font paths is a list
|
|
666
670
|
if isinstance(searchPaths, tuple):
|
|
667
671
|
searchPaths = list(searchPaths)
|
|
668
|
-
#
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
searchPaths.append(thisDir)
|
|
672
|
+
# if local directory has a /fonts or /assets subdirectory, search those
|
|
673
|
+
for localSubdir in (
|
|
674
|
+
currentDir / "fonts",
|
|
675
|
+
currentDir / "assets" / "fonts"
|
|
676
|
+
):
|
|
677
|
+
if localSubdir.is_dir():
|
|
678
|
+
searchPaths.append(localSubdir.absolute())
|
|
676
679
|
# always look inside the app
|
|
677
680
|
searchPaths.append(Path(prefs.paths['assets']) / "fonts")
|
|
678
681
|
# always look in the user folder
|
|
@@ -797,14 +800,14 @@ class FontManager():
|
|
|
797
800
|
return self.fontStyles
|
|
798
801
|
|
|
799
802
|
def getFontsMatching(self, fontName, bold=False, italic=False,
|
|
800
|
-
fontStyle=None, fallback=True):
|
|
803
|
+
fontStyle=None, fallback=True, currentDir=Path(".")):
|
|
801
804
|
"""
|
|
802
805
|
Returns the list of FontInfo instances that match the provided
|
|
803
806
|
fontName and style information. If no matching fonts are
|
|
804
807
|
found, None is returned.
|
|
805
808
|
"""
|
|
806
|
-
if not self._fontInfos:
|
|
807
|
-
self.updateFontInfo()
|
|
809
|
+
if not self._fontInfos or currentDir != Path("."):
|
|
810
|
+
self.updateFontInfo(currentDir=currentDir)
|
|
808
811
|
if type(fontName) != bytes:
|
|
809
812
|
fontName = bytes(fontName, sys.getfilesystemencoding())
|
|
810
813
|
# Convert value of "bold" to a numeric font weight
|
|
@@ -971,10 +974,10 @@ class FontManager():
|
|
|
971
974
|
|
|
972
975
|
return glFont
|
|
973
976
|
|
|
974
|
-
def updateFontInfo(self, monospaceOnly=False):
|
|
977
|
+
def updateFontInfo(self, monospaceOnly=False, currentDir=Path(".")):
|
|
975
978
|
self._fontInfos.clear()
|
|
976
979
|
del self.fontStyles[:]
|
|
977
|
-
fonts_found = findFontFiles()
|
|
980
|
+
fonts_found = findFontFiles(currentDir=currentDir)
|
|
978
981
|
self.addFontFiles(fonts_found, monospaceOnly)
|
|
979
982
|
|
|
980
983
|
def booleansFromStyleName(self, style):
|
psychopy/tools/gltools.py
CHANGED
|
@@ -121,7 +121,6 @@ __all__ = [
|
|
|
121
121
|
'createTexImage2D',
|
|
122
122
|
'createTexImage2dFromFile',
|
|
123
123
|
'bindTexture',
|
|
124
|
-
'unbindTexture',
|
|
125
124
|
'createCubeMap',
|
|
126
125
|
'TexCubeMap',
|
|
127
126
|
'getModelViewMatrix',
|
|
@@ -405,7 +404,10 @@ def getOpenGLInfo():
|
|
|
405
404
|
|
|
406
405
|
|
|
407
406
|
# OpenGL limits for this system
|
|
408
|
-
|
|
407
|
+
try:
|
|
408
|
+
MAX_TEXTURE_UNITS = getOpenGLInfo().maxTextureUnits
|
|
409
|
+
except:
|
|
410
|
+
MAX_TEXTURE_UNITS = 32
|
|
409
411
|
|
|
410
412
|
|
|
411
413
|
# -------------------------------
|
psychopy/tools/movietools.py
CHANGED
|
@@ -11,7 +11,9 @@ __all__ = [
|
|
|
11
11
|
'MovieFileWriter',
|
|
12
12
|
'closeAllMovieWriters',
|
|
13
13
|
'addAudioToMovie',
|
|
14
|
+
'MOVIE_READER_FFPYPLAYER',
|
|
14
15
|
'MOVIE_WRITER_FFPYPLAYER',
|
|
16
|
+
'MOVIE_READER_OPENCV',
|
|
15
17
|
'MOVIE_WRITER_OPENCV',
|
|
16
18
|
'MOVIE_WRITER_NULL',
|
|
17
19
|
'VIDEO_RESOLUTIONS'
|
|
@@ -24,10 +26,11 @@ import queue
|
|
|
24
26
|
import atexit
|
|
25
27
|
import numpy as np
|
|
26
28
|
import psychopy.logging as logging
|
|
29
|
+
import sys
|
|
27
30
|
|
|
28
31
|
# constants for specifying encoders for the movie writer
|
|
29
|
-
MOVIE_WRITER_FFPYPLAYER = u'ffpyplayer'
|
|
30
|
-
MOVIE_WRITER_OPENCV = u'opencv'
|
|
32
|
+
MOVIE_WRITER_FFPYPLAYER = MOVIE_READER_FFPYPLAYER = u'ffpyplayer'
|
|
33
|
+
MOVIE_WRITER_OPENCV = MOVIE_READER_OPENCV = u'opencv'
|
|
31
34
|
MOVIE_WRITER_NULL = u'null' # use prefs for default
|
|
32
35
|
|
|
33
36
|
# Common video resolutions in pixels (width, height). Users should be able to
|
|
@@ -1066,5 +1069,43 @@ def addAudioToMovie(outputFile, videoFile, audioFile, useThreads=True,
|
|
|
1066
1069
|
compositorThread.start()
|
|
1067
1070
|
|
|
1068
1071
|
|
|
1072
|
+
def extractAudioFromMovie(videoFile, audioFile, removeFiles=False):
|
|
1073
|
+
"""Extract the audio track from a video file.
|
|
1074
|
+
|
|
1075
|
+
This function will extract the audio track from a video file and save it to
|
|
1076
|
+
a separate audio file. The audio file will be saved in the same format as
|
|
1077
|
+
the audio track in the video file.
|
|
1078
|
+
|
|
1079
|
+
Parameters
|
|
1080
|
+
----------
|
|
1081
|
+
videoFile : str
|
|
1082
|
+
Path to the input video file.
|
|
1083
|
+
audioFile : str
|
|
1084
|
+
Path to the output audio file where the audio track will be saved.
|
|
1085
|
+
removeFiles : bool
|
|
1086
|
+
If `True`, the input video file (`videoFile`) will be removed (i.e.
|
|
1087
|
+
deleted from disk) after the audio has been extracted. Defaults to
|
|
1088
|
+
`False`.
|
|
1089
|
+
|
|
1090
|
+
Examples
|
|
1091
|
+
--------
|
|
1092
|
+
Extract the audio track from a video file::
|
|
1093
|
+
|
|
1094
|
+
from psychopy.tools.movietools import extractAudioFromMovie
|
|
1095
|
+
extractAudioFromMovie('video.mp4', 'audio.mp3')
|
|
1096
|
+
|
|
1097
|
+
"""
|
|
1098
|
+
from moviepy.video.io.VideoFileClip import VideoFileClip
|
|
1099
|
+
|
|
1100
|
+
# extract the audio track from the video file
|
|
1101
|
+
videoClip = VideoFileClip(videoFile)
|
|
1102
|
+
audioClip = videoClip.audio
|
|
1103
|
+
audioClip.write_audiofile(audioFile)
|
|
1104
|
+
|
|
1105
|
+
if removeFiles:
|
|
1106
|
+
# remove the input video file
|
|
1107
|
+
os.remove(videoFile)
|
|
1108
|
+
|
|
1109
|
+
|
|
1069
1110
|
if __name__ == "__main__":
|
|
1070
1111
|
pass
|
psychopy/tools/stringtools.py
CHANGED
|
@@ -393,26 +393,51 @@ def _actualizeAstValue(item):
|
|
|
393
393
|
return tuple(_actualizeAstValue(i) for i in item.elts)
|
|
394
394
|
|
|
395
395
|
|
|
396
|
-
def
|
|
396
|
+
def getVariableDefs(code):
|
|
397
397
|
"""
|
|
398
|
-
|
|
399
|
-
|
|
398
|
+
Returns a dict of variables defined in the given code, and their values.
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
code : str
|
|
403
|
+
Code to parse for variable defs
|
|
400
404
|
"""
|
|
401
|
-
assert isinstance(code, str), "First input to `
|
|
402
|
-
#
|
|
405
|
+
assert isinstance(code, str), "First input to `getVariableDefs()` must be a string"
|
|
406
|
+
# make blank output dict
|
|
403
407
|
vars = {}
|
|
404
|
-
#
|
|
408
|
+
# construct tree
|
|
405
409
|
tree = compile(code, '', 'exec', flags=ast.PyCF_ONLY_AST)
|
|
406
|
-
#
|
|
410
|
+
# iterate through each node
|
|
407
411
|
for line in tree.body:
|
|
408
412
|
if hasattr(line, "targets") and hasattr(line, "value"):
|
|
409
|
-
#
|
|
413
|
+
# append targets and values this line to arguments dict
|
|
410
414
|
for target in line.targets:
|
|
411
415
|
if hasattr(target, "id"):
|
|
412
416
|
vars[target.id] = _actualizeAstValue(line.value)
|
|
413
417
|
|
|
414
418
|
return vars
|
|
415
419
|
|
|
420
|
+
def getVariables(code):
|
|
421
|
+
"""
|
|
422
|
+
Returns a list of variables referenced in the given code.
|
|
423
|
+
|
|
424
|
+
Parameters
|
|
425
|
+
----------
|
|
426
|
+
code : str
|
|
427
|
+
Code to parse for variables
|
|
428
|
+
"""
|
|
429
|
+
assert isinstance(code, str), "First input to `getVariables()` must be a string"
|
|
430
|
+
# make blank output list
|
|
431
|
+
vars = set()
|
|
432
|
+
# construct tree
|
|
433
|
+
tree = compile(code, '', 'exec', flags=ast.PyCF_ONLY_AST)
|
|
434
|
+
# iterate through each node
|
|
435
|
+
for node in ast.walk(tree):
|
|
436
|
+
if isinstance(node, ast.Name):
|
|
437
|
+
vars.add(node.id)
|
|
438
|
+
|
|
439
|
+
return list(vars)
|
|
440
|
+
|
|
416
441
|
|
|
417
442
|
def getArgs(code):
|
|
418
443
|
"""
|
psychopy/tools/versionchooser.py
CHANGED
|
@@ -204,7 +204,7 @@ def getPsychoJSVersionStr(currentVersion, preferredVersion=''):
|
|
|
204
204
|
# e.g. 2021.1.0 not 2021.1.0.dev3
|
|
205
205
|
useVerStr = '.'.join(useVerStr.split('.')[:3])
|
|
206
206
|
# PsychoJS doesn't have additional rc1 or dev1 releases
|
|
207
|
-
for versionSuffix in ["rc", "dev", "post", "a", "b"]:
|
|
207
|
+
for versionSuffix in ["rc", "dev", "post", "a", "b", "beta"]:
|
|
208
208
|
if versionSuffix in useVerStr:
|
|
209
209
|
useVerStr = useVerStr.split(versionSuffix)[0]
|
|
210
210
|
|
psychopy/validation/audio.py
CHANGED
|
@@ -14,7 +14,11 @@ class AudioValidator:
|
|
|
14
14
|
# set autolog
|
|
15
15
|
self.autoLog = autoLog
|
|
16
16
|
# store voicekey handle
|
|
17
|
-
|
|
17
|
+
from psychopy.hardware import DeviceManager, soundsensor
|
|
18
|
+
self.sensor = DeviceManager.resolveDevice(
|
|
19
|
+
sensor,
|
|
20
|
+
deviceClass=soundsensor.BaseSoundSensorGroup
|
|
21
|
+
)
|
|
18
22
|
self.channel = channel
|
|
19
23
|
# initial values (written during experiment)
|
|
20
24
|
self.tStart = self.tStartRefresh = self.tStartDelay = None
|
psychopy/validation/visual.py
CHANGED
|
@@ -15,7 +15,11 @@ class VisualValidator:
|
|
|
15
15
|
# store window handle
|
|
16
16
|
self.win = win
|
|
17
17
|
# store sensor handle
|
|
18
|
-
|
|
18
|
+
from psychopy.hardware import DeviceManager, lightsensor
|
|
19
|
+
self.sensor = DeviceManager.resolveDevice(
|
|
20
|
+
sensor,
|
|
21
|
+
deviceClass=lightsensor.BaseLightSensorGroup
|
|
22
|
+
)
|
|
19
23
|
self.channel = channel
|
|
20
24
|
# initial values (written during experiment)
|
|
21
25
|
self.tStart = self.tStartRefresh = self.tStartDelay = None
|
psychopy/visual/basevisual.py
CHANGED
|
@@ -1065,16 +1065,17 @@ class TextureMixin:
|
|
|
1065
1065
|
try:
|
|
1066
1066
|
im = Image.open(filename)
|
|
1067
1067
|
im = im.transpose(Image.FLIP_TOP_BOTTOM)
|
|
1068
|
-
except IOError:
|
|
1069
|
-
msg =
|
|
1070
|
-
|
|
1068
|
+
except IOError as err:
|
|
1069
|
+
msg = (
|
|
1070
|
+
"Found file '{}' ('{}'), but failed to load as an image. Reason: {}"
|
|
1071
|
+
).format(filename, os.path.abspath(tex), err)
|
|
1072
|
+
logging.error(msg)
|
|
1071
1073
|
logging.flush()
|
|
1072
|
-
msg
|
|
1073
|
-
|
|
1074
|
-
elif hasattr(tex, 'getVideoFrame'): # camera or movie textures
|
|
1074
|
+
raise IOError(msg)
|
|
1075
|
+
elif hasattr(tex, 'getRecentVideoFrame'): # camera or movie textures
|
|
1075
1076
|
# get an image to configure the initial texture store
|
|
1076
1077
|
if hasattr(tex, 'frameSize'):
|
|
1077
|
-
if tex.frameSize is None:
|
|
1078
|
+
if tex.frameSize is None or tex.frameSize == (-1, -1):
|
|
1078
1079
|
raise RuntimeError(
|
|
1079
1080
|
"`Camera.frameSize` is not yet specified, cannot "
|
|
1080
1081
|
"initialize texture!")
|
psychopy/visual/circle.py
CHANGED
|
@@ -118,12 +118,12 @@ class Circle(Polygon):
|
|
|
118
118
|
depth=0,
|
|
119
119
|
interpolate=True,
|
|
120
120
|
draggable=False,
|
|
121
|
-
lineRGB=False,
|
|
122
|
-
fillRGB=False,
|
|
123
121
|
name=None,
|
|
124
122
|
autoLog=None,
|
|
125
123
|
autoDraw=False,
|
|
126
124
|
# legacy
|
|
125
|
+
lineRGB=undefined,
|
|
126
|
+
fillRGB=undefined,
|
|
127
127
|
color=undefined,
|
|
128
128
|
fillColorSpace=undefined,
|
|
129
129
|
lineColorSpace=undefined,
|
psychopy/visual/helpers.py
CHANGED
|
@@ -216,7 +216,9 @@ def setColor(obj, color, colorSpace=None, operation='',
|
|
|
216
216
|
raw = color
|
|
217
217
|
color = colors.Color(raw, colorSpace)
|
|
218
218
|
assert color.valid, f"Could not create valid Color object from value {raw} in space {colorSpace}"
|
|
219
|
-
|
|
219
|
+
# set opacity from object if not given by color
|
|
220
|
+
if hasattr(obj, "opacity") and not hasattr(color, "_alpha"):
|
|
221
|
+
color.alpha = obj.opacity
|
|
220
222
|
# Apply new value
|
|
221
223
|
if operation in ('=', '', None):
|
|
222
224
|
# If no operation, just set color from object
|