psychopy 2024.2.1__py3-none-any.whl → 2024.2.4__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 (276) hide show
  1. psychopy/.DS_Store +0 -0
  2. psychopy/GIT_SHA +1 -1
  3. psychopy/VERSION +1 -1
  4. psychopy/__init__.py +10 -1
  5. psychopy/__init__.py.orig +65 -0
  6. psychopy/app/{locale/ar_001/.DS_Store → .DS_Store} +0 -0
  7. psychopy/app/Resources/.DS_Store +0 -0
  8. psychopy/app/_psychopyApp.py +11 -3
  9. psychopy/app/appData.spec +1 -1
  10. psychopy/app/builder/builder.py +1 -1
  11. psychopy/app/builder/builder.py.orig +3932 -0
  12. psychopy/app/builder/dialogs/__init__.py.orig +1679 -0
  13. psychopy/app/builder/dialogs/paramCtrls.py +1 -1
  14. psychopy/app/builder/dialogs/paramCtrls.py.orig +713 -0
  15. psychopy/app/colorpicker/__init__.py.orig +411 -0
  16. psychopy/app/cortex.log +0 -0
  17. psychopy/app/jobs.py +8 -1
  18. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +2452 -1731
  19. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.mo +0 -0
  20. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.po +6127 -0
  21. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.mo +0 -0
  22. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.po +7366 -0
  23. psychopy/app/plugin_manager/dialog.py +9 -7
  24. psychopy/app/ribbon.py +2 -1
  25. psychopy/app/runner/runner.py +7 -5
  26. psychopy/clock.py +8 -4
  27. psychopy/core.py.orig +169 -0
  28. psychopy/demos/builder/Design Templates/randomisedBlocks/html/index.html +23 -0
  29. psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks-legacy-browsers.js +423 -0
  30. psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks.js +427 -0
  31. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/chooseBlock.xlsx +0 -0
  32. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/facesBlock.xlsx +0 -0
  33. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/housesBlock.xlsx +0 -0
  34. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face01.jpg +0 -0
  35. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face02.jpg +0 -0
  36. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face03.jpg +0 -0
  37. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house01.jpg +0 -0
  38. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house02.jpg +0 -0
  39. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house03.jpg +0 -0
  40. psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks.py +330 -0
  41. psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks_lastrun.py +330 -0
  42. psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xml +298 -0
  43. psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xsd +120 -0
  44. psychopy/demos/builder/Tools/.DS_Store +0 -0
  45. psychopy/demos/builder/Tools/gammaCalibration/.DS_Store +0 -0
  46. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.csv +38 -0
  47. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.log +3418 -0
  48. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.psydat +0 -0
  49. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.csv +2 -0
  50. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.log +15 -0
  51. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.psydat +0 -0
  52. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.psyexp +323 -0
  53. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.py +562 -0
  54. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual_lastrun.py +562 -0
  55. psychopy/demos/builder/Tools/gammaCalibration/questStairs.xlsx +0 -0
  56. psychopy/demos/builder/Tools/gammaCalibration/readme.md +0 -0
  57. psychopy/demos/builder/Tools/gammaCalibration/resources/low_contrast.png +0 -0
  58. psychopy/demos/builder/Tools/gammaCalibration/resources/make_2nd_order_tex.py +59 -0
  59. psychopy/demos/builder/Tools/gammaCalibration/resources/second_order_tex.png +0 -0
  60. psychopy/demos/coder/.DS_Store +0 -0
  61. psychopy/demos/coder/experiment control/info_gamma.pickle +0 -0
  62. psychopy/demos/coder/iohub/.iohpid +1 -0
  63. psychopy/demos/coder/iohub/eyetracking/.iohpid +1 -0
  64. psychopy/demos/coder/iohub/wintab/.DS_Store +0 -0
  65. psychopy/demos/coder/stimuli/.DS_Store +0 -0
  66. psychopy/demos/coder/stimuli/radialGratingContracting.py +29 -0
  67. psychopy/experiment/_experiment.py.orig +1032 -0
  68. psychopy/experiment/components/.DS_Store +0 -0
  69. psychopy/experiment/components/_base.py +13 -4
  70. psychopy/experiment/components/_base.py.orig +823 -0
  71. psychopy/experiment/components/form/.DS_Store +0 -0
  72. psychopy/experiment/components/microphone/__init__.py +10 -1
  73. psychopy/experiment/components/microphone/__init__.py.orig +490 -0
  74. psychopy/experiment/components/polygon/__init__.py +21 -22
  75. psychopy/experiment/components/settings/__init__.py +13 -14
  76. psychopy/experiment/components/settings/__init__.py.orig +1337 -0
  77. psychopy/experiment/components/textbox/__init__.py.orig +310 -0
  78. psychopy/experiment/components/webcam/.DS_Store +0 -0
  79. psychopy/experiment/components/webcam/light/.DS_Store +0 -0
  80. psychopy/experiment/flow.py +10 -8
  81. psychopy/experiment/loops.py.orig +829 -0
  82. psychopy/experiment/params.py +8 -3
  83. psychopy/experiment/params.py.orig +408 -0
  84. psychopy/experiment/routine.py.orig +503 -0
  85. psychopy/experiment/routines/_base.py +15 -6
  86. psychopy/experiment/routines/counterbalance/__init__.py +1 -0
  87. psychopy/gui/qtgui.py +14 -7
  88. psychopy/gui/util.py +10 -14
  89. psychopy/gui/wxgui.py +10 -4
  90. psychopy/hardware/.DS_Store +0 -0
  91. psychopy/hardware/brainproducts.py.orig +680 -0
  92. psychopy/hardware/iolab.py.orig +238 -0
  93. psychopy/hardware/manager.py +1 -1
  94. psychopy/hardware/photodiode.py +59 -27
  95. psychopy/hardware/serialport.py +51 -0
  96. psychopy/hardware/speaker.py +4 -4
  97. psychopy/iohub/datastore/__init__.py.orig +443 -0
  98. psychopy/iohub/datastore/util.py.orig +692 -0
  99. psychopy/iohub/devices/mouse/darwin.py.orig +427 -0
  100. psychopy/iohub/devices/mouse/linux2.py.orig +198 -0
  101. psychopy/preferences/.DS_Store +0 -0
  102. psychopy/projects/pavlovia.py +10 -3
  103. psychopy/projects/pavlovia.py.orig +1295 -0
  104. psychopy/sound/backend_ptb.py +22 -5
  105. psychopy/sound/transcribe.py +24 -4
  106. psychopy/tests/.DS_Store +0 -0
  107. psychopy/tests/data/.DS_Store +0 -0
  108. psychopy/tests/data/TestCircle_fill_local.png +0 -0
  109. psychopy/tests/data/__test.png +0 -0
  110. psychopy/tests/data/aperture1_normHexbackground_local.png +0 -0
  111. psychopy/tests/data/aperture1_norm_local.png +0 -0
  112. psychopy/tests/data/aperture2_normHexbackground_local.png +0 -0
  113. psychopy/tests/data/beatandrcos_height_local.png +0 -0
  114. psychopy/tests/data/beatandrcos_normAddBlend_local.png +0 -0
  115. psychopy/tests/data/beatandrcos_normHexbackground_local.png +0 -0
  116. psychopy/tests/data/beatandrcos_norm_local.png +0 -0
  117. psychopy/tests/data/beatandrcos_stencil_local.png +0 -0
  118. psychopy/tests/data/blend_add_height_local.png +0 -0
  119. psychopy/tests/data/blend_add_normAddBlend_local.png +0 -0
  120. psychopy/tests/data/blend_add_normHexbackground_local.png +0 -0
  121. psychopy/tests/data/blend_add_normNoShade_local.png +0 -0
  122. psychopy/tests/data/blend_add_norm_local.png +0 -0
  123. psychopy/tests/data/blend_add_stencil_local.png +0 -0
  124. psychopy/tests/data/bufferimg_gabor_height_local.png +0 -0
  125. psychopy/tests/data/bufferimg_gabor_normAddBlend_local.png +0 -0
  126. psychopy/tests/data/bufferimg_gabor_normHexbackground_local.png +0 -0
  127. psychopy/tests/data/bufferimg_gabor_normNoShade_local.png +0 -0
  128. psychopy/tests/data/bufferimg_gabor_norm_local.png +0 -0
  129. psychopy/tests/data/bufferimg_gabor_stencil_local.png +0 -0
  130. psychopy/tests/data/circleHex_height_local.png +0 -0
  131. psychopy/tests/data/circleHex_normAddBlend_local.png +0 -0
  132. psychopy/tests/data/circleHex_normHexbackground_local.png +0 -0
  133. psychopy/tests/data/circleHex_normNoShade_local.png +0 -0
  134. psychopy/tests/data/circleHex_norm_local.png +0 -0
  135. psychopy/tests/data/circleHex_stencil_local.png +0 -0
  136. psychopy/tests/data/color_comparison_local.png +0 -0
  137. psychopy/tests/data/corrFullRandom_local.csv +16 -0
  138. psychopy/tests/data/corrFullRandom_local.tsv +6 -0
  139. psychopy/tests/data/correctScript/.DS_Store +0 -0
  140. psychopy/tests/data/dots_height_local.png +0 -0
  141. psychopy/tests/data/dots_normAddBlend_local.png +0 -0
  142. psychopy/tests/data/dots_normHexbackground_local.png +0 -0
  143. psychopy/tests/data/dots_normNoShade_local.png +0 -0
  144. psychopy/tests/data/dots_norm_local.png +0 -0
  145. psychopy/tests/data/dots_stencil_local.png +0 -0
  146. psychopy/tests/data/elarray1_height_local.png +0 -0
  147. psychopy/tests/data/elarray1_normAddBlend_local.png +0 -0
  148. psychopy/tests/data/elarray1_normHexbackground_local.png +0 -0
  149. psychopy/tests/data/elarray1_norm_local.png +0 -0
  150. psychopy/tests/data/elarray1_stencil_local.png +0 -0
  151. psychopy/tests/data/envelopeandrcos_height_local.png +0 -0
  152. psychopy/tests/data/envelopeandrcos_normAddBlend_local.png +0 -0
  153. psychopy/tests/data/envelopeandrcos_normHexbackground_local.png +0 -0
  154. psychopy/tests/data/envelopeandrcos_norm_local.png +0 -0
  155. psychopy/tests/data/envelopeandrcos_stencil_local.png +0 -0
  156. psychopy/tests/data/envelopepowerandrcos_height_local.png +0 -0
  157. psychopy/tests/data/envelopepowerandrcos_normAddBlend_local.png +0 -0
  158. psychopy/tests/data/envelopepowerandrcos_normHexbackground_local.png +0 -0
  159. psychopy/tests/data/envelopepowerandrcos_norm_local.png +0 -0
  160. psychopy/tests/data/envelopepowerandrcos_stencil_local.png +0 -0
  161. psychopy/tests/data/gabor1_height_local.png +0 -0
  162. psychopy/tests/data/gabor1_normAddBlend_local.png +0 -0
  163. psychopy/tests/data/gabor1_normHexbackground_local.png +0 -0
  164. psychopy/tests/data/gabor1_normNoShade_local.png +0 -0
  165. psychopy/tests/data/gabor1_norm_local.png +0 -0
  166. psychopy/tests/data/gabor1_stencil_local.png +0 -0
  167. psychopy/tests/data/greyscale_normHexbackground_local.png +0 -0
  168. psychopy/tests/data/imageAndGauss_height_local.png +0 -0
  169. psychopy/tests/data/imageAndGauss_normAddBlend_local.png +0 -0
  170. psychopy/tests/data/imageAndGauss_normHexbackground_local.png +0 -0
  171. psychopy/tests/data/imageAndGauss_normNoShade_local.png +0 -0
  172. psychopy/tests/data/imageAndGauss_norm_local.png +0 -0
  173. psychopy/tests/data/imageAndGauss_stencil_local.png +0 -0
  174. psychopy/tests/data/movFrame1_stencil_local.png +0 -0
  175. psychopy/tests/data/noiseAndRcos_height_local.png +0 -0
  176. psychopy/tests/data/noiseAndRcos_normAddBlend_local.png +0 -0
  177. psychopy/tests/data/noiseAndRcos_normHexbackground_local.png +0 -0
  178. psychopy/tests/data/noiseAndRcos_normNoShade_local.png +0 -0
  179. psychopy/tests/data/noiseAndRcos_norm_local.png +0 -0
  180. psychopy/tests/data/noiseAndRcos_stencil_local.png +0 -0
  181. psychopy/tests/data/noiseFiltersAndRcos_height_local.png +0 -0
  182. psychopy/tests/data/noiseFiltersAndRcos_normAddBlend_local.png +0 -0
  183. psychopy/tests/data/noiseFiltersAndRcos_normHexbackground_local.png +0 -0
  184. psychopy/tests/data/noiseFiltersAndRcos_normNoShade_local.png +0 -0
  185. psychopy/tests/data/noiseFiltersAndRcos_norm_local.png +0 -0
  186. psychopy/tests/data/noiseFiltersAndRcos_stencil_local.png +0 -0
  187. psychopy/tests/data/numpyImage_height_local.png +0 -0
  188. psychopy/tests/data/numpyImage_normAddBlend_local.png +0 -0
  189. psychopy/tests/data/numpyImage_normHexbackground_local.png +0 -0
  190. psychopy/tests/data/numpyImage_normNoShade_local.png +0 -0
  191. psychopy/tests/data/numpyImage_norm_local.png +0 -0
  192. psychopy/tests/data/numpyImage_stencil_local.png +0 -0
  193. psychopy/tests/data/shape2_1_normAddBlend_local.png +0 -0
  194. psychopy/tests/data/shape2_1_normHexbackground_local.png +0 -0
  195. psychopy/tests/data/shape2_1_normNoShade_local.png +0 -0
  196. psychopy/tests/data/shape2_1_norm_local.png +0 -0
  197. psychopy/tests/data/shape2_1_stencil_local.png +0 -0
  198. psychopy/tests/data/testLoopsBlocks.psyexp_local.py +328 -0
  199. psychopy/tests/data/text1_height_local.png +0 -0
  200. psychopy/tests/data/text1_normAddBlend_local.png +0 -0
  201. psychopy/tests/data/text1_normHexbackground_local.png +0 -0
  202. psychopy/tests/data/text1_norm_local.png +0 -0
  203. psychopy/tests/data/text1_stencil_local.png +0 -0
  204. psychopy/tests/data/text2_height.png +0 -0
  205. psychopy/tests/data/text2_normAddBlend.png +0 -0
  206. psychopy/tests/data/text2_normHexbackground.png +0 -0
  207. psychopy/tests/data/text2_stencil.png +0 -0
  208. psychopy/tests/data/wedge1_height_local.png +0 -0
  209. psychopy/tests/data/wedge1_normAddBlend_local.png +0 -0
  210. psychopy/tests/data/wedge1_normHexbackground_local.png +0 -0
  211. psychopy/tests/data/wedge1_normNoShade_local.png +0 -0
  212. psychopy/tests/data/wedge1_norm_local.png +0 -0
  213. psychopy/tests/data/wedge1_stencil_local.png +0 -0
  214. psychopy/tests/test_app/.DS_Store +0 -0
  215. psychopy/tests/test_app/test_builder/.DS_Store +0 -0
  216. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.csv +9 -0
  217. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.log +177 -0
  218. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.psydat +0 -0
  219. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.xlsx +0 -0
  220. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.csv +9 -0
  221. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.log +168 -0
  222. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.psydat +0 -0
  223. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.xlsx +0 -0
  224. psychopy/tests/test_data/.DS_Store +0 -0
  225. psychopy/tests/test_hardware/test_CRS_BitsSharp.py.orig +68 -0
  226. psychopy/tests/test_tools/test_arraytools.py +112 -0
  227. psychopy/tests/test_visual/test_image.py.orig +219 -0
  228. psychopy/tools/arraytools.py +47 -0
  229. psychopy/tools/versionchooser.py +1 -1
  230. psychopy/visual/backends/pygletbackend.py +26 -8
  231. psychopy/visual/basevisual.py.orig +1723 -0
  232. psychopy/visual/form.py.orig +1181 -0
  233. psychopy/visual/text.py.orig +752 -0
  234. psychopy/visual/textbox2/textbox2.py.orig +1315 -0
  235. psychopy/visual/window.py +13 -5
  236. psychopy/visual/windowwarp.py.orig +463 -0
  237. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/METADATA +9 -9
  238. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/RECORD +244 -78
  239. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/WHEEL +1 -1
  240. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/entry_points.txt +2 -0
  241. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  242. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  243. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  244. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  245. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  246. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  247. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  248. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  249. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  250. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  251. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  252. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  253. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  254. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  255. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  256. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  257. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  258. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  259. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  260. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  261. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  262. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  263. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  264. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  265. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  266. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  267. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  268. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  269. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  270. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  271. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  272. psychopy-2024.2.1.dist-info/licenses/AUTHORS.md +0 -138
  273. /psychopy/{app/locale/ar_001/LC_MESSAGE → demos/builder}/.DS_Store +0 -0
  274. /psychopy/{app/locale/es_ES/LC_MESSAGE → demos/builder/Experiments}/.DS_Store +0 -0
  275. /psychopy/{visual → demos/builder/Tools/gammaCalibration/data}/.DS_Store +0 -0
  276. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1032 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # Part of the PsychoPy library
5
+ # Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2021 Open Science Tools Ltd.
6
+ # Distributed under the terms of the GNU General Public License (GPL).
7
+
8
+ """Experiment classes:
9
+ Experiment, Flow, Routine, Param, Loop*, *Handlers, and NameSpace
10
+
11
+ The code that writes out a *_lastrun.py experiment file is (in order):
12
+ experiment.Experiment.writeScript() - starts things off, calls other parts
13
+ settings.SettingsComponent.writeStartCode()
14
+ experiment.Flow.writeBody()
15
+ which will call the .writeBody() methods from each component
16
+ settings.SettingsComponent.writeEndCode()
17
+ """
18
+
19
+ import os
20
+ import codecs
21
+ import xml.etree.ElementTree as xml
22
+ from xml.dom import minidom
23
+ from copy import deepcopy
24
+ from pathlib import Path
25
+ from pkg_resources import parse_version
26
+
27
+ import psychopy
28
+ from psychopy import data, __version__, logging
29
+ from .exports import IndentingBuffer, NameSpace
30
+ from .flow import Flow
31
+ from .loops import TrialHandler, LoopInitiator, \
32
+ LoopTerminator, StairHandler, MultiStairHandler
33
+ from .params import _findParam, Param, legacyParams
34
+ from psychopy.experiment.routines._base import Routine, BaseStandaloneRoutine
35
+ from psychopy.experiment.routines import getAllStandaloneRoutines
36
+ from . import utils, py2js
37
+ from .components import getComponents, getAllComponents
38
+
39
+ from psychopy.localization import _translate
40
+ import locale
41
+
42
+ from collections import namedtuple, OrderedDict
43
+
44
+ from ..alerts import alert
45
+
46
+ RequiredImport = namedtuple('RequiredImport',
47
+ field_names=('importName',
48
+ 'importFrom',
49
+ 'importAs'))
50
+
51
+
52
+ <<<<<<< HEAD
53
+ # Some params have previously had types which cause errors compiling in new versions, so we need to keep track of them and force them to the new type if needed
54
+ forceType = {
55
+ 'pos': 'list',
56
+ 'size': 'list',
57
+ ('KeyboardComponent', 'allowedKeys'): 'list',
58
+ ('cedrusButtonBoxComponent', 'allowedKeys'): 'list',
59
+ ('DotsComponent', 'fieldPos'): 'list',
60
+ ('JoyButtonsComponent', 'allowedKeys'): 'list',
61
+ ('JoyButtonsComponent', 'correctAns'): 'list',
62
+ ('JoystickComponent', 'clickable'): 'list',
63
+ ('JoystickComponent', 'saveParamsClickable'): 'list',
64
+ ('JoystickComponent', 'allowedButtons'): 'list',
65
+ ('MicrophoneComponent', 'transcribeWords'): 'list',
66
+ ('MouseComponent', 'clickable'): 'list',
67
+ ('MouseComponent', 'saveParamsClickable'): 'list',
68
+ ('NoiseStimComponent', 'noiseElementSize'): 'list',
69
+ ('PatchComponent', 'sf'): 'list',
70
+ ('RatingScaleComponent', 'categoryChoices'): 'list',
71
+ ('RatingScaleComponent', 'labels'): 'list',
72
+ ('RegionOfInterestComponent', 'vertices'): 'list',
73
+ ('SettingsComponent', 'Window size (pixels)'): 'list',
74
+ ('SettingsComponent', 'Resources'): 'list',
75
+ ('SettingsComponent', 'mgBlink'): 'list',
76
+ ('SliderComponent', 'ticks'): 'list',
77
+ ('SliderComponent', 'labels'): 'list',
78
+ ('SliderComponent', 'styleTweaks'): 'list'
79
+ }
80
+
81
+ # # Code to generate force list
82
+ # comps = experiment.components.getAllComponents()
83
+ # exp = experiment._experiment.Experiment()
84
+ # rt = experiment.routines.Routine("routine", exp)
85
+ # exp.addRoutine("routine", rt)
86
+ #
87
+ # forceType = {
88
+ # 'pos': 'list',
89
+ # 'size': 'list',
90
+ # 'vertices': 'list',
91
+ # }
92
+ # for Comp in comps.values():
93
+ # comp = Comp(exp=exp, parentName="routine")
94
+ # for key, param in comp.params.items():
95
+ # if param.valType == 'list' and key not in forceType:
96
+ # forceType[(Comp.__name__, key)] = 'list'
97
+
98
+
99
+ class Experiment(object):
100
+ =======
101
+ class Experiment:
102
+ >>>>>>> 0c0c90c1131172428cce7382e83322e832d06051
103
+ """
104
+ An experiment contains a single Flow and at least one
105
+ Routine. The Flow controls how Routines are organised
106
+ e.g. the nature of repeats and branching of an experiment.
107
+ """
108
+
109
+ def __init__(self, prefs=None):
110
+ super(Experiment, self).__init__()
111
+ self.name = ''
112
+ self.filename = '' # update during load/save xml
113
+ self.flow = Flow(exp=self) # every exp has exactly one flow
114
+ self.routines = {}
115
+ # get prefs (from app if poss or from cfg files)
116
+ if prefs is None:
117
+ prefs = psychopy.prefs
118
+ # deepCopy doesn't like the full prefs object to be stored, so store
119
+ # each subset
120
+ self.prefsAppDataCfg = prefs.appDataCfg
121
+ self.prefsGeneral = prefs.general
122
+ self.prefsApp = prefs.app
123
+ self.prefsCoder = prefs.coder
124
+ self.prefsBuilder = prefs.builder
125
+ self.prefsPaths = prefs.paths
126
+ # this can be checked by the builder that this is an experiment and a
127
+ # compatible version
128
+ self.psychopyVersion = __version__
129
+
130
+ # What libs are needed (make sound come first)
131
+ self.requiredImports = []
132
+ libs = ('sound', 'gui', 'visual', 'core', 'data', 'event',
133
+ 'logging', 'clock', 'colors')
134
+ self.requirePsychopyLibs(libs=libs)
135
+ self.requireImport(importName='keyboard',
136
+ importFrom='psychopy.hardware')
137
+
138
+ _settingsComp = getComponents(fetchIcons=False)['SettingsComponent']
139
+ self.settings = _settingsComp(parentName='', exp=self)
140
+ # this will be the xml.dom.minidom.doc object for saving
141
+ self._doc = xml.ElementTree()
142
+ self.namespace = NameSpace(self) # manage variable names
143
+
144
+ # _expHandler is a hack to allow saving data from components not
145
+ # inside a loop. data-saving machinery relies on loops, not worth
146
+ # rewriting. `thisExp` will be an ExperimentHandler when used in
147
+ # the generated script, but its easier to use treat it as a
148
+ # TrialHandler during script generation to avoid effectively
149
+ # duplicating code just to work around any differences
150
+ # in writeRoutineEndCode
151
+ self._expHandler = TrialHandler(exp=self, name='thisExp')
152
+ self._expHandler.type = 'ExperimentHandler' # true at run-time
153
+
154
+ def requirePsychopyLibs(self, libs=()):
155
+ """Add a list of top-level psychopy libs that the experiment
156
+ will need. e.g. [visual, event]
157
+
158
+ Notes
159
+ -----
160
+ This is a convenience method for `requireImport()`.
161
+ """
162
+ for lib in libs:
163
+ self.requireImport(importName=lib,
164
+ importFrom='psychopy')
165
+
166
+ @property
167
+ def eyetracking(self):
168
+ """What kind of eyetracker this experiment is set up for"""
169
+ return self.settings.params['eyetracker']
170
+
171
+ def requireImport(self, importName, importFrom='', importAs=''):
172
+ """Add a top-level import to the experiment.
173
+
174
+ Parameters
175
+ ----------
176
+ importName : str
177
+ Name of the package or module to import.
178
+ importFrom : str
179
+ Where to import ``from``.
180
+ importAs : str
181
+ Import ``as`` this name.
182
+ """
183
+ import_ = RequiredImport(importName=importName,
184
+ importFrom=importFrom,
185
+ importAs=importAs)
186
+
187
+ if import_ not in self.requiredImports:
188
+ self.requiredImports.append(import_)
189
+
190
+ def addRoutine(self, routineName, routine=None):
191
+ """Add a Routine to the current list of them.
192
+
193
+ Can take a Routine object directly or will create
194
+ an empty one if none is given.
195
+ """
196
+ if routine is None:
197
+ # create a deafult routine with this name
198
+ self.routines[routineName] = Routine(routineName, exp=self)
199
+ else:
200
+ self.routines[routineName] = routine
201
+ return self.routines[routineName]
202
+
203
+ def addStandaloneRoutine(self, routineName, routine):
204
+ """Add a standalone Routine to the current list of them.
205
+
206
+ Can take a Routine object directly or will create
207
+ an empty one if none is given.
208
+ """
209
+ self.routines[routineName] = routine
210
+ return self.routines[routineName]
211
+
212
+ def integrityCheck(self):
213
+ """Check the integrity of the Experiment"""
214
+ # add some checks for things outside the Flow?
215
+ # then check the contents 1-by-1 from the Flow
216
+ self.flow.integrityCheck()
217
+
218
+ def writeScript(self, expPath=None, target="PsychoPy", modular=True):
219
+ """Write a PsychoPy script for the experiment
220
+ """
221
+ # self.integrityCheck()
222
+
223
+ self.psychopyVersion = psychopy.__version__ # make sure is current
224
+ # set this so that params write for approp target
225
+ utils.scriptTarget = target
226
+ self.expPath = expPath
227
+ script = IndentingBuffer(u'') # a string buffer object
228
+
229
+ # get date info, in format preferred by current locale as set by app:
230
+ if hasattr(locale, 'nl_langinfo'):
231
+ fmt = locale.nl_langinfo(locale.D_T_FMT)
232
+ localDateTime = data.getDateStr(format=fmt)
233
+ else:
234
+ localDateTime = data.getDateStr(format="%B %d, %Y, at %H:%M")
235
+
236
+ # Remove disabled components, but leave original experiment unchanged.
237
+ self_copy = deepcopy(self)
238
+ for key, routine in list(self_copy.routines.items()): # PY2/3 compat
239
+ if isinstance(routine, BaseStandaloneRoutine):
240
+ if routine.params['disabled']:
241
+ for node in self_copy.flow:
242
+ if node == routine:
243
+ self_copy.flow.removeComponent(node)
244
+ else:
245
+ for component in routine:
246
+ try:
247
+ if component.params['disabled']:
248
+ routine.removeComponent(component)
249
+ except KeyError:
250
+ pass
251
+
252
+ if target == "PsychoPy":
253
+ self_copy.settings.writeInitCode(script, self_copy.psychopyVersion,
254
+ localDateTime)
255
+
256
+ # Write "run once" code sections
257
+ for entry in self_copy.flow:
258
+ # NB each entry is a routine or LoopInitiator/Terminator
259
+ self_copy._currentRoutine = entry
260
+ if hasattr(entry, 'writeRunOnceInitCode'):
261
+ entry.writeRunOnceInitCode(script)
262
+ if hasattr(entry, 'writePreCode'):
263
+ entry.writePreCode(script)
264
+ script.write("\n\n")
265
+
266
+ # present info, make logfile
267
+ self_copy.settings.writeStartCode(script, self_copy.psychopyVersion)
268
+ # writes any components with a writeStartCode()
269
+ self_copy.flow.writeStartCode(script)
270
+ self_copy.settings.writeWindowCode(script) # create our visual.Window()
271
+ self_copy.settings.writeIohubCode(script)
272
+ # for JS the routine begin/frame/end code are funcs so write here
273
+
274
+ # write the rest of the code for the components
275
+ self_copy.flow.writeBody(script)
276
+ self_copy.settings.writeEndCode(script) # close log file
277
+ script = script.getvalue()
278
+
279
+ elif target == "PsychoJS":
280
+ script.oneIndent = " " # use 2 spaces rather than python 4
281
+
282
+ self_copy.settings.writeInitCodeJS(script, self_copy.psychopyVersion,
283
+ localDateTime, modular)
284
+
285
+ script.writeIndentedLines("// Start code blocks for 'Before Experiment'")
286
+ routinesToWrite = list(self_copy.routines)
287
+ for entry in self_copy.flow:
288
+ # NB each entry is a routine or LoopInitiator/Terminator
289
+ self_copy._currentRoutine = entry
290
+ if hasattr(entry, 'writePreCodeJS') and entry.name in routinesToWrite:
291
+ entry.writePreCodeJS(script)
292
+ routinesToWrite.remove(entry.name) # this one's done
293
+
294
+ # Write window code
295
+ self_copy.settings.writeWindowCodeJS(script)
296
+
297
+ self_copy.flow.writeFlowSchedulerJS(script)
298
+ self_copy.settings.writeExpSetupCodeJS(script,
299
+ self_copy.psychopyVersion)
300
+
301
+ # initialise the components for all Routines in a single function
302
+ script.writeIndentedLines("\nasync function experimentInit() {")
303
+ script.setIndentLevel(1, relative=True)
304
+
305
+ # routine init sections
306
+ routinesToWrite = list(self_copy.routines)
307
+ for entry in self_copy.flow:
308
+ # NB each entry is a routine or LoopInitiator/Terminator
309
+ self_copy._currentRoutine = entry
310
+ if hasattr(entry, 'writeInitCodeJS') and entry.name in routinesToWrite:
311
+ entry.writeInitCodeJS(script)
312
+ routinesToWrite.remove(entry.name) # this one's done
313
+
314
+ # create globalClock etc
315
+ code = ("// Create some handy timers\n"
316
+ "globalClock = new util.Clock();"
317
+ " // to track the time since experiment started\n"
318
+ "routineTimer = new util.CountdownTimer();"
319
+ " // to track time remaining of each (non-slip) routine\n"
320
+ "\nreturn Scheduler.Event.NEXT;")
321
+ script.writeIndentedLines(code)
322
+ script.setIndentLevel(-1, relative=True)
323
+ script.writeIndentedLines("}\n")
324
+
325
+ # This differs to the Python script. We can loop through all
326
+ # Routines once (whether or not they get used) because we're using
327
+ # functions that may or may not get called later.
328
+ # Do the Routines of the experiment first
329
+ routinesToWrite = list(self_copy.routines)
330
+ for thisItem in self_copy.flow:
331
+ if thisItem.getType() in ['LoopInitiator', 'LoopTerminator']:
332
+ self_copy.flow.writeLoopHandlerJS(script, modular)
333
+ elif thisItem.name in routinesToWrite:
334
+ self_copy._currentRoutine = self_copy.routines[thisItem.name]
335
+ self_copy._currentRoutine.writeRoutineBeginCodeJS(script, modular)
336
+ self_copy._currentRoutine.writeEachFrameCodeJS(script, modular)
337
+ self_copy._currentRoutine.writeRoutineEndCodeJS(script, modular)
338
+ routinesToWrite.remove(thisItem.name)
339
+ self_copy.settings.writeEndCodeJS(script)
340
+
341
+ # Add JS variable declarations e.g., var msg;
342
+ script = py2js.addVariableDeclarations(script.getvalue(), fileName=self.expPath)
343
+
344
+ # Reset loop controller ready for next call to writeScript
345
+ self_copy.flow._resetLoopController()
346
+
347
+ return script
348
+
349
+ @property
350
+ def xml(self):
351
+ # Create experiment root element
352
+ experimentNode = xml.Element("PsychoPy2experiment")
353
+ experimentNode.set('encoding', 'utf-8')
354
+ experimentNode.set('version', __version__)
355
+ # Add settings node
356
+ settingsNode = self.settings.xml
357
+ experimentNode.append(settingsNode)
358
+ # Add routines node
359
+ routineNode = xml.Element("Routines")
360
+ for key, routine in self.routines.items():
361
+ routineNode.append(routine.xml)
362
+ experimentNode.append(routineNode)
363
+ # Add flow node
364
+ flowNode = self.flow.xml
365
+ experimentNode.append(flowNode)
366
+
367
+ return experimentNode
368
+
369
+ def saveToXML(self, filename):
370
+ self.psychopyVersion = psychopy.__version__ # make sure is current
371
+ # create the dom object
372
+ self.xmlRoot = self.xml
373
+ # convert to a pretty string
374
+ # update our document to use the new root
375
+ self._doc._setroot(self.xmlRoot)
376
+ simpleString = xml.tostring(self.xmlRoot, 'utf-8')
377
+ pretty = minidom.parseString(simpleString).toprettyxml(indent=" ")
378
+ # then write to file
379
+ if not filename.endswith(".psyexp"):
380
+ filename += ".psyexp"
381
+
382
+ with codecs.open(filename, 'wb', encoding='utf-8-sig') as f:
383
+ f.write(pretty)
384
+
385
+ self.filename = filename
386
+ return filename # this may have been updated to include an extension
387
+
388
+ def _getShortName(self, longName):
389
+ return longName.replace('(', '').replace(')', '').replace(' ', '')
390
+
391
+ def _getXMLparam(self, params, paramNode, componentNode=None):
392
+ """params is the dict of params of the builder component
393
+ (e.g. stimulus) into which the parameters will be inserted
394
+ (so the object to store the params should be created first)
395
+ paramNode is the parameter node fetched from the xml file
396
+ """
397
+ name = paramNode.get('name')
398
+ valType = paramNode.get('valType')
399
+ val = paramNode.get('val')
400
+ # many components need web char newline replacement
401
+ if not name == 'advancedParams':
402
+ val = val.replace("&#10;", "\n")
403
+
404
+ # custom settings (to be used when
405
+ if valType == 'fixedList': # convert the string to a list
406
+ try:
407
+ params[name].val = eval('list({})'.format(val))
408
+ except NameError: # if val is a single string it will look like variable
409
+ params[name].val = [val]
410
+ elif name == 'storeResponseTime':
411
+ return # deprecated in v1.70.00 because it was redundant
412
+ elif name == 'nVertices': # up to 1.85 there was no shape param
413
+ # if no shape param then use "n vertices" only
414
+ if _findParam('shape', componentNode) is None:
415
+ if val == '2':
416
+ params['shape'].val = "line"
417
+ elif val == '3':
418
+ params['shape'].val = "triangle"
419
+ elif val == '4':
420
+ params['shape'].val = "rectangle"
421
+ else:
422
+ params['shape'].val = "regular polygon..."
423
+ params['nVertices'].val = val
424
+ elif name == 'startTime': # deprecated in v1.70.00
425
+ params['startType'].val = "{}".format('time (s)')
426
+ params['startVal'].val = "{}".format(val)
427
+ return # times doesn't need to update its type or 'updates' rule
428
+ elif name == 'forceEndTrial': # deprecated in v1.70.00
429
+ params['forceEndRoutine'].val = bool(val)
430
+ return # forceEndTrial doesn't need to update type or 'updates'
431
+ elif name == 'forceEndTrialOnPress': # deprecated in v1.70.00
432
+ params['forceEndRoutineOnPress'].val = bool(val)
433
+ return # forceEndTrial doesn't need to update type or 'updates'
434
+ elif name == 'forceEndRoutineOnPress':
435
+ if val == 'True':
436
+ val = "any click"
437
+ elif val == 'False':
438
+ val = "never"
439
+ params['forceEndRoutineOnPress'].val = val
440
+ return
441
+ elif name == 'trialList': # deprecated in v1.70.00
442
+ params['conditions'].val = eval(val)
443
+ return # forceEndTrial doesn't need to update type or 'updates'
444
+ elif name == 'trialListFile': # deprecated in v1.70.00
445
+ params['conditionsFile'].val = "{}".format(val)
446
+ return # forceEndTrial doesn't need to update type or 'updates'
447
+ elif name == 'duration': # deprecated in v1.70.00
448
+ params['stopType'].val = u'duration (s)'
449
+ params['stopVal'].val = "{}".format(val)
450
+ return # times doesn't need to update its type or 'updates' rule
451
+ elif name == 'allowedKeys' and valType == 'str': # changed v1.70.00
452
+ # ynq used to be allowed, now should be 'y','n','q' or
453
+ # ['y','n','q']
454
+ if len(val) == 0:
455
+ newVal = val
456
+ elif val[0] == '$':
457
+ newVal = val[1:] # they were using code (which we can reuse)
458
+ elif val.startswith('[') and val.endswith(']'):
459
+ # they were using code (slightly incorectly!)
460
+ newVal = val[1:-1]
461
+ elif val in ['return', 'space', 'left', 'right', 'escape']:
462
+ newVal = val # they were using code
463
+ else:
464
+ # convert string to list of keys then represent again as a
465
+ # string!
466
+ newVal = repr(list(val))
467
+ params['allowedKeys'].val = newVal
468
+ params['allowedKeys'].valType = 'code'
469
+ elif name == 'correctIf': # deprecated in v1.60.00
470
+ corrIf = val
471
+ corrAns = corrIf.replace(
472
+ 'resp.keys==unicode(', '').replace(')', '')
473
+ params['correctAns'].val = corrAns
474
+ name = 'correctAns' # then we can fetch other aspects below
475
+ elif 'olour' in name: # colour parameter was Americanised v1.61.00
476
+ name = name.replace('olour', 'olor')
477
+ params[name].val = val
478
+ elif name == 'times': # deprecated in v1.60.00
479
+ times = eval('%s' % val)
480
+ params['startType'].val = "{}".format('time (s)')
481
+ params['startVal'].val = "{}".format(times[0])
482
+ params['stopType'].val = "{}".format('time (s)')
483
+ params['stopVal'].val = "{}".format(times[1])
484
+ return # times doesn't need to update its type or 'updates' rule
485
+ elif name in ('Before Experiment', 'Begin Experiment', 'Begin Routine', 'Each Frame',
486
+ 'End Routine', 'End Experiment',
487
+ 'Before JS Experiment', 'Begin JS Experiment', 'Begin JS Routine', 'Each JS Frame',
488
+ 'End JS Routine', 'End JS Experiment'):
489
+ # up to version 1.78.00 and briefly in 2021.1.0-1.1 these were 'code'
490
+ params[name].val = val
491
+ params[name].valType = 'extendedCode'
492
+ return # so that we don't update valType again below
493
+ elif name == 'Saved data folder':
494
+ # deprecated in 1.80 for more complete data filename control
495
+ params[name] = Param(
496
+ val, valType='code', allowedTypes=[],
497
+ hint=_translate("Name of the folder in which to save data"
498
+ " and log files (blank defaults to the "
499
+ "builder pref)"),
500
+ categ='Data')
501
+ elif name == 'channel': # was incorrectly set to be valType='str' until 3.1.2
502
+ params[name].val = val
503
+ params[name].valType = 'code' # override
504
+ elif 'val' in list(paramNode.keys()):
505
+ if val == 'window units': # changed this value in 1.70.00
506
+ params[name].val = 'from exp settings'
507
+ # in v1.80.00, some RatingScale API and Param fields were changed
508
+ # Try to avoid a KeyError in these cases so can load the expt
509
+ elif name in ('choiceLabelsAboveLine', 'lowAnchorText',
510
+ 'highAnchorText'):
511
+ # not handled, just ignored; want labels=[lowAnchor,
512
+ # highAnchor]
513
+ return
514
+ elif name == 'customize_everything':
515
+ # Try to auto-update the code:
516
+ v = val # python code, not XML
517
+ v = v.replace('markerStyle', 'marker').replace(
518
+ 'customMarker', 'marker')
519
+ v = v.replace('stretchHoriz', 'stretch').replace(
520
+ 'displaySizeFactor', 'size')
521
+ v = v.replace('textSizeFactor', 'textSize')
522
+ v = v.replace('ticksAboveLine=False', 'tickHeight=-1')
523
+ v = v.replace('showScale=False', 'scale=None').replace(
524
+ 'allowSkip=False', 'skipKeys=None')
525
+ v = v.replace('showAnchors=False', 'labels=None')
526
+ # lowAnchorText highAnchorText will trigger obsolete error
527
+ # when run the script
528
+ params[name].val = v
529
+ elif name == 'storeResponseTime':
530
+ return # deprecated in v1.70.00 because it was redundant
531
+ elif name == 'Resources':
532
+ # if the xml import hasn't automatically converted from string?
533
+ if type(val) == str:
534
+ resources = data.utils.listFromString(val)
535
+ if self.psychopyVersion == '2020.2.5':
536
+ # in 2020.2.5 only, problems were:
537
+ # a) resources list was saved as a string and
538
+ # b) with wrong root folder
539
+ resList = []
540
+ for resourcePath in resources:
541
+ # doing this the blunt way but should we check for existence?
542
+ resourcePath = resourcePath.replace("../", "") # it was created using wrong root
543
+ resourcePath = resourcePath.replace("\\", "/") # created using windows \\
544
+ resList.append(resourcePath)
545
+ resources = resList # push our new list back to resources
546
+ params[name].val = resources
547
+ else:
548
+ if name in params:
549
+ params[name].val = val
550
+ else:
551
+ # we found an unknown parameter (probably from the future)
552
+ params[name] = Param(
553
+ val, valType=paramNode.get('valType'),
554
+ allowedTypes=[], label=_translate(name),
555
+ hint=_translate(
556
+ "This parameter is not known by this version "
557
+ "of PsychoPy. It might be worth upgrading"))
558
+ params[name].allowedTypes = paramNode.get('allowedTypes')
559
+ if params[name].allowedTypes is None:
560
+ params[name].allowedTypes = []
561
+ params[name].readOnly = True
562
+ if name not in legacyParams + ['JS libs', 'OSF Project ID']:
563
+ # don't warn people if we know it's OK (e.g. for params
564
+ # that have been removed
565
+ msg = _translate(
566
+ "Parameter %r is not known to this version of "
567
+ "PsychoPy but has come from your experiment file "
568
+ "(saved by a future version of PsychoPy?). This "
569
+ "experiment may not run correctly in the current "
570
+ "version.")
571
+ logging.warn(msg % name)
572
+ logging.flush()
573
+
574
+ # get the value type and update rate
575
+ if 'valType' in list(paramNode.keys()):
576
+ params[name].valType = paramNode.get('valType')
577
+ # compatibility checks:
578
+ if name in ['allowedKeys'] and paramNode.get('valType') == 'str':
579
+ # these components were changed in v1.70.00
580
+ params[name].valType = 'code'
581
+ elif name == 'Selected rows':
582
+ # changed in 1.81.00 from 'code' to 'str': allow string or var
583
+ params[name].valType = 'str'
584
+ # conversions based on valType
585
+ if params[name].valType == 'bool':
586
+ params[name].val = eval("%s" % params[name].val)
587
+ if 'updates' in list(paramNode.keys()):
588
+ params[name].updates = paramNode.get('updates')
589
+
590
+ def loadFromXML(self, filename):
591
+ """Loads an xml file and parses the builder Experiment from it
592
+ """
593
+ self._doc.parse(filename)
594
+ root = self._doc.getroot()
595
+
596
+
597
+ # some error checking on the version (and report that this isn't valid
598
+ # .psyexp)?
599
+ filenameBase = os.path.basename(filename)
600
+
601
+ if root.tag != "PsychoPy2experiment":
602
+ logging.error('%s is not a valid .psyexp file, "%s"' %
603
+ (filenameBase, root.tag))
604
+ # the current exp is already vaporized at this point, oops
605
+ return
606
+ self.psychopyVersion = root.get('version')
607
+ # If running an experiment from a future version, send alert to change "Use Version"
608
+ if parse_version(psychopy.__version__) < parse_version(self.psychopyVersion):
609
+ alert(code=4051, strFields={'version': self.psychopyVersion})
610
+ # If versions are either side of 2021, send alert
611
+ if parse_version(psychopy.__version__) >= parse_version("2021.1.0") > parse_version(self.psychopyVersion):
612
+ alert(code=4052, strFields={'version': self.psychopyVersion})
613
+
614
+ # Parse document nodes
615
+ # first make sure we're empty
616
+ self.flow = Flow(exp=self) # every exp has exactly one flow
617
+ self.routines = {}
618
+ self.namespace = NameSpace(self) # start fresh
619
+ modifiedNames = []
620
+ duplicateNames = []
621
+
622
+ # fetch exp settings
623
+ settingsNode = root.find('Settings')
624
+ for child in settingsNode:
625
+ self._getXMLparam(params=self.settings.params, paramNode=child,
626
+ componentNode=settingsNode)
627
+ # name should be saved as a settings parameter (only from 1.74.00)
628
+ if self.settings.params['expName'].val in ['', None, 'None']:
629
+ shortName = os.path.splitext(filenameBase)[0]
630
+ self.setExpName(shortName)
631
+ # fetch routines
632
+ routinesNode = root.find('Routines')
633
+ allCompons = getAllComponents(
634
+ self.prefsBuilder['componentsFolders'], fetchIcons=False)
635
+ allRoutines = getAllStandaloneRoutines(fetchIcons=False)
636
+ # get each routine node from the list of routines
637
+ for routineNode in routinesNode:
638
+ if routineNode.tag == "Routine":
639
+ routineGoodName = self.namespace.makeValid(
640
+ routineNode.get('name'))
641
+ if routineGoodName != routineNode.get('name'):
642
+ modifiedNames.append(routineNode.get('name'))
643
+ self.namespace.user.append(routineGoodName)
644
+ routine = Routine(name=routineGoodName, exp=self)
645
+ # self._getXMLparam(params=routine.params, paramNode=routineNode)
646
+ self.routines[routineNode.get('name')] = routine
647
+ for componentNode in routineNode:
648
+
649
+ componentType = componentNode.tag
650
+ if componentType in allCompons:
651
+ # create an actual component of that type
652
+ component = allCompons[componentType](
653
+ name=componentNode.get('name'),
654
+ parentName=routineNode.get('name'), exp=self)
655
+ else:
656
+ # create UnknownComponent instead
657
+ component = allCompons['UnknownComponent'](
658
+ name=componentNode.get('name'),
659
+ parentName=routineNode.get('name'), exp=self)
660
+ # check for components that were absent in older versions of
661
+ # the builder and change the default behavior
662
+ # (currently only the new behavior of choices for RatingScale,
663
+ # HS, November 2012)
664
+ # HS's modification superceded Jan 2014, removing several
665
+ # RatingScale options
666
+ if componentType == 'RatingScaleComponent':
667
+ if (componentNode.get('choiceLabelsAboveLine') or
668
+ componentNode.get('lowAnchorText') or
669
+ componentNode.get('highAnchorText')):
670
+ pass
671
+ # if not componentNode.get('choiceLabelsAboveLine'):
672
+ # # this rating scale was created using older version
673
+ # component.params['choiceLabelsAboveLine'].val=True
674
+ # populate the component with its various params
675
+ for paramNode in componentNode:
676
+ self._getXMLparam(params=component.params,
677
+ paramNode=paramNode,
678
+ componentNode=componentNode)
679
+ compGoodName = self.namespace.makeValid(
680
+ componentNode.get('name'))
681
+ if compGoodName != componentNode.get('name'):
682
+ modifiedNames.append(componentNode.get('name'))
683
+ self.namespace.add(compGoodName)
684
+ component.params['name'].val = compGoodName
685
+ routine.append(component)
686
+ else:
687
+ if routineNode.tag in allRoutines:
688
+ # If not a routine, may be a standalone routine
689
+ routine = allRoutines[routineNode.tag](exp=self, name=routineNode.get('name'))
690
+ else:
691
+ # Otherwise treat as unknown
692
+ routine = allRoutines['UnknownRoutine'](exp=self, name=routineNode.get('name'))
693
+ # Apply all params
694
+ for paramNode in routineNode:
695
+ if paramNode.tag == "Param":
696
+ for key, val in paramNode.items():
697
+ setattr(routine.params[paramNode.get("name")], key, val)
698
+ # Add routine to experiment
699
+ self.addStandaloneRoutine(routine.name, routine)
700
+ # for each component that uses a Static for updates, we need to set
701
+ # that
702
+ for thisRoutine in list(self.routines.values()):
703
+ for thisComp in thisRoutine:
704
+ for thisParamName in thisComp.params:
705
+ thisParam = thisComp.params[thisParamName]
706
+ if thisParamName == 'advancedParams':
707
+ continue # advanced isn't a normal param
708
+ elif thisParam.updates and "during:" in thisParam.updates:
709
+ # remove the part that says 'during'
710
+ updates = thisParam.updates.split(': ')[1]
711
+ routine, static = updates.split('.')
712
+ if routine not in self.routines:
713
+ msg = ("%s was set to update during %s Static "
714
+ "Component, but that component no longer "
715
+ "exists")
716
+ logging.warning(msg % (thisParamName, static))
717
+ else:
718
+ self.routines[routine].getComponentFromName(
719
+ static).addComponentUpdate(
720
+ thisRoutine.params['name'],
721
+ thisComp.params['name'], thisParamName)
722
+ # fetch flow settings
723
+ flowNode = root.find('Flow')
724
+ loops = {}
725
+ for elementNode in flowNode:
726
+ if elementNode.tag == "LoopInitiator":
727
+ loopType = elementNode.get('loopType')
728
+ loopName = self.namespace.makeValid(elementNode.get('name'))
729
+ if loopName != elementNode.get('name'):
730
+ modifiedNames.append(elementNode.get('name'))
731
+ self.namespace.add(loopName)
732
+ loop = eval('%s(exp=self,name="%s")' % (loopType, loopName))
733
+ loops[loopName] = loop
734
+ for paramNode in elementNode:
735
+ self._getXMLparam(paramNode=paramNode, params=loop.params)
736
+ # for conditions convert string rep to list of dicts
737
+ if paramNode.get('name') == 'conditions':
738
+ param = loop.params['conditions']
739
+ # e.g. param.val=[{'ori':0},{'ori':3}]
740
+ try:
741
+ param.val = eval('%s' % (param.val))
742
+ except SyntaxError:
743
+ # This can occur if Python2.7 conditions string
744
+ # contained long ints (e.g. 8L) and these can't be
745
+ # parsed by Py3. But allow the file to carry on
746
+ # loading and the conditions will still be loaded
747
+ # from the xlsx file
748
+ pass
749
+ # get condition names from within conditionsFile, if any:
750
+ try:
751
+ # psychophysicsstaircase demo has no such param
752
+ conditionsFile = loop.params['conditionsFile'].val
753
+ except Exception:
754
+ conditionsFile = None
755
+ if conditionsFile in ['None', '']:
756
+ conditionsFile = None
757
+ if conditionsFile:
758
+ try:
759
+ trialList, fieldNames = data.importConditions(
760
+ conditionsFile, returnFieldNames=True)
761
+ for fname in fieldNames:
762
+ if fname != self.namespace.makeValid(fname):
763
+ duplicateNames.append(fname)
764
+ else:
765
+ self.namespace.add(fname)
766
+ except Exception:
767
+ pass # couldn't load the conditions file for now
768
+ self.flow.append(LoopInitiator(loop=loops[loopName]))
769
+ elif elementNode.tag == "LoopTerminator":
770
+ self.flow.append(LoopTerminator(
771
+ loop=loops[elementNode.get('name')]))
772
+ else:
773
+ if elementNode.get('name') in self.routines:
774
+ self.flow.append(self.routines[elementNode.get('name')])
775
+ else:
776
+ logging.error("A Routine called '{}' was on the Flow but "
777
+ "could not be found (failed rename?). You "
778
+ "may need to re-insert it".format(
779
+ elementNode.get('name')))
780
+ logging.flush()
781
+
782
+ if modifiedNames:
783
+ msg = 'duplicate variable name(s) changed in loadFromXML: %s\n'
784
+ logging.warning(msg % ', '.join(list(set(modifiedNames))))
785
+ if duplicateNames:
786
+ msg = 'duplicate variable names: %s'
787
+ logging.warning(msg % ', '.join(list(set(duplicateNames))))
788
+ # Modernise params
789
+ for rt in self.routines.values():
790
+ if not isinstance(rt, list):
791
+ # Treat standalone routines as a routine with one component
792
+ rt = [rt]
793
+ for comp in rt:
794
+ # For each param, if it's pointed to in the forceType array, set it to the new valType
795
+ for paramName, param in comp.params.items():
796
+ # Param pointed to by name
797
+ if paramName in forceType:
798
+ param.valType = forceType[paramName]
799
+ if (type(comp).__name__, paramName) in forceType:
800
+ param.valType = forceType[(type(comp).__name__, paramName)]
801
+
802
+ # if we succeeded then save current filename to self
803
+ self.filename = filename
804
+
805
+ def setExpName(self, name):
806
+ self.settings.params['expName'].val = name
807
+
808
+ def getExpName(self):
809
+ return self.settings.params['expName'].val
810
+
811
+ @property
812
+ def htmlFolder(self):
813
+ return self.settings.params['HTML path'].val
814
+
815
+ def getComponentFromName(self, name):
816
+ """Searches all the Routines in the Experiment for a matching Comp name
817
+
818
+ :param name: str name of a component
819
+ :return: a component class or None
820
+ """
821
+ for routine in self.routines.values():
822
+ comp = routine.getComponentFromName(name)
823
+ if comp:
824
+ return comp
825
+ return None
826
+
827
+ def getComponentFromType(self, type):
828
+ """Searches all the Routines in the Experiment for a matching component type
829
+
830
+ :param name: str type of a component e.g., 'KeyBoard'
831
+ :return: True if component exists in experiment
832
+ """
833
+ for routine in self.routines.values():
834
+ exists = routine.getComponentFromType(type)
835
+ if exists:
836
+ return True
837
+ return False
838
+
839
+ def getResourceFiles(self):
840
+ """Returns a list of known files needed for the experiment
841
+ Interrogates each loop looking for conditions files and each
842
+
843
+ """
844
+ join = os.path.join
845
+ abspath = os.path.abspath
846
+ srcRoot = os.path.split(self.filename)[0]
847
+
848
+ def getPaths(filePath):
849
+ """Helper to return absolute and relative paths (or None)
850
+
851
+ :param filePath: str to a potential file path (rel or abs)
852
+ :return: dict of 'asb' and 'rel' paths or None
853
+ """
854
+ # Only construct paths if filePath is a string
855
+ if type(filePath) != str:
856
+ return None
857
+
858
+ thisFile = {}
859
+ # NB: Pathlib might be neater here but need to be careful
860
+ # e.g. on mac:
861
+ # Path('C:/test/test.xlsx').is_absolute() returns False
862
+ # Path('/folder/file.xlsx').relative_to('/Applications') gives error
863
+ # but os.path.relpath('/folder/file.xlsx', '/Applications') correctly uses ../
864
+ if len(filePath) > 2 and (filePath[0] == "/" or filePath[1] == ":")\
865
+ and os.path.isfile(filePath):
866
+ thisFile['abs'] = filePath
867
+ thisFile['rel'] = os.path.relpath(filePath, srcRoot)
868
+ return thisFile
869
+ else:
870
+ thisFile['rel'] = filePath
871
+ thisFile['abs'] = os.path.normpath(join(srcRoot, filePath))
872
+ if len(thisFile['abs']) <= 256 and os.path.isfile(thisFile['abs']):
873
+ return thisFile
874
+
875
+ def findPathsInFile(filePath):
876
+ """Recursively search a conditions file (xlsx or csv)
877
+ extracting valid file paths in any param/cond
878
+
879
+ :param filePath: str to a potential file path (rel or abs)
880
+ :return: list of dicts{'rel','abs'} of valid file paths
881
+ """
882
+ # Clean up filePath that cannot be eval'd
883
+ if filePath.startswith('$'):
884
+ try:
885
+ filePath = filePath.strip('$')
886
+ filePath = eval(filePath)
887
+ except NameError:
888
+ # List files in directory and get condition files
889
+ if 'xlsx' in filePath or 'xls' in filePath or 'csv' in filePath:
890
+ # Get all xlsx and csv files
891
+ expFolder = Path(self.filename).parent
892
+ spreadsheets = []
893
+ for pattern in ['*.xlsx', '*.xls', '*.csv', '*.tsv']:
894
+ # NB potentially make this search recursive with
895
+ # '**/*.xlsx' but then need to exclude 'data/*.xlsx'
896
+ spreadsheets.extend(expFolder.glob(pattern))
897
+ files = []
898
+ for condFile in spreadsheets:
899
+ # call the function recursively for each excel file
900
+ files.extend(findPathsInFile(str(condFile)))
901
+ return files
902
+
903
+ paths = []
904
+ # is it a file?
905
+ thisFile = getPaths(filePath) # get the abs/rel paths
906
+ # does it exist?
907
+ if not thisFile:
908
+ return paths
909
+ # OK, this file itself is valid so add to resources
910
+ if thisFile not in paths:
911
+ paths.append(thisFile)
912
+ # does it look at all like an excel file?
913
+ if (not isinstance(filePath, str)
914
+ or not os.path.splitext(filePath)[1] in ['.csv', '.xlsx',
915
+ '.xls']):
916
+ return paths
917
+ conds = data.importConditions(thisFile['abs']) # load the abs path
918
+ for thisCond in conds: # thisCond is a dict
919
+ for param, val in list(thisCond.items()):
920
+ if isinstance(val, str) and len(val):
921
+ # only add unique entries (can't use set() on a dict)
922
+ for thisFile in findPathsInFile(val):
923
+ if thisFile not in paths:
924
+ paths.append(thisFile)
925
+
926
+ return paths
927
+
928
+ resources = []
929
+ for thisEntry in self.flow:
930
+ if thisEntry.getType() == 'LoopInitiator':
931
+ # find all loops and check for conditions filename
932
+ params = thisEntry.loop.params
933
+ if 'conditionsFile' in params:
934
+ condsPaths = findPathsInFile(params['conditionsFile'].val)
935
+ resources.extend(condsPaths)
936
+ elif thisEntry.getType() == 'Routine':
937
+ # find all params of all compons and check if valid filename
938
+ for thisComp in thisEntry:
939
+ for paramName in thisComp.params:
940
+ thisParam = thisComp.params[paramName]
941
+ thisFile = ''
942
+ if isinstance(thisParam, str):
943
+ thisFile = getPaths(thisParam)
944
+ elif isinstance(thisParam.val, str):
945
+ thisFile = getPaths(thisParam.val)
946
+ # then check if it's a valid path and not yet included
947
+ if thisFile and thisFile not in resources:
948
+ resources.append(thisFile)
949
+
950
+ # Add files from additional resources box
951
+ val = self.settings.params['Resources'].val
952
+ for thisEntry in val:
953
+ thisFile = getPaths(thisEntry)
954
+ if thisFile:
955
+ resources.append(thisFile)
956
+ # Check for any resources not in experiment path
957
+ for res in resources:
958
+ if srcRoot not in res['abs']:
959
+ psychopy.logging.warning("{} is not in the experiment path and "
960
+ "so will not be copied to Pavlovia"
961
+ .format(res['rel']))
962
+
963
+ return resources
964
+
965
+
966
+ class ExpFile(list):
967
+ """An ExpFile is similar to a Routine except that it generates its code
968
+ from the Flow of a separate, complete psyexp file.
969
+ """
970
+
971
+ def __init__(self, name, exp, filename=''):
972
+ super(ExpFile, self).__init__()
973
+ self.params = {'name': name}
974
+ self.name = name
975
+ self.exp = exp # the exp we belong to
976
+ # the experiment we represent on disk (see self.loadExp)
977
+ self.expObject = None
978
+ self.filename = filename
979
+ self._clockName = None # used in script "t = trialClock.GetTime()"
980
+ self.type = 'ExpFile'
981
+
982
+ def __repr__(self):
983
+ _rep = "psychopy.experiment.ExpFile(name='%s',exp=%s,filename='%s')"
984
+ return _rep % (self.name, self.exp, self.filename)
985
+
986
+ def writeStartCode(self, buff):
987
+ # tell each object on our flow to write its start code
988
+ for entry in self.flow:
989
+ # NB each entry is a routine or LoopInitiator/Terminator
990
+ self._currentRoutine = entry
991
+ if hasattr(entry, 'writeStartCode'):
992
+ entry.writeStartCode(buff)
993
+
994
+ def loadExp(self):
995
+ # fetch the file
996
+ self.expObject = Experiment()
997
+ self.expObject.loadFromXML(self.filename)
998
+ # extract the flow, which is the key part for us:
999
+ self.flow = self.expObject.flow
1000
+
1001
+ def writeInitCode(self, buff):
1002
+ # tell each object on our flow to write its init code
1003
+ for entry in self.flow:
1004
+ # NB each entry is a routine or LoopInitiator/Terminator
1005
+ self._currentRoutine = entry
1006
+ entry.writeInitCode(buff)
1007
+
1008
+ def writeMainCode(self, buff):
1009
+ """This defines the code for the frames of a single routine
1010
+ """
1011
+ # tell each object on our flow to write its run code
1012
+ for entry in self.flow:
1013
+ self._currentRoutine = entry
1014
+ entry.writeMainCode(buff)
1015
+
1016
+ def writeExperimentEndCode(self, buff):
1017
+ """This defines the code for the frames of a single routine
1018
+ """
1019
+ for entry in self.flow:
1020
+ self._currentRoutine = entry
1021
+ entry.writeExperimentEndCode(buff)
1022
+
1023
+ def getType(self):
1024
+ return 'ExpFile'
1025
+
1026
+ def getMaxTime(self):
1027
+ """What the last (predetermined) stimulus time to be presented. If
1028
+ there are no components or they have code-based times then will
1029
+ default to 10secs
1030
+ """
1031
+ pass
1032
+ # todo?: currently only Routines perform this action