boris-behav-obs 8.9.16__py3-none-any.whl → 9.7.6__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 boris-behav-obs might be problematic. Click here for more details.

Files changed (129) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +36 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +161 -77
  24. boris/config_file.py +63 -83
  25. boris/connections.py +112 -57
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2511 -1824
  30. boris/core_qrc.py +15895 -10185
  31. boris/core_ui.py +946 -792
  32. boris/db_functions.py +21 -41
  33. boris/dev.py +134 -0
  34. boris/dialog.py +505 -244
  35. boris/duration_widget.py +15 -20
  36. boris/edit_event.py +84 -28
  37. boris/edit_event_ui.py +214 -78
  38. boris/event_operations.py +517 -415
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +213 -583
  43. boris/export_observation.py +98 -611
  44. boris/external_processes.py +156 -97
  45. boris/geometric_measurement.py +652 -287
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +9 -9
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +26 -63
  51. boris/latency.py +34 -25
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +52 -84
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +655 -310
  60. boris/observation_operations.py +1036 -404
  61. boris/observation_ui.py +584 -356
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -80
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +43 -46
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -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 +306 -83
  80. boris/preferences_ui.py +685 -228
  81. boris/project.py +448 -293
  82. boris/project_functions.py +689 -254
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -199
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +53 -37
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +766 -266
  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 +125 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/boris_ui.py +0 -886
  111. boris/converters.ui +0 -289
  112. boris/core.qrc +0 -35
  113. boris/core.ui +0 -1543
  114. boris/edit_event.ui +0 -175
  115. boris/icons/logo_eye.ico +0 -0
  116. boris/map_creator.py +0 -850
  117. boris/observation.ui +0 -773
  118. boris/param_panel.ui +0 -379
  119. boris/preferences.ui +0 -537
  120. boris/project.ui +0 -1069
  121. boris/project_server.py +0 -236
  122. boris/vlc.py +0 -10343
  123. boris/vlc_local.py +0 -90
  124. boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
  125. boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
  126. boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
  127. boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
  128. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  129. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/plugins.py ADDED
@@ -0,0 +1,431 @@
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
+
28
+ from PySide6.QtGui import QAction
29
+ from PySide6.QtWidgets import QMessageBox
30
+
31
+ from . import config as cfg
32
+ from . import project_functions
33
+ from . import dialog
34
+ from . import view_df
35
+
36
+
37
+ def add_plugins_to_menu(self):
38
+ """
39
+ add plugins to the plugins menu
40
+ """
41
+ for plugin_name in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
42
+ logging.debug(f"adding plugin '{plugin_name}' to menu")
43
+ # Create an action for each submenu option
44
+ action = QAction(self, triggered=lambda checked=False, name=plugin_name: run_plugin(self, name))
45
+ action.setText(plugin_name)
46
+
47
+ self.menu_plugins.addAction(action)
48
+
49
+
50
+ def get_plugin_name(plugin_path: str) -> str | None:
51
+ """
52
+ get name of a Python plugin
53
+ """
54
+ # search plugin name
55
+ plugin_name: str | None = None
56
+ with open(plugin_path, "r") as f_in:
57
+ for line in f_in:
58
+ if line.startswith("__plugin_name__"):
59
+ plugin_name = line.split("=")[1].strip().replace('"', "")
60
+ break
61
+ return plugin_name
62
+
63
+
64
+ def get_r_plugin_name(plugin_path: str) -> str | None:
65
+ """
66
+ get name of a R plugin
67
+ """
68
+ # search plugin name
69
+ plugin_name: str | None = None
70
+ with open(plugin_path, "r") as f_in:
71
+ for line in f_in:
72
+ if line.startswith("plugin_name"):
73
+ if "=" in line:
74
+ plugin_name = line.split("=")[1].strip().replace('"', "").replace("'", "")
75
+ break
76
+ elif "<-" in line:
77
+ plugin_name = line.split("<-")[1].strip().replace('"', "").replace("'", "")
78
+ break
79
+ else:
80
+ plugin_name = None
81
+ break
82
+ return plugin_name
83
+
84
+
85
+ def get_r_plugin_description(plugin_path: str) -> str | None:
86
+ """
87
+ get description of a R plugin
88
+ """
89
+ # search plugin name
90
+ plugin_description: str | None = None
91
+ with open(plugin_path, "r") as f_in:
92
+ for line in f_in:
93
+ if line.startswith("description"):
94
+ if "=" in line:
95
+ plugin_description = line.split("=")[1].strip().replace('"', "").replace("'", "")
96
+ break
97
+ elif "<-" in line:
98
+ plugin_description = line.split("<-")[1].strip().replace('"', "").replace("'", "")
99
+ break
100
+ else:
101
+ plugin_description = None
102
+ break
103
+ return plugin_description
104
+
105
+
106
+ def load_plugins(self):
107
+ """
108
+ load selected plugins in config_param
109
+ """
110
+
111
+ logging.debug("Loading plugins")
112
+
113
+ def msg():
114
+ QMessageBox.warning(
115
+ self,
116
+ cfg.programName,
117
+ 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.",
118
+ QMessageBox.Ok | QMessageBox.Default,
119
+ QMessageBox.NoButton,
120
+ )
121
+
122
+ self.menu_plugins.clear()
123
+ self.config_param[cfg.ANALYSIS_PLUGINS] = {}
124
+
125
+ # load BORIS plugins
126
+ for file_ in sorted((Path(__file__).parent / "analysis_plugins").glob("*.py")):
127
+ if file_.name.startswith("_"):
128
+ continue
129
+
130
+ logging.debug(f"Loading plugin: {Path(file_).stem}")
131
+
132
+ # test module
133
+ module_name = Path(file_).stem
134
+ spec = importlib.util.spec_from_file_location(module_name, file_)
135
+ plugin_module = importlib.util.module_from_spec(spec)
136
+ spec.loader.exec_module(plugin_module)
137
+ attributes_list = dir(plugin_module)
138
+
139
+ if "__plugin_name__" in attributes_list:
140
+ plugin_name = plugin_module.__plugin_name__
141
+ else:
142
+ continue
143
+
144
+ if "run" not in attributes_list:
145
+ continue
146
+
147
+ # plugin_name = get_plugin_name(file_)
148
+ if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
149
+ # check if plugin with same name already loaded
150
+ if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
151
+ msg()
152
+ continue
153
+
154
+ self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
155
+
156
+ # load personal plugins
157
+ if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
158
+ for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.py")):
159
+ if file_.name.startswith("_"):
160
+ continue
161
+
162
+ logging.debug(f"Loading personal plugin: {Path(file_).stem}")
163
+
164
+ # test module
165
+ module_name = Path(file_).stem
166
+ spec = importlib.util.spec_from_file_location(module_name, file_)
167
+ plugin_module = importlib.util.module_from_spec(spec)
168
+ spec.loader.exec_module(plugin_module)
169
+ attributes_list = dir(plugin_module)
170
+
171
+ if "__plugin_name__" in attributes_list:
172
+ plugin_name = plugin_module.__plugin_name__
173
+ else:
174
+ continue
175
+
176
+ if "run" not in attributes_list:
177
+ continue
178
+
179
+ # plugin_name = get_plugin_name(file_)
180
+ if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
181
+ # check if plugin with same name already loaded
182
+ if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
183
+ msg()
184
+ continue
185
+
186
+ self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
187
+
188
+ # load personal R plugins
189
+ if self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, ""):
190
+ for file_ in sorted(Path(self.config_param.get(cfg.PERSONAL_PLUGINS_DIR, "")).glob("*.R")):
191
+ if file_.name.startswith("_"):
192
+ continue
193
+ plugin_name = get_r_plugin_name(file_)
194
+ if plugin_name is not None and plugin_name not in self.config_param.get(cfg.EXCLUDED_PLUGINS, set()):
195
+ # check if plugin with same name already loaded
196
+ if plugin_name in self.config_param[cfg.ANALYSIS_PLUGINS]:
197
+ msg()
198
+ continue
199
+
200
+ self.config_param[cfg.ANALYSIS_PLUGINS][plugin_name] = str(file_)
201
+
202
+ logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
203
+
204
+
205
+ def plugin_df_filter(df: pd.DataFrame, observations_list: list = [], parameters: dict = {}) -> pd.DataFrame:
206
+ """
207
+ filter the dataframe following parameters
208
+
209
+ filter by selected observations.
210
+ filter by selected subjects.
211
+ filter by selected behaviors.
212
+ filter by time interval.
213
+ """
214
+
215
+ # filter selected observations
216
+ df = df[df["Observation id"].isin(observations_list)]
217
+
218
+ if parameters:
219
+ # filter selected subjects
220
+ df = df[df["Subject"].isin(parameters["selected subjects"])]
221
+
222
+ # filter selected behaviors
223
+ df = df[df["Behavior"].isin(parameters["selected behaviors"])]
224
+
225
+ if parameters["time"] == cfg.TIME_OBS_INTERVAL:
226
+ # filter each observation with observation interval start/stop
227
+
228
+ # keep events between observation interval start time and observation interval stop/end
229
+ df_interval = df[
230
+ (
231
+ ((df["Start (s)"] >= df["Observation interval start"]) & (df["Start (s)"] <= df["Observation interval stop"]))
232
+ | ((df["Stop (s)"] >= df["Observation interval start"]) & (df["Stop (s)"] <= df["Observation interval stop"]))
233
+ )
234
+ | ((df["Start (s)"] < df["Observation interval start"]) & (df["Stop (s)"] > df["Observation interval stop"]))
235
+ ]
236
+
237
+ df_interval.loc[df["Start (s)"] < df["Observation interval start"], "Start (s)"] = df["Observation interval start"]
238
+ df_interval.loc[df["Stop (s)"] > df["Observation interval stop"], "Stop (s)"] = df["Observation interval stop"]
239
+
240
+ df_interval.loc[:, "Duration (s)"] = (df_interval["Stop (s)"] - df_interval["Start (s)"]).replace(0, np.nan)
241
+
242
+ df = df_interval
243
+
244
+ else:
245
+ # filter selected time interval
246
+ if parameters["start time"] is not None and parameters["end time"] is not None:
247
+ MIN_TIME = parameters["start time"]
248
+ MAX_TIME = parameters["end time"]
249
+
250
+ # keep events between start time and end_time
251
+ df_interval = df[
252
+ (
253
+ ((df["Start (s)"] >= MIN_TIME) & (df["Start (s)"] <= MAX_TIME))
254
+ | ((df["Stop (s)"] >= MIN_TIME) & (df["Stop (s)"] <= MAX_TIME))
255
+ )
256
+ | ((df["Start (s)"] < MIN_TIME) & (df["Stop (s)"] > MAX_TIME))
257
+ ]
258
+
259
+ # cut state events to interval
260
+ df_interval.loc[df["Start (s)"] < MIN_TIME, "Start (s)"] = MIN_TIME
261
+ df_interval.loc[df["Stop (s)"] > MAX_TIME, "Stop (s)"] = MAX_TIME
262
+
263
+ df_interval.loc[:, "Duration (s)"] = (df_interval["Stop (s)"] - df_interval["Start (s)"]).replace(0, np.nan)
264
+
265
+ df = df_interval
266
+
267
+ print("filtered")
268
+ print("=" * 50)
269
+
270
+ # print(f"{df=}")
271
+
272
+ return df
273
+
274
+
275
+ def run_plugin(self, plugin_name):
276
+ """
277
+ run plugin
278
+ """
279
+
280
+ if not self.project:
281
+ QMessageBox.warning(
282
+ self,
283
+ cfg.programName,
284
+ "No observations found. Open a project first",
285
+ QMessageBox.Ok | QMessageBox.Default,
286
+ QMessageBox.NoButton,
287
+ )
288
+ return
289
+
290
+ logging.debug(f"{self.config_param.get(cfg.ANALYSIS_PLUGINS, {})=}")
291
+
292
+ if plugin_name not in self.config_param.get(cfg.ANALYSIS_PLUGINS, {}):
293
+ QMessageBox.critical(self, cfg.programName, f"Plugin '{plugin_name}' not found")
294
+ return
295
+
296
+ plugin_path: str = self.config_param.get(cfg.ANALYSIS_PLUGINS, {}).get(plugin_name, "")
297
+
298
+ logging.debug(f"{plugin_path=}")
299
+
300
+ if not Path(plugin_path).is_file():
301
+ QMessageBox.critical(self, cfg.programName, f"The plugin {plugin_path} was not found.")
302
+ return
303
+
304
+ logging.debug(f"run plugin from {plugin_path}")
305
+
306
+ # select observations to analyze
307
+ selected_observations, parameters = self.obs_param()
308
+ if not selected_observations:
309
+ return
310
+
311
+ logging.info("preparing dataframe for plugin")
312
+
313
+ message, df = project_functions.project2dataframe(self.pj, selected_observations)
314
+ if message:
315
+ logging.critical(message)
316
+ QMessageBox.critical(self, cfg.programName, message)
317
+ return
318
+
319
+ logging.info("done")
320
+
321
+ """
322
+ logging.debug("dataframe info")
323
+ logging.debug(f"{df.info()}")
324
+ logging.debug(f"{df.head()}")
325
+ """
326
+
327
+ # filter the dataframe with parameters
328
+ logging.info("filtering dataframe for plugin")
329
+ filtered_df = plugin_df_filter(df, observations_list=selected_observations, parameters=parameters)
330
+ logging.info("done")
331
+
332
+ if Path(plugin_path).suffix == ".py":
333
+ # load plugin as module
334
+ module_name = Path(plugin_path).stem
335
+
336
+ spec = importlib.util.spec_from_file_location(module_name, plugin_path)
337
+ plugin_module = importlib.util.module_from_spec(spec)
338
+
339
+ logging.debug(f"{plugin_module=}")
340
+
341
+ spec.loader.exec_module(plugin_module)
342
+
343
+ plugin_version = plugin_module.__version__
344
+ plugin_version_date = plugin_module.__version_date__
345
+
346
+ logging.info(
347
+ f"{plugin_module.__plugin_name__} loaded v.{getattr(plugin_module, '__version__')} v. {getattr(plugin_module, '__version_date__')}"
348
+ )
349
+
350
+ # run plugin
351
+ plugin_results = plugin_module.run(filtered_df)
352
+
353
+ if Path(plugin_path).suffix in (".R", ".r"):
354
+ try:
355
+ from rpy2 import robjects
356
+ from rpy2.robjects import pandas2ri
357
+ from rpy2.robjects.packages import SignatureTranslatedAnonymousPackage
358
+ from rpy2.robjects.conversion import localconverter
359
+ except Exception:
360
+ QMessageBox.critical(self, cfg.programName, "The rpy2 Python module is not installed. R plugins cannot be used")
361
+ return
362
+
363
+ # Read code from file
364
+ try:
365
+ with open(plugin_path, "r") as f:
366
+ r_code = f.read()
367
+ except Exception:
368
+ QMessageBox.critical(self, cfg.programName, f"Error reading the plugin {plugin_path}.")
369
+ return
370
+
371
+ # read version
372
+ plugin_version = next(
373
+ (
374
+ x.split("<-")[1].replace('"', "").replace("'", "").strip()
375
+ for x in r_code.splitlines()
376
+ if x.replace(" ", "").startswith("version<-")
377
+ ),
378
+ None,
379
+ )
380
+ # read version date
381
+ plugin_version_date = next(
382
+ (
383
+ x.split("<-")[1].replace('"', "").replace("'", "").strip()
384
+ for x in r_code.split("\n")
385
+ if x.replace(" ", "").startswith("version_date<")
386
+ ),
387
+ None,
388
+ )
389
+
390
+ r_plugin = SignatureTranslatedAnonymousPackage(r_code, "r_plugin")
391
+
392
+ with localconverter(robjects.default_converter + pandas2ri.converter):
393
+ r_df = robjects.conversion.py2rpy(filtered_df)
394
+
395
+ try:
396
+ r_result = r_plugin.run(r_df)
397
+ except Exception as e:
398
+ QMessageBox.critical(self, cfg.programName, f"Error in the plugin {plugin_path}: {e}.")
399
+ return
400
+
401
+ with localconverter(robjects.default_converter + pandas2ri.converter):
402
+ plugin_results = robjects.conversion.rpy2py(r_result)
403
+
404
+ # test if plugin_results is a tuple: if not transform it to tuple
405
+ if not isinstance(plugin_results, tuple):
406
+ plugin_results = tuple([plugin_results])
407
+
408
+ self.plugin_visu: list = []
409
+ for result in plugin_results:
410
+ if isinstance(result, str):
411
+ self.plugin_visu.append(dialog.Results_dialog())
412
+ self.plugin_visu[-1].setWindowTitle(plugin_name)
413
+ self.plugin_visu[-1].ptText.clear()
414
+ self.plugin_visu[-1].ptText.appendPlainText(result)
415
+ self.plugin_visu[-1].show()
416
+ elif isinstance(result, pd.DataFrame):
417
+ self.plugin_visu.append(view_df.View_df(plugin_name, f"{plugin_version} ({plugin_version_date})", result))
418
+ self.plugin_visu[-1].show()
419
+ else:
420
+ # result is not str nor dataframe
421
+ QMessageBox.critical(
422
+ None,
423
+ cfg.programName,
424
+ (
425
+ f"Plugin returns an unknown object type: {type(result)}\n\n"
426
+ "Plugins must return str and/or Pandas Dataframes.\n"
427
+ "Check the plugin code."
428
+ ),
429
+ QMessageBox.Ok | QMessageBox.Default,
430
+ QMessageBox.NoButton,
431
+ )
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)