stouputils 1.14.2__py3-none-any.whl → 1.15.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 (113) hide show
  1. stouputils/continuous_delivery/pypi.py +1 -1
  2. stouputils/continuous_delivery/pypi.pyi +3 -2
  3. stouputils/data_science/config/get.py +51 -51
  4. stouputils/data_science/data_processing/image/__init__.py +66 -66
  5. stouputils/data_science/data_processing/image/auto_contrast.py +79 -79
  6. stouputils/data_science/data_processing/image/axis_flip.py +58 -58
  7. stouputils/data_science/data_processing/image/bias_field_correction.py +74 -74
  8. stouputils/data_science/data_processing/image/binary_threshold.py +73 -73
  9. stouputils/data_science/data_processing/image/blur.py +59 -59
  10. stouputils/data_science/data_processing/image/brightness.py +54 -54
  11. stouputils/data_science/data_processing/image/canny.py +110 -110
  12. stouputils/data_science/data_processing/image/clahe.py +92 -92
  13. stouputils/data_science/data_processing/image/common.py +30 -30
  14. stouputils/data_science/data_processing/image/contrast.py +53 -53
  15. stouputils/data_science/data_processing/image/curvature_flow_filter.py +74 -74
  16. stouputils/data_science/data_processing/image/denoise.py +378 -378
  17. stouputils/data_science/data_processing/image/histogram_equalization.py +123 -123
  18. stouputils/data_science/data_processing/image/invert.py +64 -64
  19. stouputils/data_science/data_processing/image/laplacian.py +60 -60
  20. stouputils/data_science/data_processing/image/median_blur.py +52 -52
  21. stouputils/data_science/data_processing/image/noise.py +59 -59
  22. stouputils/data_science/data_processing/image/normalize.py +65 -65
  23. stouputils/data_science/data_processing/image/random_erase.py +66 -66
  24. stouputils/data_science/data_processing/image/resize.py +69 -69
  25. stouputils/data_science/data_processing/image/rotation.py +80 -80
  26. stouputils/data_science/data_processing/image/salt_pepper.py +68 -68
  27. stouputils/data_science/data_processing/image/sharpening.py +55 -55
  28. stouputils/data_science/data_processing/image/shearing.py +64 -64
  29. stouputils/data_science/data_processing/image/threshold.py +64 -64
  30. stouputils/data_science/data_processing/image/translation.py +71 -71
  31. stouputils/data_science/data_processing/image/zoom.py +83 -83
  32. stouputils/data_science/data_processing/image_augmentation.py +118 -118
  33. stouputils/data_science/data_processing/image_preprocess.py +183 -183
  34. stouputils/data_science/data_processing/prosthesis_detection.py +359 -359
  35. stouputils/data_science/data_processing/technique.py +481 -481
  36. stouputils/data_science/dataset/__init__.py +45 -45
  37. stouputils/data_science/dataset/dataset.py +292 -292
  38. stouputils/data_science/dataset/dataset_loader.py +135 -135
  39. stouputils/data_science/dataset/grouping_strategy.py +296 -296
  40. stouputils/data_science/dataset/image_loader.py +100 -100
  41. stouputils/data_science/dataset/xy_tuple.py +696 -696
  42. stouputils/data_science/metric_dictionnary.py +106 -106
  43. stouputils/data_science/mlflow_utils.py +206 -206
  44. stouputils/data_science/models/abstract_model.py +149 -149
  45. stouputils/data_science/models/all.py +85 -85
  46. stouputils/data_science/models/keras/all.py +38 -38
  47. stouputils/data_science/models/keras/convnext.py +62 -62
  48. stouputils/data_science/models/keras/densenet.py +50 -50
  49. stouputils/data_science/models/keras/efficientnet.py +60 -60
  50. stouputils/data_science/models/keras/mobilenet.py +56 -56
  51. stouputils/data_science/models/keras/resnet.py +52 -52
  52. stouputils/data_science/models/keras/squeezenet.py +233 -233
  53. stouputils/data_science/models/keras/vgg.py +42 -42
  54. stouputils/data_science/models/keras/xception.py +38 -38
  55. stouputils/data_science/models/keras_utils/callbacks/__init__.py +20 -20
  56. stouputils/data_science/models/keras_utils/callbacks/colored_progress_bar.py +219 -219
  57. stouputils/data_science/models/keras_utils/callbacks/learning_rate_finder.py +148 -148
  58. stouputils/data_science/models/keras_utils/callbacks/model_checkpoint_v2.py +31 -31
  59. stouputils/data_science/models/keras_utils/callbacks/progressive_unfreezing.py +249 -249
  60. stouputils/data_science/models/keras_utils/callbacks/warmup_scheduler.py +66 -66
  61. stouputils/data_science/models/keras_utils/losses/__init__.py +12 -12
  62. stouputils/data_science/models/keras_utils/losses/next_generation_loss.py +56 -56
  63. stouputils/data_science/models/keras_utils/visualizations.py +416 -416
  64. stouputils/data_science/models/sandbox.py +116 -116
  65. stouputils/data_science/range_tuple.py +234 -234
  66. stouputils/data_science/utils.py +285 -285
  67. stouputils/decorators.py +53 -39
  68. stouputils/decorators.pyi +12 -2
  69. stouputils/installer/__init__.py +18 -18
  70. stouputils/installer/linux.py +144 -144
  71. stouputils/installer/main.py +223 -223
  72. stouputils/installer/windows.py +136 -136
  73. stouputils/io.py +16 -9
  74. stouputils/parallel.pyi +12 -7
  75. stouputils/print.py +229 -2
  76. stouputils/print.pyi +92 -3
  77. stouputils/py.typed +1 -1
  78. {stouputils-1.14.2.dist-info → stouputils-1.15.0.dist-info}/METADATA +1 -1
  79. stouputils-1.15.0.dist-info/RECORD +140 -0
  80. {stouputils-1.14.2.dist-info → stouputils-1.15.0.dist-info}/WHEEL +1 -1
  81. stouputils/stouputils/__init__.pyi +0 -15
  82. stouputils/stouputils/_deprecated.pyi +0 -12
  83. stouputils/stouputils/all_doctests.pyi +0 -46
  84. stouputils/stouputils/applications/__init__.pyi +0 -2
  85. stouputils/stouputils/applications/automatic_docs.pyi +0 -106
  86. stouputils/stouputils/applications/upscaler/__init__.pyi +0 -3
  87. stouputils/stouputils/applications/upscaler/config.pyi +0 -18
  88. stouputils/stouputils/applications/upscaler/image.pyi +0 -109
  89. stouputils/stouputils/applications/upscaler/video.pyi +0 -60
  90. stouputils/stouputils/archive.pyi +0 -67
  91. stouputils/stouputils/backup.pyi +0 -109
  92. stouputils/stouputils/collections.pyi +0 -86
  93. stouputils/stouputils/continuous_delivery/__init__.pyi +0 -5
  94. stouputils/stouputils/continuous_delivery/cd_utils.pyi +0 -129
  95. stouputils/stouputils/continuous_delivery/github.pyi +0 -162
  96. stouputils/stouputils/continuous_delivery/pypi.pyi +0 -53
  97. stouputils/stouputils/continuous_delivery/pyproject.pyi +0 -67
  98. stouputils/stouputils/continuous_delivery/stubs.pyi +0 -39
  99. stouputils/stouputils/ctx.pyi +0 -211
  100. stouputils/stouputils/decorators.pyi +0 -252
  101. stouputils/stouputils/image.pyi +0 -172
  102. stouputils/stouputils/installer/__init__.pyi +0 -5
  103. stouputils/stouputils/installer/common.pyi +0 -39
  104. stouputils/stouputils/installer/downloader.pyi +0 -24
  105. stouputils/stouputils/installer/linux.pyi +0 -39
  106. stouputils/stouputils/installer/main.pyi +0 -57
  107. stouputils/stouputils/installer/windows.pyi +0 -31
  108. stouputils/stouputils/io.pyi +0 -213
  109. stouputils/stouputils/parallel.pyi +0 -216
  110. stouputils/stouputils/print.pyi +0 -136
  111. stouputils/stouputils/version_pkg.pyi +0 -15
  112. stouputils-1.14.2.dist-info/RECORD +0 -171
  113. {stouputils-1.14.2.dist-info → stouputils-1.15.0.dist-info}/entry_points.txt +0 -0
@@ -1,136 +1,136 @@
1
- """ Installer module for Windows specific functions.
2
-
3
- Provides Windows specific implementations for checking administrator privileges,
4
- determining appropriate installation paths (global/local), and modifying
5
- the user's PATH environment variable.
6
- """
7
- # Imports
8
- import os
9
-
10
- from ..decorators import LogLevels, handle_error
11
- from ..io import clean_path
12
- from ..print import debug, info, warning
13
- from .common import ask_install_type, prompt_for_path
14
-
15
-
16
- # Functions
17
- @handle_error(message="Failed to add to PATH (Windows)", error_log=LogLevels.WARNING_TRACEBACK)
18
- def add_to_path_windows(install_path: str) -> bool | None:
19
- """ Add install_path to the User PATH environment variable on Windows.
20
-
21
- Args:
22
- install_path (str): The path to add to the User PATH environment variable.
23
-
24
- Returns:
25
- bool | None: True if the path was added to the User PATH environment variable, None otherwise.
26
- """
27
- # Convert install_path to a Windows path if it's not already
28
- install_path = install_path.replace("/", "\\")
29
- os.makedirs(install_path, exist_ok=True)
30
-
31
- # Get current user PATH
32
- import winreg
33
- with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment", 0, winreg.KEY_READ | winreg.KEY_WRITE) as key:
34
-
35
- # Get the number of values in the registry key
36
- num_values = winreg.QueryInfoKey(key)[1]
37
-
38
- # Find the index of the 'Path' value
39
- path_index = -1
40
- for i in range(num_values):
41
- if winreg.EnumValue(key, i)[0] == 'Path':
42
- path_index = i
43
- break
44
-
45
- # Get the current path value
46
- current_path: str = winreg.EnumValue(key, path_index)[1]
47
-
48
- # Check if path is already present
49
- if install_path not in current_path.split(';'):
50
- new_path: str = f"{current_path};{install_path}"
51
- winreg.SetValueEx(key, "Path", 0, winreg.REG_EXPAND_SZ, new_path)
52
- debug(f"Added '{install_path}' to user PATH. Please restart your terminal for changes to take effect.")
53
- else:
54
- debug(f"'{install_path}' is already in user PATH.")
55
- return True
56
-
57
-
58
- def check_admin_windows() -> bool:
59
- """ Check if the script is running with administrator privileges on Windows. """
60
- try:
61
- import ctypes
62
- return ctypes.windll.shell32.IsUserAnAdmin() != 0
63
- except Exception:
64
- return False
65
-
66
-
67
- @handle_error(message="Failed to get installation path (Windows)", error_log=LogLevels.ERROR_TRACEBACK)
68
- def get_install_path_windows(
69
- program_name: str,
70
- ask_global: int = 0,
71
- add_path: bool = True,
72
- append_to_path: str = "",
73
- default_global: str = os.environ.get("ProgramFiles", "C:\\Program Files")
74
- ) -> str:
75
- """ Get the installation path for the program
76
-
77
- Args:
78
- program_name (str): The name of the program to install.
79
- ask_global (int): 0 = ask for anything, 1 = install globally, 2 = install locally
80
- add_path (bool): Whether to add the program to the PATH environment variable. (Only if installed globally)
81
- append_to_path (str): String to append to the installation path when adding to PATH.
82
- (ex: "bin" if executables are in the bin folder)
83
- default_global (str): The default global installation path.
84
- (Default is "C:\\Program Files" which is the most common location for executables on Windows)
85
-
86
- Returns:
87
- str: The installation path.
88
- """
89
- # Default path is located in the current working directory
90
- default_local_path: str = clean_path(os.path.join(os.getcwd(), program_name))
91
-
92
- # Define default global path (used in prompt even if not chosen initially)
93
- default_global_path: str = clean_path(os.path.join(default_global, program_name))
94
-
95
- # Ask user for installation type (global/local)
96
- install_type: str = ask_install_type(ask_global, default_local_path, default_global_path)
97
-
98
- # If the user wants to install globally,
99
- if install_type == 'g':
100
-
101
- # Check if the user has admin privileges,
102
- if not check_admin_windows():
103
-
104
- # If the user doesn't have admin privileges, fallback to local
105
- warning(
106
- f"Global installation requires administrator privileges. Please re-run as administrator.\n"
107
- f"Install locally instead to '{default_local_path}'? (Y/n): "
108
- )
109
- if input().lower() == 'n':
110
- info("Installation cancelled.")
111
- return ""
112
- else:
113
- # Fallback to local path if user agrees
114
- return prompt_for_path(
115
- f"Falling back to local installation path: {default_local_path}.",
116
- default_local_path
117
- )
118
-
119
- # If the user has admin privileges,
120
- else:
121
- # Ask it user wants to override the default global install path
122
- install_path: str = prompt_for_path(
123
- f"Default global installation path is {default_global_path}.",
124
- default_global_path
125
- )
126
- if add_path:
127
- add_to_path_windows(os.path.join(install_path, append_to_path))
128
- return install_path
129
-
130
- # Local install
131
- else: # install_type == 'l'
132
- return prompt_for_path(
133
- f"Default local installation path is {default_local_path}.",
134
- default_local_path
135
- )
136
-
1
+ """ Installer module for Windows specific functions.
2
+
3
+ Provides Windows specific implementations for checking administrator privileges,
4
+ determining appropriate installation paths (global/local), and modifying
5
+ the user's PATH environment variable.
6
+ """
7
+ # Imports
8
+ import os
9
+
10
+ from ..decorators import LogLevels, handle_error
11
+ from ..io import clean_path
12
+ from ..print import debug, info, warning
13
+ from .common import ask_install_type, prompt_for_path
14
+
15
+
16
+ # Functions
17
+ @handle_error(message="Failed to add to PATH (Windows)", error_log=LogLevels.WARNING_TRACEBACK)
18
+ def add_to_path_windows(install_path: str) -> bool | None:
19
+ """ Add install_path to the User PATH environment variable on Windows.
20
+
21
+ Args:
22
+ install_path (str): The path to add to the User PATH environment variable.
23
+
24
+ Returns:
25
+ bool | None: True if the path was added to the User PATH environment variable, None otherwise.
26
+ """
27
+ # Convert install_path to a Windows path if it's not already
28
+ install_path = install_path.replace("/", "\\")
29
+ os.makedirs(install_path, exist_ok=True)
30
+
31
+ # Get current user PATH
32
+ import winreg
33
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment", 0, winreg.KEY_READ | winreg.KEY_WRITE) as key:
34
+
35
+ # Get the number of values in the registry key
36
+ num_values = winreg.QueryInfoKey(key)[1]
37
+
38
+ # Find the index of the 'Path' value
39
+ path_index = -1
40
+ for i in range(num_values):
41
+ if winreg.EnumValue(key, i)[0] == 'Path':
42
+ path_index = i
43
+ break
44
+
45
+ # Get the current path value
46
+ current_path: str = winreg.EnumValue(key, path_index)[1]
47
+
48
+ # Check if path is already present
49
+ if install_path not in current_path.split(';'):
50
+ new_path: str = f"{current_path};{install_path}"
51
+ winreg.SetValueEx(key, "Path", 0, winreg.REG_EXPAND_SZ, new_path)
52
+ debug(f"Added '{install_path}' to user PATH. Please restart your terminal for changes to take effect.")
53
+ else:
54
+ debug(f"'{install_path}' is already in user PATH.")
55
+ return True
56
+
57
+
58
+ def check_admin_windows() -> bool:
59
+ """ Check if the script is running with administrator privileges on Windows. """
60
+ try:
61
+ import ctypes
62
+ return ctypes.windll.shell32.IsUserAnAdmin() != 0
63
+ except Exception:
64
+ return False
65
+
66
+
67
+ @handle_error(message="Failed to get installation path (Windows)", error_log=LogLevels.ERROR_TRACEBACK)
68
+ def get_install_path_windows(
69
+ program_name: str,
70
+ ask_global: int = 0,
71
+ add_path: bool = True,
72
+ append_to_path: str = "",
73
+ default_global: str = os.environ.get("ProgramFiles", "C:\\Program Files")
74
+ ) -> str:
75
+ """ Get the installation path for the program
76
+
77
+ Args:
78
+ program_name (str): The name of the program to install.
79
+ ask_global (int): 0 = ask for anything, 1 = install globally, 2 = install locally
80
+ add_path (bool): Whether to add the program to the PATH environment variable. (Only if installed globally)
81
+ append_to_path (str): String to append to the installation path when adding to PATH.
82
+ (ex: "bin" if executables are in the bin folder)
83
+ default_global (str): The default global installation path.
84
+ (Default is "C:\\Program Files" which is the most common location for executables on Windows)
85
+
86
+ Returns:
87
+ str: The installation path.
88
+ """
89
+ # Default path is located in the current working directory
90
+ default_local_path: str = clean_path(os.path.join(os.getcwd(), program_name))
91
+
92
+ # Define default global path (used in prompt even if not chosen initially)
93
+ default_global_path: str = clean_path(os.path.join(default_global, program_name))
94
+
95
+ # Ask user for installation type (global/local)
96
+ install_type: str = ask_install_type(ask_global, default_local_path, default_global_path)
97
+
98
+ # If the user wants to install globally,
99
+ if install_type == 'g':
100
+
101
+ # Check if the user has admin privileges,
102
+ if not check_admin_windows():
103
+
104
+ # If the user doesn't have admin privileges, fallback to local
105
+ warning(
106
+ f"Global installation requires administrator privileges. Please re-run as administrator.\n"
107
+ f"Install locally instead to '{default_local_path}'? (Y/n): "
108
+ )
109
+ if input().lower() == 'n':
110
+ info("Installation cancelled.")
111
+ return ""
112
+ else:
113
+ # Fallback to local path if user agrees
114
+ return prompt_for_path(
115
+ f"Falling back to local installation path: {default_local_path}.",
116
+ default_local_path
117
+ )
118
+
119
+ # If the user has admin privileges,
120
+ else:
121
+ # Ask it user wants to override the default global install path
122
+ install_path: str = prompt_for_path(
123
+ f"Default global installation path is {default_global_path}.",
124
+ default_global_path
125
+ )
126
+ if add_path:
127
+ add_to_path_windows(os.path.join(install_path, append_to_path))
128
+ return install_path
129
+
130
+ # Local install
131
+ else: # install_type == 'l'
132
+ return prompt_for_path(
133
+ f"Default local installation path is {default_local_path}.",
134
+ default_local_path
135
+ )
136
+
stouputils/io.py CHANGED
@@ -188,16 +188,23 @@ def csv_dump(
188
188
  done: bool = False
189
189
 
190
190
  # Handle Polars DataFrame
191
- try:
192
- import polars as pl # type: ignore
193
- if isinstance(data, pl.DataFrame):
194
- copy_kwargs = kwargs.copy()
195
- copy_kwargs.setdefault("separator", delimiter)
196
- copy_kwargs.setdefault("include_header", has_header)
197
- data.write_csv(output, *args, **copy_kwargs)
198
- done = True
199
- except Exception:
191
+ import sys
192
+ if sys.version_info >= (3, 14) and not sys._is_gil_enabled(): # pyright: ignore[reportPrivateUsage]
193
+ # Skip Polars on free-threaded Python 3.14 due to segfault
194
+ # TODO: Remove this check when Polars is fixed
195
+ # See https://github.com/pola-rs/polars/issues/21889 and https://github.com/durandtibo/coola/issues/1066
200
196
  pass
197
+ else:
198
+ try:
199
+ import polars as pl # type: ignore
200
+ if isinstance(data, pl.DataFrame):
201
+ copy_kwargs = kwargs.copy()
202
+ copy_kwargs.setdefault("separator", delimiter)
203
+ copy_kwargs.setdefault("include_header", has_header)
204
+ data.write_csv(output, *args, **copy_kwargs)
205
+ done = True
206
+ except Exception:
207
+ pass
201
208
 
202
209
  # Handle pandas DataFrame
203
210
  if not done:
stouputils/parallel.pyi CHANGED
@@ -10,7 +10,7 @@ CPU_COUNT: int
10
10
  T = TypeVar('T')
11
11
  R = TypeVar('R')
12
12
 
13
- def multiprocessing[T, R](func: Callable[..., R] | list[Callable[..., R]], args: Iterable[T], use_starmap: bool = False, chunksize: int = 1, desc: str = '', max_workers: int | float = ..., delay_first_calls: float = 0, color: str = ..., bar_format: str = ..., ascii: bool = False) -> list[R]:
13
+ def multiprocessing[T, R](func: Callable[..., R] | list[Callable[..., R]], args: Iterable[T], use_starmap: bool = False, chunksize: int = 1, desc: str = '', max_workers: int | float = ..., delay_first_calls: float = 0, color: str = ..., bar_format: str = ..., ascii: bool = False, smooth_tqdm: bool = True, **tqdm_kwargs: Any) -> list[R]:
14
14
  ''' Method to execute a function in parallel using multiprocessing
15
15
 
16
16
  \t- For CPU-bound operations where the GIL (Global Interpreter Lock) is a bottleneck.
@@ -35,6 +35,8 @@ def multiprocessing[T, R](func: Callable[..., R] | list[Callable[..., R]], args:
35
35
  \t\tcolor\t\t\t\t(str):\t\t\t\tColor of the progress bar (Defaults to MAGENTA)
36
36
  \t\tbar_format\t\t\t(str):\t\t\t\tFormat of the progress bar (Defaults to BAR_FORMAT)
37
37
  \t\tascii\t\t\t\t(bool):\t\t\t\tWhether to use ASCII or Unicode characters for the progress bar
38
+ \t\tsmooth_tqdm\t\t\t(bool):\t\t\t\tWhether to enable smooth progress bar updates by setting miniters and mininterval (Defaults to True)
39
+ \t\t**tqdm_kwargs\t\t(Any):\t\t\t\tAdditional keyword arguments to pass to tqdm
38
40
 
39
41
  \tReturns:
40
42
  \t\tlist[object]:\tResults of the function execution
@@ -66,7 +68,7 @@ def multiprocessing[T, R](func: Callable[..., R] | list[Callable[..., R]], args:
66
68
  \t\t\t. )
67
69
  \t\t\t[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
68
70
  \t'''
69
- def multithreading[T, R](func: Callable[..., R] | list[Callable[..., R]], args: Iterable[T], use_starmap: bool = False, desc: str = '', max_workers: int | float = ..., delay_first_calls: float = 0, color: str = ..., bar_format: str = ..., ascii: bool = False) -> list[R]:
71
+ def multithreading[T, R](func: Callable[..., R] | list[Callable[..., R]], args: Iterable[T], use_starmap: bool = False, desc: str = '', max_workers: int | float = ..., delay_first_calls: float = 0, color: str = ..., bar_format: str = ..., ascii: bool = False, smooth_tqdm: bool = True, **tqdm_kwargs: Any) -> list[R]:
70
72
  ''' Method to execute a function in parallel using multithreading, you should use it:
71
73
 
72
74
  \t- For I/O-bound operations where the GIL is not a bottleneck, such as network requests or disk operations.
@@ -89,6 +91,8 @@ def multithreading[T, R](func: Callable[..., R] | list[Callable[..., R]], args:
89
91
  \t\tcolor\t\t\t\t(str):\t\t\t\tColor of the progress bar (Defaults to MAGENTA)
90
92
  \t\tbar_format\t\t\t(str):\t\t\t\tFormat of the progress bar (Defaults to BAR_FORMAT)
91
93
  \t\tascii\t\t\t\t(bool):\t\t\t\tWhether to use ASCII or Unicode characters for the progress bar
94
+ \t\tsmooth_tqdm\t\t\t(bool):\t\t\t\tWhether to enable smooth progress bar updates by setting miniters and mininterval (Defaults to True)
95
+ \t\t**tqdm_kwargs\t\t(Any):\t\t\t\tAdditional keyword arguments to pass to tqdm
92
96
 
93
97
  \tReturns:
94
98
  \t\tlist[object]:\tResults of the function execution
@@ -120,7 +124,7 @@ def multithreading[T, R](func: Callable[..., R] | list[Callable[..., R]], args:
120
124
  \t\t\t. )
121
125
  \t\t\t[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
122
126
  \t'''
123
- def run_in_subprocess[R](func: Callable[..., R], *args: Any, timeout: float | None = None, **kwargs: Any) -> R:
127
+ def run_in_subprocess[R](func: Callable[..., R], *args: Any, timeout: float | None = None, no_join: bool = False, **kwargs: Any) -> R:
124
128
  ''' Execute a function in a subprocess with positional and keyword arguments.
125
129
 
126
130
  \tThis is useful when you need to run a function in isolation to avoid memory leaks,
@@ -133,6 +137,7 @@ def run_in_subprocess[R](func: Callable[..., R], *args: Any, timeout: float | No
133
137
  \t\t*args (Any): Positional arguments to pass to the function.
134
138
  \t\ttimeout (float | None): Maximum time in seconds to wait for the subprocess.
135
139
  \t\t\tIf None, wait indefinitely. If the subprocess exceeds this time, it will be terminated.
140
+ \t\tno_join (bool): If True, do not wait for the subprocess to finish (fire-and-forget).
136
141
  \t\t**kwargs (Any): Keyword arguments to pass to the function.
137
142
 
138
143
  \tReturns:
@@ -170,10 +175,10 @@ def _subprocess_wrapper[R](result_queue: Any, func: Callable[..., R], args: tupl
170
175
  \tMust be at module level to be pickable on Windows (spawn context).
171
176
 
172
177
  \tArgs:
173
- \t\tresult_queue (multiprocessing.Queue): Queue to store the result or exception.
174
- \t\tfunc (Callable): The target function to execute.
175
- \t\targs (tuple): Positional arguments for the function.
176
- \t\tkwargs (dict): Keyword arguments for the function.
178
+ \t\tresult_queue (multiprocessing.Queue | None): Queue to store the result or exception (None if detached).
179
+ \t\tfunc (Callable): The target function to execute.
180
+ \t\targs (tuple): Positional arguments for the function.
181
+ \t\tkwargs (dict): Keyword arguments for the function.
177
182
  \t"""
178
183
  def _starmap[T, R](args: tuple[Callable[[T], R], list[T]]) -> R:
179
184
  """ Private function to use starmap using args[0](\\*args[1])
stouputils/print.py CHANGED
@@ -4,6 +4,10 @@ This module provides utility functions for printing messages with different leve
4
4
  If a message is printed multiple times, it will be displayed as "(xN) message"
5
5
  where N is the number of times the message has been printed.
6
6
 
7
+ The module also includes a `colored()` function that formats text with Python 3.14 style coloring
8
+ for file paths, line numbers, function names (in magenta), and exception names (in bold magenta).
9
+ All functions have their colored counterparts with a 'c' suffix (e.g., `infoc()`, `debugc()`, etc.)
10
+
7
11
  .. image:: https://raw.githubusercontent.com/Stoupy51/stouputils/refs/heads/main/assets/print_module.gif
8
12
  :alt: stouputils print examples
9
13
  """
@@ -24,6 +28,7 @@ BLUE: str = "\033[94m"
24
28
  MAGENTA: str = "\033[95m"
25
29
  CYAN: str = "\033[96m"
26
30
  LINE_UP: str = "\033[1A"
31
+ BOLD: str = "\033[1m"
27
32
 
28
33
  # Constants
29
34
  BAR_FORMAT: str = "{l_bar}{bar}" + MAGENTA + "| {n_fmt}/{total_fmt} [{rate_fmt}{postfix}, {elapsed}<{remaining}]" + RESET
@@ -84,12 +89,210 @@ def colored_for_loop[T](
84
89
  from tqdm.auto import tqdm
85
90
  yield from tqdm(iterable, desc=desc, bar_format=bar_format, ascii=ascii, **kwargs)
86
91
 
92
+ def format_colored(*values: Any) -> str:
93
+ """ Format text with Python 3.14 style colored formatting.
94
+
95
+ Dynamically colors text by analyzing each word:
96
+ - File paths in magenta
97
+ - Numbers in magenta
98
+ - Function names (built-in and callable objects) in magenta
99
+ - Exception names in bold magenta
100
+
101
+ Args:
102
+ values (Any): Values to format (like the print function)
103
+
104
+ Returns:
105
+ str: The formatted text with ANSI color codes
106
+
107
+ Examples:
108
+ >>> # Test function names with parentheses
109
+ >>> result = format_colored("Call print() with 42 items")
110
+ >>> result.count(MAGENTA) == 2 # print and 42
111
+ True
112
+
113
+ >>> # Test function names without parentheses
114
+ >>> result = format_colored("Use len and sum functions")
115
+ >>> result.count(MAGENTA) == 2 # len and sum
116
+ True
117
+
118
+ >>> # Test exceptions (bold magenta)
119
+ >>> result = format_colored("Got ValueError when parsing")
120
+ >>> result.count(MAGENTA) == 1 and result.count(BOLD) == 1 # ValueError in bold magenta
121
+ True
122
+
123
+ >>> # Test file paths
124
+ >>> result = format_colored("Processing ./data.csv file")
125
+ >>> result.count(MAGENTA) == 1 # ./data.csv
126
+ True
127
+
128
+ >>> # Test file paths with quotes
129
+ >>> result = format_colored('File "/path/to/script.py" line 42')
130
+ >>> result.count(MAGENTA) == 2 # /path/to/script.py and 42
131
+ True
132
+
133
+ >>> # Test numbers
134
+ >>> result = format_colored("Found 100 items and 3.14 value")
135
+ >>> result.count(MAGENTA) == 2 # 100 and 3.14
136
+ True
137
+
138
+ >>> # Test mixed content
139
+ >>> result = format_colored("Call sum() got IndexError at line 256 in utils.py")
140
+ >>> result.count(MAGENTA) == 3 # sum, IndexError (bold), and 256
141
+ True
142
+ >>> result.count(BOLD) == 1 # IndexError is bold
143
+ True
144
+
145
+ >>> # Test plain text (no coloring)
146
+ >>> result = format_colored("This is plain text")
147
+ >>> result.count(MAGENTA) == 0 and result == "This is plain text"
148
+ True
149
+ """
150
+ import builtins
151
+ import re
152
+
153
+ # Dynamically retrieve all Python exception names and function names
154
+ EXCEPTION_NAMES: set[str] = {
155
+ name for name in dir(builtins)
156
+ if isinstance(getattr(builtins, name, None), type)
157
+ and issubclass(getattr(builtins, name), BaseException)
158
+ }
159
+ BUILTIN_FUNCTIONS: set[str] = {
160
+ name for name in dir(builtins)
161
+ if callable(getattr(builtins, name, None))
162
+ and not (isinstance(getattr(builtins, name, None), type)
163
+ and issubclass(getattr(builtins, name), BaseException))
164
+ }
165
+
166
+ def is_filepath(word: str) -> bool:
167
+ """ Check if a word looks like a file path """
168
+ # Remove quotes if present
169
+ clean_word: str = word.strip('"\'')
170
+
171
+ # Check for path separators and file extensions
172
+ if ('/' in clean_word or '\\' in clean_word) and '.' in clean_word:
173
+ # Check if it has a reasonable extension (2-4 chars)
174
+ parts = clean_word.split('.')
175
+ if len(parts) >= 2 and 2 <= len(parts[-1]) <= 4:
176
+ return True
177
+
178
+ # Check for Windows absolute paths (C:\, D:\, etc.)
179
+ if len(clean_word) > 3 and clean_word[1:3] == ':\\':
180
+ return True
181
+
182
+ # Check for Unix absolute paths starting with /
183
+ if clean_word.startswith('/') and '.' in clean_word:
184
+ return True
185
+
186
+ return False
187
+
188
+ def is_number(word: str) -> bool:
189
+ try:
190
+ float(word.strip('.,;:!?'))
191
+ return True
192
+ except ValueError:
193
+ return False
194
+
195
+ def is_function_name(word: str) -> tuple[bool, str]:
196
+ # Check if word ends with () or just (, or it's a known built-in function
197
+ clean_word: str = word.rstrip('.,;:!?')
198
+ if clean_word.endswith(('()','(')) or clean_word in BUILTIN_FUNCTIONS:
199
+ return (True, clean_word)
200
+ return (False, "")
201
+
202
+ def is_exception(word: str) -> bool:
203
+ """ Check if a word is a known exception name """
204
+ return word.strip('.,;:!?') in EXCEPTION_NAMES
205
+
206
+ # Convert all values to strings and join them and split into words while preserving separators
207
+ text: str = " ".join(str(v) for v in values)
208
+ words: list[str] = re.split(r'(\s+)', text)
209
+
210
+ # Process each word
211
+ colored_words: list[str] = []
212
+ i: int = 0
213
+ while i < len(words):
214
+ word = words[i]
215
+
216
+ # Skip whitespace
217
+ if word.isspace():
218
+ colored_words.append(word)
219
+ i += 1
220
+ continue
221
+
222
+ # Try to identify and color the word
223
+ colored: bool = False
224
+ if is_filepath(word):
225
+ colored_words.append(f"{MAGENTA}{word}{RESET}")
226
+ colored = True
227
+ elif is_exception(word):
228
+ colored_words.append(f"{BOLD}{MAGENTA}{word}{RESET}")
229
+ colored = True
230
+ elif is_number(word):
231
+ # Preserve punctuation
232
+ clean_word = word.strip('.,;:!?')
233
+ prefix = word[:len(word) - len(word.lstrip('.,;:!?'))]
234
+ suffix = word[len(clean_word) + len(prefix):]
235
+ colored_words.append(f"{prefix}{MAGENTA}{clean_word}{RESET}{suffix}")
236
+ colored = True
237
+ elif is_function_name(word)[0]:
238
+ func_name = is_function_name(word)[1]
239
+ # Find where the function name ends in the original word
240
+ func_start = word.find(func_name)
241
+ if func_start != -1:
242
+ prefix = word[:func_start]
243
+ func_end = func_start + len(func_name)
244
+ suffix = word[func_end:]
245
+ colored_words.append(f"{prefix}{MAGENTA}{func_name}{RESET}{suffix}")
246
+ else:
247
+ # Fallback if we can't find it (shouldn't happen)
248
+ colored_words.append(f"{MAGENTA}{word}{RESET}")
249
+ colored = True
250
+
251
+ # If nothing matched, keep the word as is
252
+ if not colored:
253
+ colored_words.append(word)
254
+ i += 1
255
+
256
+ # Join and return
257
+ return "".join(colored_words)
258
+
259
+ def colored(
260
+ *values: Any,
261
+ file: TextIO | None = None,
262
+ **print_kwargs: Any,
263
+ ) -> None:
264
+ """ Print with Python 3.14 style colored formatting.
265
+
266
+ Dynamically colors text by analyzing each word:
267
+ - File paths in magenta
268
+ - Numbers in magenta
269
+ - Function names (built-in and callable objects) in magenta
270
+ - Exception names in bold magenta
271
+
272
+ Args:
273
+ values (Any): Values to print (like the print function)
274
+ file (TextIO): File to write the message to (default: sys.stdout)
275
+ print_kwargs (dict): Keyword arguments to pass to the print function
276
+
277
+ Examples:
278
+ >>> colored("File '/path/to/file.py', line 42, in function_name") # doctest: +SKIP
279
+ >>> colored("KeyboardInterrupt") # doctest: +SKIP
280
+ >>> colored("Processing data.csv with 100 items") # doctest: +SKIP
281
+ >>> colored("Using print and len functions") # doctest: +SKIP
282
+ """
283
+ if file is None:
284
+ file = sys.stdout
285
+
286
+ result: str = format_colored(*values)
287
+ print(result, file=file, **print_kwargs)
288
+
87
289
  def info(
88
290
  *values: Any,
89
291
  color: str = GREEN,
90
292
  text: str = "INFO ",
91
293
  prefix: str = "",
92
294
  file: TextIO | list[TextIO] | None = None,
295
+ use_colored: bool = False,
93
296
  **print_kwargs: Any,
94
297
  ) -> None:
95
298
  """ Print an information message looking like "[INFO HH:MM:SS] message" in green by default.
@@ -100,6 +303,7 @@ def info(
100
303
  text (str): Text of the message (default: "INFO ")
101
304
  prefix (str): Prefix to add to the values
102
305
  file (TextIO|list[TextIO]): File(s) to write the message to (default: sys.stdout)
306
+ use_colored (bool): Whether to use the colored() function to format the message
103
307
  print_kwargs (dict): Keyword arguments to pass to the print function
104
308
  """
105
309
  # Use stdout if no file is specified
@@ -109,7 +313,7 @@ def info(
109
313
  # If file is a list, recursively call info() for each file
110
314
  if isinstance(file, list):
111
315
  for f in file:
112
- info(*values, color=color, text=text, prefix=prefix, file=f, **print_kwargs)
316
+ info(*values, color=color, text=text, prefix=prefix, file=f, use_colored=use_colored, **print_kwargs)
113
317
  else:
114
318
  # Build the message with prefix, color, text and timestamp
115
319
  message: str = f"{prefix}{color}[{text} {current_time()}]"
@@ -119,7 +323,10 @@ def info(
119
323
  message = f"{LINE_UP}{message} (x{nb_values})"
120
324
 
121
325
  # Print the message with the values and reset color
122
- print(message, *values, RESET, file=file, **print_kwargs)
326
+ if use_colored:
327
+ print(message, format_colored(*values).replace(RESET, RESET+color), RESET, file=file, **print_kwargs)
328
+ else:
329
+ print(message, *values, RESET, file=file, **print_kwargs)
123
330
 
124
331
  def debug(*values: Any, **print_kwargs: Any) -> None:
125
332
  """ Print a debug message looking like "[DEBUG HH:MM:SS] message" in cyan by default. """
@@ -444,6 +651,26 @@ def current_time() -> str:
444
651
  else:
445
652
  return time.strftime("%H:%M:%S")
446
653
 
654
+ # Convenience colored functions
655
+ def infoc(*args: Any, **kwargs: Any) -> None:
656
+ return info(*args, use_colored=True, **kwargs)
657
+ def debugc(*args: Any, **kwargs: Any) -> None:
658
+ return debug(*args, use_colored=True, **kwargs)
659
+ def alt_debugc(*args: Any, **kwargs: Any) -> None:
660
+ return alt_debug(*args, use_colored=True, **kwargs)
661
+ def warningc(*args: Any, **kwargs: Any) -> None:
662
+ return warning(*args, use_colored=True, **kwargs)
663
+ def errorc(*args: Any, **kwargs: Any) -> None:
664
+ return error(*args, use_colored=True, **kwargs)
665
+ def progressc(*args: Any, **kwargs: Any) -> None:
666
+ return progress(*args, use_colored=True, **kwargs)
667
+ def suggestionc(*args: Any, **kwargs: Any) -> None:
668
+ return suggestion(*args, use_colored=True, **kwargs)
669
+ def whatisitc(*args: Any, **kwargs: Any) -> None:
670
+ return whatisit(*args, use_colored=True, **kwargs)
671
+ def breakpointc(*args: Any, **kwargs: Any) -> None:
672
+ return breakpoint(*args, use_colored=True, **kwargs)
673
+
447
674
 
448
675
  # Test the print functions
449
676
  if __name__ == "__main__":