psychopy 2024.1.1__py3-none-any.whl → 2024.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of psychopy might be problematic. Click here for more details.

Files changed (96) hide show
  1. psychopy/__init__.py +2 -2
  2. psychopy/alerts/_alerts.py +6 -2
  3. psychopy/alerts/alertsCatalogue/alertmsg.py +15 -0
  4. psychopy/app/builder/dialogs/paramCtrls.py +4 -2
  5. psychopy/app/builder/localizedStrings.py +16 -4
  6. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  7. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  8. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  9. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  10. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  11. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  12. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  13. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  14. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  15. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  16. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  17. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  18. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  19. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  20. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  21. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  22. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +3548 -5422
  23. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  24. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  25. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  26. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  27. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  28. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  29. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  30. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  31. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  32. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  33. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  34. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  35. psychopy/app/plugin_manager/plugins.py +1 -1
  36. psychopy/app/preferencesDlg.py +2 -1
  37. psychopy/demos/builder/Hardware/EEG_parallel_component/EEG_triggers_parallel_comp.psyexp +552 -550
  38. psychopy/demos/builder/Hardware/EEG_serial_component/EEG_triggers_serial_comp.psyexp +572 -570
  39. psychopy/demos/coder/timing/timeByFrames.py +7 -1
  40. psychopy/experiment/components/_base.py +122 -0
  41. psychopy/experiment/components/aperture/__init__.py +6 -2
  42. psychopy/experiment/components/brush/__init__.py +3 -1
  43. psychopy/experiment/components/button/__init__.py +6 -2
  44. psychopy/experiment/components/buttonBox/__init__.py +2 -1
  45. psychopy/experiment/components/camera/__init__.py +13 -0
  46. psychopy/experiment/components/code/__init__.py +34 -1
  47. psychopy/experiment/components/form/formItems.xltx +0 -0
  48. psychopy/experiment/components/mouse/__init__.py +12 -0
  49. psychopy/experiment/components/polygon/__init__.py +14 -3
  50. psychopy/experiment/components/settings/__init__.py +4 -1
  51. psychopy/experiment/components/sound/__init__.py +1 -1
  52. psychopy/experiment/components/text/__init__.py +1 -1
  53. psychopy/experiment/params.py +17 -0
  54. psychopy/experiment/routines/_base.py +122 -0
  55. psychopy/experiment/routines/counterbalance/__init__.py +1 -1
  56. psychopy/hardware/button.py +4 -3
  57. psychopy/hardware/camera/__init__.py +6 -0
  58. psychopy/hardware/keyboard.py +8 -3
  59. psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +1 -1
  60. psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/__init__.py +1 -1
  61. psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/calibration.py +1 -1
  62. psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/eyetracker.py +1 -1
  63. psychopy/localization/generateTranslationTemplate.py +45 -19
  64. psychopy/localization/messages.pot +5049 -3418
  65. psychopy/preferences/Darwin.spec +2 -0
  66. psychopy/preferences/FreeBSD.spec +2 -0
  67. psychopy/preferences/Linux.spec +2 -0
  68. psychopy/preferences/Windows.spec +2 -0
  69. psychopy/preferences/baseNoArch.spec +2 -0
  70. psychopy/preferences/generateHints.py +2 -1
  71. psychopy/preferences/hints.py +118 -97
  72. psychopy/tests/test_experiment/test_components/__init__.py +1 -1
  73. psychopy/tests/test_experiment/test_components/{test_ButtonBox.py → test_ButtonBoxComponent.py} +9 -27
  74. psychopy/tests/test_experiment/test_components/{test_Code.py → test_CodeComponent.py} +8 -20
  75. psychopy/tests/test_experiment/test_components/test_GratingComponent.py +7 -0
  76. psychopy/tests/test_experiment/test_components/test_ImageComponent.py +8 -0
  77. psychopy/tests/test_experiment/test_components/{test_Mouse.py → test_MouseComponent.py} +44 -57
  78. psychopy/tests/test_experiment/test_components/{test_Polygon.py → test_PolygonComponent.py} +9 -25
  79. psychopy/tests/test_experiment/test_components/{test_ResourceManager.py → test_ResourceManagerComponent.py} +3 -13
  80. psychopy/tests/test_experiment/test_components/{test_Settings.py → test_SettingsComponent.py} +1 -3
  81. psychopy/tests/test_experiment/test_components/{test_Static.py → test_StaticComponent.py} +3 -12
  82. psychopy/tests/test_experiment/test_components/test_all_components.py +8 -66
  83. psychopy/tests/test_experiment/test_components/test_base_components.py +212 -125
  84. psychopy/tools/fontmanager.py +1 -1
  85. psychopy/tools/versionchooser.py +1 -1
  86. psychopy/visual/noise.py +9 -0
  87. psychopy/visual/radial.py +8 -0
  88. psychopy/visual/secondorder.py +9 -0
  89. psychopy/visual/textbox2/textbox2.py +19 -21
  90. {psychopy-2024.1.1.dist-info → psychopy-2024.1.3.dist-info}/METADATA +1 -1
  91. {psychopy-2024.1.1.dist-info → psychopy-2024.1.3.dist-info}/RECORD +95 -66
  92. {psychopy-2024.1.1.dist-info → psychopy-2024.1.3.dist-info}/WHEEL +1 -1
  93. psychopy/tests/test_experiment/test_components/test_Image.py +0 -24
  94. {psychopy-2024.1.1.dist-info → psychopy-2024.1.3.dist-info}/entry_points.txt +0 -0
  95. {psychopy-2024.1.1.dist-info → psychopy-2024.1.3.dist-info}/licenses/AUTHORS.md +0 -0
  96. {psychopy-2024.1.1.dist-info → psychopy-2024.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -6,27 +6,9 @@ from pathlib import Path
6
6
  import pytest
7
7
 
8
8
  from psychopy import experiment
9
+ from psychopy.experiment.loops import TrialHandler
9
10
  from psychopy.experiment.components import BaseComponent
10
-
11
-
12
- def _make_minimal_experiment(obj):
13
- """
14
- Make a minimal experiment with just one routine containing just one component, of the same class as the current
15
- component but with all default params.
16
- """
17
- # Skip whole test if required attributes aren't present
18
- if not hasattr(obj, "comp"):
19
- pytest.skip()
20
- # Make blank experiment
21
- exp = experiment.Experiment()
22
- rt = exp.addRoutine(routineName='TestRoutine')
23
- exp.flow.addRoutine(rt, 0)
24
- # Create instance of this component with all default params
25
- compClass = type(obj.comp)
26
- comp = compClass(exp=exp, parentName='TestRoutine', name=f"test{compClass.__name__}")
27
- rt.append(comp)
28
- # Return experiment, routine and component
29
- return comp, rt, exp
11
+ from psychopy.experiment.exports import IndentingBuffer
30
12
 
31
13
 
32
14
  def _find_global_resource_in_js_experiment(script, resource):
@@ -48,34 +30,228 @@ def _find_global_resource_in_js_experiment(script, resource):
48
30
  return resource in resourcesStr
49
31
 
50
32
 
51
- class _TestBaseComponentsMixin:
52
- # Class in the PsychoPy libraries (visual, sound, hardware, etc.) corresponding to this component
53
- libraryClass = None
33
+ class BaseComponentTests:
34
+ # component class to test
35
+ comp = None
54
36
 
55
- def test_icons(self):
56
- """Check that component has icons for each app theme"""
57
- # Skip whole test if required attributes aren't present
58
- if not hasattr(self, "comp"):
37
+ # --- Utility methods ---
38
+ def make_minimal_experiment(self):
39
+ """
40
+ Make a minimal experiment with just one routine containing just one component, of the same class as the current component but with all default params.
41
+ """
42
+ # make blank experiment
43
+ exp = experiment.Experiment()
44
+ # add a Routine
45
+ rt = exp.addRoutine(routineName='TestRoutine')
46
+ exp.flow.addRoutine(rt, 0)
47
+ # add a loop around the Routine
48
+ loop = TrialHandler(exp=exp, name="testLoop")
49
+ exp.flow.addLoop(loop, 0, -1)
50
+ # create instance of this test's Component with all default params
51
+ comp = self.comp(exp=exp, parentName='TestRoutine', name=f"test{self.comp.__name__}")
52
+ rt.append(comp)
53
+ # return experiment, Routine and Component
54
+ return comp, rt, exp
55
+
56
+ @pytest.fixture(autouse=True)
57
+ def assert_comp_class(self):
58
+ """
59
+ Make sure this test object has an associated Component class - and skip the test if not. This is run before each test by default.
60
+ """
61
+ # skip whole test if there is no Component connected to test class
62
+ if self.comp is None:
59
63
  pytest.skip()
60
- # Pathify icon file path
64
+ # continue with the test as normal
65
+ yield
66
+
67
+ # --- Heritable tests ---
68
+
69
+ def test_icons(self):
70
+ """
71
+ Check that Component has icons for each app theme and that these point to real files
72
+ """
73
+ # pathify icon file path
61
74
  icon = Path(self.comp.iconFile)
62
- # Get paths for each theme
75
+ # get paths for each theme
63
76
  files = [
64
77
  icon.parent / "light" / icon.name,
65
78
  icon.parent / "dark" / icon.name,
66
79
  icon.parent / "classic" / icon.name,
67
80
  ]
68
- # Check that each path is a file
81
+ # check that each path is a file
69
82
  for file in files:
70
- assert file.is_file()
83
+ assert file.is_file(), (
84
+ f"Could not find file: {file}"
85
+ )
86
+
87
+ def test_indentation_consistency(self):
88
+ """
89
+ No component should exit any of its write methods at a different indent level as it entered, as this would break subsequent components / routines.
90
+ """
91
+ # make minimal experiment just for this test
92
+ comp, rt, exp = self.make_minimal_experiment()
93
+ # skip if component doesn't have a start/stop time
94
+ if "startVal" not in comp.params or "stopVal" not in comp.params:
95
+ pytest.skip()
96
+ # create a text buffer to write to
97
+ buff = IndentingBuffer(target="PsychoPy")
98
+ # template message for if test fails
99
+ errMsgTemplate = "Writing {} code for {} changes indent level by {} when start is `{}` and stop is `{}`."
100
+ # setup flow for writing
101
+ exp.flow.writeStartCode(buff)
102
+ # combinations of start/stop being set/unset to try
103
+ cases = [
104
+ {"startVal": "0", "stopVal": "1"},
105
+ {"startVal": "", "stopVal": "1"},
106
+ {"startVal": "0", "stopVal": ""},
107
+ {"startVal": "", "stopVal": ""},
108
+ ]
109
+ for case in cases:
110
+ # update error message for this case
111
+ errMsg = errMsgTemplate.format(
112
+ "{}", type(comp).__name__, "{}", case['startVal'], case['stopVal']
113
+ )
114
+ # set start/stop types
115
+ comp.params["startType"].val = "time (s)"
116
+ comp.params["stopType"].val = "time (s)"
117
+ # set start/stop values
118
+ for param, val in case.items():
119
+ comp.params[param].val = val
120
+ # write init code
121
+ comp.writeInitCode(buff)
122
+ # check indent
123
+ assert buff.indentLevel == 0, errMsg.format(
124
+ "init", buff.indentLevel
125
+ )
126
+ # write routine start code
127
+ comp.writeRoutineStartCode(buff)
128
+ # check indent
129
+ assert buff.indentLevel == 0, errMsg.format(
130
+ "routine start", buff.indentLevel
131
+ )
132
+ # write each frame code
133
+ comp.writeFrameCode(buff)
134
+ # check indent
135
+ assert buff.indentLevel == 0, errMsg.format(
136
+ "each frame", buff.indentLevel
137
+ )
138
+ # write end routine code
139
+ comp.writeRoutineEndCode(buff)
140
+ # check indent
141
+ assert buff.indentLevel == 0, errMsg.format(
142
+ "routine end", buff.indentLevel
143
+ )
144
+ # write end experiment code
145
+ comp.writeExperimentEndCode(buff)
146
+ # check indent
147
+ assert buff.indentLevel == 0, errMsg.format(
148
+ "experiment end", buff.indentLevel
149
+ )
150
+
151
+ def test_disabled_default_val(self):
152
+ """
153
+ Test that components created with default params are not disabled
154
+ """
155
+ # make minimal experiment just for this test
156
+ comp, rt, exp = self.make_minimal_experiment()
157
+ # check whether it can be disabled
158
+ assert 'disabled' in comp.params, (
159
+ f"{type(comp).__name__} does not have a 'disabled' attribute."
160
+ )
161
+ # check that disabled defaults to False
162
+ assert comp.params['disabled'].val is False, f"{type(comp).__name__} is defaulting to disabled."
163
+
164
+ def test_disabled_code_muting(self):
165
+ """
166
+ Test that components are only written when enabled and targets match.
167
+ """
168
+ # Code Component is never referenced by name, so skip it for this test
169
+ if self.comp.__name__ == "CodeComponent":
170
+ pytest.skip()
171
+ # Make minimal experiment just for this test
172
+ comp, rt, exp = self.make_minimal_experiment()
173
+ # Write experiment and check that component is written
174
+ pyScript = exp.writeScript(target="PsychoPy")
175
+ if "PsychoPy" in type(comp).targets:
176
+ assert comp.name in pyScript, (
177
+ f"{type(comp).__name__} not found in compiled Python script when enabled and PsychoPy in targets."
178
+ )
179
+ else:
180
+ assert comp.name not in pyScript, (
181
+ f"{type(comp).__name__} found in compiled Python script when enabled but PsychoPy not in targets."
182
+ )
183
+ # ## disabled until js can compile without saving
184
+ # jsScript = exp.writeScript(target="PsychoJS")
185
+ # if "PsychoJS" in type(comp).targets:
186
+ # assert comp.name in jsScript, (
187
+ # f"{type(comp).__name__} not found in compiled Python script when enabled and PsychoJS in targets."
188
+ # )
189
+ # else:
190
+ # assert comp.name not in jsScript, (
191
+ # f"{type(comp).__name__} found in compiled Python script when enabled but PsychoJS not in targets."
192
+ # )
193
+
194
+ # disable component then do same tests but assert not present
195
+ comp.params['disabled'].val = True
196
+
197
+ pyScript = exp.writeScript(target="PsychoPy")
198
+ if "PsychoPy" in type(comp).targets:
199
+ assert comp.name not in pyScript, (
200
+ f"{type(comp).__name__} found in compiled Python script when disabled but PsychoPy in targets."
201
+ )
202
+ else:
203
+ assert comp.name not in pyScript, (
204
+ f"{type(comp).__name__} found in compiled Python script when disabled and PsychoPy not in targets."
205
+ )
206
+ # ## disabled until js can compile without saving
207
+ # jsScript = exp.writeScript(target="PsychoJS")
208
+ # if "PsychoJS" in type(comp).targets:
209
+ # assert comp.name not in jsScript, (
210
+ # f"{type(comp).__name__} found in compiled Python script when disabled but PsychoJS in targets."
211
+ # )
212
+ # else:
213
+ # assert comp.name not in jsScript, (
214
+ # f"{type(comp).__name__} found in compiled Python script when disabled and PsychoJS not in targets."
215
+ # )
71
216
 
72
- def testDeviceClassRefs(self):
217
+ def test_disabled_components_stay_in_routine(self):
218
+ """
219
+ Test that disabled components aren't removed from their routine when experiment is written.
220
+ """
221
+ comp, rt, exp = self.make_minimal_experiment()
222
+ # Disable component
223
+ comp.params['disabled'].val = True
224
+ # Writing the script drops the component but, if working properly, only from a copy of the routine, not the
225
+ # original!
226
+ exp.writeScript()
227
+
228
+ assert comp in rt, f"Disabling {type(comp).name} appears to remove it from its routine on compile."
229
+ class _TestLibraryClassMixin:
230
+ # class in the PsychoPy libraries (visual, sound, hardware, etc.) corresponding to this component
231
+ libraryClass = None
232
+
233
+ # --- Utility methods ---
234
+
235
+ @pytest.fixture(autouse=True)
236
+ def assert_lib_class(self):
237
+ """
238
+ Make sure this test object has an associated library class - and skip the test if not. This is run before each test by default.
239
+ """
240
+ # skip whole test if there is no Component connected to test class
241
+ if self.libraryClass is None:
242
+ pytest.skip()
243
+ # continue with the test as normal
244
+ yield
245
+
246
+ # --- Heritable tests ---
247
+
248
+ def test_device_class_refs(self):
73
249
  """
74
250
  Check that any references to device classes in this Routine object point to classes which
75
251
  exist.
76
252
  """
77
253
  # make minimal experiment just for this test
78
- comp, rt, exp = _make_minimal_experiment(self)
254
+ comp, rt, exp = self.make_minimal_experiment()
79
255
  # skip test if this element doesn't point to any hardware class
80
256
  if not hasattr(comp, "deviceClasses"):
81
257
  pytest.skip()
@@ -91,13 +267,7 @@ class _TestBaseComponentsMixin:
91
267
 
92
268
  def test_params_used(self):
93
269
  # Make minimal experiment just for this test
94
- comp, rt, exp = _make_minimal_experiment(self)
95
- # Skip if component shouldn't use all of its params
96
- if type(comp).__name__ in ["SettingsComponent", "CodeComponent"]:
97
- pytest.skip()
98
- # Skip if component is deprecated
99
- if type(comp).__name__ in ['RatingScaleComponent', 'PatchComponent']:
100
- pytest.skip()
270
+ comp, rt, exp = self.make_minimal_experiment()
101
271
  # Try with PsychoPy and PsychoJS
102
272
  for target in ("PsychoPy", "PsychoJS"):
103
273
  ## Skip PsychoJS until can write script without saving
@@ -134,11 +304,8 @@ class _TestBaseComponentsMixin:
134
304
  """
135
305
  Check that all params which are settable each frame/repeat have a set method in the corresponding class.
136
306
  """
137
- # Skip if there's no corresponding library class
138
- if self.libraryClass is None:
139
- return
140
307
  # Make minimal experiment just for this test
141
- comp, rt, exp = _make_minimal_experiment(self)
308
+ comp, rt, exp = self.make_minimal_experiment()
142
309
  # Check each param
143
310
  for paramName, param in comp.params.items():
144
311
  if not param.direct:
@@ -167,91 +334,11 @@ class _TestBaseComponentsMixin:
167
334
  )
168
335
 
169
336
 
170
- class _TestDisabledMixin:
171
- def test_disabled_default_val(self):
172
- """
173
- Test that components created with default params are not disabled
174
- """
175
- # Make minimal experiment just for this test
176
- comp, rt, exp = _make_minimal_experiment(self)
177
- # Check whether it can be disabled
178
- assert 'disabled' in comp.params, (
179
- f"{type(comp).__name__} does not have a 'disabled' attribute."
180
- )
181
- # Check that disabled defaults to False
182
- assert comp.params['disabled'].val is False, f"{type(comp).__name__} is defaulting to disabled."
183
-
184
- def test_code_muting(self):
185
- """
186
- Test that components are only written when enabled and targets match.
187
- """
188
- # Make minimal experiment just for this test
189
- comp, rt, exp = _make_minimal_experiment(self)
190
- # Skip for Code components as these purely inject code, name isn't used
191
- if type(comp).__name__ in ("CodeComponent"):
192
- pytest.skip()
193
- # Write experiment and check that component is written
194
- pyScript = exp.writeScript(target="PsychoPy")
195
- if "PsychoPy" in type(comp).targets:
196
- assert comp.name in pyScript, (
197
- f"{type(comp).__name__} not found in compiled Python script when enabled and PsychoPy in targets."
198
- )
199
- else:
200
- assert comp.name not in pyScript, (
201
- f"{type(comp).__name__} found in compiled Python script when enabled but PsychoPy not in targets."
202
- )
203
- # ## disabled until js can compile without saving
204
- # jsScript = exp.writeScript(target="PsychoJS")
205
- # if "PsychoJS" in type(comp).targets:
206
- # assert comp.name in jsScript, (
207
- # f"{type(comp).__name__} not found in compiled Python script when enabled and PsychoJS in targets."
208
- # )
209
- # else:
210
- # assert comp.name not in jsScript, (
211
- # f"{type(comp).__name__} found in compiled Python script when enabled but PsychoJS not in targets."
212
- # )
213
-
214
- # Disable component then do same tests but assert not present
215
- comp.params['disabled'].val = True
216
-
217
- pyScript = exp.writeScript(target="PsychoPy")
218
- if "PsychoPy" in type(comp).targets:
219
- assert comp.name not in pyScript, (
220
- f"{type(comp).__name__} found in compiled Python script when disabled but PsychoPy in targets."
221
- )
222
- else:
223
- assert comp.name not in pyScript, (
224
- f"{type(comp).__name__} found in compiled Python script when disabled and PsychoPy not in targets."
225
- )
226
- # ## disabled until js can compile without saving
227
- # jsScript = exp.writeScript(target="PsychoJS")
228
- # if "PsychoJS" in type(comp).targets:
229
- # assert comp.name not in jsScript, (
230
- # f"{type(comp).__name__} found in compiled Python script when disabled but PsychoJS in targets."
231
- # )
232
- # else:
233
- # assert comp.name not in jsScript, (
234
- # f"{type(comp).__name__} found in compiled Python script when disabled and PsychoJS not in targets."
235
- # )
236
-
237
- def test_disabled_components_stay_in_routine(self):
238
- """
239
- Test that disabled components aren't removed from their routine when experiment is written.
240
- """
241
- comp, rt, exp = _make_minimal_experiment(self)
242
- # Disable component
243
- comp.params['disabled'].val = True
244
- # Writing the script drops the component but, if working properly, only from a copy of the routine, not the
245
- # original!
246
- exp.writeScript()
247
-
248
- assert comp in rt, f"Disabling {type(comp).name} appears to remove it from its routine on compile."
249
-
250
337
 
251
338
  class _TestDepthMixin:
252
339
  def test_depth(self):
253
340
  # Make minimal experiment
254
- comp, rt, exp = _make_minimal_experiment(self)
341
+ comp, rt, exp = self.make_minimal_experiment()
255
342
  # Get class we're currently working with
256
343
  compClass = type(comp)
257
344
  # Add index to component name
@@ -947,7 +947,7 @@ class FontManager():
947
947
  return False
948
948
  # If font is found, make glfont
949
949
  fontInfo = fontInfos[0]
950
- identifier = "{}_{}".format(str(fontInfo), size)
950
+ identifier = "{}_{}_{}".format(str(fontInfo), size, lineSpacing)
951
951
  glFont = self._glFonts.get(identifier)
952
952
  if glFont is None:
953
953
  glFont = GLFont(fontInfo.path, size, lineSpacing=lineSpacing)
@@ -33,7 +33,7 @@ _remoteVersionsCache = []
33
33
  versionMap = OrderedDict({
34
34
  Version('2.7'): (Version("0.0"), Version("2020.2.0")),
35
35
  Version('3.6'): (Version("1.9"), Version("2022.1.0")),
36
- Version('3.8'): (Version("2022.1.0"), Version("2024.1.0")),
36
+ Version('3.8'): (Version("2022.1.0"), None),
37
37
  Version('3.10'): (Version("2023.2.0"), None),
38
38
  })
39
39
  # fill out intermediate versions
psychopy/visual/noise.py CHANGED
@@ -30,5 +30,14 @@ except (ModuleNotFoundError, ImportError):
30
30
  "Support for `NoiseStim` is not available this session. Please install "
31
31
  "`psychopy-visionscience` and restart the session to enable support.")
32
32
 
33
+
34
+ class NoiseStim:
35
+ """
36
+ `psychopy.visual.NoiseStim` is now located within the `psychopy-visionscience` plugin. You
37
+ can find the documentation for it `here
38
+ <https://psychopy.github.io/psychopy-visionscience/coder/NoiseStim>`_
39
+ """
40
+
41
+
33
42
  if __name__ == "__main__":
34
43
  pass
psychopy/visual/radial.py CHANGED
@@ -26,6 +26,14 @@ except (ModuleNotFoundError, ImportError):
26
26
  "Support for `RadialStim` is not available this session. Please install "
27
27
  "`psychopy-visionscience` and restart the session to enable support.")
28
28
 
29
+
30
+ class RadialStim:
31
+ """
32
+ `psychopy.visual.RadialStim` is now located within the `psychopy-visionscience` plugin. You
33
+ can find the documentation for it `here <https://psychopy.github.io/psychopy-visionscience/coder/RadialStim>`_
34
+ """
35
+
36
+
29
37
  if __name__ == "__main__":
30
38
  pass
31
39
 
@@ -25,5 +25,14 @@ except (ModuleNotFoundError, ImportError):
25
25
  "install `psychopy-visionscience` and restart the session to enable "
26
26
  "support.")
27
27
 
28
+
29
+ class EnvelopeGrating:
30
+ """
31
+ `psychopy.visual.EnvelopeGrating` is now located within the `psychopy-visionscience` plugin. You
32
+ can find the documentation for it
33
+ `here <https://psychopy.github.io/psychopy-visionscience/coder/EnvelopeGrating>`_
34
+ """
35
+
36
+
28
37
  if __name__ == "__main__":
29
38
  pass
@@ -79,7 +79,7 @@ class TextBox2(BaseVisualStim, DraggingMixin, ContainerMixin, ColorMixin):
79
79
  bold=False,
80
80
  italic=False,
81
81
  placeholder="Type here...",
82
- lineSpacing=None,
82
+ lineSpacing=1.0,
83
83
  letterSpacing=None,
84
84
  padding=None, # gap between box and text
85
85
  speechPoint=None,
@@ -190,10 +190,11 @@ class TextBox2(BaseVisualStim, DraggingMixin, ContainerMixin, ColorMixin):
190
190
  self._pixelScaling = self.letterHeightPix / self.letterHeight
191
191
  self.bold = bold
192
192
  self.italic = italic
193
+ if lineSpacing is None:
194
+ lineSpacing = 1.0
195
+ self.lineSpacing = lineSpacing
193
196
  self.glFont = None # will be set by the self.font attribute setter
194
197
  self.font = font
195
- if lineSpacing is not None:
196
- self.lineSpacing = lineSpacing
197
198
  self.letterSpacing = letterSpacing
198
199
  # If font not found, default to Open Sans Regular and raise alert
199
200
  if not self.glFont:
@@ -353,7 +354,7 @@ class TextBox2(BaseVisualStim, DraggingMixin, ContainerMixin, ColorMixin):
353
354
  self.caret.color = self._foreColor
354
355
 
355
356
  @attributeSetter
356
- def font(self, fontName, italic=False, bold=False):
357
+ def font(self, fontName):
357
358
  if isinstance(fontName, GLFont):
358
359
  self.glFont = fontName
359
360
  self.__dict__['font'] = fontName.name
@@ -362,7 +363,9 @@ class TextBox2(BaseVisualStim, DraggingMixin, ContainerMixin, ColorMixin):
362
363
  self.glFont = allFonts.getFont(
363
364
  fontName,
364
365
  size=self.letterHeightPix,
365
- bold=self.bold, italic=self.italic)
366
+ bold=self.bold,
367
+ italic=self.italic,
368
+ lineSpacing=self.lineSpacing)
366
369
 
367
370
  @attributeSetter
368
371
  def overflow(self, value):
@@ -610,19 +613,6 @@ class TextBox2(BaseVisualStim, DraggingMixin, ContainerMixin, ColorMixin):
610
613
  """
611
614
  return self._letterHeight.pix[1]
612
615
 
613
- @property
614
- def lineSpacing(self):
615
- if hasattr(self.glFont, "lineSpacing"):
616
- return self.glFont.lineSpacing
617
-
618
- @lineSpacing.setter
619
- def lineSpacing(self, value):
620
- if hasattr(self, "_placeholder"):
621
- self._placeholder.lineSpacing = value
622
- if hasattr(self.glFont, "lineSpacing"):
623
- self.glFont.lineSpacing = value
624
- self._needVertexUpdate = True
625
-
626
616
  @attributeSetter
627
617
  def letterSpacing(self, value):
628
618
  """
@@ -930,6 +920,9 @@ class TextBox2(BaseVisualStim, DraggingMixin, ContainerMixin, ColorMixin):
930
920
 
931
921
  # are we wrapping the line?
932
922
  if charcode == "\n":
923
+ # check if we have stored the top/bottom of the previous line yet
924
+ if lineN + 1 > len(_lineBottoms):
925
+ _lineBottoms.append(current[1])
933
926
  lineWPix = current[0]
934
927
  current[0] = 0
935
928
  current[1] -= font.height
@@ -1628,6 +1621,8 @@ class Caret(ColorMixin):
1628
1621
  self.index = len(self.textbox._lineNs)
1629
1622
  # Get line of index
1630
1623
  if self.index >= len(self.textbox._lineNs):
1624
+ if len(self.textbox._lineBottoms) - 1 > self.textbox._lineNs[-1]:
1625
+ return len(self.textbox._lineBottoms) - 1
1631
1626
  return self.textbox._lineNs[-1]
1632
1627
  else:
1633
1628
  return self.textbox._lineNs[self.index]
@@ -1718,9 +1713,12 @@ class Caret(ColorMixin):
1718
1713
  else:
1719
1714
  # Otherwise, get caret position from character vertices
1720
1715
  if self.index >= len(textbox._lineNs):
1721
- # If the caret is after the last char, position it to the right
1722
- chrVerts = textbox._vertices.pix[range((ii-1) * 4, (ii-1) * 4 + 4)]
1723
- x = chrVerts[2, 0] # x-coord of left edge (of final char)
1716
+ if len(textbox._lineBottoms) - 1 > textbox._lineNs[-1]:
1717
+ x = textbox._lineWidths[len(textbox._lineBottoms) - 1]
1718
+ else:
1719
+ # If the caret is after the last char, position it to the right
1720
+ chrVerts = textbox._vertices.pix[range((ii-1) * 4, (ii-1) * 4 + 4)]
1721
+ x = chrVerts[2, 0] # x-coord of left edge (of final char)
1724
1722
  else:
1725
1723
  # Otherwise, position it to the left
1726
1724
  chrVerts = textbox._vertices.pix[range(ii * 4, ii * 4 + 4)]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: psychopy
3
- Version: 2024.1.1
3
+ Version: 2024.1.3
4
4
  Summary: PsychoPy provides easy, precise, flexible experiments in behavioural sciences
5
5
  Author-Email: Open Science Tools Ltd <support@opensciencetools.org>
6
6
  Maintainer-Email: Open Science Tools Ltd <support@opensciencetools.org>