datalab-platform 0.0.1.dev0__py3-none-any.whl → 1.0.0__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 (496) hide show
  1. datalab/__init__.py +35 -2
  2. datalab/adapters_metadata/__init__.py +31 -0
  3. datalab/adapters_metadata/base_adapter.py +316 -0
  4. datalab/adapters_metadata/common.py +422 -0
  5. datalab/adapters_metadata/geometry_adapter.py +98 -0
  6. datalab/adapters_metadata/table_adapter.py +84 -0
  7. datalab/adapters_plotpy/__init__.py +54 -0
  8. datalab/adapters_plotpy/annotations.py +124 -0
  9. datalab/adapters_plotpy/base.py +110 -0
  10. datalab/adapters_plotpy/converters.py +86 -0
  11. datalab/adapters_plotpy/factories.py +80 -0
  12. datalab/adapters_plotpy/objects/__init__.py +0 -0
  13. datalab/adapters_plotpy/objects/base.py +197 -0
  14. datalab/adapters_plotpy/objects/image.py +157 -0
  15. datalab/adapters_plotpy/objects/scalar.py +565 -0
  16. datalab/adapters_plotpy/objects/signal.py +264 -0
  17. datalab/adapters_plotpy/roi/__init__.py +0 -0
  18. datalab/adapters_plotpy/roi/base.py +146 -0
  19. datalab/adapters_plotpy/roi/factory.py +93 -0
  20. datalab/adapters_plotpy/roi/image.py +207 -0
  21. datalab/adapters_plotpy/roi/signal.py +72 -0
  22. datalab/app.py +98 -0
  23. datalab/config.py +817 -0
  24. datalab/control/__init__.py +0 -0
  25. datalab/control/baseproxy.py +776 -0
  26. datalab/control/proxy.py +343 -0
  27. datalab/control/remote.py +1005 -0
  28. datalab/data/doc/DataLab_en.pdf +0 -0
  29. datalab/data/doc/DataLab_fr.pdf +0 -0
  30. datalab/data/icons/analysis/delete_results.svg +109 -0
  31. datalab/data/icons/analysis/fw1e2.svg +156 -0
  32. datalab/data/icons/analysis/fwhm.svg +156 -0
  33. datalab/data/icons/analysis/histogram.svg +49 -0
  34. datalab/data/icons/analysis/peak_detect.svg +160 -0
  35. datalab/data/icons/analysis/plot_results.svg +151 -0
  36. datalab/data/icons/analysis/show_results.svg +83 -0
  37. datalab/data/icons/analysis/stats.svg +49 -0
  38. datalab/data/icons/analysis.svg +120 -0
  39. datalab/data/icons/apply.svg +3 -0
  40. datalab/data/icons/check_all.svg +15 -0
  41. datalab/data/icons/collapse.svg +44 -0
  42. datalab/data/icons/collapse_selection.svg +63 -0
  43. datalab/data/icons/console.svg +101 -0
  44. datalab/data/icons/create/1d-normal.svg +8 -0
  45. datalab/data/icons/create/1d-poisson.svg +9 -0
  46. datalab/data/icons/create/1d-uniform.svg +8 -0
  47. datalab/data/icons/create/1d-zero.svg +57 -0
  48. datalab/data/icons/create/2d-gaussian.svg +56 -0
  49. datalab/data/icons/create/2d-normal.svg +38 -0
  50. datalab/data/icons/create/2d-poisson.svg +38 -0
  51. datalab/data/icons/create/2d-ramp.svg +90 -0
  52. datalab/data/icons/create/2d-sinc.svg +62 -0
  53. datalab/data/icons/create/2d-uniform.svg +38 -0
  54. datalab/data/icons/create/2d-zero.svg +13 -0
  55. datalab/data/icons/create/checkerboard.svg +39 -0
  56. datalab/data/icons/create/cosine.svg +12 -0
  57. datalab/data/icons/create/exponential.svg +55 -0
  58. datalab/data/icons/create/gaussian.svg +12 -0
  59. datalab/data/icons/create/grating.svg +29 -0
  60. datalab/data/icons/create/linear_chirp.svg +7 -0
  61. datalab/data/icons/create/logistic.svg +7 -0
  62. datalab/data/icons/create/lorentzian.svg +12 -0
  63. datalab/data/icons/create/planck.svg +12 -0
  64. datalab/data/icons/create/polynomial.svg +7 -0
  65. datalab/data/icons/create/pulse.svg +12 -0
  66. datalab/data/icons/create/ring.svg +18 -0
  67. datalab/data/icons/create/sawtooth.svg +7 -0
  68. datalab/data/icons/create/siemens.svg +35 -0
  69. datalab/data/icons/create/sinc.svg +12 -0
  70. datalab/data/icons/create/sine.svg +7 -0
  71. datalab/data/icons/create/square.svg +7 -0
  72. datalab/data/icons/create/square_pulse.svg +7 -0
  73. datalab/data/icons/create/step.svg +7 -0
  74. datalab/data/icons/create/step_pulse.svg +12 -0
  75. datalab/data/icons/create/triangle.svg +7 -0
  76. datalab/data/icons/create/voigt.svg +12 -0
  77. datalab/data/icons/edit/annotations.svg +72 -0
  78. datalab/data/icons/edit/annotations_copy.svg +114 -0
  79. datalab/data/icons/edit/annotations_delete.svg +83 -0
  80. datalab/data/icons/edit/annotations_edit.svg +98 -0
  81. datalab/data/icons/edit/annotations_export.svg +85 -0
  82. datalab/data/icons/edit/annotations_import.svg +85 -0
  83. datalab/data/icons/edit/annotations_paste.svg +100 -0
  84. datalab/data/icons/edit/copy_titles.svg +109 -0
  85. datalab/data/icons/edit/delete.svg +84 -0
  86. datalab/data/icons/edit/delete_all.svg +214 -0
  87. datalab/data/icons/edit/duplicate.svg +64 -0
  88. datalab/data/icons/edit/goto_source.svg +60 -0
  89. datalab/data/icons/edit/metadata.svg +60 -0
  90. datalab/data/icons/edit/metadata_add.svg +80 -0
  91. datalab/data/icons/edit/metadata_copy.svg +96 -0
  92. datalab/data/icons/edit/metadata_delete.svg +62 -0
  93. datalab/data/icons/edit/metadata_export.svg +68 -0
  94. datalab/data/icons/edit/metadata_import.svg +68 -0
  95. datalab/data/icons/edit/metadata_paste.svg +79 -0
  96. datalab/data/icons/edit/move_down.svg +55 -0
  97. datalab/data/icons/edit/move_up.svg +54 -0
  98. datalab/data/icons/edit/new_group.svg +76 -0
  99. datalab/data/icons/edit/recompute.svg +60 -0
  100. datalab/data/icons/edit/rename.svg +49 -0
  101. datalab/data/icons/edit.svg +16 -0
  102. datalab/data/icons/expand.svg +44 -0
  103. datalab/data/icons/expand_selection.svg +63 -0
  104. datalab/data/icons/fit/cdf_fit.svg +56 -0
  105. datalab/data/icons/fit/exponential_fit.svg +55 -0
  106. datalab/data/icons/fit/gaussian_fit.svg +62 -0
  107. datalab/data/icons/fit/interactive_fit.svg +101 -0
  108. datalab/data/icons/fit/linear_fit.svg +57 -0
  109. datalab/data/icons/fit/lorentzian_fit.svg +209 -0
  110. datalab/data/icons/fit/multigaussian_fit.svg +85 -0
  111. datalab/data/icons/fit/multilorentzian_fit.svg +85 -0
  112. datalab/data/icons/fit/piecewiseexponential_fit.svg +209 -0
  113. datalab/data/icons/fit/planckian_fit.svg +62 -0
  114. datalab/data/icons/fit/polynomial_fit.svg +59 -0
  115. datalab/data/icons/fit/sigmoid_fit.svg +56 -0
  116. datalab/data/icons/fit/sinusoidal_fit.svg +72 -0
  117. datalab/data/icons/fit/twohalfgaussian_fit.svg +63 -0
  118. datalab/data/icons/fit/voigt_fit.svg +57 -0
  119. datalab/data/icons/group.svg +56 -0
  120. datalab/data/icons/h5/h5array.svg +59 -0
  121. datalab/data/icons/h5/h5attrs.svg +75 -0
  122. datalab/data/icons/h5/h5browser.svg +133 -0
  123. datalab/data/icons/h5/h5file.svg +69 -0
  124. datalab/data/icons/h5/h5group.svg +49 -0
  125. datalab/data/icons/h5/h5scalar.svg +1 -0
  126. datalab/data/icons/help_pdf.svg +46 -0
  127. datalab/data/icons/history.svg +7 -0
  128. datalab/data/icons/image.svg +135 -0
  129. datalab/data/icons/io/fileopen_directory.svg +60 -0
  130. datalab/data/icons/io/fileopen_h5.svg +84 -0
  131. datalab/data/icons/io/fileopen_ima.svg +187 -0
  132. datalab/data/icons/io/fileopen_py.svg +123 -0
  133. datalab/data/icons/io/fileopen_sig.svg +138 -0
  134. datalab/data/icons/io/filesave_h5.svg +97 -0
  135. datalab/data/icons/io/filesave_ima.svg +200 -0
  136. datalab/data/icons/io/filesave_py.svg +136 -0
  137. datalab/data/icons/io/filesave_sig.svg +151 -0
  138. datalab/data/icons/io/import_text.svg +144 -0
  139. datalab/data/icons/io/save_to_directory.svg +134 -0
  140. datalab/data/icons/io.svg +84 -0
  141. datalab/data/icons/libre-camera-flash-off.svg +1 -0
  142. datalab/data/icons/libre-camera-flash-on.svg +1 -0
  143. datalab/data/icons/libre-gui-about.svg +1 -0
  144. datalab/data/icons/libre-gui-action-delete.svg +1 -0
  145. datalab/data/icons/libre-gui-add.svg +1 -0
  146. datalab/data/icons/libre-gui-arrow-down.svg +1 -0
  147. datalab/data/icons/libre-gui-arrow-left.svg +1 -0
  148. datalab/data/icons/libre-gui-arrow-right.svg +1 -0
  149. datalab/data/icons/libre-gui-arrow-up.svg +1 -0
  150. datalab/data/icons/libre-gui-close.svg +40 -0
  151. datalab/data/icons/libre-gui-cogs.svg +1 -0
  152. datalab/data/icons/libre-gui-globe.svg +1 -0
  153. datalab/data/icons/libre-gui-help.svg +1 -0
  154. datalab/data/icons/libre-gui-link.svg +1 -0
  155. datalab/data/icons/libre-gui-menu.svg +1 -0
  156. datalab/data/icons/libre-gui-pencil.svg +1 -0
  157. datalab/data/icons/libre-gui-plugin.svg +1 -0
  158. datalab/data/icons/libre-gui-questions.svg +1 -0
  159. datalab/data/icons/libre-gui-settings.svg +1 -0
  160. datalab/data/icons/libre-gui-unlink.svg +1 -0
  161. datalab/data/icons/libre-tech-ram.svg +1 -0
  162. datalab/data/icons/libre-toolbox.svg +1 -0
  163. datalab/data/icons/logs.svg +1 -0
  164. datalab/data/icons/markers.svg +74 -0
  165. datalab/data/icons/menu.svg +13 -0
  166. datalab/data/icons/new_ima.svg +148 -0
  167. datalab/data/icons/new_sig.svg +123 -0
  168. datalab/data/icons/operations/abs.svg +116 -0
  169. datalab/data/icons/operations/arithmetic.svg +123 -0
  170. datalab/data/icons/operations/average.svg +124 -0
  171. datalab/data/icons/operations/complex_from_magnitude_phase.svg +116 -0
  172. datalab/data/icons/operations/complex_from_real_imag.svg +124 -0
  173. datalab/data/icons/operations/constant.svg +116 -0
  174. datalab/data/icons/operations/constant_add.svg +109 -0
  175. datalab/data/icons/operations/constant_divide.svg +109 -0
  176. datalab/data/icons/operations/constant_multiply.svg +109 -0
  177. datalab/data/icons/operations/constant_subtract.svg +109 -0
  178. datalab/data/icons/operations/convert_dtype.svg +117 -0
  179. datalab/data/icons/operations/convolution.svg +46 -0
  180. datalab/data/icons/operations/deconvolution.svg +57 -0
  181. datalab/data/icons/operations/derivative.svg +127 -0
  182. datalab/data/icons/operations/difference.svg +52 -0
  183. datalab/data/icons/operations/division.svg +139 -0
  184. datalab/data/icons/operations/exp.svg +116 -0
  185. datalab/data/icons/operations/flip_horizontally.svg +69 -0
  186. datalab/data/icons/operations/flip_vertically.svg +74 -0
  187. datalab/data/icons/operations/im.svg +124 -0
  188. datalab/data/icons/operations/integral.svg +50 -0
  189. datalab/data/icons/operations/inverse.svg +143 -0
  190. datalab/data/icons/operations/log10.svg +109 -0
  191. datalab/data/icons/operations/phase.svg +116 -0
  192. datalab/data/icons/operations/power.svg +118 -0
  193. datalab/data/icons/operations/product.svg +124 -0
  194. datalab/data/icons/operations/profile.svg +379 -0
  195. datalab/data/icons/operations/profile_average.svg +399 -0
  196. datalab/data/icons/operations/profile_radial.svg +261 -0
  197. datalab/data/icons/operations/profile_segment.svg +262 -0
  198. datalab/data/icons/operations/quadratic_difference.svg +84 -0
  199. datalab/data/icons/operations/re.svg +124 -0
  200. datalab/data/icons/operations/rotate_left.svg +72 -0
  201. datalab/data/icons/operations/rotate_right.svg +72 -0
  202. datalab/data/icons/operations/signals_to_image.svg +314 -0
  203. datalab/data/icons/operations/sqrt.svg +110 -0
  204. datalab/data/icons/operations/std.svg +124 -0
  205. datalab/data/icons/operations/sum.svg +102 -0
  206. datalab/data/icons/play_demo.svg +9 -0
  207. datalab/data/icons/processing/axis_transform.svg +62 -0
  208. datalab/data/icons/processing/bandpass.svg +79 -0
  209. datalab/data/icons/processing/bandstop.svg +71 -0
  210. datalab/data/icons/processing/binning.svg +126 -0
  211. datalab/data/icons/processing/clip.svg +119 -0
  212. datalab/data/icons/processing/detrending.svg +173 -0
  213. datalab/data/icons/processing/distribute_on_grid.svg +769 -0
  214. datalab/data/icons/processing/edge_detection.svg +46 -0
  215. datalab/data/icons/processing/erase.svg +1 -0
  216. datalab/data/icons/processing/exposure.svg +143 -0
  217. datalab/data/icons/processing/fourier.svg +104 -0
  218. datalab/data/icons/processing/highpass.svg +59 -0
  219. datalab/data/icons/processing/interpolation.svg +71 -0
  220. datalab/data/icons/processing/level_adjustment.svg +70 -0
  221. datalab/data/icons/processing/lowpass.svg +60 -0
  222. datalab/data/icons/processing/morphology.svg +49 -0
  223. datalab/data/icons/processing/noise_addition.svg +114 -0
  224. datalab/data/icons/processing/noise_reduction.svg +38 -0
  225. datalab/data/icons/processing/normalize.svg +84 -0
  226. datalab/data/icons/processing/offset_correction.svg +131 -0
  227. datalab/data/icons/processing/resampling1d.svg +101 -0
  228. datalab/data/icons/processing/resampling2d.svg +240 -0
  229. datalab/data/icons/processing/reset_positions.svg +185 -0
  230. datalab/data/icons/processing/resize.svg +9 -0
  231. datalab/data/icons/processing/reverse_signal_x.svg +171 -0
  232. datalab/data/icons/processing/stability.svg +11 -0
  233. datalab/data/icons/processing/swap_x_y.svg +65 -0
  234. datalab/data/icons/processing/thresholding.svg +63 -0
  235. datalab/data/icons/processing/windowing.svg +45 -0
  236. datalab/data/icons/properties.svg +26 -0
  237. datalab/data/icons/reset.svg +9 -0
  238. datalab/data/icons/restore.svg +40 -0
  239. datalab/data/icons/roi/roi.svg +76 -0
  240. datalab/data/icons/roi/roi_coordinate.svg +78 -0
  241. datalab/data/icons/roi/roi_copy.svg +112 -0
  242. datalab/data/icons/roi/roi_delete.svg +81 -0
  243. datalab/data/icons/roi/roi_export.svg +87 -0
  244. datalab/data/icons/roi/roi_graphical.svg +78 -0
  245. datalab/data/icons/roi/roi_grid.svg +67 -0
  246. datalab/data/icons/roi/roi_ima.svg +188 -0
  247. datalab/data/icons/roi/roi_import.svg +87 -0
  248. datalab/data/icons/roi/roi_new.svg +81 -0
  249. datalab/data/icons/roi/roi_new_circle.svg +95 -0
  250. datalab/data/icons/roi/roi_new_polygon.svg +110 -0
  251. datalab/data/icons/roi/roi_new_rectangle.svg +70 -0
  252. datalab/data/icons/roi/roi_paste.svg +98 -0
  253. datalab/data/icons/roi/roi_sig.svg +124 -0
  254. datalab/data/icons/shapes.svg +134 -0
  255. datalab/data/icons/signal.svg +103 -0
  256. datalab/data/icons/table.svg +85 -0
  257. datalab/data/icons/table_unavailable.svg +102 -0
  258. datalab/data/icons/to_signal.svg +124 -0
  259. datalab/data/icons/tour/next.svg +44 -0
  260. datalab/data/icons/tour/previous.svg +44 -0
  261. datalab/data/icons/tour/rewind.svg +51 -0
  262. datalab/data/icons/tour/stop.svg +47 -0
  263. datalab/data/icons/tour/tour.svg +16 -0
  264. datalab/data/icons/uncheck_all.svg +78 -0
  265. datalab/data/icons/view/curve_antialiasing.svg +50 -0
  266. datalab/data/icons/view/new_window.svg +98 -0
  267. datalab/data/icons/view/refresh-auto.svg +57 -0
  268. datalab/data/icons/view/refresh-manual.svg +51 -0
  269. datalab/data/icons/view/reset_curve_styles.svg +96 -0
  270. datalab/data/icons/view/show_first.svg +55 -0
  271. datalab/data/icons/view/show_titles.svg +46 -0
  272. datalab/data/icons/visualization.svg +51 -0
  273. datalab/data/logo/DataLab-Banner-150.png +0 -0
  274. datalab/data/logo/DataLab-Banner-200.png +0 -0
  275. datalab/data/logo/DataLab-Banner2-100.png +0 -0
  276. datalab/data/logo/DataLab-Splash.png +0 -0
  277. datalab/data/logo/DataLab-watermark.png +0 -0
  278. datalab/data/logo/DataLab.svg +83 -0
  279. datalab/data/tests/reordering_test.h5 +0 -0
  280. datalab/data/tutorials/fabry_perot/fabry-perot1.jpg +0 -0
  281. datalab/data/tutorials/fabry_perot/fabry-perot2.jpg +0 -0
  282. datalab/data/tutorials/laser_beam/TEM00_z_13.jpg +0 -0
  283. datalab/data/tutorials/laser_beam/TEM00_z_18.jpg +0 -0
  284. datalab/data/tutorials/laser_beam/TEM00_z_23.jpg +0 -0
  285. datalab/data/tutorials/laser_beam/TEM00_z_30.jpg +0 -0
  286. datalab/data/tutorials/laser_beam/TEM00_z_35.jpg +0 -0
  287. datalab/data/tutorials/laser_beam/TEM00_z_40.jpg +0 -0
  288. datalab/data/tutorials/laser_beam/TEM00_z_45.jpg +0 -0
  289. datalab/data/tutorials/laser_beam/TEM00_z_50.jpg +0 -0
  290. datalab/data/tutorials/laser_beam/TEM00_z_55.jpg +0 -0
  291. datalab/data/tutorials/laser_beam/TEM00_z_60.jpg +0 -0
  292. datalab/data/tutorials/laser_beam/TEM00_z_65.jpg +0 -0
  293. datalab/data/tutorials/laser_beam/TEM00_z_70.jpg +0 -0
  294. datalab/data/tutorials/laser_beam/TEM00_z_75.jpg +0 -0
  295. datalab/data/tutorials/laser_beam/TEM00_z_80.jpg +0 -0
  296. datalab/env.py +542 -0
  297. datalab/gui/__init__.py +89 -0
  298. datalab/gui/actionhandler.py +1701 -0
  299. datalab/gui/docks.py +473 -0
  300. datalab/gui/h5io.py +150 -0
  301. datalab/gui/macroeditor.py +310 -0
  302. datalab/gui/main.py +2081 -0
  303. datalab/gui/newobject.py +217 -0
  304. datalab/gui/objectview.py +766 -0
  305. datalab/gui/panel/__init__.py +48 -0
  306. datalab/gui/panel/base.py +3254 -0
  307. datalab/gui/panel/image.py +157 -0
  308. datalab/gui/panel/macro.py +607 -0
  309. datalab/gui/panel/signal.py +164 -0
  310. datalab/gui/plothandler.py +800 -0
  311. datalab/gui/processor/__init__.py +84 -0
  312. datalab/gui/processor/base.py +2456 -0
  313. datalab/gui/processor/catcher.py +75 -0
  314. datalab/gui/processor/image.py +1214 -0
  315. datalab/gui/processor/signal.py +755 -0
  316. datalab/gui/profiledialog.py +333 -0
  317. datalab/gui/roieditor.py +633 -0
  318. datalab/gui/roigrideditor.py +208 -0
  319. datalab/gui/settings.py +612 -0
  320. datalab/gui/tour.py +908 -0
  321. datalab/h5/__init__.py +12 -0
  322. datalab/h5/common.py +314 -0
  323. datalab/h5/generic.py +580 -0
  324. datalab/h5/native.py +39 -0
  325. datalab/h5/utils.py +95 -0
  326. datalab/objectmodel.py +640 -0
  327. datalab/plugins/_readme_.txt +9 -0
  328. datalab/plugins/datalab_imageformats.py +175 -0
  329. datalab/plugins/datalab_testdata.py +190 -0
  330. datalab/plugins.py +355 -0
  331. datalab/tests/__init__.py +199 -0
  332. datalab/tests/backbone/__init__.py +1 -0
  333. datalab/tests/backbone/config_unit_test.py +170 -0
  334. datalab/tests/backbone/config_versioning_unit_test.py +34 -0
  335. datalab/tests/backbone/dictlistserial_app_test.py +38 -0
  336. datalab/tests/backbone/errorcatcher_unit_test.py +69 -0
  337. datalab/tests/backbone/errormsgbox_unit_test.py +50 -0
  338. datalab/tests/backbone/execenv_unit.py +262 -0
  339. datalab/tests/backbone/loadtest_gdi.py +147 -0
  340. datalab/tests/backbone/long_callback.py +96 -0
  341. datalab/tests/backbone/main_app_test.py +137 -0
  342. datalab/tests/backbone/memory_leak.py +43 -0
  343. datalab/tests/backbone/procisolation1_unit.py +128 -0
  344. datalab/tests/backbone/procisolation2_unit.py +171 -0
  345. datalab/tests/backbone/procisolation_unit_test.py +22 -0
  346. datalab/tests/backbone/profiling_app.py +27 -0
  347. datalab/tests/backbone/strings_unit_test.py +65 -0
  348. datalab/tests/backbone/title_formatting_unit_test.py +82 -0
  349. datalab/tests/conftest.py +131 -0
  350. datalab/tests/features/__init__.py +1 -0
  351. datalab/tests/features/applauncher/__init__.py +1 -0
  352. datalab/tests/features/applauncher/launcher1_app_test.py +28 -0
  353. datalab/tests/features/applauncher/launcher2_app_test.py +30 -0
  354. datalab/tests/features/common/__init__.py +1 -0
  355. datalab/tests/features/common/add_metadata_app_test.py +134 -0
  356. datalab/tests/features/common/add_metadata_unit_test.py +267 -0
  357. datalab/tests/features/common/annotations_management_unit_test.py +152 -0
  358. datalab/tests/features/common/auto_analysis_recompute_unit_test.py +240 -0
  359. datalab/tests/features/common/createobject_unit_test.py +50 -0
  360. datalab/tests/features/common/geometry_results_app_test.py +135 -0
  361. datalab/tests/features/common/interactive_processing_test.py +1109 -0
  362. datalab/tests/features/common/io_app_test.py +75 -0
  363. datalab/tests/features/common/large_results_app_test.py +187 -0
  364. datalab/tests/features/common/metadata_all_patterns_test.py +103 -0
  365. datalab/tests/features/common/metadata_app_test.py +139 -0
  366. datalab/tests/features/common/metadata_io_unit_test.py +60 -0
  367. datalab/tests/features/common/misc_app_test.py +236 -0
  368. datalab/tests/features/common/multiple_geometry_results_unit_test.py +122 -0
  369. datalab/tests/features/common/multiple_table_results_unit_test.py +64 -0
  370. datalab/tests/features/common/operation_modes_app_test.py +392 -0
  371. datalab/tests/features/common/plot_results_app_test.py +278 -0
  372. datalab/tests/features/common/reorder_app_test.py +75 -0
  373. datalab/tests/features/common/result_deletion_unit_test.py +96 -0
  374. datalab/tests/features/common/result_merged_label_unit_test.py +154 -0
  375. datalab/tests/features/common/result_shape_settings_unit_test.py +223 -0
  376. datalab/tests/features/common/roi_plotitem_unit_test.py +64 -0
  377. datalab/tests/features/common/roieditor_unit_test.py +102 -0
  378. datalab/tests/features/common/save_to_dir_app_test.py +163 -0
  379. datalab/tests/features/common/save_to_dir_unit_test.py +474 -0
  380. datalab/tests/features/common/stat_app_test.py +40 -0
  381. datalab/tests/features/common/stats_tools_unit_test.py +77 -0
  382. datalab/tests/features/common/table_results_app_test.py +52 -0
  383. datalab/tests/features/common/textimport_unit_test.py +131 -0
  384. datalab/tests/features/common/uuid_preservation_test.py +281 -0
  385. datalab/tests/features/common/worker_unit_test.py +402 -0
  386. datalab/tests/features/control/__init__.py +1 -0
  387. datalab/tests/features/control/connect_dialog.py +28 -0
  388. datalab/tests/features/control/embedded1_unit_test.py +304 -0
  389. datalab/tests/features/control/embedded2_unit_test.py +52 -0
  390. datalab/tests/features/control/remoteclient_app_test.py +219 -0
  391. datalab/tests/features/control/remoteclient_unit.py +75 -0
  392. datalab/tests/features/control/simpleclient_unit_test.py +321 -0
  393. datalab/tests/features/hdf5/__init__.py +1 -0
  394. datalab/tests/features/hdf5/h5browser1_unit_test.py +31 -0
  395. datalab/tests/features/hdf5/h5browser2_unit.py +55 -0
  396. datalab/tests/features/hdf5/h5browser_app_test.py +77 -0
  397. datalab/tests/features/hdf5/h5import_app_test.py +25 -0
  398. datalab/tests/features/hdf5/h5importer_app_test.py +34 -0
  399. datalab/tests/features/image/__init__.py +1 -0
  400. datalab/tests/features/image/annotations_app_test.py +28 -0
  401. datalab/tests/features/image/annotations_unit_test.py +80 -0
  402. datalab/tests/features/image/average_app_test.py +46 -0
  403. datalab/tests/features/image/background_dialog_test.py +70 -0
  404. datalab/tests/features/image/blobs_app_test.py +50 -0
  405. datalab/tests/features/image/contour_app_test.py +42 -0
  406. datalab/tests/features/image/contour_fabryperot_app_test.py +51 -0
  407. datalab/tests/features/image/denoise_app_test.py +31 -0
  408. datalab/tests/features/image/distribute_on_grid_app_test.py +95 -0
  409. datalab/tests/features/image/edges_app_test.py +31 -0
  410. datalab/tests/features/image/erase_app_test.py +21 -0
  411. datalab/tests/features/image/fft2d_app_test.py +27 -0
  412. datalab/tests/features/image/flatfield_app_test.py +40 -0
  413. datalab/tests/features/image/geometry_transform_unit_test.py +396 -0
  414. datalab/tests/features/image/imagetools_app_test.py +51 -0
  415. datalab/tests/features/image/imagetools_unit_test.py +27 -0
  416. datalab/tests/features/image/load_app_test.py +73 -0
  417. datalab/tests/features/image/morph_app_test.py +32 -0
  418. datalab/tests/features/image/offsetcorrection_app_test.py +30 -0
  419. datalab/tests/features/image/peak2d_app_test.py +53 -0
  420. datalab/tests/features/image/profile_app_test.py +73 -0
  421. datalab/tests/features/image/profile_dialog_test.py +56 -0
  422. datalab/tests/features/image/roi_app_test.py +98 -0
  423. datalab/tests/features/image/roi_circ_app_test.py +62 -0
  424. datalab/tests/features/image/roi_manipulation_app_test.py +268 -0
  425. datalab/tests/features/image/roigrid_unit_test.py +60 -0
  426. datalab/tests/features/image/side_by_side_app_test.py +52 -0
  427. datalab/tests/features/macro/__init__.py +1 -0
  428. datalab/tests/features/macro/macro_app_test.py +28 -0
  429. datalab/tests/features/macro/macroeditor_unit_test.py +102 -0
  430. datalab/tests/features/signal/__init__.py +1 -0
  431. datalab/tests/features/signal/baseline_dialog_test.py +53 -0
  432. datalab/tests/features/signal/deltax_dialog_unit_test.py +34 -0
  433. datalab/tests/features/signal/fft1d_app_test.py +26 -0
  434. datalab/tests/features/signal/filter_app_test.py +44 -0
  435. datalab/tests/features/signal/fitdialog_unit_test.py +50 -0
  436. datalab/tests/features/signal/interpolation_app_test.py +110 -0
  437. datalab/tests/features/signal/loadbigsignal_app_test.py +80 -0
  438. datalab/tests/features/signal/multiple_rois_unit_test.py +132 -0
  439. datalab/tests/features/signal/pulse_features_app_test.py +118 -0
  440. datalab/tests/features/signal/pulse_features_roi_app_test.py +55 -0
  441. datalab/tests/features/signal/roi_app_test.py +78 -0
  442. datalab/tests/features/signal/roi_manipulation_app_test.py +261 -0
  443. datalab/tests/features/signal/select_xy_cursor_unit_test.py +46 -0
  444. datalab/tests/features/signal/signalpeakdetection_dialog_test.py +33 -0
  445. datalab/tests/features/signal/signals_to_image_app_test.py +98 -0
  446. datalab/tests/features/signal/xarray_compat_app_test.py +128 -0
  447. datalab/tests/features/tour_unit_test.py +22 -0
  448. datalab/tests/features/utilities/__init__.py +1 -0
  449. datalab/tests/features/utilities/installconf_unit_test.py +21 -0
  450. datalab/tests/features/utilities/logview_app_test.py +21 -0
  451. datalab/tests/features/utilities/logview_error.py +24 -0
  452. datalab/tests/features/utilities/logview_unit_test.py +21 -0
  453. datalab/tests/features/utilities/memstatus_app_test.py +42 -0
  454. datalab/tests/features/utilities/settings_unit_test.py +88 -0
  455. datalab/tests/scenarios/__init__.py +1 -0
  456. datalab/tests/scenarios/beautiful_app.py +121 -0
  457. datalab/tests/scenarios/common.py +463 -0
  458. datalab/tests/scenarios/demo.py +212 -0
  459. datalab/tests/scenarios/example_app_test.py +47 -0
  460. datalab/tests/scenarios/scenario_h5_app_test.py +75 -0
  461. datalab/tests/scenarios/scenario_ima1_app_test.py +34 -0
  462. datalab/tests/scenarios/scenario_ima2_app_test.py +34 -0
  463. datalab/tests/scenarios/scenario_mac_app_test.py +58 -0
  464. datalab/tests/scenarios/scenario_sig1_app_test.py +36 -0
  465. datalab/tests/scenarios/scenario_sig2_app_test.py +35 -0
  466. datalab/utils/__init__.py +1 -0
  467. datalab/utils/conf.py +304 -0
  468. datalab/utils/dephash.py +105 -0
  469. datalab/utils/qthelpers.py +633 -0
  470. datalab/utils/strings.py +34 -0
  471. datalab/utils/tests.py +0 -0
  472. datalab/widgets/__init__.py +1 -0
  473. datalab/widgets/connection.py +138 -0
  474. datalab/widgets/filedialog.py +91 -0
  475. datalab/widgets/fileviewer.py +84 -0
  476. datalab/widgets/fitdialog.py +788 -0
  477. datalab/widgets/h5browser.py +1048 -0
  478. datalab/widgets/imagebackground.py +111 -0
  479. datalab/widgets/instconfviewer.py +175 -0
  480. datalab/widgets/logviewer.py +80 -0
  481. datalab/widgets/signalbaseline.py +90 -0
  482. datalab/widgets/signalcursor.py +208 -0
  483. datalab/widgets/signaldeltax.py +151 -0
  484. datalab/widgets/signalpeak.py +199 -0
  485. datalab/widgets/status.py +249 -0
  486. datalab/widgets/textimport.py +786 -0
  487. datalab/widgets/warningerror.py +223 -0
  488. datalab/widgets/wizard.py +286 -0
  489. datalab_platform-1.0.0.dist-info/METADATA +121 -0
  490. datalab_platform-1.0.0.dist-info/RECORD +494 -0
  491. datalab_platform-0.0.1.dev0.dist-info/METADATA +0 -67
  492. datalab_platform-0.0.1.dev0.dist-info/RECORD +0 -7
  493. {datalab_platform-0.0.1.dev0.dist-info → datalab_platform-1.0.0.dist-info}/WHEEL +0 -0
  494. {datalab_platform-0.0.1.dev0.dist-info → datalab_platform-1.0.0.dist-info}/entry_points.txt +0 -0
  495. {datalab_platform-0.0.1.dev0.dist-info → datalab_platform-1.0.0.dist-info}/licenses/LICENSE +0 -0
  496. {datalab_platform-0.0.1.dev0.dist-info → datalab_platform-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,633 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ DataLab Qt utilities
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import faulthandler
10
+ import functools
11
+ import inspect
12
+ import logging
13
+ import os
14
+ import os.path as osp
15
+ import shutil
16
+ import sys
17
+ import time
18
+ import traceback
19
+ from collections.abc import Callable, Generator
20
+ from contextlib import contextmanager
21
+ from typing import Any
22
+
23
+ import guidata
24
+ from guidata.configtools import get_icon
25
+ from guidata.qthelpers import grab_save_window as guidata_grab_save_window
26
+ from guidata.utils.misc import to_string
27
+ from qtpy import QtCore as QC
28
+ from qtpy import QtGui as QG
29
+ from qtpy import QtWidgets as QW
30
+
31
+ from datalab.config import (
32
+ APP_NAME,
33
+ DATETIME_FORMAT,
34
+ SHOTPATH,
35
+ Conf,
36
+ _,
37
+ get_old_log_fname,
38
+ )
39
+ from datalab.env import execenv
40
+
41
+
42
+ def close_widgets_and_quit(screenshot=False) -> None:
43
+ """Close Qt top level widgets and quit Qt event loop"""
44
+ for widget in QW.QApplication.instance().topLevelWidgets():
45
+ try:
46
+ wname = widget.objectName()
47
+ except RuntimeError:
48
+ # Object has been deleted
49
+ continue
50
+ if screenshot and wname and widget.isVisible(): # pragma: no cover
51
+ grab_save_window(widget, wname.lower())
52
+ assert widget.close()
53
+ QW.QApplication.instance().quit()
54
+
55
+
56
+ QAPP_INSTANCE = None
57
+
58
+
59
+ def get_log_contents(fname: str) -> str | None:
60
+ """Return True if file exists and something was logged in it"""
61
+ if osp.exists(fname):
62
+ with open(fname, "rb") as fdesc:
63
+ return to_string(fdesc.read()).strip()
64
+ return None
65
+
66
+
67
+ def initialize_log_file(fname: str) -> bool:
68
+ """Eventually keep the previous log file
69
+ Returns True if there was a previous log file"""
70
+ contents = get_log_contents(fname)
71
+ if contents:
72
+ try:
73
+ shutil.move(fname, get_old_log_fname(fname))
74
+ except Exception: # pylint: disable=broad-except
75
+ pass
76
+ return True
77
+ return False
78
+
79
+
80
+ def remove_empty_log_file(fname: str) -> None:
81
+ """Eventually remove empty log files"""
82
+ if not get_log_contents(fname):
83
+ try:
84
+ os.remove(fname)
85
+ except Exception: # pylint: disable=broad-except
86
+ pass
87
+
88
+
89
+ @contextmanager
90
+ def datalab_app_context(
91
+ exec_loop=False, enable_logs=True
92
+ ) -> Generator[QW.QApplication, None, None]:
93
+ """DataLab Qt application context manager, handling Qt application creation
94
+ and persistance, faulthandler/traceback logging features, screenshot mode
95
+ and unattended mode.
96
+
97
+ Args:
98
+ exec_loop: whether to execute Qt event loop (default: False)
99
+ enable_logs: whether to enable logs (default: True)
100
+ """
101
+ global QAPP_INSTANCE # pylint: disable=global-statement
102
+ if QAPP_INSTANCE is None:
103
+ QAPP_INSTANCE = guidata.qapplication()
104
+
105
+ # === Set application name and version ---------------------------------------------
106
+ # pylint: disable=import-outside-toplevel
107
+ import datalab
108
+
109
+ QAPP_INSTANCE.setApplicationName(APP_NAME)
110
+ QAPP_INSTANCE.setApplicationVersion(datalab.__version__)
111
+ QAPP_INSTANCE.setOrganizationName(APP_NAME + " project")
112
+
113
+ if enable_logs:
114
+ # === Create a logger for standard exceptions ----------------------------------
115
+ tb_log_fname = Conf.main.traceback_log_path.get()
116
+ Conf.main.traceback_log_available.set(initialize_log_file(tb_log_fname))
117
+ logger = logging.getLogger(__name__)
118
+ fmt = "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s"
119
+ logging.basicConfig(
120
+ filename=tb_log_fname,
121
+ filemode="w",
122
+ level=logging.ERROR,
123
+ format=fmt,
124
+ datefmt=DATETIME_FORMAT,
125
+ )
126
+
127
+ def custom_excepthook(exc_type, exc_value, exc_traceback):
128
+ "Custom exception hook"
129
+ logger.critical(
130
+ "Unhandled exception", exc_info=(exc_type, exc_value, exc_traceback)
131
+ )
132
+ return sys.__excepthook__(exc_type, exc_value, exc_traceback)
133
+
134
+ sys.excepthook = custom_excepthook
135
+
136
+ # === Use faulthandler for other exceptions ------------------------------------
137
+ fh_log_fname = Conf.main.faulthandler_log_path.get()
138
+ Conf.main.faulthandler_log_available.set(initialize_log_file(fh_log_fname))
139
+
140
+ with open(fh_log_fname, "w", encoding="utf-8") as fh_log_fn:
141
+ if enable_logs and Conf.main.faulthandler_enabled.get(True):
142
+ faulthandler.enable(file=fh_log_fn)
143
+ exception_occured = False
144
+ try:
145
+ yield QAPP_INSTANCE
146
+ except Exception: # pylint: disable=broad-except
147
+ exception_occured = True
148
+ finally:
149
+ if (
150
+ execenv.unattended or execenv.screenshot
151
+ ) and not execenv.do_not_quit: # pragma: no cover
152
+ if execenv.delay > 0:
153
+ mode = "Screenshot" if execenv.screenshot else "Unattended"
154
+ message = f"{mode} mode (delay: {execenv.delay}ms)"
155
+ msec = execenv.delay - 200
156
+ for widget in QW.QApplication.instance().topLevelWidgets():
157
+ if isinstance(widget, QW.QMainWindow):
158
+ widget.statusBar().showMessage(message, msec)
159
+ QC.QTimer.singleShot(
160
+ execenv.delay,
161
+ lambda: close_widgets_and_quit(screenshot=execenv.screenshot),
162
+ )
163
+ if exec_loop and not exception_occured:
164
+ QAPP_INSTANCE.exec()
165
+ if exception_occured:
166
+ raise # pylint: disable=misplaced-bare-raise
167
+
168
+ if enable_logs and Conf.main.faulthandler_enabled.get():
169
+ faulthandler.disable()
170
+ remove_empty_log_file(fh_log_fname)
171
+ if enable_logs:
172
+ logging.shutdown()
173
+ remove_empty_log_file(tb_log_fname)
174
+
175
+
176
+ def is_running_tests() -> bool:
177
+ """Check if code is running during test execution"""
178
+ return "pytest" in sys.modules
179
+
180
+
181
+ @contextmanager
182
+ def try_or_log_error(context: str) -> Generator[None, None, None]:
183
+ """Try to execute a function and log an error message if it fails"""
184
+ try:
185
+ yield
186
+ except Exception: # pylint: disable=broad-except
187
+ if is_running_tests():
188
+ # If we are running tests, we want to raise the exception
189
+ raise
190
+ traceback.print_exc()
191
+ logger = logging.getLogger(__name__)
192
+ logger.error("Error in %s", context, exc_info=traceback.format_exc())
193
+ Conf.main.traceback_log_available.set(True)
194
+ finally:
195
+ pass
196
+
197
+
198
+ @contextmanager
199
+ def create_progress_bar(
200
+ parent: QW.QWidget, label: str, max_: int, show_after: int = 1000
201
+ ) -> Generator[QW.QProgressDialog, None, None]:
202
+ """Create modal progress bar
203
+
204
+ Args:
205
+ parent: Parent widget
206
+ label: Progress dialog title
207
+ max_: Maximum progress value
208
+ show_after: Delay before showing the progress dialog (ms, default: 1000)
209
+ """
210
+ prog = QW.QProgressDialog(label, _("Cancel"), 0, max_, parent, QC.Qt.SplashScreen)
211
+ prog.setWindowModality(QC.Qt.WindowModal)
212
+ prog.setMinimumDuration(show_after)
213
+ try:
214
+ yield prog
215
+ finally:
216
+ prog.close()
217
+ prog.deleteLater()
218
+
219
+
220
+ class CallbackWorker(QC.QThread):
221
+ """Worker for executing long operations in a separate thread (this must not be
222
+ confused with the :py:class:`datalab.gui.processor.base.Worker` class, which
223
+ handles the execution of computations in a another process)
224
+
225
+ Implements `CallbackWorkerProtocol` from `sigima.worker`, used for computations
226
+ that support cancellation and progress reporting.
227
+
228
+ Args:
229
+ callback: The function to be executed in a separate thread, that takes
230
+ optionnally 'worker' as argument (instance of this class), and any other
231
+ argument passed with **kwargs
232
+ kwargs: Callback keyword arguments
233
+ """
234
+
235
+ SIG_PROGRESS_UPDATE = QC.Signal(int)
236
+
237
+ def __init__(self, callback: Callable, **kwargs) -> None:
238
+ super().__init__()
239
+ self.callback = callback
240
+ if "worker" in inspect.signature(callback).parameters:
241
+ kwargs["worker"] = self
242
+ self.kwargs = kwargs
243
+ self.result: Any | None = None
244
+ self.__canceled = False
245
+ self.__exc = None
246
+
247
+ def run(self) -> None:
248
+ """Start thread"""
249
+ # Initialize progress bar: setting progress to 0.0 has the effect of
250
+ # showing the progress dialog after the `minimumDuration` time has elapsed.
251
+ # If we don't set the progress to 0.0, the progress dialog will be shown only
252
+ # after the first call to `set_progress` method even if the `minimumDuration`
253
+ # time has elapsed.
254
+ self.set_progress(0.0)
255
+
256
+ try:
257
+ self.result = self.callback(**self.kwargs)
258
+ except Exception as exc: # pylint: disable=broad-except
259
+ self.__exc = exc
260
+
261
+ def cancel(self) -> None:
262
+ """Progress bar was canceled"""
263
+ self.__canceled = True
264
+
265
+ def was_canceled(self) -> bool:
266
+ """Return whether the progress dialog was canceled by user"""
267
+ return self.__canceled
268
+
269
+ def set_progress(self, value: float) -> None:
270
+ """Set progress bar value
271
+
272
+ Args:
273
+ value: float between 0.0 and 1.0
274
+ """
275
+ self.SIG_PROGRESS_UPDATE.emit(int(100 * value))
276
+
277
+ def get_result(self) -> Any:
278
+ """Return callback result"""
279
+ if self.__exc is not None:
280
+ raise self.__exc
281
+ return self.result
282
+
283
+
284
+ def qt_long_callback(
285
+ parent: QW.QWidget,
286
+ label: str,
287
+ worker: CallbackWorker,
288
+ progress: bool,
289
+ show_after: int = 500,
290
+ ) -> Any:
291
+ """Handle long callbacks: run in a separate thread while showing a busy bar
292
+
293
+ Args:
294
+ parent: Parent widget
295
+ label: Progress dialog title
296
+ worker: Callback worker handling the function execution in a separate thread
297
+ progress: Whether the progress feature is handled or not. If True, a progress
298
+ bar and a 'Cancel' button are shown on the progress dialog. The progress value
299
+ is updated by the `worker.set_progress` method (which takes a float between
300
+ 0.0 and 1.0). Moreover, if `progress` is True, we wait for the callback
301
+ function to return (it means that the callback function must implement a
302
+ mechanism to return an intermediate result or `None` if the
303
+ `worker.was_canceled` method returns True).
304
+ show_after: Delay before showing the progress dialog (ms, default: 1000)
305
+
306
+ Returns:
307
+ Callback result
308
+ """
309
+ if progress:
310
+ prog = QW.QProgressDialog(
311
+ label, _("Cancel"), 0, 100, parent, QC.Qt.SplashScreen
312
+ )
313
+ prog.setMinimumDuration(show_after)
314
+ worker.SIG_PROGRESS_UPDATE.connect(prog.setValue)
315
+ prog.canceled.connect(worker.cancel)
316
+ else:
317
+ prog = QW.QProgressDialog(label, None, 0, 0, parent, QC.Qt.SplashScreen)
318
+ prog.setMinimumDuration(0)
319
+ prog.setCancelButton(None)
320
+ prog.setRange(0, 0)
321
+ prog.show()
322
+ prog.setWindowModality(QC.Qt.WindowModal)
323
+
324
+ worker.start()
325
+ while worker.isRunning() and not worker.was_canceled():
326
+ QW.QApplication.processEvents()
327
+ time.sleep(0.005)
328
+ if progress:
329
+ worker.SIG_PROGRESS_UPDATE.disconnect(prog.setValue)
330
+ worker.wait()
331
+ try:
332
+ result = worker.get_result()
333
+ except Exception as exc: # pylint: disable=broad-except
334
+ prog.close()
335
+ prog.deleteLater()
336
+ raise exc
337
+ prog.close()
338
+ prog.deleteLater()
339
+ return result
340
+
341
+
342
+ def qt_handle_error_message(widget: QW.QWidget, message: str, context: str = None):
343
+ """Handles application (QWidget) error message"""
344
+ traceback.print_exc()
345
+ txt = str(message)
346
+ msglines = txt.splitlines()
347
+ firstline = _("Error:") if context is None else f"%s: {context}" % _("Context")
348
+ msglines.insert(0, firstline)
349
+ if len(msglines) > 10:
350
+ msglines = msglines[:10] + ["..."]
351
+ title = widget.window().objectName()
352
+ QW.QMessageBox.critical(widget, title, os.linesep.join(msglines))
353
+
354
+
355
+ def qt_try_except(message=None, context=None):
356
+ """Try...except Qt widget method decorator"""
357
+
358
+ def qt_try_except_decorator(func):
359
+ """Try...except Qt widget method decorator"""
360
+
361
+ @functools.wraps(func)
362
+ def method_wrapper(*args, **kwargs):
363
+ """Decorator wrapper function"""
364
+ self = args[0] # extracting 'self' from method arguments
365
+ # If "self" is a BaseProcessor, then we need to get the panel instance
366
+ panel = getattr(self, "panel", self)
367
+ if message is not None:
368
+ panel.SIG_STATUS_MESSAGE.emit(message, 0)
369
+ QW.QApplication.setOverrideCursor(QG.QCursor(QC.Qt.WaitCursor))
370
+ panel.repaint()
371
+ output = None
372
+ try:
373
+ output = func(*args, **kwargs)
374
+ except Exception as msg: # pylint: disable=broad-except
375
+ if is_running_tests():
376
+ # If we are running tests, we want to raise the exception
377
+ raise
378
+ qt_handle_error_message(panel.parentWidget(), msg, context)
379
+ finally:
380
+ if message is not None:
381
+ panel.SIG_STATUS_MESSAGE.emit("", 0)
382
+ QW.QApplication.restoreOverrideCursor()
383
+ return output
384
+
385
+ return method_wrapper
386
+
387
+ return qt_try_except_decorator
388
+
389
+
390
+ @contextmanager
391
+ def qt_try_loadsave_file(
392
+ parent: QW.QWidget, filename: str, operation: str
393
+ ) -> Generator[str, None, None]:
394
+ """Try and open file (operation: "load" or "save")"""
395
+ if operation not in ("load", "save"):
396
+ raise ValueError("operation argument must be 'load' or 'save'")
397
+ try:
398
+ yield filename
399
+ except Exception as msg: # pylint: disable=broad-except
400
+ if is_running_tests():
401
+ # If we are running tests, we want to raise the exception
402
+ raise
403
+ traceback.print_exc()
404
+ url = osp.dirname(filename).replace("\\", "/")
405
+ if operation == "load":
406
+ text = _("The file %s could not be read:")
407
+ else:
408
+ text = _("The file %s could not be written:")
409
+ in_folder = _("in this folder")
410
+ message = text % (
411
+ f"<span style='font-weight:bold;color:#555555;'>{osp.basename(filename)}"
412
+ f"</span> (<a href='file:///{url}'>{in_folder}</a>)"
413
+ )
414
+ QW.QMessageBox.critical(parent, APP_NAME, f"{message}<br><br>{str(msg)}")
415
+ finally:
416
+ pass
417
+
418
+
419
+ def grab_save_window(
420
+ widget: QW.QWidget, name: str | None = None, add_timestamp: bool = True
421
+ ) -> None: # pragma: no cover
422
+ """Grab window screenshot and save it.
423
+
424
+ Args:
425
+ widget: Widget to grab
426
+ name: Screenshot name (if None, uses widget.objectName())
427
+ add_timestamp: Whether to add a timestamp to the screenshot name (default: True)
428
+ """
429
+ if name is None:
430
+ name = widget.objectName()
431
+
432
+ # DataLab-specific logic: determine if timestamp should be added
433
+ # based on name patterns and DataLab conventions
434
+ if name.endswith("_"):
435
+ # Name ending with underscore always gets timestamp
436
+ add_timestamp = True
437
+ elif name[-1].isdigit() or name.startswith(("s_", "i_")):
438
+ # DataLab screenshot names or numbered items don't get timestamp
439
+ add_timestamp = False
440
+
441
+ # Use guidata's grab_save_window with DataLab-specific configuration
442
+ guidata_grab_save_window(
443
+ widget=widget, name=name, save_dir=SHOTPATH, add_timestamp=add_timestamp
444
+ )
445
+
446
+
447
+ @contextmanager
448
+ def save_restore_stds() -> Generator[None, None, None]:
449
+ """Save/restore standard I/O before/after doing some things
450
+ (e.g. calling Qt open/save dialogs)"""
451
+ saved_in, saved_out, saved_err = sys.stdin, sys.stdout, sys.stderr
452
+ sys.stdout = None
453
+ try:
454
+ yield
455
+ finally:
456
+ sys.stdin, sys.stdout, sys.stderr = saved_in, saved_out, saved_err
457
+
458
+
459
+ @contextmanager
460
+ def block_signals(
461
+ widget: QW.QWidget, enable: bool = True, children: bool = False
462
+ ) -> Generator[None, None, None]:
463
+ """Eventually block/unblock widget Qt signals before/after doing some things
464
+
465
+ Args:
466
+ widget: Widget to block/unblock signals
467
+ enable: Whether to block/unblock signals (default: True). This is useful
468
+ to avoid blocking signals when not needed without having to handle it by
469
+ adding an `if` statement which would require to duplicate the code that is
470
+ inside the `with` statement in the `else` branch.
471
+ children: Whether to block/unblock signals for child widgets (default: False).
472
+
473
+ Returns:
474
+ Context manager
475
+ """
476
+ if enable:
477
+ widget.blockSignals(True)
478
+ if children:
479
+ for child in widget.findChildren(QW.QWidget):
480
+ child.blockSignals(True)
481
+ try:
482
+ yield
483
+ finally:
484
+ if enable:
485
+ widget.blockSignals(False)
486
+ if children:
487
+ for child in widget.findChildren(QW.QWidget):
488
+ child.blockSignals(False)
489
+
490
+
491
+ def create_menu_button(
492
+ parent: QW.QWidget | None = None, menu: QW.QMenu | None = None
493
+ ) -> QW.QPushButton:
494
+ """Create a menu button
495
+
496
+ Args:
497
+ parent (QWidget): Parent widget
498
+ menu (QMenu): Menu to attach to the button
499
+
500
+ Returns:
501
+ QW.QPushButton: Menu button
502
+ """
503
+ button = QW.QPushButton(get_icon("libre-gui-menu.svg"), "", parent)
504
+ button.setFlat(True)
505
+ if menu is not None:
506
+ button.setMenu(menu)
507
+ return button
508
+
509
+
510
+ def bring_to_front(window: QW.QWidget) -> None:
511
+ """Bring window to front
512
+
513
+ Args:
514
+ window: Window to bring to front
515
+ """
516
+ # Show window on top of others
517
+ eflags = window.windowFlags()
518
+ window.setWindowFlags(eflags | QC.Qt.WindowStaysOnTopHint)
519
+ window.show()
520
+ window.setWindowFlags(eflags)
521
+ window.show()
522
+ # If window is minimized, restore it
523
+ if window.isMinimized():
524
+ window.showNormal()
525
+
526
+
527
+ def configure_menu_about_to_show(menu: QW.QMenu, slot: Callable) -> None:
528
+ """Configure menu about to show.
529
+ This method is only used to connect the "aboutToShow" signal of menus,
530
+ and more importantly to fix Issue #15 (Part 2) which is the fact that
531
+ dynamic menus are not supported on MacOS unless an action is added to
532
+ the menu before it is displayed.
533
+
534
+ Args:
535
+ menu: menu
536
+ slot: slot
537
+ """
538
+ # On MacOS, add an empty action to the menu before connecting the
539
+ # "aboutToShow" signal to the slot. This is required to fix Issue #15 (Part 2)
540
+ if sys.platform == "darwin":
541
+ menu.addAction(QW.QAction(menu))
542
+ menu.aboutToShow.connect(slot)
543
+
544
+
545
+ def resize_widget_to_parent(
546
+ widget: QW.QWidget,
547
+ parent: QW.QWidget | None = None,
548
+ ratio: float = 0.95,
549
+ aspect_ratio: float = 1.0,
550
+ min_size: int = 500,
551
+ ) -> None:
552
+ """Resize widget based on parent widget's dimensions
553
+
554
+ Args:
555
+ widget: Widget to resize
556
+ parent: Parent widget (if None, uses widget.parentWidget())
557
+ ratio: Ratio of parent size to use (0.0 to 1.0, default: 0.95 for 95%).
558
+ This represents the percentage of the maximum dimension with respect
559
+ to the widget.
560
+ aspect_ratio: Width/height ratio (1.0 for square, >1.0 for landscape,
561
+ <1.0 for portrait, default: 1.0)
562
+ min_size: Minimum size in pixels (default: 500)
563
+ """
564
+ if parent is None:
565
+ parent = widget.parentWidget()
566
+
567
+ if parent is not None:
568
+ parent_size = parent.size()
569
+ parent_width = parent_size.width()
570
+ parent_height = parent_size.height()
571
+
572
+ # Calculate maximum available dimensions
573
+ max_width = parent_width * ratio
574
+ max_height = parent_height * ratio
575
+
576
+ # Determine which dimension is limiting based on aspect ratio
577
+ # For aspect_ratio = w/h, we have: w = aspect_ratio * h
578
+ # Check which constraint is more restrictive
579
+ width_from_height = max_height * aspect_ratio
580
+ height_from_width = max_width / aspect_ratio
581
+
582
+ if width_from_height <= max_width:
583
+ # Height is the limiting factor
584
+ height = int(max_height)
585
+ width = int(width_from_height)
586
+ else:
587
+ # Width is the limiting factor
588
+ width = int(max_width)
589
+ height = int(height_from_width)
590
+
591
+ # Ensure minimum size while preserving aspect ratio
592
+ if width < min_size or height < min_size:
593
+ if aspect_ratio >= 1.0:
594
+ # Landscape or square: scale up from minimum width
595
+ width = max(width, min_size)
596
+ height = max(height, int(min_size / aspect_ratio))
597
+ else:
598
+ # Portrait: scale up from minimum height
599
+ height = max(height, min_size)
600
+ width = max(width, int(min_size * aspect_ratio))
601
+
602
+ # Final check: ensure we don't exceed parent dimensions
603
+ width = min(width, parent_width)
604
+ height = min(height, parent_height)
605
+
606
+ widget.resize(width, height)
607
+ else:
608
+ # Fallback: use square with min_size if no parent
609
+ widget.resize(min_size, min_size)
610
+
611
+
612
+ def add_corner_menu(
613
+ tabwidget: QW.QTabWidget, corner: QC.Qt.Corner | None = None
614
+ ) -> QW.QMenu:
615
+ """Add menu as corner widget to tab widget
616
+
617
+ Args:
618
+ tabwidget: Tab widget
619
+ corner: Corner
620
+
621
+ Returns:
622
+ Menu
623
+ """
624
+ if corner is None:
625
+ corner = QC.Qt.TopRightCorner
626
+ menu = QW.QMenu(tabwidget)
627
+ btn = QW.QToolButton(tabwidget)
628
+ btn.setMenu(menu)
629
+ btn.setPopupMode(QW.QToolButton.InstantPopup)
630
+ btn.setIcon(get_icon("menu.svg"))
631
+ btn.setToolTip(_("Open tab menu"))
632
+ tabwidget.setCornerWidget(btn, corner)
633
+ return menu
@@ -0,0 +1,34 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ DataLab strings utilities
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import difflib
10
+ import os.path as osp
11
+ import webbrowser
12
+
13
+
14
+ def save_html_diff(
15
+ text1: str, text2: str, desc1: str, desc2: str, filename: str
16
+ ) -> None:
17
+ """Generates HTML diff between two strings, saves it to a file, and opens it
18
+ in a web browser (Windows only).
19
+
20
+ Args:
21
+ text1 (str): The first string to compare.
22
+ text2 (str): The second string to compare.
23
+ desc1 (str): Description of the first string.
24
+ desc2 (str): Description of the second string.
25
+ filename (str): The name of the file to save the HTML diff to.
26
+
27
+ Returns:
28
+ None
29
+ """
30
+ differ = difflib.HtmlDiff()
31
+ diff_html = differ.make_file(text1.splitlines(), text2.splitlines(), desc1, desc2)
32
+ with open(filename, "w", encoding="utf-8") as file:
33
+ file.write(diff_html)
34
+ webbrowser.open("file://" + osp.realpath(filename))
datalab/utils/tests.py ADDED
File without changes
@@ -0,0 +1 @@
1
+ #