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.
- psychopy/.DS_Store +0 -0
- psychopy/GIT_SHA +1 -1
- psychopy/VERSION +1 -1
- psychopy/__init__.py +10 -1
- psychopy/__init__.py.orig +65 -0
- psychopy/app/{locale/ar_001/.DS_Store → .DS_Store} +0 -0
- psychopy/app/Resources/.DS_Store +0 -0
- psychopy/app/_psychopyApp.py +11 -3
- psychopy/app/appData.spec +1 -1
- psychopy/app/builder/builder.py +1 -1
- psychopy/app/builder/builder.py.orig +3932 -0
- psychopy/app/builder/dialogs/__init__.py.orig +1679 -0
- psychopy/app/builder/dialogs/paramCtrls.py +1 -1
- psychopy/app/builder/dialogs/paramCtrls.py.orig +713 -0
- psychopy/app/colorpicker/__init__.py.orig +411 -0
- psychopy/app/cortex.log +0 -0
- psychopy/app/jobs.py +8 -1
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +2452 -1731
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.po +6127 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.po +7366 -0
- psychopy/app/plugin_manager/dialog.py +9 -7
- psychopy/app/ribbon.py +2 -1
- psychopy/app/runner/runner.py +7 -5
- psychopy/clock.py +8 -4
- psychopy/core.py.orig +169 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/index.html +23 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks-legacy-browsers.js +423 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks.js +427 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/chooseBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/facesBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/housesBlock.xlsx +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face01.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face02.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face03.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house01.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house02.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house03.jpg +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks.py +330 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks_lastrun.py +330 -0
- psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xml +298 -0
- psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xsd +120 -0
- psychopy/demos/builder/Tools/.DS_Store +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/.DS_Store +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.csv +38 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.log +3418 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.psydat +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.csv +2 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.log +15 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.psydat +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.psyexp +323 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.py +562 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual_lastrun.py +562 -0
- psychopy/demos/builder/Tools/gammaCalibration/questStairs.xlsx +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/readme.md +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/low_contrast.png +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/make_2nd_order_tex.py +59 -0
- psychopy/demos/builder/Tools/gammaCalibration/resources/second_order_tex.png +0 -0
- psychopy/demos/coder/.DS_Store +0 -0
- psychopy/demos/coder/experiment control/info_gamma.pickle +0 -0
- psychopy/demos/coder/iohub/.iohpid +1 -0
- psychopy/demos/coder/iohub/eyetracking/.iohpid +1 -0
- psychopy/demos/coder/iohub/wintab/.DS_Store +0 -0
- psychopy/demos/coder/stimuli/.DS_Store +0 -0
- psychopy/demos/coder/stimuli/radialGratingContracting.py +29 -0
- psychopy/experiment/_experiment.py.orig +1032 -0
- psychopy/experiment/components/.DS_Store +0 -0
- psychopy/experiment/components/_base.py +13 -4
- psychopy/experiment/components/_base.py.orig +823 -0
- psychopy/experiment/components/form/.DS_Store +0 -0
- psychopy/experiment/components/microphone/__init__.py +10 -1
- psychopy/experiment/components/microphone/__init__.py.orig +490 -0
- psychopy/experiment/components/polygon/__init__.py +21 -22
- psychopy/experiment/components/settings/__init__.py +13 -14
- psychopy/experiment/components/settings/__init__.py.orig +1337 -0
- psychopy/experiment/components/textbox/__init__.py.orig +310 -0
- psychopy/experiment/components/webcam/.DS_Store +0 -0
- psychopy/experiment/components/webcam/light/.DS_Store +0 -0
- psychopy/experiment/flow.py +10 -8
- psychopy/experiment/loops.py.orig +829 -0
- psychopy/experiment/params.py +8 -3
- psychopy/experiment/params.py.orig +408 -0
- psychopy/experiment/routine.py.orig +503 -0
- psychopy/experiment/routines/_base.py +15 -6
- psychopy/experiment/routines/counterbalance/__init__.py +1 -0
- psychopy/gui/qtgui.py +14 -7
- psychopy/gui/util.py +10 -14
- psychopy/gui/wxgui.py +10 -4
- psychopy/hardware/.DS_Store +0 -0
- psychopy/hardware/brainproducts.py.orig +680 -0
- psychopy/hardware/iolab.py.orig +238 -0
- psychopy/hardware/manager.py +1 -1
- psychopy/hardware/photodiode.py +59 -27
- psychopy/hardware/serialport.py +51 -0
- psychopy/hardware/speaker.py +4 -4
- psychopy/iohub/datastore/__init__.py.orig +443 -0
- psychopy/iohub/datastore/util.py.orig +692 -0
- psychopy/iohub/devices/mouse/darwin.py.orig +427 -0
- psychopy/iohub/devices/mouse/linux2.py.orig +198 -0
- psychopy/preferences/.DS_Store +0 -0
- psychopy/projects/pavlovia.py +10 -3
- psychopy/projects/pavlovia.py.orig +1295 -0
- psychopy/sound/backend_ptb.py +22 -5
- psychopy/sound/transcribe.py +24 -4
- psychopy/tests/.DS_Store +0 -0
- psychopy/tests/data/.DS_Store +0 -0
- psychopy/tests/data/TestCircle_fill_local.png +0 -0
- psychopy/tests/data/__test.png +0 -0
- psychopy/tests/data/aperture1_normHexbackground_local.png +0 -0
- psychopy/tests/data/aperture1_norm_local.png +0 -0
- psychopy/tests/data/aperture2_normHexbackground_local.png +0 -0
- psychopy/tests/data/beatandrcos_height_local.png +0 -0
- psychopy/tests/data/beatandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/beatandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/beatandrcos_norm_local.png +0 -0
- psychopy/tests/data/beatandrcos_stencil_local.png +0 -0
- psychopy/tests/data/blend_add_height_local.png +0 -0
- psychopy/tests/data/blend_add_normAddBlend_local.png +0 -0
- psychopy/tests/data/blend_add_normHexbackground_local.png +0 -0
- psychopy/tests/data/blend_add_normNoShade_local.png +0 -0
- psychopy/tests/data/blend_add_norm_local.png +0 -0
- psychopy/tests/data/blend_add_stencil_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_height_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normAddBlend_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normHexbackground_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normNoShade_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_norm_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_stencil_local.png +0 -0
- psychopy/tests/data/circleHex_height_local.png +0 -0
- psychopy/tests/data/circleHex_normAddBlend_local.png +0 -0
- psychopy/tests/data/circleHex_normHexbackground_local.png +0 -0
- psychopy/tests/data/circleHex_normNoShade_local.png +0 -0
- psychopy/tests/data/circleHex_norm_local.png +0 -0
- psychopy/tests/data/circleHex_stencil_local.png +0 -0
- psychopy/tests/data/color_comparison_local.png +0 -0
- psychopy/tests/data/corrFullRandom_local.csv +16 -0
- psychopy/tests/data/corrFullRandom_local.tsv +6 -0
- psychopy/tests/data/correctScript/.DS_Store +0 -0
- psychopy/tests/data/dots_height_local.png +0 -0
- psychopy/tests/data/dots_normAddBlend_local.png +0 -0
- psychopy/tests/data/dots_normHexbackground_local.png +0 -0
- psychopy/tests/data/dots_normNoShade_local.png +0 -0
- psychopy/tests/data/dots_norm_local.png +0 -0
- psychopy/tests/data/dots_stencil_local.png +0 -0
- psychopy/tests/data/elarray1_height_local.png +0 -0
- psychopy/tests/data/elarray1_normAddBlend_local.png +0 -0
- psychopy/tests/data/elarray1_normHexbackground_local.png +0 -0
- psychopy/tests/data/elarray1_norm_local.png +0 -0
- psychopy/tests/data/elarray1_stencil_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_height_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_norm_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_stencil_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_height_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_norm_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_stencil_local.png +0 -0
- psychopy/tests/data/gabor1_height_local.png +0 -0
- psychopy/tests/data/gabor1_normAddBlend_local.png +0 -0
- psychopy/tests/data/gabor1_normHexbackground_local.png +0 -0
- psychopy/tests/data/gabor1_normNoShade_local.png +0 -0
- psychopy/tests/data/gabor1_norm_local.png +0 -0
- psychopy/tests/data/gabor1_stencil_local.png +0 -0
- psychopy/tests/data/greyscale_normHexbackground_local.png +0 -0
- psychopy/tests/data/imageAndGauss_height_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normAddBlend_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normHexbackground_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normNoShade_local.png +0 -0
- psychopy/tests/data/imageAndGauss_norm_local.png +0 -0
- psychopy/tests/data/imageAndGauss_stencil_local.png +0 -0
- psychopy/tests/data/movFrame1_stencil_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_height_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normNoShade_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_norm_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_stencil_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_height_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normNoShade_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_norm_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_stencil_local.png +0 -0
- psychopy/tests/data/numpyImage_height_local.png +0 -0
- psychopy/tests/data/numpyImage_normAddBlend_local.png +0 -0
- psychopy/tests/data/numpyImage_normHexbackground_local.png +0 -0
- psychopy/tests/data/numpyImage_normNoShade_local.png +0 -0
- psychopy/tests/data/numpyImage_norm_local.png +0 -0
- psychopy/tests/data/numpyImage_stencil_local.png +0 -0
- psychopy/tests/data/shape2_1_normAddBlend_local.png +0 -0
- psychopy/tests/data/shape2_1_normHexbackground_local.png +0 -0
- psychopy/tests/data/shape2_1_normNoShade_local.png +0 -0
- psychopy/tests/data/shape2_1_norm_local.png +0 -0
- psychopy/tests/data/shape2_1_stencil_local.png +0 -0
- psychopy/tests/data/testLoopsBlocks.psyexp_local.py +328 -0
- psychopy/tests/data/text1_height_local.png +0 -0
- psychopy/tests/data/text1_normAddBlend_local.png +0 -0
- psychopy/tests/data/text1_normHexbackground_local.png +0 -0
- psychopy/tests/data/text1_norm_local.png +0 -0
- psychopy/tests/data/text1_stencil_local.png +0 -0
- psychopy/tests/data/text2_height.png +0 -0
- psychopy/tests/data/text2_normAddBlend.png +0 -0
- psychopy/tests/data/text2_normHexbackground.png +0 -0
- psychopy/tests/data/text2_stencil.png +0 -0
- psychopy/tests/data/wedge1_height_local.png +0 -0
- psychopy/tests/data/wedge1_normAddBlend_local.png +0 -0
- psychopy/tests/data/wedge1_normHexbackground_local.png +0 -0
- psychopy/tests/data/wedge1_normNoShade_local.png +0 -0
- psychopy/tests/data/wedge1_norm_local.png +0 -0
- psychopy/tests/data/wedge1_stencil_local.png +0 -0
- psychopy/tests/test_app/.DS_Store +0 -0
- psychopy/tests/test_app/test_builder/.DS_Store +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.csv +9 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.log +177 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.psydat +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.xlsx +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.csv +9 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.log +168 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.psydat +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.xlsx +0 -0
- psychopy/tests/test_data/.DS_Store +0 -0
- psychopy/tests/test_hardware/test_CRS_BitsSharp.py.orig +68 -0
- psychopy/tests/test_tools/test_arraytools.py +112 -0
- psychopy/tests/test_visual/test_image.py.orig +219 -0
- psychopy/tools/arraytools.py +47 -0
- psychopy/tools/versionchooser.py +1 -1
- psychopy/visual/backends/pygletbackend.py +26 -8
- psychopy/visual/basevisual.py.orig +1723 -0
- psychopy/visual/form.py.orig +1181 -0
- psychopy/visual/text.py.orig +752 -0
- psychopy/visual/textbox2/textbox2.py.orig +1315 -0
- psychopy/visual/window.py +13 -5
- psychopy/visual/windowwarp.py.orig +463 -0
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/METADATA +9 -9
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/RECORD +244 -78
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/WHEEL +1 -1
- {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/entry_points.txt +2 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
- psychopy-2024.2.1.dist-info/licenses/AUTHORS.md +0 -138
- /psychopy/{app/locale/ar_001/LC_MESSAGE → demos/builder}/.DS_Store +0 -0
- /psychopy/{app/locale/es_ES/LC_MESSAGE → demos/builder/Experiments}/.DS_Store +0 -0
- /psychopy/{visual → demos/builder/Tools/gammaCalibration/data}/.DS_Store +0 -0
- {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(" ", "\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
|