boris-behav-obs 8.16.5__py3-none-any.whl → 9.7.12__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.
Files changed (126) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -40
  4. boris/add_modifier.py +88 -80
  5. boris/add_modifier_ui.py +266 -144
  6. boris/advanced_event_filtering.py +23 -29
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_export_to_feral.py +225 -0
  9. boris/analysis_plugins/_latency.py +59 -0
  10. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  11. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  13. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  14. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  15. boris/analysis_plugins/number_of_occurences.py +22 -0
  16. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  17. boris/analysis_plugins/time_budget.py +61 -0
  18. boris/behav_coding_map_creator.py +235 -236
  19. boris/behavior_binary_table.py +33 -50
  20. boris/behaviors_coding_map.py +17 -18
  21. boris/boris_cli.py +6 -25
  22. boris/cmd_arguments.py +12 -1
  23. boris/coding_pad.py +19 -36
  24. boris/config.py +109 -50
  25. boris/config_file.py +58 -67
  26. boris/connections.py +105 -58
  27. boris/converters.py +13 -37
  28. boris/converters_ui.py +187 -110
  29. boris/cooccurence.py +250 -0
  30. boris/core.py +2174 -1303
  31. boris/core_qrc.py +15892 -10829
  32. boris/core_ui.py +941 -806
  33. boris/db_functions.py +17 -42
  34. boris/dev.py +27 -7
  35. boris/dialog.py +461 -242
  36. boris/duration_widget.py +9 -14
  37. boris/edit_event.py +61 -31
  38. boris/edit_event_ui.py +208 -97
  39. boris/event_operations.py +405 -281
  40. boris/events_cursor.py +25 -17
  41. boris/events_snapshots.py +36 -82
  42. boris/exclusion_matrix.py +4 -9
  43. boris/export_events.py +180 -203
  44. boris/export_observation.py +60 -73
  45. boris/external_processes.py +123 -98
  46. boris/geometric_measurement.py +427 -218
  47. boris/gui_utilities.py +91 -14
  48. boris/image_overlay.py +4 -4
  49. boris/import_observations.py +190 -98
  50. boris/ipc_mpv.py +325 -0
  51. boris/irr.py +20 -57
  52. boris/latency.py +31 -24
  53. boris/measurement_widget.py +14 -18
  54. boris/media_file.py +17 -19
  55. boris/menu_options.py +16 -6
  56. boris/modifier_coding_map_creator.py +1013 -0
  57. boris/modifiers_coding_map.py +7 -9
  58. boris/mpv2.py +128 -35
  59. boris/observation.py +501 -211
  60. boris/observation_operations.py +1037 -393
  61. boris/observation_ui.py +573 -363
  62. boris/observations_list.py +51 -58
  63. boris/otx_parser.py +74 -68
  64. boris/param_panel.py +45 -59
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +91 -56
  67. boris/plot_data_module.py +20 -53
  68. boris/plot_events.py +56 -153
  69. boris/plot_events_rt.py +16 -30
  70. boris/plot_spectrogram_rt.py +83 -56
  71. boris/plot_waveform_rt.py +27 -49
  72. boris/plugins.py +468 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +307 -123
  80. boris/preferences_ui.py +686 -227
  81. boris/project.py +294 -271
  82. boris/project_functions.py +626 -537
  83. boris/project_import_export.py +204 -213
  84. boris/project_ui.py +673 -441
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +62 -90
  88. boris/select_observations.py +19 -197
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +51 -33
  91. boris/subjects_pad.py +7 -9
  92. boris/synthetic_time_budget.py +42 -26
  93. boris/time_budget_functions.py +169 -169
  94. boris/time_budget_widget.py +77 -89
  95. boris/transitions.py +41 -41
  96. boris/utilities.py +594 -226
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +86 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +240 -136
  104. boris_behav_obs-9.7.12.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.12.dist-info/RECORD +110 -0
  106. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.12.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/converters.ui +0 -289
  111. boris/core.qrc +0 -37
  112. boris/core.ui +0 -1571
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -982
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1074
  120. boris/vlc_local.py +0 -90
  121. boris_behav_obs-8.16.5.dist-info/LICENSE.TXT +0 -674
  122. boris_behav_obs-8.16.5.dist-info/METADATA +0 -134
  123. boris_behav_obs-8.16.5.dist-info/RECORD +0 -107
  124. boris_behav_obs-8.16.5.dist-info/entry_points.txt +0 -2
  125. {boris → boris_behav_obs-9.7.12.dist-info/licenses}/LICENSE.TXT +0 -0
  126. {boris_behav_obs-8.16.5.dist-info → boris_behav_obs-9.7.12.dist-info}/top_level.txt +0 -0
boris/plugins.py ADDED
@@ -0,0 +1,468 @@
1
+ """
2
+ BORIS
3
+ Behavioral Observation Research Interactive Software
4
+ Copyright 2012-2025 Olivier Friard
5
+
6
+ This program is free software; you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation; either version 2 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program; if not, write to the Free Software
18
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19
+ MA 02110-1301, USA.
20
+ """
21
+
22
+ import importlib
23
+ import logging
24
+ import numpy as np
25
+ import pandas as pd
26
+ from pathlib import Path
27
+ import copy
28
+ import inspect
29
+
30
+ from PySide6.QtGui import QAction
31
+ from PySide6.QtWidgets import QMessageBox
32
+
33
+ from . import config as cfg
34
+ from . import project_functions
35
+ from . import dialog
36
+ from . import view_df
37
+
38
+
39
+ def add_plugins_to_menu(self):
40
+ """
41
+ add plugins to the plugins menu
42
+ """
43
+ for plugin_name in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
44
+ logging.debug(f"adding plugin '{plugin_name}' to menu")
45
+ # Create an action for each submenu option
46
+ action = QAction(self, triggered=lambda checked=False, name=plugin_name: run_plugin(self, name))
47
+ action.setText(plugin_name)
48
+
49
+ self.menu_plugins.addAction(action)
50
+
51
+
52
+ def get_plugin_name(plugin_path: str) -> str | None:
53
+ """
54
+ get name of a Python plugin
55
+ """
56
+ # search plugin name
57
+ plugin_name: str | None = None
58
+ with open(plugin_path, "r") as f_in:
59
+ for line in f_in:
60
+ if line.startswith("__plugin_name__"):
61
+ plugin_name = line.split("=")[1].strip().replace('"', "")
62
+ break
63
+ return plugin_name
64
+
65
+
66
+ def get_r_plugin_name(plugin_path: str) -> str | None:
67
+ """
68
+ get name of a R plugin
69
+ """
70
+ # search plugin name
71
+ plugin_name: str | None = None
72
+ with open(plugin_path, "r") as f_in:
73
+ for line in f_in:
74
+ if line.startswith("plugin_name"):
75
+ if "=" in line:
76
+ plugin_name = line.split("=")[1].strip().replace('"', "").replace("'", "")
77
+ break
78
+ elif "<-" in line:
79
+ plugin_name = line.split("<-")[1].strip().replace('"', "").replace("'", "")
80
+ break
81
+ else:
82
+ plugin_name = None
83
+ break
84
+ return plugin_name
85
+
86
+
87
+ def get_r_plugin_description(plugin_path: str) -> str | None:
88
+ """
89
+ get description of a R plugin
90
+ """
91
+ # search plugin name
92
+ plugin_description: str | None = None
93
+ with open(plugin_path, "r") as f_in:
94
+ for line in f_in:
95
+ if line.startswith("description"):
96
+ if "=" in line:
97
+ plugin_description = line.split("=")[1].strip().replace('"', "").replace("'", "")
98
+ break
99
+ elif "<-" in line:
100
+ plugin_description = line.split("<-")[1].strip().replace('"', "").replace("'", "")
101
+ break
102
+ else:
103
+ plugin_description = None
104
+ break
105
+ return plugin_description
106
+
107
+
108
+ def load_plugins(self):
109
+ """
110
+ load selected plugins in config_param
111
+ """
112
+
113
+ logging.debug("Loading plugins")
114
+
115
+ def msg():
116
+ QMessageBox.warning(
117
+ self,
118
+ cfg.programName,
119
+ f"A plugin with the same name is already loaded ({self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name]}).\n\nThe plugin from {file_} is not loaded.",
120
+ QMessageBox.Ok | QMessageBox.Default,
121
+ QMessageBox.NoButton,
122
+ )
123
+
124
+ self.menu_plugins.clear()
125
+ self.config_param[cfg.ANALYSIS_PLUGINS] = {}
126
+
127
+ # load BORIS plugins
128
+ for file_ in sorted((Path(__file__).parent / "analysis_plugins").glob("*.py")):
129
+ if file_.name.startswith("_"):
130
+ continue
131
+
132
+ logging.debug(f"Loading plugin: {Path(file_).stem}")
133
+
134
+ # test module
135
+ module_name = Path(file_).stem
136
+ spec = importlib.util.spec_from_file_location(module_name, file_)
137
+ plugin_module = importlib.util.module_from_spec(spec)
138
+ spec.loader.exec_module(plugin_module)
139
+ attributes_list = dir(plugin_module)
140
+
141
+ if "__plugin_name__" in attributes_list:
142
+ plugin_name = plugin_module.__plugin_name__
143
+ else:
144
+ continue
145
+
146
+ if "run" not in attributes_list:
147
+ continue
148
+
149
+ # plugin_name = get_plugin_name(file_)
150
+ if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
151
+ # check if plugin with same name already loaded
152
+ if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
153
+ msg()
154
+ continue
155
+
156
+ self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
157
+
158
+ # load personal plugins
159
+ if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
160
+ for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py")):
161
+ if file_.name.startswith("_"):
162
+ continue
163
+
164
+ logging.debug(f"Loading personal plugin: {Path(file_).stem}")
165
+
166
+ # test module
167
+ module_name = Path(file_).stem
168
+ spec = importlib.util.spec_from_file_location(module_name, file_)
169
+ plugin_module = importlib.util.module_from_spec(spec)
170
+ spec.loader.exec_module(plugin_module)
171
+ attributes_list = dir(plugin_module)
172
+
173
+ if "__plugin_name__" in attributes_list:
174
+ plugin_name = plugin_module.__plugin_name__
175
+ else:
176
+ continue
177
+
178
+ if "run" not in attributes_list:
179
+ continue
180
+
181
+ # plugin_name = get_plugin_name(file_)
182
+ if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
183
+ # check if plugin with same name already loaded
184
+ if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
185
+ msg()
186
+ continue
187
+
188
+ self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
189
+
190
+ # load personal R plugins
191
+ if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
192
+ for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.R")):
193
+ if file_.name.startswith("_"):
194
+ continue
195
+ plugin_name = get_r_plugin_name(file_)
196
+ if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
197
+ # check if plugin with same name already loaded
198
+ if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
199
+ msg()
200
+ continue
201
+
202
+ self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
203
+
204
+ logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
205
+
206
+
207
+ def plugin_df_filter(df: pd.DataFrame, observations_list: list = [], parameters: dict = {}) -> pd.DataFrame:
208
+ """
209
+ filter the dataframe following parameters
210
+
211
+ filter by selected observations.
212
+ filter by selected subjects.
213
+ filter by selected behaviors.
214
+ filter by time interval.
215
+ """
216
+
217
+ # filter selected observations
218
+ df = df[df["Observation id"].isin(observations_list)]
219
+
220
+ if parameters:
221
+ # filter selected subjects
222
+ df = df[df["Subject"].isin(parameters["selected subjects"])]
223
+
224
+ # filter selected behaviors
225
+ df = df[df["Behavior"].isin(parameters["selected behaviors"])]
226
+
227
+ if parameters["time"] == cfg.TIME_OBS_INTERVAL:
228
+ # filter each observation with observation interval start/stop
229
+
230
+ # keep events between observation interval start time and observation interval stop/end
231
+ df_interval = df[
232
+ (
233
+ ((df["Start (s)"] >= df["Observation interval start"]) & (df["Start (s)"] <= df["Observation interval stop"]))
234
+ | ((df["Stop (s)"] >= df["Observation interval start"]) & (df["Stop (s)"] <= df["Observation interval stop"]))
235
+ )
236
+ | ((df["Start (s)"] < df["Observation interval start"]) & (df["Stop (s)"] > df["Observation interval stop"]))
237
+ ]
238
+
239
+ df_interval.loc[df["Start (s)"] < df["Observation interval start"], "Start (s)"] = df["Observation interval start"]
240
+ df_interval.loc[df["Stop (s)"] > df["Observation interval stop"], "Stop (s)"] = df["Observation interval stop"]
241
+
242
+ df_interval.loc[:, "Duration (s)"] = (df_interval["Stop (s)"] - df_interval["Start (s)"]).replace(0, np.nan)
243
+
244
+ df = df_interval
245
+
246
+ else:
247
+ # filter selected time interval
248
+ if parameters["start time"] is not None and parameters["end time"] is not None:
249
+ MIN_TIME = parameters["start time"]
250
+ MAX_TIME = parameters["end time"]
251
+
252
+ # keep events between start time and end_time
253
+ df_interval = df[
254
+ (
255
+ ((df["Start (s)"] >= MIN_TIME) & (df["Start (s)"] <= MAX_TIME))
256
+ | ((df["Stop (s)"] >= MIN_TIME) & (df["Stop (s)"] <= MAX_TIME))
257
+ )
258
+ | ((df["Start (s)"] < MIN_TIME) & (df["Stop (s)"] > MAX_TIME))
259
+ ]
260
+
261
+ # cut state events to interval
262
+ df_interval.loc[df["Start (s)"] < MIN_TIME, "Start (s)"] = MIN_TIME
263
+ df_interval.loc[df["Stop (s)"] > MAX_TIME, "Stop (s)"] = MAX_TIME
264
+
265
+ df_interval.loc[:, "Duration (s)"] = (df_interval["Stop (s)"] - df_interval["Start (s)"]).replace(0, np.nan)
266
+
267
+ df = df_interval
268
+
269
+ print("filtered")
270
+ print("=" * 50)
271
+
272
+ # print(f"{df=}")
273
+
274
+ return df
275
+
276
+
277
+ def run_plugin(self, plugin_name):
278
+ """
279
+ run plugin
280
+ """
281
+
282
+ if not self.project:
283
+ QMessageBox.warning(
284
+ self,
285
+ cfg.programName,
286
+ "No observations found. Open a project first",
287
+ QMessageBox.Ok | QMessageBox.Default,
288
+ QMessageBox.NoButton,
289
+ )
290
+ return
291
+
292
+ logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
293
+
294
+ if plugin_name not in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
295
+ QMessageBox.critical(self, cfg.programName, f"Plugin '{plugin_name}' not found")
296
+ return
297
+
298
+ plugin_path: str = self.config_param.get(cfg.ANALYSIS_PLUGINS, {}).get(plugin_name, "")
299
+
300
+ logging.debug(f"{plugin_path=}")
301
+
302
+ # check if plugin file exists
303
+ if not Path(plugin_path).is_file():
304
+ QMessageBox.critical(self, cfg.programName, f"The plugin {plugin_path} was not found.")
305
+ return
306
+
307
+ logging.debug(f"run plugin from {plugin_path}")
308
+
309
+ # select observations to analyze
310
+ selected_observations, parameters = self.obs_param()
311
+ if not selected_observations:
312
+ return
313
+
314
+ # Python plugin
315
+ if Path(plugin_path).suffix == ".py":
316
+ # load plugin as module
317
+ module_name = Path(plugin_path).stem
318
+
319
+ spec = importlib.util.spec_from_file_location(module_name, plugin_path)
320
+ plugin_module = importlib.util.module_from_spec(spec)
321
+
322
+ logging.debug(f"{plugin_module=}")
323
+
324
+ spec.loader.exec_module(plugin_module)
325
+
326
+ plugin_version = plugin_module.__version__
327
+ plugin_version_date = plugin_module.__version_date__
328
+
329
+ logging.info(
330
+ f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
331
+ )
332
+
333
+ # check arguments required by the run function of the plugin
334
+ dataframe_required = False
335
+ project_required = False
336
+ # for param in inspect.signature(plugin_module.run).parameters.values():
337
+ for name, annotation in inspect.getfullargspec(plugin_module.run).annotations.items():
338
+ if name == "df" and annotation is pd.DataFrame:
339
+ dataframe_required = True
340
+ if name == "project" and annotation is dict:
341
+ project_required = True
342
+
343
+ # create arguments for the plugin run function
344
+ plugin_kwargs: dict = {}
345
+
346
+ if dataframe_required:
347
+ logging.info("preparing dataframe for plugin")
348
+ message, df = project_functions.project2dataframe(self.pj, selected_observations)
349
+ if message:
350
+ logging.critical(message)
351
+ QMessageBox.critical(self, cfg.programName, message)
352
+ return
353
+ logging.info("done")
354
+
355
+ # filter the dataframe with parameters
356
+ logging.info("filtering dataframe for plugin")
357
+ filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
358
+ logging.info("done")
359
+
360
+ plugin_kwargs["df"] = filtered_df
361
+
362
+ if project_required:
363
+ pj_copy = copy.deepcopy(self.pj)
364
+
365
+ # remove unselected observations from project
366
+ for obs_id in self.pj[cfg.OBSERVATIONS]:
367
+ if obs_id not in selected_observations:
368
+ del pj_copy[cfg.OBSERVATIONS][obs_id]
369
+
370
+ plugin_kwargs["project"] = pj_copy
371
+
372
+ plugin_results = plugin_module.run(**plugin_kwargs)
373
+
374
+ # R plugin
375
+ if Path(plugin_path).suffix in (".R", ".r"):
376
+ try:
377
+ from rpy2 import robjects
378
+ from rpy2.robjects import pandas2ri
379
+ from rpy2.robjects.packages import SignatureTranslatedAnonymousPackage
380
+ from rpy2.robjects.conversion import localconverter
381
+ except Exception:
382
+ QMessageBox.critical(self, cfg.programName, "The rpy2 Python module is not installed. R plugins cannot be used")
383
+ return
384
+
385
+ logging.info("preparing dataframe for plugin")
386
+
387
+ message, df = project_functions.project2dataframe(self.pj, selected_observations)
388
+ if message:
389
+ logging.critical(message)
390
+ QMessageBox.critical(self, cfg.programName, message)
391
+ return
392
+
393
+ logging.info("done")
394
+
395
+ # filter the dataframe with parameters
396
+ logging.info("filtering dataframe for plugin")
397
+ filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
398
+ logging.info("done")
399
+
400
+ # Read code from file
401
+ try:
402
+ with open(plugin_path, "r") as f:
403
+ r_code = f.read()
404
+ except Exception:
405
+ QMessageBox.critical(self, cfg.programName, f"Error reading the plugin {plugin_path}.")
406
+ return
407
+
408
+ # read version
409
+ plugin_version = next(
410
+ (
411
+ x.split("<-")[1].replace('"', "").replace("'", "").strip()
412
+ for x in r_code.splitlines()
413
+ if x.replace(" ", "").startswith("version<-")
414
+ ),
415
+ None,
416
+ )
417
+ # read version date
418
+ plugin_version_date = next(
419
+ (
420
+ x.split("<-")[1].replace('"', "").replace("'", "").strip()
421
+ for x in r_code.split("\n")
422
+ if x.replace(" ", "").startswith("version_date<")
423
+ ),
424
+ None,
425
+ )
426
+
427
+ r_plugin = SignatureTranslatedAnonymousPackage(r_code, "r_plugin")
428
+
429
+ with localconverter(robjects.default_converter + pandas2ri.converter):
430
+ r_df = robjects.conversion.py2rpy(filtered_df)
431
+
432
+ try:
433
+ r_result = r_plugin.run(r_df)
434
+ except Exception as e:
435
+ QMessageBox.critical(self, cfg.programName, f"Error in the plugin {plugin_path}: {e}.")
436
+ return
437
+
438
+ with localconverter(robjects.default_converter + pandas2ri.converter):
439
+ plugin_results = robjects.conversion.rpy2py(r_result)
440
+
441
+ # test if plugin_results is a tuple: if not transform it to tuple
442
+ if not isinstance(plugin_results, tuple):
443
+ plugin_results = tuple([plugin_results])
444
+
445
+ self.plugin_visu: list = []
446
+ for result in plugin_results:
447
+ if isinstance(result, str):
448
+ self.plugin_visu.append(dialog.Results_dialog())
449
+ self.plugin_visu[-1].setWindowTitle(plugin_name)
450
+ self.plugin_visu[-1].ptText.clear()
451
+ self.plugin_visu[-1].ptText.appendPlainText(result)
452
+ self.plugin_visu[-1].show()
453
+ elif isinstance(result, pd.DataFrame):
454
+ self.plugin_visu.append(view_df.View_df(plugin_name, f"{plugin_version} ({plugin_version_date})", result))
455
+ self.plugin_visu[-1].show()
456
+ else:
457
+ # result is not str nor dataframe
458
+ QMessageBox.critical(
459
+ None,
460
+ cfg.programName,
461
+ (
462
+ f"Plugin returns an unknown object type: {type(result)}\n\n"
463
+ "Plugins must return str and/or Pandas Dataframes.\n"
464
+ "Check the plugin code."
465
+ ),
466
+ QMessageBox.Ok | QMessageBox.Default,
467
+ QMessageBox.NoButton,
468
+ )
boris/portion/__init__.py CHANGED
@@ -3,18 +3,28 @@ from .interval import Interval, open, closed, openclosed, closedopen, empty, sin
3
3
  from .func import iterate
4
4
  from .io import from_string, to_string, from_data, to_data
5
5
 
6
- # disabled because BORIS does not need IntervalDict
6
+ # disabled because BORIS does not need IntervalDict
7
7
  # so the sortedcontainers module is not required
8
- #from .dict import IntervalDict
8
+ # from .dict import IntervalDict
9
9
 
10
10
 
11
11
  __all__ = [
12
- 'inf', 'CLOSED', 'OPEN',
13
- 'Interval',
14
- 'open', 'closed', 'openclosed', 'closedopen', 'singleton', 'empty',
15
- 'iterate',
16
- 'from_string', 'to_string', 'from_data', 'to_data',
17
- 'IntervalDict',
12
+ "inf",
13
+ "CLOSED",
14
+ "OPEN",
15
+ "Interval",
16
+ "open",
17
+ "closed",
18
+ "openclosed",
19
+ "closedopen",
20
+ "singleton",
21
+ "empty",
22
+ "iterate",
23
+ "from_string",
24
+ "to_string",
25
+ "from_data",
26
+ "to_data",
27
+ "IntervalDict",
18
28
  ]
19
29
 
20
30
  CLOSED = Bound.CLOSED
boris/portion/const.py CHANGED
@@ -5,11 +5,12 @@ class Bound(enum.Enum):
5
5
  """
6
6
  Bound types, either CLOSED for inclusive, or OPEN for exclusive.
7
7
  """
8
+
8
9
  CLOSED = True
9
10
  OPEN = False
10
11
 
11
12
  def __bool__(self):
12
- raise ValueError('The truth value of a bound is ambiguous.')
13
+ raise ValueError("The truth value of a bound is ambiguous.")
13
14
 
14
15
  def __invert__(self):
15
16
  return Bound.CLOSED if self is Bound.OPEN else Bound.OPEN
@@ -21,7 +22,7 @@ class Bound(enum.Enum):
21
22
  return self.name
22
23
 
23
24
 
24
- class _Singleton():
25
+ class _Singleton:
25
26
  __instance = None
26
27
 
27
28
  def __new__(cls, *args, **kwargs):
@@ -35,21 +36,29 @@ class _PInf(_Singleton):
35
36
  Represent positive infinity.
36
37
  """
37
38
 
38
- def __neg__(self): return _NInf()
39
+ def __neg__(self):
40
+ return _NInf()
39
41
 
40
- def __lt__(self, o): return False
42
+ def __lt__(self, o):
43
+ return False
41
44
 
42
- def __le__(self, o): return isinstance(o, _PInf)
45
+ def __le__(self, o):
46
+ return isinstance(o, _PInf)
43
47
 
44
- def __gt__(self, o): return not isinstance(o, _PInf)
48
+ def __gt__(self, o):
49
+ return not isinstance(o, _PInf)
45
50
 
46
- def __ge__(self, o): return True
51
+ def __ge__(self, o):
52
+ return True
47
53
 
48
- def __eq__(self, o): return isinstance(o, _PInf)
54
+ def __eq__(self, o):
55
+ return isinstance(o, _PInf)
49
56
 
50
- def __repr__(self): return '+inf'
57
+ def __repr__(self):
58
+ return "+inf"
51
59
 
52
- def __hash__(self): return hash(float('+inf'))
60
+ def __hash__(self):
61
+ return hash(float("+inf"))
53
62
 
54
63
 
55
64
  class _NInf(_Singleton):
@@ -57,21 +66,29 @@ class _NInf(_Singleton):
57
66
  Represent negative infinity.
58
67
  """
59
68
 
60
- def __neg__(self): return _PInf()
69
+ def __neg__(self):
70
+ return _PInf()
61
71
 
62
- def __lt__(self, o): return not isinstance(o, _NInf)
72
+ def __lt__(self, o):
73
+ return not isinstance(o, _NInf)
63
74
 
64
- def __le__(self, o): return True
75
+ def __le__(self, o):
76
+ return True
65
77
 
66
- def __gt__(self, o): return False
78
+ def __gt__(self, o):
79
+ return False
67
80
 
68
- def __ge__(self, o): return isinstance(o, _NInf)
81
+ def __ge__(self, o):
82
+ return isinstance(o, _NInf)
69
83
 
70
- def __eq__(self, o): return isinstance(o, _NInf)
84
+ def __eq__(self, o):
85
+ return isinstance(o, _NInf)
71
86
 
72
- def __repr__(self): return '-inf'
87
+ def __repr__(self):
88
+ return "-inf"
73
89
 
74
- def __hash__(self): return hash(float('-inf'))
90
+ def __hash__(self):
91
+ return hash(float("-inf"))
75
92
 
76
93
 
77
94
  # Positive infinity
boris/portion/dict.py CHANGED
@@ -28,7 +28,7 @@ class IntervalDict(MutableMapping):
28
28
  number of distinct values (not keys) that are stored.
29
29
  """
30
30
 
31
- __slots__ = ('_storage', )
31
+ __slots__ = ("_storage",)
32
32
 
33
33
  def __init__(self, mapping_or_iterable=None):
34
34
  """
@@ -352,10 +352,10 @@ class IntervalDict(MutableMapping):
352
352
  return key in self.domain()
353
353
 
354
354
  def __repr__(self):
355
- return '{}{}{}'.format(
356
- '{',
357
- ', '.join('{!r}: {!r}'.format(i, v) for i, v in self.items()),
358
- '}',
355
+ return "{}{}{}".format(
356
+ "{",
357
+ ", ".join("{!r}: {!r}".format(i, v) for i, v in self.items()),
358
+ "}",
359
359
  )
360
360
 
361
361
  def __eq__(self, other):
boris/portion/func.py CHANGED
@@ -31,7 +31,7 @@ def iterate(interval, step, *, base=None, reverse=False):
31
31
  :return: a lazy iterator.
32
32
  """
33
33
  if base is None:
34
- base = (lambda x: x)
34
+ base = lambda x: x
35
35
 
36
36
  exclude = operator.lt if not reverse else operator.gt
37
37
  include = operator.le if not reverse else operator.ge
@@ -39,7 +39,7 @@ def iterate(interval, step, *, base=None, reverse=False):
39
39
 
40
40
  value = base(interval.lower if not reverse else interval.upper)
41
41
  if (value == -inf and not reverse) or (value == inf and reverse):
42
- raise ValueError('Cannot start iteration with infinity.')
42
+ raise ValueError("Cannot start iteration with infinity.")
43
43
 
44
44
  for i in interval if not reverse else reversed(interval):
45
45
  value = base(i.lower if not reverse else i.upper)