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.

Files changed (220) hide show
  1. psychopy/VERSION +1 -1
  2. psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
  3. psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
  4. psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
  5. psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
  6. psychopy/app/Resources/classic/add_many.png +0 -0
  7. psychopy/app/Resources/classic/add_many@2x.png +0 -0
  8. psychopy/app/Resources/classic/devices.png +0 -0
  9. psychopy/app/Resources/classic/devices@2x.png +0 -0
  10. psychopy/app/Resources/classic/photometer.png +0 -0
  11. psychopy/app/Resources/classic/photometer@2x.png +0 -0
  12. psychopy/app/Resources/dark/add_many.png +0 -0
  13. psychopy/app/Resources/dark/add_many@2x.png +0 -0
  14. psychopy/app/Resources/dark/devices.png +0 -0
  15. psychopy/app/Resources/dark/devices@2x.png +0 -0
  16. psychopy/app/Resources/dark/photometer.png +0 -0
  17. psychopy/app/Resources/dark/photometer@2x.png +0 -0
  18. psychopy/app/Resources/light/add_many.png +0 -0
  19. psychopy/app/Resources/light/add_many@2x.png +0 -0
  20. psychopy/app/Resources/light/devices.png +0 -0
  21. psychopy/app/Resources/light/devices@2x.png +0 -0
  22. psychopy/app/Resources/light/photometer.png +0 -0
  23. psychopy/app/Resources/light/photometer@2x.png +0 -0
  24. psychopy/app/_psychopyApp.py +35 -13
  25. psychopy/app/builder/builder.py +88 -35
  26. psychopy/app/builder/dialogs/__init__.py +69 -220
  27. psychopy/app/builder/dialogs/dlgsCode.py +29 -8
  28. psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
  29. psychopy/app/builder/validators.py +25 -17
  30. psychopy/app/coder/coder.py +12 -1
  31. psychopy/app/coder/repl.py +5 -2
  32. psychopy/app/colorpicker/__init__.py +1 -1
  33. psychopy/app/deviceManager/__init__.py +1 -0
  34. psychopy/app/deviceManager/addDialog.py +218 -0
  35. psychopy/app/deviceManager/dialog.py +185 -0
  36. psychopy/app/deviceManager/panel.py +191 -0
  37. psychopy/app/deviceManager/utils.py +60 -0
  38. psychopy/app/idle.py +7 -0
  39. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  40. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
  41. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  42. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
  43. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  44. psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
  45. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  46. psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
  47. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  48. psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
  49. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  50. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
  51. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  52. psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
  53. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  54. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
  55. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  56. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
  57. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  58. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
  59. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  60. psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
  61. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  62. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
  63. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  64. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
  65. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  66. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
  67. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  68. psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
  69. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  70. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
  71. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  72. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
  73. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  74. psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
  75. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  76. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
  77. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  78. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
  79. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  80. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
  81. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  82. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
  83. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  84. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
  85. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  86. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
  87. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  88. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
  89. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  90. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
  91. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  92. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
  93. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  94. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
  95. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  96. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
  97. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  98. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
  99. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  100. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
  101. psychopy/app/plugin_manager/dialog.py +12 -3
  102. psychopy/app/plugin_manager/packageIndex.py +303 -0
  103. psychopy/app/plugin_manager/packages.py +203 -63
  104. psychopy/app/plugin_manager/plugins.py +120 -240
  105. psychopy/app/preferencesDlg.py +6 -1
  106. psychopy/app/psychopyApp.py +16 -4
  107. psychopy/app/runner/runner.py +10 -2
  108. psychopy/app/runner/scriptProcess.py +8 -3
  109. psychopy/app/stdout/stdOutRich.py +11 -4
  110. psychopy/app/themes/icons.py +3 -0
  111. psychopy/app/utils.py +61 -0
  112. psychopy/data/experiment.py +133 -23
  113. psychopy/data/routine.py +12 -0
  114. psychopy/data/staircase.py +42 -20
  115. psychopy/data/trial.py +20 -12
  116. psychopy/data/utils.py +42 -2
  117. psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
  118. psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
  119. psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
  120. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
  121. psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
  122. psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
  123. psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
  124. psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
  125. psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
  126. psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
  127. psychopy/event.py +20 -15
  128. psychopy/experiment/_experiment.py +86 -10
  129. psychopy/experiment/components/__init__.py +3 -10
  130. psychopy/experiment/components/_base.py +9 -20
  131. psychopy/experiment/components/button/__init__.py +1 -1
  132. psychopy/experiment/components/buttonBox/__init__.py +50 -54
  133. psychopy/experiment/components/camera/__init__.py +137 -359
  134. psychopy/experiment/components/keyboard/__init__.py +17 -24
  135. psychopy/experiment/components/microphone/__init__.py +61 -110
  136. psychopy/experiment/components/movie/__init__.py +2 -3
  137. psychopy/experiment/components/serialOut/__init__.py +192 -93
  138. psychopy/experiment/components/settings/__init__.py +45 -27
  139. psychopy/experiment/components/sound/__init__.py +82 -73
  140. psychopy/experiment/components/soundsensor/__init__.py +43 -80
  141. psychopy/experiment/devices.py +303 -0
  142. psychopy/experiment/exports.py +20 -18
  143. psychopy/experiment/flow.py +7 -0
  144. psychopy/experiment/loops.py +47 -29
  145. psychopy/experiment/monitor.py +74 -0
  146. psychopy/experiment/params.py +48 -10
  147. psychopy/experiment/plugins.py +28 -108
  148. psychopy/experiment/py2js_transpiler.py +1 -1
  149. psychopy/experiment/routines/__init__.py +1 -1
  150. psychopy/experiment/routines/_base.py +59 -24
  151. psychopy/experiment/routines/audioValidator/__init__.py +19 -155
  152. psychopy/experiment/routines/visualValidator/__init__.py +25 -25
  153. psychopy/hardware/__init__.py +20 -57
  154. psychopy/hardware/button.py +15 -2
  155. psychopy/hardware/camera/__init__.py +2237 -1394
  156. psychopy/hardware/joystick/__init__.py +1 -1
  157. psychopy/hardware/keyboard.py +5 -8
  158. psychopy/hardware/listener.py +4 -1
  159. psychopy/hardware/manager.py +75 -35
  160. psychopy/hardware/microphone.py +52 -6
  161. psychopy/hardware/monitor.py +144 -0
  162. psychopy/hardware/photometer/__init__.py +156 -117
  163. psychopy/hardware/serialdevice.py +16 -2
  164. psychopy/hardware/soundsensor.py +4 -1
  165. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  166. psychopy/iohub/devices/keyboard/darwin.py +8 -5
  167. psychopy/iohub/util/__init__.py +7 -8
  168. psychopy/localization/generateTranslationTemplate.py +208 -116
  169. psychopy/localization/messages.pot +4305 -3502
  170. psychopy/monitors/MonitorCenter.py +174 -74
  171. psychopy/plugins/__init__.py +6 -4
  172. psychopy/preferences/devices.py +80 -0
  173. psychopy/preferences/generateHints.py +2 -1
  174. psychopy/preferences/preferences.py +35 -11
  175. psychopy/scripts/psychopy-pkgutil.py +969 -0
  176. psychopy/scripts/psyexpCompile.py +1 -1
  177. psychopy/session.py +34 -38
  178. psychopy/sound/__init__.py +6 -260
  179. psychopy/sound/audioclip.py +164 -0
  180. psychopy/sound/backend_ptb.py +8 -0
  181. psychopy/sound/backend_pygame.py +10 -0
  182. psychopy/sound/backend_pysound.py +9 -0
  183. psychopy/sound/backends/__init__.py +0 -0
  184. psychopy/sound/microphone.py +3 -0
  185. psychopy/sound/sound.py +58 -0
  186. psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
  187. psychopy/tests/data/duplicateHeaders.csv +2 -0
  188. psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
  189. psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
  190. psychopy/tests/test_data/test_utils.py +5 -1
  191. psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
  192. psychopy/tests/test_hardware/test_ports.py +0 -12
  193. psychopy/tests/test_tools/test_stringtools.py +1 -1
  194. psychopy/tools/attributetools.py +12 -5
  195. psychopy/tools/fontmanager.py +17 -14
  196. psychopy/tools/movietools.py +43 -2
  197. psychopy/tools/stringtools.py +33 -8
  198. psychopy/tools/versionchooser.py +1 -1
  199. psychopy/validation/audio.py +5 -1
  200. psychopy/validation/visual.py +5 -1
  201. psychopy/visual/basevisual.py +8 -7
  202. psychopy/visual/circle.py +2 -2
  203. psychopy/visual/image.py +29 -109
  204. psychopy/visual/movies/__init__.py +1800 -313
  205. psychopy/visual/polygon.py +4 -0
  206. psychopy/visual/shape.py +2 -2
  207. psychopy/visual/window.py +34 -11
  208. psychopy/voicekey/__init__.py +41 -669
  209. psychopy/voicekey/labjack_vks.py +7 -48
  210. psychopy/voicekey/parallel_vks.py +7 -42
  211. psychopy/voicekey/vk_tools.py +114 -263
  212. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/METADATA +17 -11
  213. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/RECORD +216 -184
  214. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
  215. psychopy/visual/movies/players/__init__.py +0 -62
  216. psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
  217. psychopy/voicekey/demo_vks.py +0 -12
  218. psychopy/voicekey/signal.py +0 -42
  219. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
  220. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -6,6 +6,8 @@ import wx
6
6
  from psychopy import prefs
7
7
  from psychopy.app import getAppInstance
8
8
  from psychopy.app.plugin_manager import PluginManagerPanel, PackageManagerPanel, InstallStdoutPanel
9
+ from psychopy.app.plugin_manager.packageIndex import (
10
+ loadPackageIndex, refreshPackageIndex, freePackageIndex)
9
11
  from psychopy.experiment import getAllElements
10
12
  from psychopy.localization import _translate
11
13
  import psychopy.logging as logging
@@ -16,7 +18,7 @@ import os
16
18
  import subprocess as sp
17
19
  import psychopy.plugins as plugins
18
20
 
19
- pkgtools.refreshPackages() # build initial package cache
21
+
20
22
 
21
23
 
22
24
  # flag to indicate if PsychoPy needs to be restarted after installing a package
@@ -68,6 +70,8 @@ class EnvironmentManagerDlg(wx.Dialog):
68
70
 
69
71
  self.notebook.ChangeSelection(0)
70
72
 
73
+ loadPackageIndex()
74
+
71
75
  @staticmethod
72
76
  def getPackageVersionInfo(packageName):
73
77
  """Query packages for available versions.
@@ -231,7 +235,7 @@ class EnvironmentManagerDlg(wx.Dialog):
231
235
  # if forceReinstall is None, work out from version
232
236
  if forceReinstall is None:
233
237
  forceReinstall = version is not None
234
- # use package tools to install
238
+ # use package tools to install
235
239
  self.pipProcess = pkgtools.installPackage(
236
240
  packageName,
237
241
  upgrade=version is None,
@@ -349,8 +353,10 @@ class EnvironmentManagerDlg(wx.Dialog):
349
353
  # clear pip process
350
354
  self.pipProcess = None
351
355
  # refresh view
352
- pkgtools.refreshPackages()
356
+ refreshPackageIndex()
357
+ loadPackageIndex()
353
358
  self.pluginMgr.updateInfo()
359
+ self.packageMgr.refresh()
354
360
 
355
361
  def onUninstallExit(self, pid, exitCode):
356
362
  # write installation termination statement
@@ -375,6 +381,9 @@ class EnvironmentManagerDlg(wx.Dialog):
375
381
  # if they change their mind, cancel closing
376
382
  if dlg.ShowModal() == wx.ID_NO:
377
383
  return
384
+
385
+ # free package index
386
+ freePackageIndex()
378
387
 
379
388
  if evt is not None:
380
389
  evt.Skip()
@@ -0,0 +1,303 @@
1
+ # get script path
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from psychopy import logging
5
+ from psychopy import prefs
6
+ import sys
7
+ import subprocess as sp
8
+ import os
9
+ import json
10
+ import wx
11
+
12
+ _packageIndex = None
13
+ _isIndexing = True # Flag to indicate if the package index is being updated
14
+
15
+
16
+ def refreshPackageIndex(fetch=False):
17
+ """Refresh the package index.
18
+
19
+ Parameters
20
+ ----------
21
+ fetch : bool, optional
22
+ If True, fetch the latest package index from the remote server
23
+ regardless of whether it is already present on disk. The default is
24
+ False, which means it will only update if the index is not present or is
25
+ outdated.
26
+
27
+ """
28
+ global _isIndexing
29
+ _isIndexing = True
30
+ scriptDir = prefs.paths['scripts']
31
+
32
+ # Construct the command to run the script
33
+ _cmd = [
34
+ sys.executable,
35
+ os.path.normpath(os.path.join(scriptDir, 'psychopy-pkgutil.py')),
36
+ '--app-pref-dir', prefs.paths['userPrefsDir'],
37
+ 'update']
38
+ _cmd += ['--fetch'] if fetch else []
39
+
40
+ # Execute the command
41
+ try:
42
+ headerText = ' Updating package index '
43
+ headerText = headerText.center(80, '=')
44
+ print(headerText)
45
+
46
+ env = os.environ.copy()
47
+ print(f"Running command: {' '.join(_cmd)}")
48
+
49
+ proc = sp.Popen(_cmd,
50
+ stdout=sp.PIPE,
51
+ stderr=sp.PIPE,
52
+ env=env,
53
+ universal_newlines=True)
54
+ _, error = proc.communicate()
55
+ if proc.returncode != 0:
56
+ return
57
+ if error:
58
+ logging.error(f"Error refreshing package index: {error}")
59
+ print(f"Error: {error}")
60
+
61
+ logging.info("Package index refreshed successfully.")
62
+ except sp.CalledProcessError as e:
63
+ logging.error(f"Error refreshing package index: {e}")
64
+ except FileNotFoundError:
65
+ logging.error("The script was not found. Please check the script path.")
66
+ except Exception as e:
67
+ logging.error(f"An unexpected error occurred: {e}")
68
+ finally:
69
+ _isIndexing = False
70
+
71
+
72
+ def isIndexing():
73
+ """Check if the package index is currently being updated.
74
+
75
+ Returns
76
+ -------
77
+ bool
78
+ True if the package index is being updated, False otherwise. This is to
79
+ check if the package index is currently being refreshed from another
80
+ thread.
81
+
82
+ """
83
+ global _isIndexing
84
+ return _isIndexing
85
+
86
+
87
+ def downloadPluginAssets(fetch=False):
88
+ """Download assets for the specified plugin.
89
+
90
+ Parameters
91
+ ----------
92
+ fetch : bool, optional
93
+ If True, fetch the plugin assets even if present on disk.
94
+ The default is False.
95
+
96
+ """
97
+ global _packageIndex
98
+ if _packageIndex is None:
99
+ loadPackageIndex()
100
+
101
+ # get all plugin icon URLs
102
+ pluginIconsURLs = []
103
+ for plugin in _packageIndex['available']['plugins']['packages'].values():
104
+ pluginIcon = plugin.get('icon', None)
105
+ if pluginIcon is not None:
106
+ pluginIconsURLs.append(pluginIcon)
107
+
108
+ # cache directory for the plugin icons
109
+ appPluginCacheDir = os.path.join(
110
+ prefs.paths['userCacheDir'], 'appCache', 'plugins')
111
+
112
+ # make sure we have a directory to put these files in
113
+ try:
114
+ os.makedirs(appPluginCacheDir, exist_ok=True)
115
+ except OSError as err:
116
+ if err.errno != os.errno.EEXIST:
117
+ logging.error(f"Error creating directory {appPluginCacheDir}: {err}")
118
+ raise
119
+
120
+ headerText = ' Downloading plugin icons '
121
+ headerText = headerText.center(80, '=')
122
+ print(headerText)
123
+
124
+ for iconUrl in pluginIconsURLs:
125
+ # get the icon file name from URL
126
+ iconFileName = os.path.basename(iconUrl)
127
+ # get the icon file path
128
+ iconPath = os.path.join(appPluginCacheDir, iconFileName)
129
+
130
+ # check if we already have a copy of the icon
131
+ if os.path.exists(iconPath) and not fetch:
132
+ print(f"Plugin icon already exists at {iconPath}")
133
+ continue
134
+
135
+ print(f"Downloading plugin icon from {iconUrl} to {iconPath}")
136
+
137
+ import requests
138
+
139
+ try:
140
+ # Make a GET request to download the file
141
+ response = requests.get(iconUrl, stream=True)
142
+ response.raise_for_status() # Raise an error for bad responses
143
+
144
+ # Open the local file in write-binary mode and save the content
145
+ with open(iconPath, 'wb') as file:
146
+ for chunk in response.iter_content(chunk_size=8192):
147
+ file.write(chunk)
148
+ wx.YieldIfNeeded()
149
+
150
+ print(f"Plugin icon downloaded successfully to {iconPath}")
151
+ except requests.exceptions.RequestException as e:
152
+ logging.error(f"Error downloading plugin icon: {e}")
153
+ except IOError as e:
154
+ logging.error(f"Error saving plugin icon: {e}")
155
+ except Exception as e:
156
+ logging.error(f"An unexpected error occurred: {e}")
157
+
158
+
159
+ def loadPackageIndex():
160
+ """Load the package index from the specified file.
161
+ """
162
+ global _packageIndex
163
+ try:
164
+ # Load the package index from the specified file
165
+ packageIndexPath = os.path.join(
166
+ prefs.paths['userPrefsDir'], 'cache', 'appCache', 'psychopy_packages.json')
167
+ with open(packageIndexPath, 'r') as f:
168
+ indexData = f.read()
169
+ _packageIndex = json.loads(indexData)
170
+ except FileNotFoundError:
171
+ logging.error("Package index file not found.")
172
+ except json.JSONDecodeError:
173
+ logging.error("Error decoding package index JSON.")
174
+
175
+
176
+ def freePackageIndex():
177
+ """Free the package index to allow it to be reloaded.
178
+ """
179
+ global _packageIndex
180
+ _packageIndex = None
181
+
182
+
183
+ def getInstalledPackages():
184
+ """Get the list of installed packages from the package index.
185
+ """
186
+ global _packageIndex
187
+ if _packageIndex is None:
188
+ loadPackageIndex()
189
+
190
+ return _packageIndex['installed'] if _packageIndex else {}
191
+
192
+
193
+ def getRemotePackages():
194
+ """Get the list of remote packages from the package index.
195
+ """
196
+ global _packageIndex
197
+ if _packageIndex is None:
198
+ loadPackageIndex()
199
+
200
+ return _packageIndex['available']['remote']['PyPI'] if _packageIndex else []
201
+
202
+
203
+ def getPluginPackages(asList=True):
204
+ """Get the list of plugin packages from the package index.
205
+ """
206
+ global _packageIndex
207
+ if _packageIndex is None:
208
+ loadPackageIndex()
209
+
210
+ if asList: # legacy
211
+ return list(_packageIndex['available']['plugins']['packages'].values())
212
+
213
+ return _packageIndex['available']['plugins']['packages'] if _packageIndex else {}
214
+
215
+
216
+ def isPackageInstalled(packageName):
217
+ """Check if a package is installed.
218
+
219
+ Returns
220
+ -------
221
+ tuple
222
+ A tuple containing a boolean indicating if the package is installed,
223
+ and its version if installed.
224
+ If the package is not installed, the version will be None.
225
+
226
+ """
227
+ global _packageIndex
228
+ if _packageIndex is None:
229
+ loadPackageIndex()
230
+
231
+ if isUserPackageInstalled(packageName):
232
+ state = 'u'
233
+ elif isSystemPackageInstalled(packageName):
234
+ state = 's'
235
+ elif packageName in getRemotePackages():
236
+ state = 'n'
237
+ else:
238
+ state = None
239
+
240
+ if state is not None:
241
+ version = None
242
+ else:
243
+ version = None
244
+
245
+ return state, version
246
+
247
+
248
+ def isSystemPackageInstalled(packageName):
249
+ """Check if a package is installed in the system directory.
250
+
251
+ Returns
252
+ -------
253
+ bool
254
+ True if the package is installed in the system directory, False otherwise.
255
+
256
+ """
257
+ global _packageIndex
258
+ if _packageIndex is None:
259
+ loadPackageIndex()
260
+
261
+ # Check if the package is in the system packages list
262
+ return packageName in _packageIndex['installed']['system']['packages'].keys()
263
+
264
+
265
+ def isUserPackageInstalled(packageName):
266
+ """Check if a package is installed in the user directory.
267
+
268
+ Returns
269
+ -------
270
+ bool
271
+ True if the package is installed in the user directory, False otherwise.
272
+
273
+ """
274
+ global _packageIndex
275
+ if _packageIndex is None:
276
+ loadPackageIndex()
277
+
278
+ print(list(_packageIndex['installed']['user']['packages'].keys()))
279
+
280
+ # Check if the package is in the user packages list
281
+ return packageName in _packageIndex['installed']['user']['packages'].keys()
282
+
283
+
284
+ def getAvailablePackages():
285
+ """Get the list of available packages from the package index.
286
+ """
287
+ global _packageIndex
288
+ if _packageIndex is None:
289
+ loadPackageIndex()
290
+
291
+ return _packageIndex['available']['PyPI']['packages'] if _packageIndex else {}
292
+
293
+
294
+ def refreshPackageIndexTask(app=None):
295
+ """
296
+ Run the refreshPackageIndex.py script to update the package index.
297
+ """
298
+ refreshPackageIndex()
299
+ downloadPluginAssets()
300
+
301
+
302
+ if __name__ == "__main__":
303
+ pass