psychopy 2024.1.3__py3-none-any.whl → 2024.2.0__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 (331) hide show
  1. psychopy/.DS_Store +0 -0
  2. psychopy/CHANGELOG.txt +206 -0
  3. psychopy/GIT_SHA +1 -0
  4. psychopy/VERSION +1 -0
  5. psychopy/__init__.py +77 -15
  6. psychopy/app/Resources/classic/plugin16.png +0 -0
  7. psychopy/app/Resources/classic/plugin16@2x.png +0 -0
  8. psychopy/app/Resources/dark/plugin16.png +0 -0
  9. psychopy/app/Resources/dark/plugin16@2x.png +0 -0
  10. psychopy/app/Resources/light/plugin16.png +0 -0
  11. psychopy/app/Resources/light/plugin16@2x.png +0 -0
  12. psychopy/app/__init__.py +76 -2
  13. psychopy/app/_psychopyApp.py +126 -101
  14. psychopy/app/builder/builder.py +14 -10
  15. psychopy/app/builder/dialogs/__init__.py +8 -8
  16. psychopy/app/builder/dialogs/dlgsConditions.py +12 -13
  17. psychopy/app/builder/dialogs/paramCtrls.py +24 -57
  18. psychopy/app/builder/localizedStrings.py +11 -9
  19. psychopy/app/builder/validators.py +2 -2
  20. psychopy/app/coder/codeEditorBase.py +8 -8
  21. psychopy/app/coder/coder.py +4 -4
  22. psychopy/app/connections/sendusage.py +2 -2
  23. psychopy/app/connections/updates.py +9 -9
  24. psychopy/app/dialogs.py +34 -2
  25. psychopy/app/idle.py +31 -0
  26. psychopy/app/jobs.py +21 -3
  27. psychopy/app/linuxconfig/__init__.py +9 -0
  28. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  29. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +4602 -2540
  30. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  31. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +56 -54
  32. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +53 -43
  33. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  34. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +56 -54
  35. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  36. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1258 -1176
  37. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +9415 -5
  38. psychopy/app/pavlovia_ui/_base.py +33 -3
  39. psychopy/app/pavlovia_ui/search.py +0 -1
  40. psychopy/app/plugin_manager/dialog.py +104 -51
  41. psychopy/app/plugin_manager/packages.py +5 -0
  42. psychopy/app/plugin_manager/plugins.py +152 -67
  43. psychopy/app/preferencesDlg.py +8 -8
  44. psychopy/app/psychopyApp.py +11 -5
  45. psychopy/app/ribbon.py +124 -14
  46. psychopy/app/runner/runner.py +6 -1
  47. psychopy/app/stdout/stdOutRich.py +27 -11
  48. psychopy/app/themes/icons.py +52 -2
  49. psychopy/assets/__init__.py +0 -0
  50. psychopy/assets/click.png +0 -0
  51. psychopy/assets/clicknext.png +0 -0
  52. psychopy/assets/next.png +0 -0
  53. psychopy/assets/psychopy.ico +0 -0
  54. psychopy/assets/psychopy.png +0 -0
  55. psychopy/assets/templates/__init__.py +0 -0
  56. psychopy/assets/touch.png +0 -0
  57. psychopy/assets/touchnext.png +0 -0
  58. psychopy/assets/window.ico +0 -0
  59. psychopy/changes/2023.1.0.md +9 -0
  60. psychopy/changes/2024.1.0.md +16 -0
  61. psychopy/changes/__init__.py +0 -0
  62. psychopy/clock.py +2 -2
  63. psychopy/colors.py +2 -1
  64. psychopy/compatibility.py +53 -1
  65. psychopy/contrib/.DS_Store +0 -0
  66. psychopy/contrib/configobj/__init__.py +10 -8
  67. psychopy/data/__init__.py +3 -2
  68. psychopy/data/base.py +5 -5
  69. psychopy/data/experiment.py +130 -4
  70. psychopy/data/routine.py +56 -0
  71. psychopy/data/staircase.py +2 -2
  72. psychopy/data/trial.py +559 -97
  73. psychopy/data/utils.py +56 -21
  74. psychopy/demos/.DS_Store +0 -0
  75. psychopy/demos/builder/.DS_Store +0 -0
  76. psychopy/demos/builder/Design Templates/.DS_Store +0 -0
  77. psychopy/demos/builder/Experiments/.DS_Store +0 -0
  78. psychopy/demos/builder/Feature Demos/.DS_Store +0 -0
  79. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +375 -0
  80. psychopy/demos/builder/Feature Demos/buttonBox/readme.md +5 -0
  81. psychopy/demos/builder/Feature Demos/pilotMode/pilotMode.psyexp +433 -0
  82. psychopy/demos/builder/Feature Demos/pilotMode/readme.md +7 -0
  83. psychopy/demos/builder/Feature Demos/progress/progressBar.psyexp +4 -4
  84. psychopy/demos/builder/Hardware/.DS_Store +0 -0
  85. psychopy/demos/builder/Helper Tools/.DS_Store +0 -0
  86. psychopy/demos/coder/.DS_Store +0 -0
  87. psychopy/demos/coder/hardware/testSoundLatency.py +2 -2
  88. psychopy/demos/coder/iohub/.DS_Store +0 -0
  89. psychopy/demos/coder/misc/hdf5_2_csv +33 -0
  90. psychopy/event.py +30 -29
  91. psychopy/experiment/.DS_Store +0 -0
  92. psychopy/experiment/_experiment.py +6 -6
  93. psychopy/experiment/components/.DS_Store +0 -0
  94. psychopy/experiment/components/__init__.py +6 -3
  95. psychopy/experiment/components/_base.py +286 -131
  96. psychopy/experiment/components/aperture/.DS_Store +0 -0
  97. psychopy/experiment/components/brush/.DS_Store +0 -0
  98. psychopy/experiment/components/button/.DS_Store +0 -0
  99. psychopy/experiment/components/button/__init__.py +5 -1
  100. psychopy/experiment/components/buttonBox/.DS_Store +0 -0
  101. psychopy/experiment/components/buttonBox/__init__.py +21 -12
  102. psychopy/experiment/components/camera/.DS_Store +0 -0
  103. psychopy/experiment/components/code/.DS_Store +0 -0
  104. psychopy/experiment/components/dots/.DS_Store +0 -0
  105. psychopy/experiment/components/eyetracker_record/.DS_Store +0 -0
  106. psychopy/experiment/components/eyetracker_record/__init__.py +92 -30
  107. psychopy/experiment/components/form/.DS_Store +0 -0
  108. psychopy/experiment/components/form/__init__.py +6 -2
  109. psychopy/experiment/components/grating/.DS_Store +0 -0
  110. psychopy/experiment/components/grating/__init__.py +14 -3
  111. psychopy/experiment/components/image/.DS_Store +0 -0
  112. psychopy/experiment/components/image/__init__.py +14 -3
  113. psychopy/experiment/components/joyButtons/.DS_Store +0 -0
  114. psychopy/experiment/components/joystick/.DS_Store +0 -0
  115. psychopy/experiment/components/keyboard/.DS_Store +0 -0
  116. psychopy/experiment/components/keyboard/__init__.py +22 -10
  117. psychopy/experiment/components/microphone/.DS_Store +0 -0
  118. psychopy/experiment/components/microphone/__init__.py +59 -39
  119. psychopy/experiment/components/mouse/.DS_Store +0 -0
  120. psychopy/experiment/components/mouse/__init__.py +44 -29
  121. psychopy/experiment/components/movie/.DS_Store +0 -0
  122. psychopy/experiment/components/movie/__init__.py +1 -1
  123. psychopy/experiment/components/panorama/.DS_Store +0 -0
  124. psychopy/experiment/components/parallelOut/.DS_Store +0 -0
  125. psychopy/experiment/components/patch/.DS_Store +0 -0
  126. psychopy/experiment/components/polygon/.DS_Store +0 -0
  127. psychopy/experiment/components/polygon/__init__.py +26 -6
  128. psychopy/experiment/components/progress/.DS_Store +0 -0
  129. psychopy/experiment/components/progress/__init__.py +1 -1
  130. psychopy/experiment/components/ratingScale/.DS_Store +0 -0
  131. psychopy/experiment/components/resourceManager/.DS_Store +0 -0
  132. psychopy/experiment/components/roi/.DS_Store +0 -0
  133. psychopy/experiment/components/roi/__init__.py +5 -0
  134. psychopy/experiment/components/routineSettings/.DS_Store +0 -0
  135. psychopy/experiment/components/routineSettings/__init__.py +57 -10
  136. psychopy/experiment/components/serialOut/.DS_Store +0 -0
  137. psychopy/experiment/components/settings/.DS_Store +0 -0
  138. psychopy/experiment/components/settings/__init__.py +117 -42
  139. psychopy/experiment/components/slider/.DS_Store +0 -0
  140. psychopy/experiment/components/sound/.DS_Store +0 -0
  141. psychopy/experiment/components/sound/__init__.py +54 -19
  142. psychopy/experiment/components/static/.DS_Store +0 -0
  143. psychopy/experiment/components/static/__init__.py +1 -1
  144. psychopy/experiment/components/text/.DS_Store +0 -0
  145. psychopy/experiment/components/text/__init__.py +28 -3
  146. psychopy/experiment/components/textbox/.DS_Store +0 -0
  147. psychopy/experiment/components/textbox/__init__.py +12 -2
  148. psychopy/experiment/components/unknown/.DS_Store +0 -0
  149. psychopy/experiment/components/unknown/__init__.py +1 -2
  150. psychopy/experiment/components/unknownPlugin/.DS_Store +0 -0
  151. psychopy/experiment/components/unknownPlugin/__init__.py +2 -2
  152. psychopy/experiment/components/variable/.DS_Store +0 -0
  153. psychopy/experiment/flow.py +11 -4
  154. psychopy/experiment/loops.py +85 -37
  155. psychopy/experiment/params.py +74 -32
  156. psychopy/experiment/py2js_transpiler.py +8 -1
  157. psychopy/experiment/routines/.DS_Store +0 -0
  158. psychopy/experiment/routines/_base.py +102 -22
  159. psychopy/experiment/routines/counterbalance/.DS_Store +0 -0
  160. psychopy/experiment/routines/counterbalance/__init__.py +5 -1
  161. psychopy/experiment/routines/eyetracker_calibrate/.DS_Store +0 -0
  162. psychopy/experiment/routines/eyetracker_validate/.DS_Store +0 -0
  163. psychopy/experiment/routines/pavlovia_survey/.DS_Store +0 -0
  164. psychopy/experiment/routines/photodiodeValidator/.DS_Store +0 -0
  165. psychopy/experiment/routines/photodiodeValidator/__init__.py +7 -6
  166. psychopy/experiment/routines/unknown/.DS_Store +0 -0
  167. psychopy/gui/wxgui.py +4 -4
  168. psychopy/hardware/.DS_Store +0 -0
  169. psychopy/hardware/__init__.py +1 -1
  170. psychopy/hardware/base.py +12 -0
  171. psychopy/hardware/camera/__init__.py +1 -15
  172. psychopy/hardware/cedrus.py +10 -11
  173. psychopy/hardware/crs/colorcal.py +13 -22
  174. psychopy/hardware/crs/optical.py +10 -20
  175. psychopy/hardware/emulator.py +17 -14
  176. psychopy/hardware/eyetracker.py +42 -118
  177. psychopy/hardware/gammasci.py +4 -15
  178. psychopy/hardware/keyboard.py +102 -11
  179. psychopy/hardware/listener.py +3 -0
  180. psychopy/hardware/microphone.py +148 -18
  181. psychopy/hardware/minolta.py +8 -15
  182. psychopy/hardware/photodiode.py +191 -16
  183. psychopy/hardware/photometer/__init__.py +11 -19
  184. psychopy/hardware/pr.py +8 -15
  185. psychopy/hardware/speaker.py +39 -4
  186. psychopy/info.py +0 -71
  187. psychopy/iohub/.DS_Store +0 -0
  188. psychopy/iohub/__init__.py +1 -1
  189. psychopy/iohub/client/__init__.py +30 -20
  190. psychopy/iohub/client/keyboard.py +24 -24
  191. psychopy/iohub/datastore/__init__.py +2 -2
  192. psychopy/iohub/datastore/util.py +2 -2
  193. psychopy/iohub/default_config.yaml +1 -1
  194. psychopy/iohub/devices/.DS_Store +0 -0
  195. psychopy/iohub/devices/__init__.py +112 -25
  196. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  197. psychopy/iohub/devices/experiment/default_experiment.yaml +12 -1
  198. psychopy/iohub/devices/experiment/supported_config_settings.yaml +5 -1
  199. psychopy/iohub/devices/eyetracker/.DS_Store +0 -0
  200. psychopy/iohub/devices/eyetracker/__init__.py +46 -0
  201. psychopy/iohub/devices/eyetracker/calibration/procedure.py +2 -2
  202. psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +14 -2
  203. psychopy/iohub/devices/eyetracker/hw/mouse/eyetracker.py +3 -4
  204. psychopy/iohub/server.py +2 -2
  205. psychopy/iohub/start_iohub_process.py +3 -0
  206. psychopy/iohub/util/__init__.py +62 -70
  207. psychopy/layout.py +5 -5
  208. psychopy/logging.py +8 -1
  209. psychopy/microphone.py +10 -37
  210. psychopy/platform_specific/__init__.py +0 -2
  211. psychopy/platform_specific/darwin.py +1 -3
  212. psychopy/platform_specific/linux.py +31 -33
  213. psychopy/platform_specific/win32.py +38 -13
  214. psychopy/plugins/__init__.py +148 -116
  215. psychopy/plugins/util.py +39 -0
  216. psychopy/preferences/Darwin.spec +4 -2
  217. psychopy/preferences/FreeBSD.spec +4 -2
  218. psychopy/preferences/Linux.spec +4 -2
  219. psychopy/preferences/Windows.spec +4 -2
  220. psychopy/preferences/baseNoArch.spec +4 -2
  221. psychopy/preferences/preferences.py +47 -24
  222. psychopy/projects/pavlovia.py +47 -4
  223. psychopy/scripts/psyexpCompile.py +0 -4
  224. psychopy/session.py +153 -21
  225. psychopy/sound/__init__.py +31 -21
  226. psychopy/sound/_base.py +20 -3
  227. psychopy/sound/audioclip.py +320 -33
  228. psychopy/sound/backend_ptb.py +47 -58
  229. psychopy/sound/backend_pygame.py +1 -1
  230. psychopy/sound/backend_pysound.py +6 -15
  231. psychopy/sound/transcribe.py +53 -0
  232. psychopy/tests/.DS_Store +0 -0
  233. psychopy/tests/data/.DS_Store +0 -0
  234. psychopy/tests/data/TestUnknownPluginComponent_load_resave.psyexp +135 -0
  235. psychopy/tests/data/Test_textbox/test_ori_0_bottom right.png +0 -0
  236. psychopy/tests/data/Test_textbox/test_ori_0_center.png +0 -0
  237. psychopy/tests/data/Test_textbox/test_ori_0_top left.png +0 -0
  238. psychopy/tests/data/Test_textbox/test_ori_120_bottom right.png +0 -0
  239. psychopy/tests/data/Test_textbox/test_ori_120_center.png +0 -0
  240. psychopy/tests/data/Test_textbox/test_ori_120_top left.png +0 -0
  241. psychopy/tests/data/Test_textbox/test_ori_180_bottom right.png +0 -0
  242. psychopy/tests/data/Test_textbox/test_ori_180_center.png +0 -0
  243. psychopy/tests/data/Test_textbox/test_ori_180_top left.png +0 -0
  244. psychopy/tests/data/Test_textbox/test_ori_240_bottom right.png +0 -0
  245. psychopy/tests/data/Test_textbox/test_ori_240_center.png +0 -0
  246. psychopy/tests/data/Test_textbox/test_ori_240_top left.png +0 -0
  247. psychopy/tests/data/correctScript/.DS_Store +0 -0
  248. psychopy/tests/data/test_components/testClearKeyboard/testClearKeyboard.psyexp +200 -0
  249. psychopy/tests/data/test_session/.DS_Store +0 -0
  250. psychopy/tests/data/test_session/root/testFutureTrials/testFutureTrials.psyexp +155 -0
  251. psychopy/tests/data/test_session/root/testTrialNav/trialNav.psyexp +158 -0
  252. psychopy/tests/test_app/.DS_Store +0 -0
  253. psychopy/tests/test_app/conftest.py +2 -2
  254. psychopy/tests/test_app/test_speed.py +4 -1
  255. psychopy/tests/test_data/test_TrialHandler2.py +146 -1
  256. psychopy/tests/test_experiment/.DS_Store +0 -0
  257. psychopy/tests/test_experiment/needs_wx/genComponsTemplate.py +3 -3
  258. psychopy/tests/test_experiment/needs_wx/test_components.py +2 -2
  259. psychopy/tests/test_experiment/test_components/test_KeyboardComponent.py +28 -0
  260. psychopy/tests/test_experiment/test_components/test_UnknownPluginComponent.py +27 -0
  261. psychopy/tests/test_experiment/test_components/test_base_components.py +58 -0
  262. psychopy/tests/test_experiment/test_py2js.py +1 -1
  263. psychopy/tests/test_hardware/test_keyboard.py +184 -16
  264. psychopy/tests/test_hardware/test_ports.py +1 -11
  265. psychopy/tests/test_liaison/test_Liaison.py +47 -0
  266. psychopy/tests/test_misc/test_core.py +5 -0
  267. psychopy/tests/test_session/test_Session.py +5 -1
  268. psychopy/tests/test_tools/test_versionchooser.py +39 -8
  269. psychopy/tests/test_visual/test_all_stimuli.py +0 -97
  270. psychopy/tests/test_visual/test_image.py +6 -5
  271. psychopy/tests/test_visual/test_textbox.py +36 -0
  272. psychopy/tests/utils.py +4 -0
  273. psychopy/tools/filetools.py +1 -1
  274. psychopy/tools/pkgtools.py +160 -137
  275. psychopy/tools/versionchooser.py +10 -10
  276. psychopy/tools/wizard.py +3 -3
  277. psychopy/visual/.DS_Store +0 -0
  278. psychopy/visual/backends/pygletbackend.py +24 -13
  279. psychopy/visual/basevisual.py +5 -11
  280. psychopy/visual/button.py +2 -14
  281. psychopy/visual/helpers.py +5 -5
  282. psychopy/visual/line.py +1 -2
  283. psychopy/visual/movie2.py +7 -816
  284. psychopy/visual/movie3.py +7 -589
  285. psychopy/visual/movies/__init__.py +8 -11
  286. psychopy/visual/movies/frame.py +5 -2
  287. psychopy/visual/movies/players/ffpyplayer_player.py +5 -2
  288. psychopy/visual/noise.py +8 -7
  289. psychopy/visual/patch.py +7 -16
  290. psychopy/visual/progress.py +1 -1
  291. psychopy/visual/radial.py +9 -7
  292. psychopy/visual/ratingscale.py +8 -1415
  293. psychopy/visual/secondorder.py +10 -9
  294. psychopy/visual/shape.py +7 -2
  295. psychopy/visual/text.py +1 -1
  296. psychopy/visual/textbox2/textbox2.py +28 -5
  297. psychopy/web.py +5 -2
  298. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/METADATA +8 -13
  299. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/RECORD +313 -219
  300. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/WHEEL +1 -1
  301. psychopy/app/Resources/click.png +0 -0
  302. psychopy/app/Resources/next.png +0 -0
  303. psychopy/experiment/components/patch/__init__.py +0 -121
  304. psychopy/experiment/components/patch/classic/patch.png +0 -0
  305. psychopy/experiment/components/patch/dark/patch.png +0 -0
  306. psychopy/experiment/components/patch/dark/patch@2x.png +0 -0
  307. psychopy/experiment/components/patch/light/patch.png +0 -0
  308. psychopy/experiment/components/patch/light/patch@2x.png +0 -0
  309. psychopy/experiment/components/ratingScale/__init__.py +0 -337
  310. psychopy/experiment/components/ratingScale/classic/ratingscale.png +0 -0
  311. psychopy/experiment/components/ratingScale/classic/ratingscale@2x.png +0 -0
  312. psychopy/experiment/components/ratingScale/dark/ratingScale@2x.png +0 -0
  313. psychopy/experiment/components/ratingScale/dark/ratingscale.png +0 -0
  314. psychopy/experiment/components/ratingScale/light/ratingScale@2x.png +0 -0
  315. psychopy/experiment/components/ratingScale/light/ratingscale.png +0 -0
  316. psychopy/platform_specific/posix.py +0 -16
  317. psychopy/tests/test_sound/test_microphone.py +0 -217
  318. psychopy/tests/test_visual/test_ratingScale.py +0 -299
  319. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@16w.png +0 -0
  320. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@32w.png +0 -0
  321. /psychopy/{app/Resources → assets}/USB-C.png +0 -0
  322. /psychopy/{app/Resources → assets}/USB.png +0 -0
  323. /psychopy/{app/Resources → assets}/creditCard.png +0 -0
  324. /psychopy/{app/Resources → assets}/default.mp3 +0 -0
  325. /psychopy/{app/Resources → assets}/default.mp4 +0 -0
  326. /psychopy/{app/Resources → assets}/default.png +0 -0
  327. /psychopy/{app/Resources → assets/templates}/instruct1.png +0 -0
  328. /psychopy/{app/Resources → assets/templates}/instruct2.png +0 -0
  329. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/entry_points.txt +0 -0
  330. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/AUTHORS.md +0 -0
  331. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -902,7 +902,7 @@ class RunnerPanel(wx.Panel, ScriptProcess, handlers.ThemeMixin):
902
902
  # disable stop
903
903
  self.ribbon.buttons['pystop'].Disable()
904
904
  # switch mode
905
- self.ribbon.buttons['pyswitch'].setMode(runMode == "run")
905
+ self.ribbon.buttons['pyswitch'].setMode(runMode == "run", silent=True)
906
906
  # update
907
907
  self.updateAlerts()
908
908
  self.app.updateWindowMenu()
@@ -1235,6 +1235,11 @@ class RunnerRibbon(ribbon.FrameRibbon):
1235
1235
  section="pavlovia", name="pavuser", frame=parent
1236
1236
  )
1237
1237
 
1238
+ self.addSeparator()
1239
+
1240
+ # --- Plugin sections ---
1241
+ self.addPluginSections("psychopy.app.builder")
1242
+
1238
1243
  # --- Views ---
1239
1244
  self.addStretchSpacer()
1240
1245
  self.addSeparator()
@@ -93,6 +93,18 @@ class StdOutRich(wx.richtext.RichTextCtrl, _BaseErrorHandler, handlers.ThemeMixi
93
93
  font=font.obj,
94
94
  )),
95
95
  }
96
+ # connect logging levels to styles
97
+ from psychopy import logging
98
+ self.logLevelStyles = {
99
+ logging.NOTSET: self._styles['base'],
100
+ logging.DEBUG: self._styles['info'],
101
+ logging.INFO: self._styles['info'],
102
+ logging.EXP: self._styles['info'],
103
+ logging.DATA: self._styles['info'],
104
+ logging.WARNING: self._styles['warning'],
105
+ logging.ERROR: self._styles['error'],
106
+ logging.CRITICAL: self._styles['error'],
107
+ }
96
108
 
97
109
  def onURL(self, evt=None):
98
110
  wx.BeginBusyCursor()
@@ -119,6 +131,7 @@ class StdOutRich(wx.richtext.RichTextCtrl, _BaseErrorHandler, handlers.ThemeMixi
119
131
  class WordFetcher:
120
132
  File "C:\\Program Files\\wxPython2.8 Docs and Demos\\samples\\hangman\\hangman.py", line 23, in WordFetcher
121
133
  """
134
+ from psychopy import logging
122
135
 
123
136
  if type(inStr) == AlertEntry:
124
137
  alert = inStr
@@ -174,17 +187,20 @@ class StdOutRich(wx.richtext.RichTextCtrl, _BaseErrorHandler, handlers.ThemeMixi
174
187
  self.BeginURL(thisLine)
175
188
  self.WriteText(thisLine)
176
189
  self.EndURL()
177
- elif len(re.findall('CRITICAL|ERROR', thisLine)) > 0:
178
- # this line contains an error
179
- self.BeginStyle(self._styles['error'])
180
- self.WriteText(thisLine)
181
- elif len(re.findall('WARNING|DEPRECATION', thisLine)) > 0:
182
- # this line contains a warning
183
- self.BeginStyle(self._styles['warning'])
184
- self.WriteText(thisLine)
185
- elif len(re.findall('DATA|EXP|INFO|DEBUG', thisLine)):
186
- # this line contains logging
187
- self.BeginStyle(self._styles['info'])
190
+ elif re.findall(logging._levelNamesRe, thisLine):
191
+ # this line contains a logging message
192
+ lvl = logging.NOTSET
193
+ # for each styled level...
194
+ for thisLvl, style in self.logLevelStyles.items():
195
+ # get its name
196
+ name = logging._levelNames[thisLvl]
197
+ # look for name in the current line
198
+ if len(re.findall(name, thisLine)) > 0:
199
+ # if found, set level and stop looking
200
+ lvl = thisLvl
201
+ break
202
+ # if level allowed by prefs, set style and write
203
+ self.BeginStyle(self.logLevelStyles[lvl])
188
204
  self.WriteText(thisLine)
189
205
  else:
190
206
  # anything else
@@ -4,7 +4,7 @@ from abc import ABC
4
4
  import numpy
5
5
  import wx
6
6
  from pathlib import Path
7
- from psychopy import prefs
7
+ from psychopy import prefs, logging
8
8
  from . import theme as appTheme
9
9
 
10
10
  retStr = ""
@@ -18,7 +18,7 @@ class BaseIcon:
18
18
  self.bitmaps = {}
19
19
  self._bitmap = None
20
20
  self.size = size
21
-
21
+ self.stem = stem
22
22
  if theme in (appTheme.icons, None) and stem in iconCache:
23
23
  # Duplicate relevant attributes if relevant (depends on subclass)
24
24
  self.bitmaps = iconCache[stem].bitmaps
@@ -30,6 +30,19 @@ class BaseIcon:
30
30
  # Store ref to self in iconCache if using app theme
31
31
  if theme in (appTheme.icons, None):
32
32
  iconCache[stem] = self
33
+
34
+ def reload(self, theme=None):
35
+ """
36
+ Get all images associated with this icon again. This is useful when changeing theme to one
37
+ with different icons.
38
+
39
+ Parameters
40
+ ----------
41
+ theme : str, optional
42
+ Theme to get icons from, by default will use the current theme
43
+ """
44
+ self._populate(stem=self.stem, theme=theme)
45
+
33
46
 
34
47
  def _populate(self, stem, theme=None):
35
48
  raise NotImplementedError(
@@ -132,6 +145,8 @@ class ButtonIcon(BaseIcon):
132
145
  theme = appTheme.icons
133
146
  # Get all files in the resource folder containing the given stem
134
147
  matches = [f for f in resources.glob(f"**/{stem}*.png")]
148
+ # get all icons from plugins
149
+ matches += findPluginIcons(stem)
135
150
  # Create blank arrays to store retina and non-retina files
136
151
  ret = {}
137
152
  nret = {}
@@ -248,3 +263,38 @@ class ComponentIcon(BaseIcon):
248
263
  self._beta = wx.Bitmap(combined)
249
264
 
250
265
  return self._beta
266
+
267
+
268
+ def findPluginIcons(stem):
269
+ """
270
+ Search icons added by plugins for any matching the given file stem.
271
+
272
+ Parameters
273
+ ----------
274
+ stem : str
275
+ The file stem (aka the filename without `.png` on the end) to search for - will also find
276
+ files beginning with the given stem (e.g. "globe" will also find "globe@2x.png")
277
+
278
+ Returns
279
+ -------
280
+ list[Path]
281
+ List of file paths for found icons
282
+ """
283
+ from psychopy.plugins import getEntryPointGroup
284
+ # start off with no files
285
+ files = []
286
+ # iterate through found entry points
287
+ for ep in getEntryPointGroup("psychopy.app.themes.icons"):
288
+ try:
289
+ # try to load module
290
+ mod = ep.load()
291
+ # get module folder
292
+ folder = Path(mod.__file__).parent
293
+ # add all matching .png files from that folder
294
+ for file in folder.glob(f"**/{stem}*.png"):
295
+ files.append(file)
296
+ except:
297
+ # if it fails for any reason, skip it
298
+ logging.warn(f"Failed to load {ep.value}")
299
+
300
+ return files
File without changes
Binary file
Binary file
Binary file
Binary file
Binary file
File without changes
Binary file
Binary file
Binary file
@@ -0,0 +1,9 @@
1
+ # Changes in 2023.1.0
2
+
3
+ ## Introducing: Plugins
4
+ Over the years, PsychoPy has gotten pretty big! We have a lot more functionality than when we
5
+ first started, but with that comes more packages and dependencies. In 2023.1.0 we introduced
6
+ plugins - making certain functinality optional rather than coming with PsychoPy by default.
7
+ Check out the "Plugins & packages manager" in the "Tools" menu to see what's available.
8
+
9
+ [Click here for more information about plugins.](https://www.psychopy.org/usingplugins.html)
@@ -0,0 +1,16 @@
1
+ # Changes in 2024.1.0
2
+
3
+ ## Introducing: Pilot mode
4
+ You may have noticed a new toggle button in the Builder toolbar - pressing it changes the run
5
+ buttons from green to orange. This button toggles the new "pilot mode" on and off. When in pilot
6
+ mode, the "Run in JS" button will run your experiment locally in your browser rather than on
7
+ Pavlovia, and the "Run in Python" button will run your experiment with a few (configurable)
8
+ adjustments, most noticeably:
9
+ - It will always run in Windowed mode, making it easier to escape if something goes wrong
10
+ - It will always run with full logging, so you get the maximum amount of info
11
+
12
+ To make it extra clear that you are in piloting mode, there's also a pretty noticeable orange
13
+ border around the window, so you don't run on actual participants while in this mode.
14
+
15
+ The purpose of piloting mode is to give you a safer environment to build your experiment in,
16
+ after which you can switch to running mode when you're ready to start gathering data.
File without changes
psychopy/clock.py CHANGED
@@ -23,7 +23,7 @@ import time
23
23
  import sys
24
24
  from datetime import datetime
25
25
 
26
- from pkg_resources import parse_version
26
+ from packaging.version import Version
27
27
 
28
28
  try:
29
29
  import pyglet
@@ -507,7 +507,7 @@ def _dispatchWindowEvents():
507
507
  # let's see if pyglet collected any event in meantime
508
508
  try:
509
509
  # this takes focus away from command line terminal window:
510
- if parse_version(pyglet.version) < parse_version('1.2'):
510
+ if Version(pyglet.version) < Version('1.2'):
511
511
  # events for sounds/video should run independently of wait()
512
512
  pyglet.media.dispatch_events()
513
513
  except AttributeError:
psychopy/colors.py CHANGED
@@ -389,7 +389,8 @@ class Color:
389
389
  adj = np.clip(self.rgb * contrast, -1, 1)
390
390
  buffer = self.copy()
391
391
  buffer.rgb = adj
392
- return getattr(buffer, space)
392
+ self._renderCache[space] = getattr(buffer, space)
393
+ return self._renderCache[space]
393
394
 
394
395
  def __repr__(self):
395
396
  """If colour is printed, it will display its class and value.
psychopy/compatibility.py CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
-
3
+ from pathlib import Path
4
+ from packaging.version import Version
4
5
  import psychopy.data
5
6
 
6
7
  ######### Begin Compatibility Class Definitions #########
@@ -93,3 +94,54 @@ def checkCompatibility(old, new, prefs=None, fix=True):
93
94
  msg += "\nNo known compatibility issues"
94
95
 
95
96
  return (not warning), msg
97
+
98
+
99
+ def checkUpdatesInfo(old, new):
100
+ """
101
+ Checks whether we need to display information from a new update, e.g. introducing a new feature.
102
+
103
+ Parameters
104
+ ----------
105
+ old : str or packaging.version.Version
106
+ Last version which was opened
107
+ new : str or packaging.version.Version
108
+ Current version
109
+ prefs : psychopy.preferences.Preferences
110
+ Preferences for the app
111
+
112
+ Returns
113
+ -------
114
+ list[str]
115
+ List of strings with markdown content for relevant updates
116
+ """
117
+ from psychopy.preferences import prefs
118
+ # make sure both versions are Version objects
119
+ if isinstance(old, str):
120
+ old = Version(old)
121
+ if isinstance(new, str):
122
+ new = Version(new)
123
+ # start off with no messages
124
+ messages = []
125
+ # if not a new version, no action needed
126
+ if old >= new:
127
+ return messages
128
+ # find changes folder
129
+ changesDir = Path(prefs.paths['psychopy']) / "changes"
130
+ # if it is a new version, check for updates
131
+ for file in changesDir.glob("*.md"):
132
+ # try to Version-ise target
133
+ try:
134
+ target = Version(file.stem)
135
+ except (TypeError, ValueError):
136
+ # skip if it fails
137
+ continue
138
+ # have we just crossed the target version?
139
+ if old < target < new:
140
+ # load the markdown file
141
+ msg = file.read_text(encoding="utf-8")
142
+ # add its contents to messages array
143
+ messages.append(msg)
144
+ # reverse messages so they go from most-to-least recent
145
+ messages.reverse()
146
+
147
+ return messages
Binary file
@@ -45,26 +45,28 @@ BOMS = {
45
45
  BOM_UTF16_LE: ('utf16_le', 'utf_16'),
46
46
  BOM_UTF16: ('utf_16', 'utf_16'),
47
47
  }
48
+
48
49
  # All legal variants of the BOM codecs.
49
- # TODO: the list of aliases is not meant to be exhaustive, is there a
50
- # better way ?
51
50
  BOM_LIST = {
52
51
  'utf_16': 'utf_16',
53
- 'u16': 'utf_16',
54
52
  'utf16': 'utf_16',
55
53
  'utf-16': 'utf_16',
56
- 'utf16_be': 'utf16_be',
57
54
  'utf_16_be': 'utf16_be',
58
55
  'utf-16be': 'utf16_be',
59
- 'utf16_le': 'utf16_le',
60
56
  'utf_16_le': 'utf16_le',
61
57
  'utf-16le': 'utf16_le',
62
58
  'utf_8': 'utf_8',
63
- 'u8': 'utf_8',
64
- 'utf': 'utf_8',
65
59
  'utf8': 'utf_8',
66
60
  'utf-8': 'utf_8',
67
- }
61
+ 'u16': 'utf_16', # Add mapping for 'u16'
62
+ 'u8': 'utf_8', # Add mapping for 'u8'
63
+ }
64
+
65
+ # Add regular expressions for matching variations
66
+ for encoding in ['utf-16', 'utf-8']:
67
+ regex = re.compile(fr'{encoding}(?:_|\-)?(?:be|le)?|u16|u8', re.IGNORECASE)
68
+ for match in filter(regex.fullmatch, BOM_LIST.keys()):
69
+ BOM_LIST[match] = BOM_LIST[encoding]
68
70
 
69
71
  # Map of encodings to the BOM to write.
70
72
  BOM_SET = {
psychopy/data/__init__.py CHANGED
@@ -3,9 +3,10 @@
3
3
 
4
4
  import sys
5
5
 
6
- from pkg_resources import parse_version
6
+ from packaging.version import Version
7
7
 
8
8
  from .base import DataHandler
9
+ from .routine import Routine
9
10
  from .experiment import ExperimentHandler
10
11
  from .trial import TrialHandler, TrialHandler2, TrialHandlerExt, TrialType
11
12
  from .staircase import (StairHandler, QuestHandler, PsiHandler,
@@ -27,7 +28,7 @@ from .fit import (FitFunction, FitCumNormal, FitLogistic, FitNakaRushton,
27
28
  try:
28
29
  # import openpyxl
29
30
  import openpyxl
30
- if parse_version(openpyxl.__version__) >= parse_version('2.4.0'):
31
+ if Version(openpyxl.__version__) >= Version('2.4.0'):
31
32
  # openpyxl moved get_column_letter to utils.cell
32
33
  from openpyxl.utils.cell import get_column_letter
33
34
  else:
psychopy/data/base.py CHANGED
@@ -11,7 +11,7 @@ import codecs
11
11
  import numpy as np
12
12
  import pandas as pd
13
13
  import json_tricks
14
- from pkg_resources import parse_version
14
+ from packaging.version import Version
15
15
 
16
16
  import psychopy
17
17
  from psychopy import logging
@@ -23,7 +23,7 @@ from .utils import _getExcelCellName
23
23
 
24
24
  try:
25
25
  import openpyxl
26
- if parse_version(openpyxl.__version__) >= parse_version('2.4.0'):
26
+ if Version(openpyxl.__version__) >= Version('2.4.0'):
27
27
  # openpyxl moved get_column_letter to utils.cell
28
28
  from openpyxl.utils.cell import get_column_letter
29
29
  else:
@@ -120,7 +120,7 @@ class _BaseTrialHandler(_ComparisonMixin):
120
120
  """
121
121
  fileName = pathToString(fileName)
122
122
 
123
- if self.thisTrialN < 1 and self.thisRepN < 1:
123
+ if self.thisTrialN < 0 and self.thisRepN < 0:
124
124
  # if both are < 1 we haven't started
125
125
  if self.autoLog:
126
126
  logging.info('.saveAsPickle() called but no trials completed.'
@@ -191,7 +191,7 @@ class _BaseTrialHandler(_ComparisonMixin):
191
191
  if stimOut is None:
192
192
  stimOut = []
193
193
 
194
- if self.thisTrialN < 1 and self.thisRepN < 1:
194
+ if self.thisTrialN < 0 and self.thisRepN < 0:
195
195
  # if both are < 1 we haven't started
196
196
  if self.autoLog:
197
197
  logging.info('TrialHandler.saveAsText called but no trials'
@@ -306,7 +306,7 @@ class _BaseTrialHandler(_ComparisonMixin):
306
306
  if stimOut is None:
307
307
  stimOut = []
308
308
 
309
- if self.thisTrialN < 1 and self.thisRepN < 1:
309
+ if self.thisTrialN < 0 and self.thisRepN < 0:
310
310
  # if both are < 1 we haven't started
311
311
  if self.autoLog:
312
312
  logging.info('TrialHandler.saveAsExcel called but no '
@@ -9,6 +9,7 @@ import pandas as pd
9
9
 
10
10
  from psychopy import constants, clock
11
11
  from psychopy import logging
12
+ from psychopy.data.trial import TrialHandler2
12
13
  from psychopy.tools.filetools import (openOutputFile, genDelimiter,
13
14
  genFilenameFromDelimiter)
14
15
  from psychopy.localization import _translate
@@ -456,6 +457,112 @@ class ExperimentHandler(_ComparisonMixin):
456
457
  # set own status
457
458
  self.status = constants.STOPPED
458
459
 
460
+ def skipTrials(self, n=1):
461
+ """
462
+ Skip ahead n trials - the trials inbetween will be marked as "skipped". If you try to
463
+ skip past the last trial, will log a warning and skip *to* the last trial.
464
+
465
+ Parameters
466
+ ----------
467
+ n : int
468
+ Number of trials to skip ahead
469
+ """
470
+ # return if there isn't a TrialHandler2 active
471
+ if not isinstance(self.currentLoop, TrialHandler2):
472
+ return
473
+ # skip trials in current loop
474
+ self.currentLoop.skipTrials(n)
475
+
476
+ def rewindTrials(self, n=1):
477
+ """
478
+ Skip ahead n trials - the trials inbetween will be marked as "skipped". If you try to
479
+ skip past the last trial, will log a warning and skip *to* the last trial.
480
+
481
+ Parameters
482
+ ----------
483
+ n : int
484
+ Number of trials to skip ahead
485
+ """
486
+ # return if there isn't a TrialHandler2 active
487
+ if not isinstance(self.currentLoop, TrialHandler2):
488
+ return
489
+ # rewind trials in current loop
490
+ self.currentLoop.rewindTrials(n)
491
+
492
+ def getAllTrials(self):
493
+ """
494
+ Returns all trials (elapsed, current and upcoming) with an index indicating which trial is
495
+ the current trial.
496
+
497
+ Returns
498
+ -------
499
+ list[Trial]
500
+ List of trials, in order (oldest to newest)
501
+ int
502
+ Index of the current trial in this list
503
+ """
504
+ # return None if there isn't a TrialHandler2 active
505
+ if not isinstance(self.currentLoop, TrialHandler2):
506
+ return [None], 0
507
+ # get all trials from current loop
508
+ return self.currentLoop.getAllTrials()
509
+
510
+ def getCurrentTrial(self):
511
+ """
512
+ Returns the current trial (`.thisTrial`)
513
+
514
+ Returns
515
+ -------
516
+ Trial
517
+ The current trial
518
+ """
519
+ # return None if there isn't a TrialHandler2 active
520
+ if not isinstance(self.currentLoop, TrialHandler2):
521
+ return None
522
+
523
+ return self.currentLoop.getCurrentTrial()
524
+
525
+ def getFutureTrial(self, n=1):
526
+ """
527
+ Returns the condition for n trials into the future, without
528
+ advancing the trials. Returns 'None' if attempting to go beyond
529
+ the last trial in the current loop, or if there is no current loop.
530
+ """
531
+ # return None if there isn't a TrialHandler2 active
532
+ if not isinstance(self.currentLoop, TrialHandler2):
533
+ return None
534
+ # get future trial from current loop
535
+ return self.currentLoop.getFutureTrial(n)
536
+
537
+ def getFutureTrials(self, n=1, start=0):
538
+ """
539
+ Returns Trial objects for a given range in the future. Will start looking at `start` trials
540
+ in the future and will return n trials from then, so e.g. to get all trials from 2 in the
541
+ future to 5 in the future you would use `start=2` and `n=3`.
542
+
543
+ Parameters
544
+ ----------
545
+ n : int, optional
546
+ How many trials into the future to look, by default 1
547
+ start : int, optional
548
+ How many trials into the future to start looking at, by default 0
549
+
550
+ Returns
551
+ -------
552
+ list[Trial or None]
553
+ List of Trial objects n long. Any trials beyond the last trial are None.
554
+ """
555
+ # blank list to store trials in
556
+ trials = []
557
+ # iterate through n trials
558
+ for i in range(n):
559
+ # add each to the list
560
+ trials.append(
561
+ self.getFutureTrial(start + i)
562
+ )
563
+
564
+ return trials
565
+
459
566
  def nextEntry(self):
460
567
  """Calling nextEntry indicates to the ExperimentHandler that the
461
568
  current trial has ended and so further addData() calls correspond
@@ -464,9 +571,7 @@ class ExperimentHandler(_ComparisonMixin):
464
571
  this = self.thisEntry
465
572
  # fetch data from each (potentially-nested) loop
466
573
  for thisLoop in self.loopsUnfinished:
467
- names, vals = self._getLoopInfo(thisLoop)
468
- for n, name in enumerate(names):
469
- this[name] = vals[n]
574
+ self.updateEntryFromLoop(thisLoop)
470
575
  # add the extraInfo dict to the data
471
576
  if type(self.extraInfo) == dict:
472
577
  this.update(self.extraInfo)
@@ -474,6 +579,24 @@ class ExperimentHandler(_ComparisonMixin):
474
579
  # add new entry with its
475
580
  self.thisEntry = {}
476
581
 
582
+ def updateEntryFromLoop(self, thisLoop):
583
+ """
584
+ Add all values from the given loop to the current entry.
585
+
586
+ Parameters
587
+ ----------
588
+ thisLoop : BaseLoopHandler
589
+ Loop to get fields from
590
+ """
591
+ # for each name and value in the current trial...
592
+ names, vals = self._getLoopInfo(thisLoop)
593
+ for n, name in enumerate(names):
594
+ # add/update value
595
+ self.thisEntry[name] = vals[n]
596
+ # make sure name is in data names
597
+ if name not in self.dataNames:
598
+ self.dataNames.append(name)
599
+
477
600
  def getAllEntries(self):
478
601
  """Fetches a copy of all the entries including a final (orphan) entry
479
602
  if that exists. This allows entries to be saved even if nextEntry() is
@@ -563,7 +686,9 @@ class ExperimentHandler(_ComparisonMixin):
563
686
  encoding=encoding)
564
687
 
565
688
  names = self._getAllParamNames()
566
- names.extend(self.dataNames)
689
+ for name in self.dataNames:
690
+ if name not in names:
691
+ names.append(name)
567
692
  # names from the extraInfo dictionary
568
693
  names.extend(self._getExtraInfo()[0])
569
694
  if len(names) < 1:
@@ -676,6 +801,7 @@ class ExperimentHandler(_ComparisonMixin):
676
801
  # put in context
677
802
  context = {
678
803
  'type': "trials_data",
804
+ 'thisTrial': self.thisEntry,
679
805
  'trials': trials.to_dict(orient="records"),
680
806
  'priority': self.columnPriority,
681
807
  'threshold': priorityThreshold,
@@ -0,0 +1,56 @@
1
+ from psychopy import constants
2
+
3
+ class Routine:
4
+ """
5
+ Object representing a Routine, used to store start/stop times and other aspects of Routine settings.
6
+
7
+ Parameters
8
+ ----------
9
+ name : str
10
+ Name of the Routine
11
+ components : list[object]
12
+ List of handles to Components associated with this Routine
13
+ maxDuration : float or None
14
+ Maximum time this Routine can take. None if there is no maximum.
15
+
16
+ Attributes
17
+ ----------
18
+ tStart : float or None
19
+ Time (UTC) when this Routine started
20
+ tStartRefresh : float or None
21
+ Time (UTC) of the first frame flip of this Routine
22
+ tStop : float or None
23
+ Time (UTC) when this Routine ended
24
+ tStopRefresh : float or None
25
+ Time (UTC) of the last frame flip of this Routine
26
+ maxDurationReached : bool
27
+ True if this Routine ended by its max duration being reached
28
+ skipped : bool
29
+ True if this Routine was skipped by the "Skip if..." parameter of its settings
30
+ forceEnded : bool
31
+ True if this Routine was forcibly ended (e.g. by a key press)
32
+ status : int
33
+ Value from psychopy.constants.status indicating whether this Routine has started, is finished, etc.
34
+ """
35
+ def __init__(
36
+ self,
37
+ name,
38
+ components=[],
39
+ maxDuration=None,
40
+ ):
41
+ self.name = name
42
+ self.components = components
43
+ self.maxDuration = maxDuration
44
+ # start all times as None
45
+ self.tStart = None
46
+ self.tStartRefresh = None
47
+ self.tStop = None
48
+ self.tStopRefresh = None
49
+ # start off assuming not skipped, timed out or force ended
50
+ self.maxDurationReached = False
51
+ self.skipped = False
52
+ self.forceEnded = False
53
+ # starting status
54
+ self.status = constants.NOT_STARTED
55
+
56
+
@@ -7,7 +7,7 @@ import pickle
7
7
  import copy
8
8
  import warnings
9
9
  import numpy as np
10
- from pkg_resources import parse_version
10
+ from packaging.version import Version
11
11
 
12
12
  import psychopy
13
13
  from psychopy import logging
@@ -26,7 +26,7 @@ except ImportError:
26
26
  try:
27
27
  # import openpyxl
28
28
  import openpyxl
29
- if parse_version(openpyxl.__version__) >= parse_version('2.4.0'):
29
+ if Version(openpyxl.__version__) >= Version('2.4.0'):
30
30
  # openpyxl moved get_column_letter to utils.cell
31
31
  from openpyxl.utils.cell import get_column_letter
32
32
  else: