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
@@ -107,7 +107,11 @@ class StdOutRich(wx.richtext.RichTextCtrl, _BaseErrorHandler, handlers.ThemeMixi
107
107
  }
108
108
 
109
109
  def onURL(self, evt=None):
110
- wx.BeginBusyCursor()
110
+ """Open link in default browser."""
111
+
112
+ if wx.Platform != '__WXGTK__':
113
+ wx.BeginBusyCursor()
114
+
111
115
  try:
112
116
  if evt.String.startswith("http"):
113
117
  webbrowser.open(evt.String)
@@ -121,7 +125,8 @@ class StdOutRich(wx.richtext.RichTextCtrl, _BaseErrorHandler, handlers.ThemeMixi
121
125
  except Exception as e:
122
126
  print("##### Could not open URL: {} #####\n".format(evt.String))
123
127
  print(e)
124
- wx.EndBusyCursor()
128
+ if wx.Platform != '__WXGTK__':
129
+ wx.EndBusyCursor()
125
130
 
126
131
  def write(self, inStr, evt=None):
127
132
  self.MoveEnd() # always 'append' text rather than 'writing' it
@@ -357,7 +362,8 @@ class ScriptOutputCtrl(StdOutRich, handlers.ThemeMixin):
357
362
 
358
363
  def onURL(self, evt):
359
364
  """Open link in default browser."""
360
- wx.BeginBusyCursor()
365
+ if wx.Platform != '__WXGTK__':
366
+ wx.BeginBusyCursor()
361
367
  try:
362
368
  if evt.String.startswith("http"):
363
369
  webbrowser.open(evt.String)
@@ -375,7 +381,8 @@ class ScriptOutputCtrl(StdOutRich, handlers.ThemeMixin):
375
381
  except Exception as e:
376
382
  print("##### Could not open URL: {} #####\n".format(evt.String))
377
383
  print(e)
378
- wx.EndBusyCursor()
384
+ if wx.Platform != '__WXGTK__':
385
+ wx.EndBusyCursor()
379
386
 
380
387
  def clear(self, evt=None):
381
388
  self.Clear()
@@ -125,6 +125,9 @@ class BaseIcon:
125
125
  # If size is None, return bitmap as is
126
126
  if size in (None, (None, None)):
127
127
  return bmp
128
+ # if given a single value, duplicate it
129
+ if isinstance(size, (int, float)):
130
+ size = (size, size)
128
131
  # Split up size value
129
132
  width, height = size
130
133
  # If size is unchanged, return bitmap as is
psychopy/app/utils.py CHANGED
@@ -1665,6 +1665,67 @@ class ListCtrl(wx.ListCtrl, listmixin.ListCtrlAutoWidthMixin):
1665
1665
  listmixin.ListCtrlAutoWidthMixin.__init__(self)
1666
1666
 
1667
1667
 
1668
+ class ShowHideBtn(wx.ToggleButton):
1669
+ """
1670
+ Button which shows/hides a panel by toggling itself.
1671
+ """
1672
+ indicators = {
1673
+ True: "▾",
1674
+ False: "▸"
1675
+ }
1676
+ def __init__(
1677
+ self,
1678
+ parent,
1679
+ target,
1680
+ label=""
1681
+ ):
1682
+ wx.ToggleButton.__init__(self)
1683
+ self.SetBackgroundStyle(wx.BG_STYLE_TRANSPARENT)
1684
+ self.Create(
1685
+ parent,
1686
+ label=label,
1687
+ style=wx.BU_LEFT | wx.BORDER_NONE
1688
+ )
1689
+ # for some reason, wx won't hide the background unless you set the foreground
1690
+ self.SetForegroundColour("black")
1691
+ # store root label
1692
+ self.rootLabel = label
1693
+ # store target
1694
+ self.target = target
1695
+ # bind toggle
1696
+ self.Bind(
1697
+ wx.EVT_TOGGLEBUTTON, self.onToggle
1698
+ )
1699
+
1700
+ def setValue(self, value):
1701
+ self.onToggle(value)
1702
+
1703
+ def toggle(self):
1704
+ self.setValue(not self.GetValue())
1705
+
1706
+ def onToggle(self, evt):
1707
+ # get value
1708
+ if isinstance(evt, bool):
1709
+ val = evt
1710
+ else:
1711
+ val = evt.Int
1712
+ # show/hide target
1713
+ if isinstance(self.target, wx.Sizer):
1714
+ self.target.ShowItems(val)
1715
+ else:
1716
+ self.target.Show(val)
1717
+ # toggle indicator
1718
+ indicator = self.indicators[val]
1719
+ self.SetLabel(f"{indicator} {self.rootLabel}")
1720
+ # layout parent
1721
+ self.GetParent().Layout()
1722
+ if hasattr(self.GetParent(), "SetupScrolling"):
1723
+ self.GetParent().SetupScrolling()
1724
+ # toggle as normal
1725
+ if hasattr(evt, "Skip"):
1726
+ evt.Skip()
1727
+
1728
+
1668
1729
  def sanitize(inStr):
1669
1730
  """
1670
1731
  Process a string to remove any sensitive information, i.e. OAUTH keys
psychopy/colors.py CHANGED
@@ -261,7 +261,6 @@ class Color:
261
261
  self._cache = {}
262
262
  self._renderCache = {}
263
263
  self.contrast = contrast if isinstance(contrast, (int, float)) else 1
264
- self.alpha = 1
265
264
  self.valid = False
266
265
  self.conematrix = conematrix
267
266
 
@@ -398,7 +397,7 @@ class Color:
398
397
  """If colour is printed, it will display its class and value.
399
398
  """
400
399
  if self.valid:
401
- if self.named:
400
+ if isinstance(self.named, str):
402
401
  return (f"<{self.__class__.__module__}."
403
402
  f"{self.__class__.__name__}: {self.named}, "
404
403
  f"alpha={self.alpha}>")
@@ -537,12 +536,18 @@ class Color:
537
536
  """How opaque (1) or transparent (0) this color is. Synonymous with
538
537
  `opacity`.
539
538
  """
540
- return self._alpha
539
+ if hasattr(self, "_alpha"):
540
+ return self._alpha
541
+
542
+ return 1
541
543
 
542
544
  @alpha.setter
543
545
  def alpha(self, value):
544
- # Treat 1x1 arrays as a float
545
- if isinstance(value, np.ndarray):
546
+ if value is None:
547
+ # setting as None should do nothing
548
+ return
549
+ elif isinstance(value, np.ndarray):
550
+ # treat 1x1 arrays as a float
546
551
  if value.size == 1:
547
552
  value = float(value[0])
548
553
  else:
@@ -85,6 +85,7 @@ class ExperimentHandler(_ComparisonMixin):
85
85
  """
86
86
  self.loops = []
87
87
  self.loopsUnfinished = []
88
+ self.currentRoutine = None
88
89
  self.name = name
89
90
  self.version = version
90
91
  self.runtimeInfo = runtimeInfo
@@ -124,20 +125,44 @@ class ExperimentHandler(_ComparisonMixin):
124
125
 
125
126
  def __del__(self):
126
127
  self.close()
127
-
128
- @property
129
- def currentLoop(self):
128
+
129
+ def getCurrentLoop(self, isTrials=True):
130
130
  """
131
131
  Return the loop which we are currently in, this will either be a handle to a loop, such as
132
132
  a :class:`~psychopy.data.TrialHandler` or :class:`~psychopy.data.StairHandler`, or the handle
133
133
  of the :class:`~psychopy.data.ExperimentHandler` itself if we are not in a loop.
134
+
135
+ Parameters
136
+ ----------
137
+ isTrials : bool
138
+ Filter for only loops which have isTrials checked
134
139
  """
135
- # If there are unfinished (aka currently active) loops, return the most recent
136
140
  if len(self.loopsUnfinished):
137
- return self.loopsUnfinished[-1]
138
- # If we are not in a loop, return handle to experiment handler
141
+ # iterate through unfinished (aka active) loops, starting with the most recent
142
+ for loop in reversed(self.loopsUnfinished):
143
+ # if not filtering, just return the first one
144
+ if not isTrials:
145
+ return loop
146
+ # otherwise, return the first one which is a trials loop
147
+ if getattr(loop, 'isTrials', False):
148
+ return loop
149
+ # if we are not in a loop, return to experiment handler
139
150
  return self
140
151
 
152
+ @property
153
+ def currentLoop(self):
154
+ """
155
+ Calls `.getCurrentLoop` with `isTrials=False`
156
+ """
157
+ return self.getCurrentLoop(isTrials=False)
158
+
159
+ @property
160
+ def currentTrialsLoop(self):
161
+ """
162
+ Calls `.getCurrentLoop` with `isTrials=True`
163
+ """
164
+ return self.getCurrentLoop(isTrials=True)
165
+
141
166
  def addLoop(self, loopHandler):
142
167
  """Add a loop such as a :class:`~psychopy.data.TrialHandler`
143
168
  or :class:`~psychopy.data.StairHandler`
@@ -274,6 +299,12 @@ class ExperimentHandler(_ComparisonMixin):
274
299
  # get entry from row number
275
300
  entry = self.thisEntry
276
301
  if row is not None:
302
+ # if row exceeds size of entries, warn and abort
303
+ if row > len(self.entries):
304
+ logging.error(_translate(
305
+ "Cannot add data to row {} as there are only {} entries"
306
+ ).format(row, len(self.entries)))
307
+ # get entry from row
277
308
  entry = self.entries[row]
278
309
  entry[name] = value
279
310
 
@@ -466,8 +497,37 @@ class ExperimentHandler(_ComparisonMixin):
466
497
  ))
467
498
  # set own status
468
499
  self.status = constants.STOPPED
500
+
501
+ def next(self, isTrials=True):
502
+ """
503
+ Move on to either the next trial (if in a trials loop) or the next Routine.
504
+
505
+ Parameters
506
+ ----------
507
+ isTrials : bool
508
+ Filter for only loops which have isTrials checked
509
+ """
510
+ if isinstance(self.getCurrentLoop(isTrials=isTrials), TrialHandler2):
511
+ # if there is a loop, skip trials
512
+ self.skipTrials(1, isTrials=isTrials)
513
+ elif self.currentRoutine is not None:
514
+ # if not, but there is a Routine, end it
515
+ self.endCurrentRoutine()
516
+ else:
517
+ # otherwise, do nothing
518
+ return
519
+
520
+ def endCurrentRoutine(self):
521
+ """
522
+ End the current Routine (via the Routine.forceEnded attribute)
523
+ """
524
+ # if there's no current Routine yet, do nothing
525
+ if self.currentRoutine is None:
526
+ return
527
+ # force end the Routine
528
+ self.currentRoutine.forceEnded = True
469
529
 
470
- def skipTrials(self, n=1):
530
+ def skipTrials(self, n=1, isTrials=True):
471
531
  """
472
532
  Skip ahead n trials - the trials inbetween will be marked as "skipped". If you try to
473
533
  skip past the last trial, will log a warning and skip *to* the last trial.
@@ -476,14 +536,23 @@ class ExperimentHandler(_ComparisonMixin):
476
536
  ----------
477
537
  n : int
478
538
  Number of trials to skip ahead
539
+ isTrials : bool
540
+ Filter for only loops which have isTrials checked
479
541
  """
542
+ loop = self.getCurrentLoop(isTrials=isTrials)
480
543
  # return if there isn't a TrialHandler2 active
481
- if not isinstance(self.currentLoop, TrialHandler2):
544
+ if not isinstance(loop, TrialHandler2):
482
545
  return
546
+ # end inner loops
547
+ for innerLoop in self.loopsUnfinished[
548
+ self.loopsUnfinished.index(loop)+1:
549
+ ].copy():
550
+ innerLoop.finished = True
551
+ self.loopEnded(innerLoop)
483
552
  # skip trials in current loop
484
- return self.currentLoop.skipTrials(n)
553
+ return loop.skipTrials(n)
485
554
 
486
- def rewindTrials(self, n=1):
555
+ def rewindTrials(self, n=1, isTrials=True):
487
556
  """
488
557
  Skip ahead n trials - the trials inbetween will be marked as "skipped". If you try to
489
558
  skip past the last trial, will log a warning and skip *to* the last trial.
@@ -492,18 +561,34 @@ class ExperimentHandler(_ComparisonMixin):
492
561
  ----------
493
562
  n : int
494
563
  Number of trials to skip ahead
564
+ isTrials : bool
565
+ Filter for only loops which have isTrials checked
495
566
  """
567
+ loop = self.getCurrentLoop(isTrials=isTrials)
496
568
  # return if there isn't a TrialHandler2 active
497
- if not isinstance(self.currentLoop, TrialHandler2):
569
+ if not isinstance(loop, TrialHandler2):
498
570
  return
571
+ # restart inner loops
572
+ for innerLoop in self.loopsUnfinished[
573
+ self.loopsUnfinished.index(loop)+1:
574
+ ]:
575
+ innerLoop.rewindTrials(
576
+ len(innerLoop.elapsedTrials)
577
+ )
499
578
  # rewind trials in current loop
500
- return self.currentLoop.rewindTrials(n)
579
+ return loop.rewindTrials(n)
580
+
501
581
 
502
- def getAllTrials(self):
582
+ def getAllTrials(self, isTrials=True):
503
583
  """
504
584
  Returns all trials (elapsed, current and upcoming) with an index indicating which trial is
505
585
  the current trial.
506
586
 
587
+ Parameters
588
+ ----------
589
+ isTrials : bool
590
+ Filter for only loops which have isTrials checked
591
+
507
592
  Returns
508
593
  -------
509
594
  list[Trial]
@@ -512,39 +597,49 @@ class ExperimentHandler(_ComparisonMixin):
512
597
  Index of the current trial in this list
513
598
  """
514
599
  # return None if there isn't a TrialHandler2 active
515
- if not isinstance(self.currentLoop, TrialHandler2):
600
+ if not isinstance(self.getCurrentLoop(isTrials=isTrials), TrialHandler2):
516
601
  return [None], 0
517
602
  # get all trials from current loop
518
- return self.currentLoop.getAllTrials()
603
+ return self.getCurrentLoop(isTrials=isTrials).getAllTrials()
519
604
 
520
- def getCurrentTrial(self):
605
+ def getCurrentTrial(self, isTrials=True):
521
606
  """
522
607
  Returns the current trial (`.thisTrial`)
523
608
 
609
+ Parameters
610
+ ----------
611
+ isTrials : bool
612
+ Filter for only loops which have isTrials checked
613
+
524
614
  Returns
525
615
  -------
526
616
  Trial
527
617
  The current trial
528
618
  """
529
619
  # return None if there isn't a TrialHandler2 active
530
- if not isinstance(self.currentLoop, TrialHandler2):
620
+ if not isinstance(self.getCurrentLoop(isTrials=isTrials), TrialHandler2):
531
621
  return None
532
622
 
533
- return self.currentLoop.getCurrentTrial()
623
+ return self.getCurrentLoop(isTrials=isTrials).getCurrentTrial()
534
624
 
535
- def getFutureTrial(self, n=1):
625
+ def getFutureTrial(self, n=1, isTrials=True):
536
626
  """
537
627
  Returns the condition for n trials into the future, without
538
628
  advancing the trials. Returns 'None' if attempting to go beyond
539
629
  the last trial in the current loop, or if there is no current loop.
630
+
631
+ Parameters
632
+ ----------
633
+ isTrials : bool
634
+ Filter for only loops which have isTrials checked
540
635
  """
541
636
  # return None if there isn't a TrialHandler2 active
542
- if not isinstance(self.currentLoop, TrialHandler2):
637
+ if not isinstance(self.getCurrentLoop(isTrials=isTrials), TrialHandler2):
543
638
  return None
544
639
  # get future trial from current loop
545
- return self.currentLoop.getFutureTrial(n)
640
+ return self.getCurrentLoop(isTrials=isTrials).getFutureTrial(n)
546
641
 
547
- def getFutureTrials(self, n=1, start=0):
642
+ def getFutureTrials(self, n=1, start=0, isTrials=True):
548
643
  """
549
644
  Returns Trial objects for a given range in the future. Will start looking at `start` trials
550
645
  in the future and will return n trials from then, so e.g. to get all trials from 2 in the
@@ -556,6 +651,8 @@ class ExperimentHandler(_ComparisonMixin):
556
651
  How many trials into the future to look, by default 1
557
652
  start : int, optional
558
653
  How many trials into the future to start looking at, by default 0
654
+ isTrials : bool
655
+ Filter for only loops which have isTrials checked
559
656
 
560
657
  Returns
561
658
  -------
@@ -568,7 +665,7 @@ class ExperimentHandler(_ComparisonMixin):
568
665
  for i in range(n):
569
666
  # add each to the list
570
667
  trials.append(
571
- self.getFutureTrial(start + i)
668
+ self.getFutureTrial(start + i, isTrials=isTrials)
572
669
  )
573
670
 
574
671
  return trials
@@ -874,9 +971,22 @@ class ExperimentHandler(_ComparisonMixin):
874
971
  origEntries = self.entries
875
972
  self.entries = self.getAllEntries()
876
973
 
974
+ # temporarily remove connected save methods so they don't get pickled
975
+ origConnectedSaveMethods = self.connectedSaveMethods
976
+ self.connectedSaveMethods = [
977
+ {
978
+ 'fcn': f"<{callback['fcn'].__module__}:{callback['fcn'].__name__}>",
979
+ 'args': callback['args'],
980
+ 'kwargs': callback['kwargs']
981
+ } for callback in origConnectedSaveMethods
982
+ ]
983
+
877
984
  with openOutputFile(fileName=fileName, append=False,
878
985
  fileCollisionMethod=fileCollisionMethod) as f:
879
986
  pickle.dump(self, f)
987
+
988
+ # reinstate connected save methods
989
+ self.connectedSaveMethods = origConnectedSaveMethods
880
990
 
881
991
  if (fileName is not None) and (fileName != 'stdout'):
882
992
  logging.info('saved data to %s' % f.name)
psychopy/data/routine.py CHANGED
@@ -53,6 +53,18 @@ class Routine:
53
53
  # starting status
54
54
  self.status = constants.NOT_STARTED
55
55
 
56
+ def __getstate__(self):
57
+ """
58
+ Components can't be pickled due to their window reference, so don't include them when
59
+ pickled
60
+ """
61
+ # capture what is normally pickled
62
+ state = self.__dict__.copy()
63
+ # replace components with their name
64
+ state['components'] = []
65
+
66
+ return state
67
+
56
68
  def getPlaybackComponents(self):
57
69
  """
58
70
  Get a list of all Components within this Routine which have a concept of playing and
@@ -55,23 +55,26 @@ class StairHandler(_BaseTrialHandler):
55
55
 
56
56
  """
57
57
 
58
- def __init__(self,
59
- startVal,
60
- nReversals=None,
61
- stepSizes=4, # dB stepsize
62
- nTrials=0,
63
- nUp=1,
64
- nDown=3, # correct responses before stim goes down
65
- applyInitialRule=True,
66
- extraInfo=None,
67
- method='2AFC',
68
- stepType='db',
69
- minVal=None,
70
- maxVal=None,
71
- originPath=None,
72
- name='',
73
- autoLog=True,
74
- **kwargs):
58
+ def __init__(
59
+ self,
60
+ startVal,
61
+ nReversals=None,
62
+ stepSizes=4, # dB stepsize
63
+ nTrials=0,
64
+ nUp=1,
65
+ nDown=3, # correct responses before stim goes down
66
+ applyInitialRule=True,
67
+ extraInfo=None,
68
+ method='2AFC',
69
+ stepType='db',
70
+ minVal=None,
71
+ maxVal=None,
72
+ originPath=None,
73
+ isTrials=True,
74
+ name='',
75
+ autoLog=True,
76
+ **kwargs
77
+ ):
75
78
  """
76
79
  :Parameters:
77
80
 
@@ -136,6 +139,10 @@ class StairHandler(_BaseTrialHandler):
136
139
  The largest legal value for the staircase, which can be
137
140
  used to prevent it reaching impossible contrast values,
138
141
  for instance.
142
+
143
+ isTrials : bool
144
+ Is this controlling trials, or created for another purpose (e.g. iterating a
145
+ stimulus within a trial)?
139
146
 
140
147
  Additional keyword arguments will be ignored.
141
148
 
@@ -155,6 +162,7 @@ class StairHandler(_BaseTrialHandler):
155
162
  self.extraInfo = extraInfo
156
163
  self.method = method
157
164
  self.stepType = stepType
165
+ self.isTrials = isTrials
158
166
 
159
167
  try:
160
168
  self.stepSizes = list(stepSizes)
@@ -1736,9 +1744,18 @@ class QuestPlusHandler(StairHandler):
1736
1744
 
1737
1745
  class MultiStairHandler(_BaseTrialHandler):
1738
1746
 
1739
- def __init__(self, stairType='simple', method='random',
1740
- conditions=None, nTrials=50, randomSeed=None,
1741
- originPath=None, name='', autoLog=True):
1747
+ def __init__(
1748
+ self,
1749
+ stairType='simple',
1750
+ method='random',
1751
+ conditions=None,
1752
+ nTrials=50,
1753
+ randomSeed=None,
1754
+ originPath=None,
1755
+ isTrials=True,
1756
+ name='',
1757
+ autoLog=True,
1758
+ ):
1742
1759
  """A Handler to allow easy interleaved staircase procedures
1743
1760
  (simple or QUEST).
1744
1761
 
@@ -1790,6 +1807,10 @@ class MultiStairHandler(_BaseTrialHandler):
1790
1807
  The seed with which to initialize the random number generator
1791
1808
  (RNG). If `None` (default), do not initialize the RNG with
1792
1809
  a specific value.
1810
+
1811
+ isTrials : bool
1812
+ Is this controlling trials, or created for another purpose (e.g. iterating a
1813
+ stimulus within a trial)?
1793
1814
 
1794
1815
  Example usage::
1795
1816
 
@@ -1829,6 +1850,7 @@ class MultiStairHandler(_BaseTrialHandler):
1829
1850
  self.nTrials = nTrials
1830
1851
  self.finished = False
1831
1852
  self.totalTrials = 0
1853
+ self.isTrials = isTrials
1832
1854
  self._checkArguments()
1833
1855
  # create staircases
1834
1856
  self.staircases = [] # all staircases
psychopy/data/trial.py CHANGED
@@ -852,16 +852,19 @@ class TrialHandler2(_BaseTrialHandler):
852
852
  Then you'll find that `dat` has the following attributes that
853
853
  """
854
854
 
855
- def __init__(self,
856
- trialList,
857
- nReps,
858
- method='random',
859
- dataTypes=None,
860
- extraInfo=None,
861
- seed=None,
862
- originPath=None,
863
- name='',
864
- autoLog=True):
855
+ def __init__(
856
+ self,
857
+ trialList,
858
+ nReps,
859
+ method='random',
860
+ dataTypes=None,
861
+ extraInfo=None,
862
+ seed=None,
863
+ originPath=None,
864
+ isTrials=True,
865
+ name='',
866
+ autoLog=True
867
+ ):
865
868
  """
866
869
 
867
870
  :Parameters:
@@ -929,10 +932,14 @@ class TrialHandler2(_BaseTrialHandler):
929
932
 
930
933
  .origin - the contents of the script or builder experiment that
931
934
  created the handler
935
+
936
+ .isTrials - is this controlling trials, or created for another purpose (e.g. iterating a
937
+ stimulus within a trial)?
932
938
 
933
939
  """
934
940
  self.name = name
935
941
  self.autoLog = autoLog
942
+ self.isTrials = isTrials
936
943
 
937
944
  if trialList in [None, [None], []]: # user wants an empty trialList
938
945
  # which corresponds to a list with a single empty entry
@@ -1284,8 +1291,9 @@ class TrialHandler2(_BaseTrialHandler):
1284
1291
  # advance row in data file
1285
1292
  if self.getExp() is not None:
1286
1293
  self.getExp().nextEntry()
1287
- # mark as recently skipped so the next iteration is cancelled
1288
- self._cancelNextIteration = True
1294
+ # mark as recently skipped so the next iteration (if there is one) is cancelled
1295
+ if n or len(self.upcomingTrials):
1296
+ self._cancelNextIteration = True
1289
1297
 
1290
1298
  return self.thisTrial
1291
1299