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.

Files changed (226) hide show
  1. psychopy/VERSION +1 -1
  2. psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
  3. psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
  4. psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
  5. psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
  6. psychopy/app/Resources/classic/add_many.png +0 -0
  7. psychopy/app/Resources/classic/add_many@2x.png +0 -0
  8. psychopy/app/Resources/classic/devices.png +0 -0
  9. psychopy/app/Resources/classic/devices@2x.png +0 -0
  10. psychopy/app/Resources/classic/photometer.png +0 -0
  11. psychopy/app/Resources/classic/photometer@2x.png +0 -0
  12. psychopy/app/Resources/dark/add_many.png +0 -0
  13. psychopy/app/Resources/dark/add_many@2x.png +0 -0
  14. psychopy/app/Resources/dark/devices.png +0 -0
  15. psychopy/app/Resources/dark/devices@2x.png +0 -0
  16. psychopy/app/Resources/dark/photometer.png +0 -0
  17. psychopy/app/Resources/dark/photometer@2x.png +0 -0
  18. psychopy/app/Resources/light/add_many.png +0 -0
  19. psychopy/app/Resources/light/add_many@2x.png +0 -0
  20. psychopy/app/Resources/light/devices.png +0 -0
  21. psychopy/app/Resources/light/devices@2x.png +0 -0
  22. psychopy/app/Resources/light/photometer.png +0 -0
  23. psychopy/app/Resources/light/photometer@2x.png +0 -0
  24. psychopy/app/_psychopyApp.py +35 -13
  25. psychopy/app/builder/builder.py +88 -35
  26. psychopy/app/builder/dialogs/__init__.py +69 -220
  27. psychopy/app/builder/dialogs/dlgsCode.py +29 -8
  28. psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
  29. psychopy/app/builder/validators.py +25 -17
  30. psychopy/app/coder/coder.py +12 -1
  31. psychopy/app/coder/repl.py +5 -2
  32. psychopy/app/colorpicker/__init__.py +1 -1
  33. psychopy/app/deviceManager/__init__.py +1 -0
  34. psychopy/app/deviceManager/addDialog.py +218 -0
  35. psychopy/app/deviceManager/dialog.py +185 -0
  36. psychopy/app/deviceManager/panel.py +191 -0
  37. psychopy/app/deviceManager/utils.py +60 -0
  38. psychopy/app/idle.py +7 -0
  39. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  40. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
  41. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  42. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
  43. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  44. psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
  45. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  46. psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
  47. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  48. psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
  49. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  50. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
  51. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  52. psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
  53. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  54. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
  55. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  56. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
  57. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  58. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
  59. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  60. psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
  61. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  62. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
  63. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  64. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
  65. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  66. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
  67. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  68. psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
  69. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  70. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
  71. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  72. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
  73. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  74. psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
  75. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  76. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
  77. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  78. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
  79. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  80. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
  81. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  82. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
  83. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  84. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
  85. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  86. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
  87. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  88. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
  89. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  90. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
  91. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  92. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
  93. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  94. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
  95. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  96. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
  97. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  98. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
  99. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  100. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
  101. psychopy/app/plugin_manager/dialog.py +12 -3
  102. psychopy/app/plugin_manager/packageIndex.py +303 -0
  103. psychopy/app/plugin_manager/packages.py +203 -63
  104. psychopy/app/plugin_manager/plugins.py +120 -240
  105. psychopy/app/preferencesDlg.py +6 -1
  106. psychopy/app/psychopyApp.py +16 -4
  107. psychopy/app/runner/runner.py +10 -2
  108. psychopy/app/runner/scriptProcess.py +8 -3
  109. psychopy/app/stdout/stdOutRich.py +11 -4
  110. psychopy/app/themes/icons.py +3 -0
  111. psychopy/app/utils.py +61 -0
  112. psychopy/colors.py +10 -5
  113. psychopy/data/experiment.py +133 -23
  114. psychopy/data/routine.py +12 -0
  115. psychopy/data/staircase.py +42 -20
  116. psychopy/data/trial.py +20 -12
  117. psychopy/data/utils.py +43 -3
  118. psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
  119. psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
  120. psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
  121. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
  122. psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
  123. psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
  124. psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
  125. psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
  126. psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
  127. psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
  128. psychopy/event.py +20 -15
  129. psychopy/experiment/_experiment.py +86 -10
  130. psychopy/experiment/components/__init__.py +3 -10
  131. psychopy/experiment/components/_base.py +9 -20
  132. psychopy/experiment/components/button/__init__.py +1 -1
  133. psychopy/experiment/components/buttonBox/__init__.py +50 -54
  134. psychopy/experiment/components/camera/__init__.py +137 -359
  135. psychopy/experiment/components/keyboard/__init__.py +17 -24
  136. psychopy/experiment/components/microphone/__init__.py +61 -110
  137. psychopy/experiment/components/movie/__init__.py +2 -3
  138. psychopy/experiment/components/serialOut/__init__.py +192 -93
  139. psychopy/experiment/components/settings/__init__.py +45 -27
  140. psychopy/experiment/components/sound/__init__.py +82 -73
  141. psychopy/experiment/components/soundsensor/__init__.py +43 -80
  142. psychopy/experiment/devices.py +303 -0
  143. psychopy/experiment/exports.py +20 -18
  144. psychopy/experiment/flow.py +7 -0
  145. psychopy/experiment/loops.py +47 -29
  146. psychopy/experiment/monitor.py +74 -0
  147. psychopy/experiment/params.py +48 -10
  148. psychopy/experiment/plugins.py +28 -108
  149. psychopy/experiment/py2js_transpiler.py +1 -1
  150. psychopy/experiment/routines/__init__.py +1 -1
  151. psychopy/experiment/routines/_base.py +59 -24
  152. psychopy/experiment/routines/audioValidator/__init__.py +19 -155
  153. psychopy/experiment/routines/visualValidator/__init__.py +25 -25
  154. psychopy/hardware/__init__.py +20 -57
  155. psychopy/hardware/button.py +15 -2
  156. psychopy/hardware/camera/__init__.py +2237 -1394
  157. psychopy/hardware/joystick/__init__.py +1 -1
  158. psychopy/hardware/keyboard.py +5 -8
  159. psychopy/hardware/listener.py +4 -1
  160. psychopy/hardware/manager.py +75 -35
  161. psychopy/hardware/microphone.py +53 -7
  162. psychopy/hardware/monitor.py +144 -0
  163. psychopy/hardware/photometer/__init__.py +156 -117
  164. psychopy/hardware/serialdevice.py +16 -2
  165. psychopy/hardware/soundsensor.py +4 -1
  166. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  167. psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +2 -2
  168. psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/__init__.py +1 -0
  169. psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/eyetracker.py +10 -0
  170. psychopy/iohub/devices/keyboard/darwin.py +8 -5
  171. psychopy/iohub/util/__init__.py +7 -8
  172. psychopy/localization/generateTranslationTemplate.py +208 -116
  173. psychopy/localization/messages.pot +4305 -3502
  174. psychopy/monitors/MonitorCenter.py +174 -74
  175. psychopy/plugins/__init__.py +6 -4
  176. psychopy/preferences/devices.py +80 -0
  177. psychopy/preferences/generateHints.py +2 -1
  178. psychopy/preferences/preferences.py +35 -11
  179. psychopy/scripts/psychopy-pkgutil.py +969 -0
  180. psychopy/scripts/psyexpCompile.py +1 -1
  181. psychopy/session.py +34 -38
  182. psychopy/sound/__init__.py +6 -260
  183. psychopy/sound/audioclip.py +164 -0
  184. psychopy/sound/backend_ptb.py +8 -0
  185. psychopy/sound/backend_pygame.py +10 -0
  186. psychopy/sound/backend_pysound.py +9 -0
  187. psychopy/sound/backends/__init__.py +0 -0
  188. psychopy/sound/microphone.py +3 -0
  189. psychopy/sound/sound.py +58 -0
  190. psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
  191. psychopy/tests/data/duplicateHeaders.csv +2 -0
  192. psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
  193. psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
  194. psychopy/tests/test_data/test_utils.py +5 -1
  195. psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
  196. psychopy/tests/test_hardware/test_ports.py +0 -12
  197. psychopy/tests/test_tools/test_stringtools.py +1 -1
  198. psychopy/tools/attributetools.py +12 -5
  199. psychopy/tools/fontmanager.py +17 -14
  200. psychopy/tools/gltools.py +4 -2
  201. psychopy/tools/movietools.py +43 -2
  202. psychopy/tools/stringtools.py +33 -8
  203. psychopy/tools/versionchooser.py +1 -1
  204. psychopy/validation/audio.py +5 -1
  205. psychopy/validation/visual.py +5 -1
  206. psychopy/visual/basevisual.py +8 -7
  207. psychopy/visual/circle.py +2 -2
  208. psychopy/visual/helpers.py +3 -1
  209. psychopy/visual/image.py +29 -109
  210. psychopy/visual/movies/__init__.py +1800 -313
  211. psychopy/visual/polygon.py +4 -0
  212. psychopy/visual/shape.py +2 -2
  213. psychopy/visual/window.py +35 -12
  214. psychopy/voicekey/__init__.py +41 -669
  215. psychopy/voicekey/labjack_vks.py +7 -48
  216. psychopy/voicekey/parallel_vks.py +7 -42
  217. psychopy/voicekey/vk_tools.py +114 -263
  218. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/METADATA +20 -13
  219. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/RECORD +222 -190
  220. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
  221. psychopy/visual/movies/players/__init__.py +0 -62
  222. psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
  223. psychopy/voicekey/demo_vks.py +0 -12
  224. psychopy/voicekey/signal.py +0 -42
  225. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
  226. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -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 = frameN + 1 # number of completed frames (so 0 is the first frame)
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
@@ -0,0 +1,2 @@
1
+ dupe;dupe;unique1;unique2
2
+ red;blue;green;green
@@ -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 `{fieldName}`: {param.val}"}, # Make sure it's picking up clearly broken code
126
- {'fieldName': "variableDef", 'param': Param(val="visual = 1", valType="code"), 'msg': "Variable name $visual is in use (by Psychopy module). Try another name."},
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'].format(**case) in dlg.warnings.messages, (
145
- "Error for param {fieldName} with value `{val}` should include:\n"
146
- "'{msg}'\n"
147
- "but instead was:\n"
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(**case, val=case['param'].val.format(**case), actual=dlg.warnings.messages)
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.stdout}"
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.getVariables(case['code']) == case['ans']
33
+ assert tools.getVariableDefs(case['code']) == case['ans']
34
34
 
35
35
 
36
36
  @pytest.mark.stringtools
@@ -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, doc=None):
61
+ def __init__(self, func):
62
62
  self.func = func
63
- if doc is not None:
64
- self.__doc__ = doc
65
- else:
66
- self.__doc__ = func.__doc__
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)
@@ -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
- # always look in the local directory, unless it's inside PsychoPy itself
669
- thisDir = Path(".").absolute()
670
- if all((
671
- thisDir not in Path(prefs.paths['psychopy']).parents,
672
- thisDir != Path(prefs.paths['psychopy']),
673
- Path(prefs.paths['psychopy']) not in thisDir.parents,
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
- MAX_TEXTURE_UNITS = getOpenGLInfo().maxTextureUnits
407
+ try:
408
+ MAX_TEXTURE_UNITS = getOpenGLInfo().maxTextureUnits
409
+ except:
410
+ MAX_TEXTURE_UNITS = 32
409
411
 
410
412
 
411
413
  # -------------------------------
@@ -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
@@ -393,26 +393,51 @@ def _actualizeAstValue(item):
393
393
  return tuple(_actualizeAstValue(i) for i in item.elts)
394
394
 
395
395
 
396
- def getVariables(code):
396
+ def getVariableDefs(code):
397
397
  """
398
- Use AST tree parsing to convert a string of valid Python code to a dict containing each variable created and its
399
- value.
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 `getArgs()` must be a string"
402
- # Make blank output dict
405
+ assert isinstance(code, str), "First input to `getVariableDefs()` must be a string"
406
+ # make blank output dict
403
407
  vars = {}
404
- # Construct tree
408
+ # construct tree
405
409
  tree = compile(code, '', 'exec', flags=ast.PyCF_ONLY_AST)
406
- # Iterate through each line
410
+ # iterate through each node
407
411
  for line in tree.body:
408
412
  if hasattr(line, "targets") and hasattr(line, "value"):
409
- # Append targets and values this line to arguments dict
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
  """
@@ -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
 
@@ -14,7 +14,11 @@ class AudioValidator:
14
14
  # set autolog
15
15
  self.autoLog = autoLog
16
16
  # store voicekey handle
17
- self.sensor = sensor
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
@@ -15,7 +15,11 @@ class VisualValidator:
15
15
  # store window handle
16
16
  self.win = win
17
17
  # store sensor handle
18
- self.sensor = sensor
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
@@ -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 = "Found file '%s', failed to load as an image"
1070
- logging.error(msg % (filename))
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 = "Found file '%s' [= %s], failed to load as an image"
1073
- raise IOError(msg % (tex, os.path.abspath(tex)))
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,
@@ -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