psychopy 2025.1.1__py3-none-any.whl → 2025.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of psychopy might be problematic. Click here for more details.
- psychopy/VERSION +1 -1
- psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
- psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
- psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
- psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
- psychopy/app/Resources/classic/add_many.png +0 -0
- psychopy/app/Resources/classic/add_many@2x.png +0 -0
- psychopy/app/Resources/classic/devices.png +0 -0
- psychopy/app/Resources/classic/devices@2x.png +0 -0
- psychopy/app/Resources/classic/photometer.png +0 -0
- psychopy/app/Resources/classic/photometer@2x.png +0 -0
- psychopy/app/Resources/dark/add_many.png +0 -0
- psychopy/app/Resources/dark/add_many@2x.png +0 -0
- psychopy/app/Resources/dark/devices.png +0 -0
- psychopy/app/Resources/dark/devices@2x.png +0 -0
- psychopy/app/Resources/dark/photometer.png +0 -0
- psychopy/app/Resources/dark/photometer@2x.png +0 -0
- psychopy/app/Resources/light/add_many.png +0 -0
- psychopy/app/Resources/light/add_many@2x.png +0 -0
- psychopy/app/Resources/light/devices.png +0 -0
- psychopy/app/Resources/light/devices@2x.png +0 -0
- psychopy/app/Resources/light/photometer.png +0 -0
- psychopy/app/Resources/light/photometer@2x.png +0 -0
- psychopy/app/_psychopyApp.py +35 -13
- psychopy/app/builder/builder.py +88 -35
- psychopy/app/builder/dialogs/__init__.py +69 -220
- psychopy/app/builder/dialogs/dlgsCode.py +29 -8
- psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
- psychopy/app/builder/validators.py +25 -17
- psychopy/app/coder/coder.py +12 -1
- psychopy/app/coder/repl.py +5 -2
- psychopy/app/colorpicker/__init__.py +1 -1
- psychopy/app/deviceManager/__init__.py +1 -0
- psychopy/app/deviceManager/addDialog.py +218 -0
- psychopy/app/deviceManager/dialog.py +185 -0
- psychopy/app/deviceManager/panel.py +191 -0
- psychopy/app/deviceManager/utils.py +60 -0
- psychopy/app/idle.py +7 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
- psychopy/app/plugin_manager/dialog.py +12 -3
- psychopy/app/plugin_manager/packageIndex.py +303 -0
- psychopy/app/plugin_manager/packages.py +203 -63
- psychopy/app/plugin_manager/plugins.py +120 -240
- psychopy/app/preferencesDlg.py +6 -1
- psychopy/app/psychopyApp.py +16 -4
- psychopy/app/runner/runner.py +10 -2
- psychopy/app/runner/scriptProcess.py +8 -3
- psychopy/app/stdout/stdOutRich.py +11 -4
- psychopy/app/themes/icons.py +3 -0
- psychopy/app/utils.py +61 -0
- psychopy/data/experiment.py +133 -23
- psychopy/data/routine.py +12 -0
- psychopy/data/staircase.py +42 -20
- psychopy/data/trial.py +20 -12
- psychopy/data/utils.py +42 -2
- psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
- psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
- psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
- psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
- psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
- psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
- psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
- psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
- psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
- psychopy/event.py +20 -15
- psychopy/experiment/_experiment.py +86 -10
- psychopy/experiment/components/__init__.py +3 -10
- psychopy/experiment/components/_base.py +9 -20
- psychopy/experiment/components/button/__init__.py +1 -1
- psychopy/experiment/components/buttonBox/__init__.py +50 -54
- psychopy/experiment/components/camera/__init__.py +137 -359
- psychopy/experiment/components/keyboard/__init__.py +17 -24
- psychopy/experiment/components/microphone/__init__.py +61 -110
- psychopy/experiment/components/movie/__init__.py +2 -3
- psychopy/experiment/components/serialOut/__init__.py +192 -93
- psychopy/experiment/components/settings/__init__.py +45 -27
- psychopy/experiment/components/sound/__init__.py +82 -73
- psychopy/experiment/components/soundsensor/__init__.py +43 -80
- psychopy/experiment/devices.py +303 -0
- psychopy/experiment/exports.py +20 -18
- psychopy/experiment/flow.py +7 -0
- psychopy/experiment/loops.py +47 -29
- psychopy/experiment/monitor.py +74 -0
- psychopy/experiment/params.py +48 -10
- psychopy/experiment/plugins.py +28 -108
- psychopy/experiment/py2js_transpiler.py +1 -1
- psychopy/experiment/routines/__init__.py +1 -1
- psychopy/experiment/routines/_base.py +59 -24
- psychopy/experiment/routines/audioValidator/__init__.py +19 -155
- psychopy/experiment/routines/visualValidator/__init__.py +25 -25
- psychopy/hardware/__init__.py +20 -57
- psychopy/hardware/button.py +15 -2
- psychopy/hardware/camera/__init__.py +2237 -1394
- psychopy/hardware/joystick/__init__.py +1 -1
- psychopy/hardware/keyboard.py +5 -8
- psychopy/hardware/listener.py +4 -1
- psychopy/hardware/manager.py +75 -35
- psychopy/hardware/microphone.py +52 -6
- psychopy/hardware/monitor.py +144 -0
- psychopy/hardware/photometer/__init__.py +156 -117
- psychopy/hardware/serialdevice.py +16 -2
- psychopy/hardware/soundsensor.py +4 -1
- psychopy/iohub/devices/deviceConfigValidation.py +2 -1
- psychopy/iohub/devices/keyboard/darwin.py +8 -5
- psychopy/iohub/util/__init__.py +7 -8
- psychopy/localization/generateTranslationTemplate.py +208 -116
- psychopy/localization/messages.pot +4305 -3502
- psychopy/monitors/MonitorCenter.py +174 -74
- psychopy/plugins/__init__.py +6 -4
- psychopy/preferences/devices.py +80 -0
- psychopy/preferences/generateHints.py +2 -1
- psychopy/preferences/preferences.py +35 -11
- psychopy/scripts/psychopy-pkgutil.py +969 -0
- psychopy/scripts/psyexpCompile.py +1 -1
- psychopy/session.py +34 -38
- psychopy/sound/__init__.py +6 -260
- psychopy/sound/audioclip.py +164 -0
- psychopy/sound/backend_ptb.py +8 -0
- psychopy/sound/backend_pygame.py +10 -0
- psychopy/sound/backend_pysound.py +9 -0
- psychopy/sound/backends/__init__.py +0 -0
- psychopy/sound/microphone.py +3 -0
- psychopy/sound/sound.py +58 -0
- psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
- psychopy/tests/data/duplicateHeaders.csv +2 -0
- psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
- psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
- psychopy/tests/test_data/test_utils.py +5 -1
- psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
- psychopy/tests/test_hardware/test_ports.py +0 -12
- psychopy/tests/test_tools/test_stringtools.py +1 -1
- psychopy/tools/attributetools.py +12 -5
- psychopy/tools/fontmanager.py +17 -14
- psychopy/tools/movietools.py +43 -2
- psychopy/tools/stringtools.py +33 -8
- psychopy/tools/versionchooser.py +1 -1
- psychopy/validation/audio.py +5 -1
- psychopy/validation/visual.py +5 -1
- psychopy/visual/basevisual.py +8 -7
- psychopy/visual/circle.py +2 -2
- psychopy/visual/image.py +29 -109
- psychopy/visual/movies/__init__.py +1800 -313
- psychopy/visual/polygon.py +4 -0
- psychopy/visual/shape.py +2 -2
- psychopy/visual/window.py +34 -11
- psychopy/voicekey/__init__.py +41 -669
- psychopy/voicekey/labjack_vks.py +7 -48
- psychopy/voicekey/parallel_vks.py +7 -42
- psychopy/voicekey/vk_tools.py +114 -263
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/METADATA +17 -11
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/RECORD +216 -184
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
- psychopy/visual/movies/players/__init__.py +0 -62
- psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
- psychopy/voicekey/demo_vks.py +0 -12
- psychopy/voicekey/signal.py +0 -42
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -107,7 +107,11 @@ class StdOutRich(wx.richtext.RichTextCtrl, _BaseErrorHandler, handlers.ThemeMixi
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
def onURL(self, evt=None):
|
|
110
|
-
|
|
110
|
+
"""Open link in default browser."""
|
|
111
|
+
|
|
112
|
+
if wx.Platform != '__WXGTK__':
|
|
113
|
+
wx.BeginBusyCursor()
|
|
114
|
+
|
|
111
115
|
try:
|
|
112
116
|
if evt.String.startswith("http"):
|
|
113
117
|
webbrowser.open(evt.String)
|
|
@@ -121,7 +125,8 @@ class StdOutRich(wx.richtext.RichTextCtrl, _BaseErrorHandler, handlers.ThemeMixi
|
|
|
121
125
|
except Exception as e:
|
|
122
126
|
print("##### Could not open URL: {} #####\n".format(evt.String))
|
|
123
127
|
print(e)
|
|
124
|
-
wx.
|
|
128
|
+
if wx.Platform != '__WXGTK__':
|
|
129
|
+
wx.EndBusyCursor()
|
|
125
130
|
|
|
126
131
|
def write(self, inStr, evt=None):
|
|
127
132
|
self.MoveEnd() # always 'append' text rather than 'writing' it
|
|
@@ -357,7 +362,8 @@ class ScriptOutputCtrl(StdOutRich, handlers.ThemeMixin):
|
|
|
357
362
|
|
|
358
363
|
def onURL(self, evt):
|
|
359
364
|
"""Open link in default browser."""
|
|
360
|
-
wx.
|
|
365
|
+
if wx.Platform != '__WXGTK__':
|
|
366
|
+
wx.BeginBusyCursor()
|
|
361
367
|
try:
|
|
362
368
|
if evt.String.startswith("http"):
|
|
363
369
|
webbrowser.open(evt.String)
|
|
@@ -375,7 +381,8 @@ class ScriptOutputCtrl(StdOutRich, handlers.ThemeMixin):
|
|
|
375
381
|
except Exception as e:
|
|
376
382
|
print("##### Could not open URL: {} #####\n".format(evt.String))
|
|
377
383
|
print(e)
|
|
378
|
-
wx.
|
|
384
|
+
if wx.Platform != '__WXGTK__':
|
|
385
|
+
wx.EndBusyCursor()
|
|
379
386
|
|
|
380
387
|
def clear(self, evt=None):
|
|
381
388
|
self.Clear()
|
psychopy/app/themes/icons.py
CHANGED
|
@@ -125,6 +125,9 @@ class BaseIcon:
|
|
|
125
125
|
# If size is None, return bitmap as is
|
|
126
126
|
if size in (None, (None, None)):
|
|
127
127
|
return bmp
|
|
128
|
+
# if given a single value, duplicate it
|
|
129
|
+
if isinstance(size, (int, float)):
|
|
130
|
+
size = (size, size)
|
|
128
131
|
# Split up size value
|
|
129
132
|
width, height = size
|
|
130
133
|
# If size is unchanged, return bitmap as is
|
psychopy/app/utils.py
CHANGED
|
@@ -1665,6 +1665,67 @@ class ListCtrl(wx.ListCtrl, listmixin.ListCtrlAutoWidthMixin):
|
|
|
1665
1665
|
listmixin.ListCtrlAutoWidthMixin.__init__(self)
|
|
1666
1666
|
|
|
1667
1667
|
|
|
1668
|
+
class ShowHideBtn(wx.ToggleButton):
|
|
1669
|
+
"""
|
|
1670
|
+
Button which shows/hides a panel by toggling itself.
|
|
1671
|
+
"""
|
|
1672
|
+
indicators = {
|
|
1673
|
+
True: "▾",
|
|
1674
|
+
False: "▸"
|
|
1675
|
+
}
|
|
1676
|
+
def __init__(
|
|
1677
|
+
self,
|
|
1678
|
+
parent,
|
|
1679
|
+
target,
|
|
1680
|
+
label=""
|
|
1681
|
+
):
|
|
1682
|
+
wx.ToggleButton.__init__(self)
|
|
1683
|
+
self.SetBackgroundStyle(wx.BG_STYLE_TRANSPARENT)
|
|
1684
|
+
self.Create(
|
|
1685
|
+
parent,
|
|
1686
|
+
label=label,
|
|
1687
|
+
style=wx.BU_LEFT | wx.BORDER_NONE
|
|
1688
|
+
)
|
|
1689
|
+
# for some reason, wx won't hide the background unless you set the foreground
|
|
1690
|
+
self.SetForegroundColour("black")
|
|
1691
|
+
# store root label
|
|
1692
|
+
self.rootLabel = label
|
|
1693
|
+
# store target
|
|
1694
|
+
self.target = target
|
|
1695
|
+
# bind toggle
|
|
1696
|
+
self.Bind(
|
|
1697
|
+
wx.EVT_TOGGLEBUTTON, self.onToggle
|
|
1698
|
+
)
|
|
1699
|
+
|
|
1700
|
+
def setValue(self, value):
|
|
1701
|
+
self.onToggle(value)
|
|
1702
|
+
|
|
1703
|
+
def toggle(self):
|
|
1704
|
+
self.setValue(not self.GetValue())
|
|
1705
|
+
|
|
1706
|
+
def onToggle(self, evt):
|
|
1707
|
+
# get value
|
|
1708
|
+
if isinstance(evt, bool):
|
|
1709
|
+
val = evt
|
|
1710
|
+
else:
|
|
1711
|
+
val = evt.Int
|
|
1712
|
+
# show/hide target
|
|
1713
|
+
if isinstance(self.target, wx.Sizer):
|
|
1714
|
+
self.target.ShowItems(val)
|
|
1715
|
+
else:
|
|
1716
|
+
self.target.Show(val)
|
|
1717
|
+
# toggle indicator
|
|
1718
|
+
indicator = self.indicators[val]
|
|
1719
|
+
self.SetLabel(f"{indicator} {self.rootLabel}")
|
|
1720
|
+
# layout parent
|
|
1721
|
+
self.GetParent().Layout()
|
|
1722
|
+
if hasattr(self.GetParent(), "SetupScrolling"):
|
|
1723
|
+
self.GetParent().SetupScrolling()
|
|
1724
|
+
# toggle as normal
|
|
1725
|
+
if hasattr(evt, "Skip"):
|
|
1726
|
+
evt.Skip()
|
|
1727
|
+
|
|
1728
|
+
|
|
1668
1729
|
def sanitize(inStr):
|
|
1669
1730
|
"""
|
|
1670
1731
|
Process a string to remove any sensitive information, i.e. OAUTH keys
|
psychopy/data/experiment.py
CHANGED
|
@@ -85,6 +85,7 @@ class ExperimentHandler(_ComparisonMixin):
|
|
|
85
85
|
"""
|
|
86
86
|
self.loops = []
|
|
87
87
|
self.loopsUnfinished = []
|
|
88
|
+
self.currentRoutine = None
|
|
88
89
|
self.name = name
|
|
89
90
|
self.version = version
|
|
90
91
|
self.runtimeInfo = runtimeInfo
|
|
@@ -124,20 +125,44 @@ class ExperimentHandler(_ComparisonMixin):
|
|
|
124
125
|
|
|
125
126
|
def __del__(self):
|
|
126
127
|
self.close()
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def currentLoop(self):
|
|
128
|
+
|
|
129
|
+
def getCurrentLoop(self, isTrials=True):
|
|
130
130
|
"""
|
|
131
131
|
Return the loop which we are currently in, this will either be a handle to a loop, such as
|
|
132
132
|
a :class:`~psychopy.data.TrialHandler` or :class:`~psychopy.data.StairHandler`, or the handle
|
|
133
133
|
of the :class:`~psychopy.data.ExperimentHandler` itself if we are not in a loop.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
isTrials : bool
|
|
138
|
+
Filter for only loops which have isTrials checked
|
|
134
139
|
"""
|
|
135
|
-
# If there are unfinished (aka currently active) loops, return the most recent
|
|
136
140
|
if len(self.loopsUnfinished):
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
# iterate through unfinished (aka active) loops, starting with the most recent
|
|
142
|
+
for loop in reversed(self.loopsUnfinished):
|
|
143
|
+
# if not filtering, just return the first one
|
|
144
|
+
if not isTrials:
|
|
145
|
+
return loop
|
|
146
|
+
# otherwise, return the first one which is a trials loop
|
|
147
|
+
if getattr(loop, 'isTrials', False):
|
|
148
|
+
return loop
|
|
149
|
+
# if we are not in a loop, return to experiment handler
|
|
139
150
|
return self
|
|
140
151
|
|
|
152
|
+
@property
|
|
153
|
+
def currentLoop(self):
|
|
154
|
+
"""
|
|
155
|
+
Calls `.getCurrentLoop` with `isTrials=False`
|
|
156
|
+
"""
|
|
157
|
+
return self.getCurrentLoop(isTrials=False)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def currentTrialsLoop(self):
|
|
161
|
+
"""
|
|
162
|
+
Calls `.getCurrentLoop` with `isTrials=True`
|
|
163
|
+
"""
|
|
164
|
+
return self.getCurrentLoop(isTrials=True)
|
|
165
|
+
|
|
141
166
|
def addLoop(self, loopHandler):
|
|
142
167
|
"""Add a loop such as a :class:`~psychopy.data.TrialHandler`
|
|
143
168
|
or :class:`~psychopy.data.StairHandler`
|
|
@@ -274,6 +299,12 @@ class ExperimentHandler(_ComparisonMixin):
|
|
|
274
299
|
# get entry from row number
|
|
275
300
|
entry = self.thisEntry
|
|
276
301
|
if row is not None:
|
|
302
|
+
# if row exceeds size of entries, warn and abort
|
|
303
|
+
if row > len(self.entries):
|
|
304
|
+
logging.error(_translate(
|
|
305
|
+
"Cannot add data to row {} as there are only {} entries"
|
|
306
|
+
).format(row, len(self.entries)))
|
|
307
|
+
# get entry from row
|
|
277
308
|
entry = self.entries[row]
|
|
278
309
|
entry[name] = value
|
|
279
310
|
|
|
@@ -466,8 +497,37 @@ class ExperimentHandler(_ComparisonMixin):
|
|
|
466
497
|
))
|
|
467
498
|
# set own status
|
|
468
499
|
self.status = constants.STOPPED
|
|
500
|
+
|
|
501
|
+
def next(self, isTrials=True):
|
|
502
|
+
"""
|
|
503
|
+
Move on to either the next trial (if in a trials loop) or the next Routine.
|
|
504
|
+
|
|
505
|
+
Parameters
|
|
506
|
+
----------
|
|
507
|
+
isTrials : bool
|
|
508
|
+
Filter for only loops which have isTrials checked
|
|
509
|
+
"""
|
|
510
|
+
if isinstance(self.getCurrentLoop(isTrials=isTrials), TrialHandler2):
|
|
511
|
+
# if there is a loop, skip trials
|
|
512
|
+
self.skipTrials(1, isTrials=isTrials)
|
|
513
|
+
elif self.currentRoutine is not None:
|
|
514
|
+
# if not, but there is a Routine, end it
|
|
515
|
+
self.endCurrentRoutine()
|
|
516
|
+
else:
|
|
517
|
+
# otherwise, do nothing
|
|
518
|
+
return
|
|
519
|
+
|
|
520
|
+
def endCurrentRoutine(self):
|
|
521
|
+
"""
|
|
522
|
+
End the current Routine (via the Routine.forceEnded attribute)
|
|
523
|
+
"""
|
|
524
|
+
# if there's no current Routine yet, do nothing
|
|
525
|
+
if self.currentRoutine is None:
|
|
526
|
+
return
|
|
527
|
+
# force end the Routine
|
|
528
|
+
self.currentRoutine.forceEnded = True
|
|
469
529
|
|
|
470
|
-
def skipTrials(self, n=1):
|
|
530
|
+
def skipTrials(self, n=1, isTrials=True):
|
|
471
531
|
"""
|
|
472
532
|
Skip ahead n trials - the trials inbetween will be marked as "skipped". If you try to
|
|
473
533
|
skip past the last trial, will log a warning and skip *to* the last trial.
|
|
@@ -476,14 +536,23 @@ class ExperimentHandler(_ComparisonMixin):
|
|
|
476
536
|
----------
|
|
477
537
|
n : int
|
|
478
538
|
Number of trials to skip ahead
|
|
539
|
+
isTrials : bool
|
|
540
|
+
Filter for only loops which have isTrials checked
|
|
479
541
|
"""
|
|
542
|
+
loop = self.getCurrentLoop(isTrials=isTrials)
|
|
480
543
|
# return if there isn't a TrialHandler2 active
|
|
481
|
-
if not isinstance(
|
|
544
|
+
if not isinstance(loop, TrialHandler2):
|
|
482
545
|
return
|
|
546
|
+
# end inner loops
|
|
547
|
+
for innerLoop in self.loopsUnfinished[
|
|
548
|
+
self.loopsUnfinished.index(loop)+1:
|
|
549
|
+
].copy():
|
|
550
|
+
innerLoop.finished = True
|
|
551
|
+
self.loopEnded(innerLoop)
|
|
483
552
|
# skip trials in current loop
|
|
484
|
-
return
|
|
553
|
+
return loop.skipTrials(n)
|
|
485
554
|
|
|
486
|
-
def rewindTrials(self, n=1):
|
|
555
|
+
def rewindTrials(self, n=1, isTrials=True):
|
|
487
556
|
"""
|
|
488
557
|
Skip ahead n trials - the trials inbetween will be marked as "skipped". If you try to
|
|
489
558
|
skip past the last trial, will log a warning and skip *to* the last trial.
|
|
@@ -492,18 +561,34 @@ class ExperimentHandler(_ComparisonMixin):
|
|
|
492
561
|
----------
|
|
493
562
|
n : int
|
|
494
563
|
Number of trials to skip ahead
|
|
564
|
+
isTrials : bool
|
|
565
|
+
Filter for only loops which have isTrials checked
|
|
495
566
|
"""
|
|
567
|
+
loop = self.getCurrentLoop(isTrials=isTrials)
|
|
496
568
|
# return if there isn't a TrialHandler2 active
|
|
497
|
-
if not isinstance(
|
|
569
|
+
if not isinstance(loop, TrialHandler2):
|
|
498
570
|
return
|
|
571
|
+
# restart inner loops
|
|
572
|
+
for innerLoop in self.loopsUnfinished[
|
|
573
|
+
self.loopsUnfinished.index(loop)+1:
|
|
574
|
+
]:
|
|
575
|
+
innerLoop.rewindTrials(
|
|
576
|
+
len(innerLoop.elapsedTrials)
|
|
577
|
+
)
|
|
499
578
|
# rewind trials in current loop
|
|
500
|
-
return
|
|
579
|
+
return loop.rewindTrials(n)
|
|
580
|
+
|
|
501
581
|
|
|
502
|
-
def getAllTrials(self):
|
|
582
|
+
def getAllTrials(self, isTrials=True):
|
|
503
583
|
"""
|
|
504
584
|
Returns all trials (elapsed, current and upcoming) with an index indicating which trial is
|
|
505
585
|
the current trial.
|
|
506
586
|
|
|
587
|
+
Parameters
|
|
588
|
+
----------
|
|
589
|
+
isTrials : bool
|
|
590
|
+
Filter for only loops which have isTrials checked
|
|
591
|
+
|
|
507
592
|
Returns
|
|
508
593
|
-------
|
|
509
594
|
list[Trial]
|
|
@@ -512,39 +597,49 @@ class ExperimentHandler(_ComparisonMixin):
|
|
|
512
597
|
Index of the current trial in this list
|
|
513
598
|
"""
|
|
514
599
|
# return None if there isn't a TrialHandler2 active
|
|
515
|
-
if not isinstance(self.
|
|
600
|
+
if not isinstance(self.getCurrentLoop(isTrials=isTrials), TrialHandler2):
|
|
516
601
|
return [None], 0
|
|
517
602
|
# get all trials from current loop
|
|
518
|
-
return self.
|
|
603
|
+
return self.getCurrentLoop(isTrials=isTrials).getAllTrials()
|
|
519
604
|
|
|
520
|
-
def getCurrentTrial(self):
|
|
605
|
+
def getCurrentTrial(self, isTrials=True):
|
|
521
606
|
"""
|
|
522
607
|
Returns the current trial (`.thisTrial`)
|
|
523
608
|
|
|
609
|
+
Parameters
|
|
610
|
+
----------
|
|
611
|
+
isTrials : bool
|
|
612
|
+
Filter for only loops which have isTrials checked
|
|
613
|
+
|
|
524
614
|
Returns
|
|
525
615
|
-------
|
|
526
616
|
Trial
|
|
527
617
|
The current trial
|
|
528
618
|
"""
|
|
529
619
|
# return None if there isn't a TrialHandler2 active
|
|
530
|
-
if not isinstance(self.
|
|
620
|
+
if not isinstance(self.getCurrentLoop(isTrials=isTrials), TrialHandler2):
|
|
531
621
|
return None
|
|
532
622
|
|
|
533
|
-
return self.
|
|
623
|
+
return self.getCurrentLoop(isTrials=isTrials).getCurrentTrial()
|
|
534
624
|
|
|
535
|
-
def getFutureTrial(self, n=1):
|
|
625
|
+
def getFutureTrial(self, n=1, isTrials=True):
|
|
536
626
|
"""
|
|
537
627
|
Returns the condition for n trials into the future, without
|
|
538
628
|
advancing the trials. Returns 'None' if attempting to go beyond
|
|
539
629
|
the last trial in the current loop, or if there is no current loop.
|
|
630
|
+
|
|
631
|
+
Parameters
|
|
632
|
+
----------
|
|
633
|
+
isTrials : bool
|
|
634
|
+
Filter for only loops which have isTrials checked
|
|
540
635
|
"""
|
|
541
636
|
# return None if there isn't a TrialHandler2 active
|
|
542
|
-
if not isinstance(self.
|
|
637
|
+
if not isinstance(self.getCurrentLoop(isTrials=isTrials), TrialHandler2):
|
|
543
638
|
return None
|
|
544
639
|
# get future trial from current loop
|
|
545
|
-
return self.
|
|
640
|
+
return self.getCurrentLoop(isTrials=isTrials).getFutureTrial(n)
|
|
546
641
|
|
|
547
|
-
def getFutureTrials(self, n=1, start=0):
|
|
642
|
+
def getFutureTrials(self, n=1, start=0, isTrials=True):
|
|
548
643
|
"""
|
|
549
644
|
Returns Trial objects for a given range in the future. Will start looking at `start` trials
|
|
550
645
|
in the future and will return n trials from then, so e.g. to get all trials from 2 in the
|
|
@@ -556,6 +651,8 @@ class ExperimentHandler(_ComparisonMixin):
|
|
|
556
651
|
How many trials into the future to look, by default 1
|
|
557
652
|
start : int, optional
|
|
558
653
|
How many trials into the future to start looking at, by default 0
|
|
654
|
+
isTrials : bool
|
|
655
|
+
Filter for only loops which have isTrials checked
|
|
559
656
|
|
|
560
657
|
Returns
|
|
561
658
|
-------
|
|
@@ -568,7 +665,7 @@ class ExperimentHandler(_ComparisonMixin):
|
|
|
568
665
|
for i in range(n):
|
|
569
666
|
# add each to the list
|
|
570
667
|
trials.append(
|
|
571
|
-
self.getFutureTrial(start + i)
|
|
668
|
+
self.getFutureTrial(start + i, isTrials=isTrials)
|
|
572
669
|
)
|
|
573
670
|
|
|
574
671
|
return trials
|
|
@@ -874,9 +971,22 @@ class ExperimentHandler(_ComparisonMixin):
|
|
|
874
971
|
origEntries = self.entries
|
|
875
972
|
self.entries = self.getAllEntries()
|
|
876
973
|
|
|
974
|
+
# temporarily remove connected save methods so they don't get pickled
|
|
975
|
+
origConnectedSaveMethods = self.connectedSaveMethods
|
|
976
|
+
self.connectedSaveMethods = [
|
|
977
|
+
{
|
|
978
|
+
'fcn': f"<{callback['fcn'].__module__}:{callback['fcn'].__name__}>",
|
|
979
|
+
'args': callback['args'],
|
|
980
|
+
'kwargs': callback['kwargs']
|
|
981
|
+
} for callback in origConnectedSaveMethods
|
|
982
|
+
]
|
|
983
|
+
|
|
877
984
|
with openOutputFile(fileName=fileName, append=False,
|
|
878
985
|
fileCollisionMethod=fileCollisionMethod) as f:
|
|
879
986
|
pickle.dump(self, f)
|
|
987
|
+
|
|
988
|
+
# reinstate connected save methods
|
|
989
|
+
self.connectedSaveMethods = origConnectedSaveMethods
|
|
880
990
|
|
|
881
991
|
if (fileName is not None) and (fileName != 'stdout'):
|
|
882
992
|
logging.info('saved data to %s' % f.name)
|
psychopy/data/routine.py
CHANGED
|
@@ -53,6 +53,18 @@ class Routine:
|
|
|
53
53
|
# starting status
|
|
54
54
|
self.status = constants.NOT_STARTED
|
|
55
55
|
|
|
56
|
+
def __getstate__(self):
|
|
57
|
+
"""
|
|
58
|
+
Components can't be pickled due to their window reference, so don't include them when
|
|
59
|
+
pickled
|
|
60
|
+
"""
|
|
61
|
+
# capture what is normally pickled
|
|
62
|
+
state = self.__dict__.copy()
|
|
63
|
+
# replace components with their name
|
|
64
|
+
state['components'] = []
|
|
65
|
+
|
|
66
|
+
return state
|
|
67
|
+
|
|
56
68
|
def getPlaybackComponents(self):
|
|
57
69
|
"""
|
|
58
70
|
Get a list of all Components within this Routine which have a concept of playing and
|
psychopy/data/staircase.py
CHANGED
|
@@ -55,23 +55,26 @@ class StairHandler(_BaseTrialHandler):
|
|
|
55
55
|
|
|
56
56
|
"""
|
|
57
57
|
|
|
58
|
-
def __init__(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
startVal,
|
|
61
|
+
nReversals=None,
|
|
62
|
+
stepSizes=4, # dB stepsize
|
|
63
|
+
nTrials=0,
|
|
64
|
+
nUp=1,
|
|
65
|
+
nDown=3, # correct responses before stim goes down
|
|
66
|
+
applyInitialRule=True,
|
|
67
|
+
extraInfo=None,
|
|
68
|
+
method='2AFC',
|
|
69
|
+
stepType='db',
|
|
70
|
+
minVal=None,
|
|
71
|
+
maxVal=None,
|
|
72
|
+
originPath=None,
|
|
73
|
+
isTrials=True,
|
|
74
|
+
name='',
|
|
75
|
+
autoLog=True,
|
|
76
|
+
**kwargs
|
|
77
|
+
):
|
|
75
78
|
"""
|
|
76
79
|
:Parameters:
|
|
77
80
|
|
|
@@ -136,6 +139,10 @@ class StairHandler(_BaseTrialHandler):
|
|
|
136
139
|
The largest legal value for the staircase, which can be
|
|
137
140
|
used to prevent it reaching impossible contrast values,
|
|
138
141
|
for instance.
|
|
142
|
+
|
|
143
|
+
isTrials : bool
|
|
144
|
+
Is this controlling trials, or created for another purpose (e.g. iterating a
|
|
145
|
+
stimulus within a trial)?
|
|
139
146
|
|
|
140
147
|
Additional keyword arguments will be ignored.
|
|
141
148
|
|
|
@@ -155,6 +162,7 @@ class StairHandler(_BaseTrialHandler):
|
|
|
155
162
|
self.extraInfo = extraInfo
|
|
156
163
|
self.method = method
|
|
157
164
|
self.stepType = stepType
|
|
165
|
+
self.isTrials = isTrials
|
|
158
166
|
|
|
159
167
|
try:
|
|
160
168
|
self.stepSizes = list(stepSizes)
|
|
@@ -1736,9 +1744,18 @@ class QuestPlusHandler(StairHandler):
|
|
|
1736
1744
|
|
|
1737
1745
|
class MultiStairHandler(_BaseTrialHandler):
|
|
1738
1746
|
|
|
1739
|
-
def __init__(
|
|
1740
|
-
|
|
1741
|
-
|
|
1747
|
+
def __init__(
|
|
1748
|
+
self,
|
|
1749
|
+
stairType='simple',
|
|
1750
|
+
method='random',
|
|
1751
|
+
conditions=None,
|
|
1752
|
+
nTrials=50,
|
|
1753
|
+
randomSeed=None,
|
|
1754
|
+
originPath=None,
|
|
1755
|
+
isTrials=True,
|
|
1756
|
+
name='',
|
|
1757
|
+
autoLog=True,
|
|
1758
|
+
):
|
|
1742
1759
|
"""A Handler to allow easy interleaved staircase procedures
|
|
1743
1760
|
(simple or QUEST).
|
|
1744
1761
|
|
|
@@ -1790,6 +1807,10 @@ class MultiStairHandler(_BaseTrialHandler):
|
|
|
1790
1807
|
The seed with which to initialize the random number generator
|
|
1791
1808
|
(RNG). If `None` (default), do not initialize the RNG with
|
|
1792
1809
|
a specific value.
|
|
1810
|
+
|
|
1811
|
+
isTrials : bool
|
|
1812
|
+
Is this controlling trials, or created for another purpose (e.g. iterating a
|
|
1813
|
+
stimulus within a trial)?
|
|
1793
1814
|
|
|
1794
1815
|
Example usage::
|
|
1795
1816
|
|
|
@@ -1829,6 +1850,7 @@ class MultiStairHandler(_BaseTrialHandler):
|
|
|
1829
1850
|
self.nTrials = nTrials
|
|
1830
1851
|
self.finished = False
|
|
1831
1852
|
self.totalTrials = 0
|
|
1853
|
+
self.isTrials = isTrials
|
|
1832
1854
|
self._checkArguments()
|
|
1833
1855
|
# create staircases
|
|
1834
1856
|
self.staircases = [] # all staircases
|
psychopy/data/trial.py
CHANGED
|
@@ -852,16 +852,19 @@ class TrialHandler2(_BaseTrialHandler):
|
|
|
852
852
|
Then you'll find that `dat` has the following attributes that
|
|
853
853
|
"""
|
|
854
854
|
|
|
855
|
-
def __init__(
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
855
|
+
def __init__(
|
|
856
|
+
self,
|
|
857
|
+
trialList,
|
|
858
|
+
nReps,
|
|
859
|
+
method='random',
|
|
860
|
+
dataTypes=None,
|
|
861
|
+
extraInfo=None,
|
|
862
|
+
seed=None,
|
|
863
|
+
originPath=None,
|
|
864
|
+
isTrials=True,
|
|
865
|
+
name='',
|
|
866
|
+
autoLog=True
|
|
867
|
+
):
|
|
865
868
|
"""
|
|
866
869
|
|
|
867
870
|
:Parameters:
|
|
@@ -929,10 +932,14 @@ class TrialHandler2(_BaseTrialHandler):
|
|
|
929
932
|
|
|
930
933
|
.origin - the contents of the script or builder experiment that
|
|
931
934
|
created the handler
|
|
935
|
+
|
|
936
|
+
.isTrials - is this controlling trials, or created for another purpose (e.g. iterating a
|
|
937
|
+
stimulus within a trial)?
|
|
932
938
|
|
|
933
939
|
"""
|
|
934
940
|
self.name = name
|
|
935
941
|
self.autoLog = autoLog
|
|
942
|
+
self.isTrials = isTrials
|
|
936
943
|
|
|
937
944
|
if trialList in [None, [None], []]: # user wants an empty trialList
|
|
938
945
|
# which corresponds to a list with a single empty entry
|
|
@@ -1284,8 +1291,9 @@ class TrialHandler2(_BaseTrialHandler):
|
|
|
1284
1291
|
# advance row in data file
|
|
1285
1292
|
if self.getExp() is not None:
|
|
1286
1293
|
self.getExp().nextEntry()
|
|
1287
|
-
# mark as recently skipped so the next iteration is cancelled
|
|
1288
|
-
self.
|
|
1294
|
+
# mark as recently skipped so the next iteration (if there is one) is cancelled
|
|
1295
|
+
if n or len(self.upcomingTrials):
|
|
1296
|
+
self._cancelNextIteration = True
|
|
1289
1297
|
|
|
1290
1298
|
return self.thisTrial
|
|
1291
1299
|
|
psychopy/data/utils.py
CHANGED
|
@@ -181,6 +181,35 @@ def indicesFromString(indsString):
|
|
|
181
181
|
pass
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
def dictFromString(val):
|
|
185
|
+
# return as-is if already a dict
|
|
186
|
+
if isinstance(val, dict):
|
|
187
|
+
return val
|
|
188
|
+
# stringify
|
|
189
|
+
if not isinstance(val, str):
|
|
190
|
+
val = str(val)
|
|
191
|
+
# strip spaces
|
|
192
|
+
val = val.strip()
|
|
193
|
+
# make sure we have curly braces
|
|
194
|
+
if not val.startswith("{") and val.endswith("}"):
|
|
195
|
+
val = f"{{{val}}}"
|
|
196
|
+
# try to evaluate with ast (works for simple values)
|
|
197
|
+
try:
|
|
198
|
+
iterable = ast.literal_eval(val)
|
|
199
|
+
assert isinstance(iterable, dict)
|
|
200
|
+
return iterable
|
|
201
|
+
except (ValueError, SyntaxError, AssertionError):
|
|
202
|
+
pass # e.g. "yes, no" won't work. We'll go on and try another way
|
|
203
|
+
# try manually if ast fails
|
|
204
|
+
parsed = {}
|
|
205
|
+
for item in val[1:-1].split(","):
|
|
206
|
+
if ":" in item:
|
|
207
|
+
key, val = item.split(":", maxsplit=1)
|
|
208
|
+
parsed[key.strip()] = val.strip()
|
|
209
|
+
|
|
210
|
+
return parsed
|
|
211
|
+
|
|
212
|
+
|
|
184
213
|
def listFromString(val, excludeEmpties=False):
|
|
185
214
|
"""Take a string that looks like a list (with commas and/or [] and make
|
|
186
215
|
an actual python list"""
|
|
@@ -191,8 +220,7 @@ def listFromString(val, excludeEmpties=False):
|
|
|
191
220
|
elif type(val) == list:
|
|
192
221
|
return list(val) # nothing to do
|
|
193
222
|
elif type(val) != str:
|
|
194
|
-
|
|
195
|
-
.format(repr(val)))
|
|
223
|
+
return [val]
|
|
196
224
|
# try to evaluate with ast (works for "'yes,'no'" or "['yes', 'no']")
|
|
197
225
|
try:
|
|
198
226
|
iterable = ast.literal_eval(val)
|
|
@@ -277,6 +305,10 @@ def importConditions(fileName, returnFieldNames=False, selection=""):
|
|
|
277
305
|
thisAttempt = pd.read_csv(
|
|
278
306
|
fileName, encoding='utf-8-sig', sep=sep, decimal=dec
|
|
279
307
|
)
|
|
308
|
+
# read in the headers separately to bypass pandas sanitization
|
|
309
|
+
thisAttempt.columns = pd.read_csv(
|
|
310
|
+
fileName, encoding='utf-8-sig', sep=sep, decimal=dec, header=None, nrows=1
|
|
311
|
+
).iloc[0, :]
|
|
280
312
|
# if there's only one header, check that it doesn't contain delimiters
|
|
281
313
|
# (one column with delims probably means it's parsed without error but not
|
|
282
314
|
# recognised columns correctly)
|
|
@@ -335,12 +367,20 @@ def importConditions(fileName, returnFieldNames=False, selection=""):
|
|
|
335
367
|
names are OK, return silently; else raise with msg
|
|
336
368
|
"""
|
|
337
369
|
fileName = pathToString(fileName)
|
|
370
|
+
# make sure all columns are named
|
|
338
371
|
if not all(fieldNames):
|
|
339
372
|
raise exceptions.ConditionsImportError(
|
|
340
373
|
"Conditions file %s: Missing parameter name(s); empty cell(s) in the first row?" % fileName,
|
|
341
374
|
translated=_translate("Conditions file %s: Missing parameter name(s); empty cell(s) in the first row?") % fileName
|
|
342
375
|
)
|
|
376
|
+
# check each name
|
|
343
377
|
for name in fieldNames:
|
|
378
|
+
# is this name duplicated?
|
|
379
|
+
if sum([otherName == name for otherName in fieldNames]) > 1:
|
|
380
|
+
raise exceptions.ConditionsImportError(_translate(
|
|
381
|
+
"Duplicate column name '{}'"
|
|
382
|
+
).format(name))
|
|
383
|
+
# is this name a valid variable name?
|
|
344
384
|
OK, msg, translated = isValidVariableName(name)
|
|
345
385
|
if not OK:
|
|
346
386
|
# tailor message to importConditions
|