rootlp 0.1.0__tar.gz

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.
rootlp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.3
2
+ Name: rootlp
3
+ Version: 0.1.0
4
+ Summary: A library that gathers root functions for custom script execution.
5
+ Requires-Dist: corelp
6
+ Requires-Dist: joblib
7
+ Requires-Dist: marimo
8
+ Requires-Dist: rich
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+
12
+ # rootLP
13
+
14
+ ```text
15
+ Author : Lancelot PINCET
16
+ GitHub : https://github.com/LancelotPincet/rootLP
17
+ HTTPS : https://github.com/LancelotPincet/rootLP.git
18
+ SSH : git@github.com:LancelotPincet/rootLP.git
19
+ PyPI : https://pypi.org/project/rootLP
20
+ Docs : https://rootLP.readthedocs.io
21
+ ```
22
+
23
+ **A library that gathers root functions for custom script execution.**
24
+
25
+ rootLP is available on [PyPI](https://pypi.org/project/rootLP) for pip installs.
26
+ For more information, do not hesitate to consult the [Documentation](https://rootLP.readthedocs.io).
27
+
28
+ ---
29
+
30
+ ## MIT License
31
+
32
+ <details>
33
+ <summary>details</summary>
34
+
35
+ Intellectual property behind this Library is protected via an [MIT license](LICENSE). This means everyone can *freely* use it in a personnal, academic or commercial manner, if they **keep the copyright name** at the top of the codes.
36
+
37
+ The library can be redistributed, *with or without modifications*, in open or closed projects. However the **MIT license must be conserved**. For example in a commercial closed project, this means the **copyright and license must be visible somewhere**, like in the documentation or credits.
38
+
39
+ The license also explains that the **code performances are not warrantied**, and you are responsible for how you are using it. For more information on your rights and obligations please refer to [descriptive websites](https://en.wikipedia.org/wiki/MIT_License), or contact author for approvales.
40
+
41
+ </details>
rootlp-0.1.0/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # rootLP
2
+
3
+ ```text
4
+ Author : Lancelot PINCET
5
+ GitHub : https://github.com/LancelotPincet/rootLP
6
+ HTTPS : https://github.com/LancelotPincet/rootLP.git
7
+ SSH : git@github.com:LancelotPincet/rootLP.git
8
+ PyPI : https://pypi.org/project/rootLP
9
+ Docs : https://rootLP.readthedocs.io
10
+ ```
11
+
12
+ **A library that gathers root functions for custom script execution.**
13
+
14
+ rootLP is available on [PyPI](https://pypi.org/project/rootLP) for pip installs.
15
+ For more information, do not hesitate to consult the [Documentation](https://rootLP.readthedocs.io).
16
+
17
+ ---
18
+
19
+ ## MIT License
20
+
21
+ <details>
22
+ <summary>details</summary>
23
+
24
+ Intellectual property behind this Library is protected via an [MIT license](LICENSE). This means everyone can *freely* use it in a personnal, academic or commercial manner, if they **keep the copyright name** at the top of the codes.
25
+
26
+ The library can be redistributed, *with or without modifications*, in open or closed projects. However the **MIT license must be conserved**. For example in a commercial closed project, this means the **copyright and license must be visible somewhere**, like in the documentation or credits.
27
+
28
+ The license also explains that the **code performances are not warrantied**, and you are responsible for how you are using it. For more information on your rights and obligations please refer to [descriptive websites](https://en.wikipedia.org/wiki/MIT_License), or contact author for approvales.
29
+
30
+ </details>
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "rootlp"
3
+ version = "0.1.0"
4
+ description = "A library that gathers root functions for custom script execution."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [ "corelp", "joblib", "marimo", "rich",]
8
+
9
+ [build-system]
10
+ requires = [ "uv_build>=0.9.18,<0.10.0",]
11
+ build-backend = "uv_build"
12
+
13
+ [dependency-groups]
14
+ dev = [ "pytest",]
15
+ docs = [ "sphinx", "sphinx-design", "sphinx-rtd-theme", "toml",]
16
+ anaconda = [ "ipykernel", "spyder-kernels",]
17
+
18
+ [tool.uv]
19
+ required-environments = [ "sys_platform == 'linux' and platform_machine == 'x86_64'", "sys_platform == 'win32' and (platform_machine == 'AMD64' or platform_machine == 'x86_64')",]
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Date : 2026-01-10
4
+ # Author : Lancelot PINCET
5
+ # GitHub : https://github.com/LancelotPincet
6
+ # Library : rootLP
7
+
8
+ """
9
+ A library that gathers root functions for custom script execution.
10
+ """
11
+
12
+
13
+
14
+ # %% Lazy imports
15
+ from corelp import getmodule
16
+ __getattr__, __all__ = getmodule(__file__)
17
+
18
+
19
+
20
+ # %% Test function run
21
+ if __name__ == "__main__":
22
+ from corelp import test
23
+ test(__file__)
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Date : 2025-08-28
4
+ # Author : Lancelot PINCET
5
+ # GitHub : https://github.com/LancelotPincet
6
+ # Library : rootLP
7
+ # Module : Section
8
+
9
+ """
10
+ This class defines decorator instances allowing to create section functions.
11
+ """
12
+
13
+
14
+
15
+ # %% Libraries
16
+ from corelp import folder, selfkwargs, kwargsself
17
+ from rootlp import print
18
+ from dataclasses import dataclass
19
+ import pickle
20
+ import joblib
21
+ from pathlib import Path
22
+ from functools import wraps
23
+ import hashlib
24
+ import inspect
25
+ import os
26
+
27
+
28
+
29
+ # %% Class
30
+ @dataclass(slots=True, kw_only=True)
31
+ class Section() :
32
+ '''
33
+ This class defines decorator instances allowing to create section functions.
34
+ Cache results into a folder, if another call occurs, can load-back the precalculated data.
35
+
36
+ Parameters
37
+ ----------
38
+ path : str or Path
39
+ path where to save section folder results.
40
+ new : bool
41
+ True to ignore pre-calculated data and crush them.
42
+ num : int
43
+ Index of section, after one call, adds 1 for next call.
44
+ parent_path : str or Path
45
+ Path to the parent folder if bulk processing.
46
+
47
+ Examples
48
+ --------
49
+ >>> from corelp import Section
50
+ ...
51
+ >>> section = Section(path=export_path)
52
+ ...
53
+ >>> @section()
54
+ ... def add(a, b=0) :
55
+ ... testfunc.print('Hello World')
56
+ ... return a + b
57
+ ...
58
+ >>> testfunc.print('3+0=', add(3)) # First call calculates and save result
59
+ >>> testfunc.print('3+0=', add(3, 0)) # Second call loads back precalculated results
60
+ >>> testfunc.print('1+3=', add(1, 3)) # New call with other parameters : crushed previous results with new ones
61
+ >>> testfunc.print('1+3=', add(1, b=3)) # Second call with these parameters : loads precalculated results
62
+ ...
63
+ >>> @section(cache=False) # Creates an index of 2, does no caching
64
+ ... def sub(a, b=0) :
65
+ ... return a - b
66
+ ...
67
+ >>> @section(num=10) # Creates an index of 10
68
+ ... def mul(a, b=0) :
69
+ ... return a * b
70
+ ...
71
+ >>> @section(new=True) # Creates an index of 11, always creates new cache
72
+ ... def div(a, b) :
73
+ ... return a / b
74
+ '''
75
+
76
+ # Attributes
77
+ path : Path | str = None
78
+ new :bool = False
79
+ num :int = 0
80
+ parent_path : Path | str = None
81
+
82
+ # Init
83
+ def __post_init__(self) :
84
+ if self.path is not None :
85
+ self.path = Path(self.path)
86
+ if self.parent_path is not None :
87
+ self.parent_path = Path(self.parent_path)
88
+
89
+ # Decorator
90
+ def __call__(self, *, new=None, num=None, symlink=None, cache=True):
91
+ if new is None :
92
+ new = self.new
93
+ if num is None :
94
+ num = self.num
95
+ self.num = num+1
96
+
97
+ def decorator(func) :
98
+ name = func.__name__
99
+
100
+ @wraps(func)
101
+ def wrapper(*args, **kwargs):
102
+ wrapper.path = self.path / f"{num:03}_{name}"
103
+ print(f'\n#### **{num}. {name.replace("_"," ")} section**\n')
104
+
105
+ # Creating hash
106
+ if cache :
107
+ print('**Call hash:**', do_stdout=False)
108
+ bound = inspect.signature(func).bind(*args, **kwargs)
109
+ bound.apply_defaults()
110
+ serialized = pickle.dumps(bound.arguments)
111
+ args_hash = hashlib.md5(serialized).hexdigest()
112
+ result_file = wrapper.path / f'{args_hash}.pkl'
113
+ print(f'*{args_hash}*\n', do_stdout=False)
114
+
115
+ # Checking already calculated exists
116
+ if result_file.exists() and not new :
117
+ print('**Loading from *precalculated* results...**')
118
+ with open(result_file, 'rb') as f:
119
+ result = joblib.load(f)
120
+ print('...loaded\n')
121
+ return result
122
+
123
+ # Calculations
124
+ folder(wrapper.path, warning=False)
125
+ print('**Calculating results:**')
126
+ print_status = kwargsself(print)
127
+ print.file = wrapper.path / f'{name}_log.md'
128
+ result = func(*args, **kwargs)
129
+ selfkwargs(print, print_status)
130
+ print('...calculated\n')
131
+
132
+ # Caching
133
+ if cache :
134
+ print('**Saving results:**')
135
+ with open(result_file, 'wb') as f:
136
+ joblib.dump(result, f)
137
+ print('...saved\n')
138
+
139
+ # Create symlink
140
+ if symlink is not None :
141
+ print('**Creating symlinks:**')
142
+ for link in symlink :
143
+ print(f"- {link}")
144
+ link_path = Path(link)
145
+ link_folder = self.parent_path.parent / f'_outputs/{link_path.stem}'
146
+ new_stem = str(self.subfolder.as_posix()).replace('/', '--').replace(' ', '_')
147
+ if not link_folder.exists() :
148
+ folder(link_folder, warning=False)
149
+ link_from = wrapper.path / link_path
150
+ link_to = link_folder / f"{new_stem}{link_path.suffix}"
151
+ if link_to.exists() or link_to.is_symlink():
152
+ link_to.unlink()
153
+ if os.name == "nt":
154
+ try :
155
+ link_to.symlink_to(link_from, link_from.is_dir())
156
+ except OSError :
157
+ print("Windows does not allow to create symlink, aborting. Consider using Windows in Developper mode.")
158
+ break
159
+ else:
160
+ link_to.symlink_to(link_from)
161
+ print('...created\n')
162
+
163
+
164
+ return result
165
+ return wrapper
166
+ return decorator
167
+
168
+ @property
169
+ def subfolder(self) :
170
+ return self.path.relative_to(self.parent_path)
171
+
172
+
173
+
174
+ # %% Test function run
175
+ if __name__ == "__main__":
176
+ from corelp import test
177
+ test(__file__)
File without changes
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Date : 2025-08-28
4
+ # Author : Lancelot PINCET
5
+ # GitHub : https://github.com/LancelotPincet
6
+ # Library : rootLP
7
+ # Module : Section
8
+
9
+ """
10
+ This file allows to test Section
11
+
12
+ Section : This class defines decorator instances allowing to create section functions.
13
+ """
14
+
15
+
16
+
17
+ # %% Libraries
18
+ from corelp import debug
19
+ from rootlp import Section
20
+ debug_folder = debug(__file__)
21
+
22
+
23
+
24
+ # %% Function test
25
+ def test_function() :
26
+ '''
27
+ Test Section function
28
+ '''
29
+ section = Section(path=debug_folder)
30
+
31
+ @section()
32
+ def add(a, b=0) :
33
+ return a+b
34
+ assert add(3) == 3
35
+ assert add(3, 0) == 3
36
+ assert add(1, 3) == 4
37
+ assert add(1, b=3) == 4
38
+
39
+
40
+
41
+ # %% Test function run
42
+ if __name__ == "__main__":
43
+ from corelp import test
44
+ test(__file__)
File without changes
File without changes
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Date : 2025-08-28
4
+ # Author : Lancelot PINCET
5
+ # GitHub : https://github.com/LancelotPincet
6
+ # Library : rootLP
7
+ # Module : main
8
+
9
+ """
10
+ This function can decorate the main function of a script.
11
+ """
12
+
13
+
14
+
15
+ # %% Libraries
16
+ from corelp import folder, selfkwargs, kwargsself, icon, Path
17
+ from rootlp import print, Section
18
+ import time
19
+ import functools
20
+ import os
21
+ from datetime import datetime
22
+ import tkinter as tk
23
+ from tkinter import filedialog
24
+ import types
25
+
26
+
27
+
28
+ # %% Function
29
+ def main() :
30
+ '''
31
+ This function can decorate the main function of a script.
32
+ User inputs parameters shoud be put in the beginning of the main file, and the decorated function will recognize them.
33
+ Decorated function can change the values of these parameters with keyword arguments when called.
34
+ Section can be created bellow the mainfunction.
35
+
36
+ Global parameters
37
+ -----------------
38
+ import_path : Path or str or None
39
+ Path where to import script data to process.
40
+ If None, will manually ask user to select it.
41
+ If not existent, will be ignored.
42
+ export_path : Path or str or None
43
+ Path where to export script data to process.
44
+ A new folder will be created inside at the call time as name.
45
+ If None, will save in import_path. If not existent, will be ignored.
46
+ If a previous call was already made in this same folder, and new is False, will try to reload data from this last folder.
47
+ new : bool
48
+ Overrides Decorator new parameter.
49
+ bulk : function
50
+ function(import_path) that returns a dictionnary of {import_subfolder:export_subfolder} for multiple decorated function run.
51
+ If bulk is not None, the decorated function will run with import_subfolder, export_subfolder instead of import_path, export_path (see below).
52
+ The import_subfolders and export_subfolder are defined from import_path and export_path respectively (they are not absolute from root path).
53
+ overnight : bool
54
+ If True and exception occurs, will skip and pass to the next run in bulk processing. To use for example for overnight bulk processing.
55
+
56
+ Examples
57
+ --------
58
+ >>> from root import main
59
+ ...
60
+ >>> import_path = None # will be asked via a GUI
61
+ >>> export_path = None # will create inside import_path
62
+ >>> new = False # True to create a new export folder, False to reload precalculated data
63
+ >>> bulk = None # function(import_path) that returns a dictionnary of {import_subfolder:export_subfolder} for multiple decorated function run.
64
+ >>> overnight= False # If True and exception occurs, will skip and pass to the next run in bulk processing.
65
+ >>> main_string = "Hello from main!" # User input parameter
66
+ ...
67
+ >>> @main(new=True) # if previous new is not defined, new is defined here
68
+ ... def myscript() :
69
+ ... print(main_string) # By default prints "Hello from main!"
70
+ ... result = mysection() # Section defined bellow, result can be reloaded from previous run
71
+ ... return result
72
+ ...
73
+ ... @main.section()
74
+ ... def mysection() :
75
+ ... print("Hello from section!")
76
+ ... return True # Will be saved into export_path and can be reuploaded at next run with same inputs
77
+ ...
78
+ >>> # Launch
79
+ >>> if __name__ == "__main__" :
80
+ ... myscript() # prints "Hello from main!"
81
+ ... myscript(main_string = "Hello changed!!") # prints "Hello changed!!" and loads section result from first run
82
+ '''
83
+
84
+
85
+
86
+ def decorator(func) :
87
+ name = func.__name__
88
+
89
+ # Get globals around function definition
90
+ definition_globals = func.__globals__
91
+
92
+ @functools.wraps(func)
93
+ def wrapper(**overrides) -> None :
94
+
95
+ # Creates new globals
96
+ exec_globals = definition_globals.copy()
97
+ exec_globals.update(overrides)
98
+ _new = exec_globals.get("new", False)
99
+ _bulk = exec_globals.get("bulk", None)
100
+ _overnight = exec_globals.get("overnight", False)
101
+
102
+ # Creates new function
103
+ new_func = types.FunctionType(
104
+ func.__code__,
105
+ exec_globals,
106
+ name=name,
107
+ argdefs=func.__defaults__,
108
+ closure=func.__closure__,
109
+ )
110
+
111
+ # Getting paths
112
+ ipath = exec_globals.get('import_path', "None")
113
+ if ipath is None :
114
+ root = tk.Tk()
115
+ root.title("Select import path")
116
+ img = tk.PhotoImage(file=icon)
117
+ root.iconphoto(True, img)
118
+ root._icon_img = img # keep reference
119
+ root.withdraw()
120
+ root.update_idletasks()
121
+ root.attributes("-topmost", True)
122
+ root.update()
123
+ root.focus_force()
124
+ ipath = filedialog.askdirectory(title=f'Select import path for {name}')
125
+ root.destroy()
126
+ if not ipath :
127
+ print('Searching for import_path was cancelled', style='red')
128
+ raise ValueError('Searching for import_path was cancelled')
129
+ epath = exec_globals.get('export_path', "None")
130
+ if ipath != "None" :
131
+ ipath = Path(ipath)
132
+ if epath != "None" :
133
+ epath = ipath.parent if epath is None else Path(epath)
134
+
135
+ # Creating new export path
136
+ prefix = name.replace('.', '_')
137
+ if epath != "None" :
138
+ if _new :
139
+ base_path = folder(epath / (f'{prefix}_' + datetime.now().strftime("%Y-%m-%d-%Hh%Mmin%Ss")), warning=False)
140
+ else :
141
+ #Searching for newest old folder
142
+ base_folder = None
143
+ _date = None
144
+ for f in epath.iterdir() :
145
+ if (not f.is_dir()) or (not f.name.startswith(f'{prefix}_')) :
146
+ continue
147
+ date_str = f.name.split('_')[-1]
148
+ date = datetime.strptime(date_str, "%Y-%m-%d-%Hh%Mmin%Ss")
149
+ if _date is None or date > _date :
150
+ _date, base_folder = date, f
151
+ base_path = base_folder if base_folder is not None else epath / (f'{prefix}_' + datetime.now().strftime("%Y-%m-%d-%Hh%Mmin%Ss"))
152
+ epath = base_path / 'export_folder'
153
+ if not epath.exists():
154
+ os.makedirs(epath) #creates folders until end
155
+ if ipath != "None" :
156
+ ilink = base_path / 'import_folder'
157
+ if ilink.exists() or ilink.is_symlink():
158
+ ilink.unlink()
159
+ if os.name == "nt":
160
+ try :
161
+ ilink.symlink_to(ipath, ipath.is_dir())
162
+ except OSError :
163
+ print("Windows does not allow to create symlink, aborting. Consider using Windows in Developper mode.")
164
+ else:
165
+ ilink.symlink_to(ipath)
166
+ md_file = epath / (name+'_log.md')
167
+ html_file = epath / (name+'_log.html')
168
+ else :
169
+ md_file = None
170
+ html_file = None
171
+
172
+ # Defining bulk processing
173
+ if _bulk is None :
174
+ subfolders = {"" : ""}
175
+ else :
176
+ subfolders = _bulk(ipath)
177
+
178
+ #Begining prints
179
+ print_status = kwargsself(print)
180
+ print.console = None
181
+ print.file = md_file
182
+ print(f'\n\n\n# **BEGIN {name}**\n')
183
+ print(f"{time.ctime()}")
184
+ if ipath != "None" :
185
+ print(f'import_path : {ipath}\n')
186
+ if epath != "None" :
187
+ print(f'export_path : {epath}\n')
188
+
189
+ # Bulk processing
190
+ results = {} # {export_subfolder : fucntion result}
191
+ for import_subfolder, export_subfolder in subfolders.items() :
192
+ if ipath != "None" :
193
+ impath = ipath / import_subfolder
194
+ exec_globals["import_path"] = impath
195
+ if epath != "None" :
196
+ expath = epath / export_subfolder
197
+ exec_globals["export_path"] = expath
198
+
199
+ # Create export subfolder
200
+ if not expath.exists() :
201
+ os.mkdir(expath)
202
+
203
+ # Updating sections
204
+ wrapper.section.parent_path = epath
205
+ wrapper.section.path = expath
206
+ wrapper.section.new = _new
207
+
208
+ #Applying function
209
+ print("\n---\n")
210
+ subfolder_string = f"{export_subfolder}" if export_subfolder != "" else ""
211
+ print(f'## **Launched script {subfolder_string}**\n')
212
+ tic = time.perf_counter()
213
+ try :
214
+ results[export_subfolder] = new_func()
215
+
216
+ # Errors
217
+ except Exception as e :
218
+ toc = time.perf_counter()
219
+ print.error()
220
+ print(f'\n## **{subfolder_string} took {toc-tic:.2f}s**')
221
+ print("\n---\n")
222
+ if not _overnight :
223
+ raise e
224
+
225
+ # No error
226
+ else :
227
+ toc = time.perf_counter()
228
+ print(f'\n## **{subfolder_string} took {toc-tic:.2f}s**')
229
+ print("\n---\n")
230
+
231
+ # END
232
+ print(time.ctime())
233
+ print(f'# **END {name}**\n\n')
234
+ print.export_html(html_file)
235
+ selfkwargs(print, print_status)
236
+ if _bulk is None :
237
+ results = results[""]
238
+ return results
239
+
240
+ # Making sections
241
+ section = Section()
242
+ wrapper.section = section
243
+
244
+ return wrapper
245
+ return decorator
246
+
247
+
248
+
249
+ # %% Test function run
250
+ if __name__ == "__main__":
251
+ from corelp import test
252
+ test(__file__)
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Date : 2025-08-28
4
+ # Author : Lancelot PINCET
5
+ # GitHub : https://github.com/LancelotPincet
6
+ # Library : rootLP
7
+ # Module : main
8
+
9
+ """
10
+ This file allows to test main
11
+
12
+ main : This function can decorate the main function of a script.
13
+ """
14
+
15
+
16
+
17
+ # %% Libraries
18
+ from corelp import debug, main
19
+ from time import sleep
20
+ import pytest
21
+ debug_folder = debug(__file__)
22
+
23
+
24
+
25
+ # %% User inputs
26
+ search = False # True to apply manual search
27
+ global import_path
28
+ global export_path
29
+ import_path = None if search else debug_folder # Path to the imported data
30
+ export_path = import_path # Path to the exported data
31
+ new = False # True to create new result folder at each run
32
+ bulk = None # function(import_path) that returns a dictionnary of {import_subfolder:export_subfolder} for multiple decorated function run.
33
+ overnight= False # If True and exception occurs, will skip and pass to the next run in bulk processing.
34
+ myparam = "Hello from main!"
35
+ apply_error = False
36
+
37
+
38
+
39
+ @main()
40
+ def mainfunc() :
41
+ if apply_error :
42
+ 1/0
43
+ print(myparam)
44
+ result = section_1()
45
+ print(f"import_path = {import_path}")
46
+ return result
47
+
48
+ @mainfunc.section()
49
+ def section_1() :
50
+ print('> Hello from section!')
51
+ return True
52
+
53
+
54
+
55
+ # %% Function test
56
+ def test_function() :
57
+ '''
58
+ Test main function
59
+ '''
60
+ mainfunc()
61
+ sleep(2) # Ensure new folder
62
+ mainfunc(myparam="Hello changed!!")
63
+ sleep(2) # Ensure new folder
64
+ mainfunc(new=True)
65
+ sleep(2) # Ensure new folder
66
+ with pytest.raises(ZeroDivisionError, match="division by zero") :
67
+ mainfunc(apply_error=True)
68
+
69
+
70
+
71
+ # %% Test function run
72
+ if __name__ == "__main__":
73
+ from corelp import test
74
+ test(__file__)
File without changes
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Date : 2025-08-27
4
+ # Author : Lancelot PINCET
5
+ # GitHub : https://github.com/LancelotPincet
6
+ # Library : coreLP
7
+ # Module : print
8
+
9
+ """
10
+ This function overrides python built in print function to add functionnalities.
11
+ """
12
+
13
+
14
+
15
+ # %% Libraries
16
+
17
+
18
+
19
+ # %% Libraries
20
+ from corelp import prop
21
+ from dataclasses import dataclass, field
22
+ from datetime import datetime, timedelta
23
+ from numpy._core.numeric import True_
24
+ from rich import print as richprint
25
+ from rich.console import Console
26
+ from rich.theme import Theme
27
+ from rich.markdown import Markdown
28
+ from rich.traceback import Traceback
29
+ from rich.progress import (
30
+ Progress,
31
+ BarColumn,
32
+ TextColumn,
33
+ TaskProgressColumn,
34
+ TimeElapsedColumn,
35
+ TimeRemainingColumn,
36
+ ProgressColumn,
37
+ )
38
+ import traceback as tb_module
39
+ from time import perf_counter
40
+ from pathlib import Path
41
+ pyprint = print
42
+
43
+
44
+
45
+ # %% Class
46
+ @dataclass(slots=True, kw_only=True)
47
+ class Print() :
48
+ r"""
49
+ Enhanced replacement for the built-in :func:`print` function, adding muting,
50
+ logging, rich formatting, and progress utilities.
51
+
52
+ This class is callable and behaves like :func:`print`, with extra arguments.
53
+
54
+ Parameters
55
+ ----------
56
+ *strings : tuple
57
+ The objects to print. Its :meth:`__str__` representation is used.
58
+ verbose : bool, optional
59
+ If ``True`` (default), printing is performed.
60
+ If ``False``, printing is skipped unless overridden.
61
+ return_string : bool, optional
62
+ If ``True``, return the processed string instead of ``None``.
63
+ file : str or pathlib.Path or None, optional
64
+ If provided, overrides the configured log file.
65
+ mode : {"w", "a"}, optional
66
+ File mode used when writing logs. Default is ``"a"``.
67
+ end : str, optional
68
+ End-of-line character(s). Defaults to ``"\n"``.
69
+ **kwargs :
70
+ Additional keyword arguments passed to :func:`print` or Rich's :func:`Console.print`.
71
+
72
+ Examples
73
+ --------
74
+ Basic usage::
75
+
76
+ >>> from corelp import print
77
+ >>> s = "Hello *world*!\nThis is a print **example**"
78
+ >>> print(s)
79
+
80
+ Muting::
81
+
82
+ >>> print.verbose = False
83
+ >>> print(s) # muted
84
+ >>> print(s, verbose=True) # forced printing
85
+ >>> print.verbose = True
86
+ >>> print(s) # prints again
87
+ >>> print(s, verbose=False) # forced mute
88
+
89
+ Access to underlying print functions::
90
+
91
+ >>> print.pyprint(s) # built-in print
92
+ >>> print.richprint(s) # rich.print
93
+ >>> print.print(s) # Console.print
94
+ >>> print.log(s) # Console.log
95
+
96
+ Logging::
97
+
98
+ >>> print.file = "log.txt"
99
+ >>> print("Hello") # also writes to file
100
+
101
+ Console styling::
102
+
103
+ >>> print.theme = {"success": "green"}
104
+ >>> print("Done!", style="success")
105
+ >>> try:
106
+ ... 1/0
107
+ ... except Exception:
108
+ ... print.error()
109
+ >>> print.export_html("log.html")
110
+
111
+ Progress / Clock::
112
+
113
+ >>> from time import sleep
114
+ >>> for i in print.clock(15, "Outer"):
115
+ ... for j in print.clock(10, "Inner"):
116
+ ... sleep(1)
117
+
118
+ Attributes
119
+ ----------
120
+ verbose : bool
121
+ Global muting switch.
122
+ pyprint : callable
123
+ Built-in Python :func:`print`.
124
+ richprint : callable
125
+ :mod:`rich` print function.
126
+ console : rich.console.Console
127
+ The Rich console instance used for styled printing.
128
+ file : pathlib.Path or None
129
+ Path to the log file.
130
+ progress : rich.progress.Progress
131
+ Active Rich progress manager.
132
+ bars : dict
133
+ Dictionary storing active progress bars.
134
+ theme : dict
135
+ Custom Rich style definitions.
136
+ """
137
+
138
+ # Main function
139
+ def __call__(self, *strings, verbose=None, do_stdout=True, do_file=True, return_string=False, file=None, mode='a', end='\n', **kwargs) :
140
+
141
+ # Muting
142
+ verbose = verbose if verbose is not None else self.verbose
143
+ if not verbose :
144
+ return None
145
+
146
+ # Formatting string
147
+ string = ", ".join([str(string) for string in strings]) + end
148
+
149
+ # Printing markdown
150
+ if do_stdout :
151
+ string2print = Markdown(string) if self.apply_markdown else string
152
+ self.print(string2print, **kwargs)
153
+
154
+ # Writting to file
155
+ if do_file :
156
+ file = file if file is not None else self.file
157
+ if file is not None :
158
+ with open(Path(file), mode) as file :
159
+ file.write(string)
160
+
161
+ # Return
162
+ if return_string :
163
+ return string
164
+
165
+
166
+ # MUTING
167
+ verbose : bool = True # True to print
168
+
169
+
170
+
171
+ # PRINT
172
+
173
+ @property
174
+ def print(self) :
175
+ return self.console.print
176
+ @property
177
+ def log(self) :
178
+ return self.console.log
179
+ pyprint = pyprint # python print
180
+ richprint = richprint # rich prints
181
+ apply_markdown : bool = field(default=True, repr=False) # True to apply rich markdown formatting in prints
182
+
183
+
184
+
185
+ # LOGGING
186
+
187
+ _file : Path = None
188
+ @property
189
+ def file(self) :
190
+ return self._file
191
+ @file.setter
192
+ def file(self, value) :
193
+ self._file = Path(value)
194
+
195
+
196
+
197
+ # CONSOLE
198
+
199
+ _theme = {}
200
+ @property
201
+ def theme(self) :
202
+ return self._theme
203
+ @theme.setter
204
+ def theme(self, value) :
205
+ self._theme.update(value)
206
+ self._console = None
207
+
208
+ _console : Console = field(default=None, repr=False)
209
+ @prop(cache=True)
210
+ def console(self) :
211
+ theme = Theme(self.theme)
212
+ return Console(theme=theme, record=True)
213
+
214
+ def error(self) :
215
+ rich_tb = Traceback.from_exception(*tb_module.sys.exc_info())
216
+ self.console.print(rich_tb)
217
+
218
+ def print_locals(self) :
219
+ self.console.log(log_locals=True)
220
+
221
+ def export_html(self, path) :
222
+ if path is None :
223
+ return
224
+ path = Path(path)
225
+ html_content = self.console.export_html(inline_styles=True)
226
+ with open(path, "w", encoding="utf-8") as file:
227
+ file.write(html_content)
228
+
229
+
230
+
231
+ # CLOCK
232
+
233
+ def clock(self, iterable, title="Working...") :
234
+
235
+ # Get iterable
236
+ iterable = range(iterable) if isinstance(iterable, int) else iterable
237
+ iterable = list(iterable)
238
+
239
+ # Detect if progressbar already exists
240
+ first_bar = getattr(self, "_progress", None) is None
241
+ progress = self.progress
242
+ bars = self.bars
243
+
244
+ # Opens progress
245
+ if first_bar :
246
+ verbose = self.verbose
247
+ self.verbose = False
248
+
249
+ # Write to file
250
+ if self.file is not None :
251
+ with open(Path(self.file), "a") as file :
252
+ file.write(f'{title}...\n')
253
+ progress.start()
254
+
255
+ # Create new task
256
+ task = bars.get(title, None)
257
+ if task is None : # No bar with this name exists
258
+ task = progress.add_task(title, total=len(iterable), avg_time=0.0)
259
+ bars[title] = task # store it
260
+ else :
261
+ progress.reset(task)
262
+
263
+ # Loop
264
+ loop_counter = 0
265
+ start = perf_counter()
266
+ for item in iterable :
267
+ yield item
268
+ loop_counter += 1
269
+ elapsed = perf_counter() - start
270
+ avg_time = elapsed / loop_counter if loop_counter else 0
271
+ progress.update(task, advance=1, avg_time=avg_time)
272
+
273
+ # Clean up
274
+ if first_bar :
275
+ progress.stop()
276
+ del(self.bars)
277
+ del(self.progress)
278
+ self.verbose = verbose
279
+
280
+ _progress : Progress = field(default=None, repr=False)
281
+ @prop(cache=True)
282
+ def progress(self) :
283
+ return Progress(
284
+ TextColumn("{task.description}"),
285
+ BarColumn(),
286
+ TaskProgressColumn(),
287
+ TextColumn("[magenta]/{task.total}[/]"),
288
+ TimeElapsedColumn(),
289
+ AvgLoopTimeColumn(),
290
+ TimeRemainingColumn(),
291
+ EndTimeColumn(),
292
+ transient=False,
293
+ console=self.console
294
+ )
295
+
296
+ _bars : dict = field(default=None, repr=False)
297
+ @prop(cache=True)
298
+ def bars(self) :
299
+ return {}
300
+
301
+
302
+
303
+ # Get instance
304
+ print = Print() # Instance to use everywhere
305
+
306
+ # Custom Progress bar columns
307
+ class AvgLoopTimeColumn(ProgressColumn):
308
+ def render(self, task):
309
+ avg_time = task.fields.get("avg_time", None)
310
+ if avg_time is not None and task.completed > 0:
311
+ string = f"[yellow]↻ {avg_time:.2f}s[/]" if avg_time > 1 else f"[yellow]↻ {avg_time*1000:.2f}ms[/]"
312
+ return string
313
+ return ""
314
+ class EndTimeColumn(ProgressColumn):
315
+ def render(self, task):
316
+ if task.time_remaining is not None:
317
+ end_time = datetime.now() + timedelta(seconds=task.time_remaining)
318
+ return f"[cyan]{end_time:%m-%d %H:%M:%S}[/] "
319
+ return ""
320
+
321
+
322
+
323
+ # %% Test function run
324
+ if __name__ == "__main__":
325
+ from corelp import test
326
+ test(__file__)
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Date : 2025-08-27
4
+ # Author : Lancelot PINCET
5
+ # GitHub : https://github.com/LancelotPincet
6
+ # Library : coreLP
7
+ # Module : print
8
+
9
+ """
10
+ This file allows to test print
11
+
12
+ print : This function overrides python built in print function to add functionnalities.
13
+ """
14
+
15
+
16
+
17
+ # %% Libraries
18
+ from rootlp import print
19
+ from corelp import debug
20
+ import pytest
21
+ from time import sleep
22
+ debug_folder = debug(__file__)
23
+
24
+
25
+ # %% test prints
26
+ def test_print() :
27
+ '''
28
+ Test print
29
+ '''
30
+ string = "# TEST\nHello *world*!\n\nThis is 1 print **example**"
31
+ print(string, style="magenta")
32
+ print.print(string, style="magenta")
33
+ print.log(string, style="magenta")
34
+ print.pyprint(string)
35
+ print.richprint(string)
36
+
37
+
38
+
39
+ # %% test verbose
40
+ def test_verbose() :
41
+ '''
42
+ Test verbose
43
+ '''
44
+ print.verbose = False # Muting
45
+ print("Should not print") # Does not print
46
+ print("Should print", verbose=True) # Does print
47
+ print("Should not print") # Does not print
48
+ print.verbose = True # Unmuting
49
+ print("Should print") # Does print
50
+ print("Should not print", verbose=False) # Does not print
51
+ print("Should print") # Does print
52
+
53
+
54
+
55
+ # %% test logging
56
+ def test_logging() :
57
+ '''
58
+ Test logging
59
+ '''
60
+ print.theme = {"success" : "green"}
61
+ string = "# TEST\nHello *world*!\n\nThis is 1 print **example**"
62
+ print(string, style="success")
63
+ print.print_locals()
64
+ try :
65
+ 1/0
66
+ except Exception :
67
+ print.error()
68
+ file = debug_folder / "log.html"
69
+ print.export_html(file)
70
+
71
+
72
+
73
+ # %% test console
74
+ def test_console() :
75
+ '''
76
+ Test console
77
+ '''
78
+ file = debug_folder / 'log.md'
79
+ print.file = file
80
+ string = "# TEST\nHello *world*!\n\nThis is 1 print **example**"
81
+ print(string, style="magenta")
82
+ assert file.exists()
83
+
84
+
85
+
86
+ # %% test clock
87
+ def test_clock() :
88
+ '''
89
+ Test clock
90
+ '''
91
+ for i in print.clock(5, "Outer loop") :
92
+ print("Should not print")
93
+ for j in print.clock(5, "Inner loop") :
94
+ sleep(1)
95
+ print("Should not print")
96
+
97
+ for i in print.clock(10, "Other loop") :
98
+ sleep(1)
99
+ print("Should not print")
100
+
101
+
102
+
103
+
104
+
105
+
106
+ # %% Test function run
107
+ if __name__ == "__main__":
108
+ from corelp import test
109
+ test(__file__)
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Date : 2025-11-30
4
+ # Author : Lancelot PINCET
5
+ # GitHub : https://github.com/LancelotPincet
6
+ # Library : rootLP
7
+ # Module : user_inputs
8
+
9
+ """
10
+ This file allows to test user_inputs
11
+
12
+ user_inputs : Gets last user inputs dictionnary from global variables.
13
+ """
14
+
15
+
16
+
17
+ # %% Libraries
18
+ from corelp import debug
19
+ import pytest
20
+ from corelp import user_inputs
21
+ debug_folder = debug(__file__)
22
+
23
+
24
+
25
+ # %% Function test
26
+ def test_user_inputs() :
27
+ '''
28
+ Test user_inputs function
29
+ '''
30
+ user_inputs() #init
31
+ a = 1
32
+ inputs = user_inputs()
33
+ if inputs != {'a': 1} :
34
+ raise ValueError(f'{inputs} should be dict(a=1)')
35
+ user_inputs() #init
36
+ b = 2
37
+ inputs = user_inputs()
38
+ if inputs != {'b': 2} :
39
+ raise ValueError(f'{inputs} should be dict(b=2)')
40
+
41
+
42
+
43
+ # %% Test function run
44
+ if __name__ == "__main__":
45
+ from corelp import test
46
+ test(__file__)
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Date : 2025-11-30
4
+ # Author : Lancelot PINCET
5
+ # GitHub : https://github.com/LancelotPincet
6
+ # Library : rootLP
7
+ # Module : user_inputs
8
+
9
+ """
10
+ Gets last user inputs dictionnary from global variables.
11
+ """
12
+
13
+
14
+
15
+ # %% Libraries
16
+ import inspect
17
+
18
+
19
+
20
+ # %% Function
21
+
22
+ def user_inputs(reset=False) :
23
+ r"""
24
+ Return a dictionary of variables defined by the user in the interactive environment.
25
+
26
+ Parameters
27
+ ----------
28
+ reset : bool or str
29
+ True to set as first call, String value to define a group of parameters
30
+
31
+ Returns
32
+ -------
33
+ dict
34
+ A dictionary containing the user's currently defined variables.
35
+
36
+ Examples
37
+ --------
38
+ >>> from corelp import user_inputs
39
+ >>> user_inputs(True) # First call (initializes and clears import-related variables)
40
+ None
41
+ >>> a = 1 # User defines a variable
42
+ >>> user_inputs() # Now returns: {'a': 1}
43
+ {'a': 1}
44
+ """
45
+ frame = inspect.currentframe().f_back
46
+ ns = {**frame.f_globals, **frame.f_locals}
47
+
48
+ # ---- Filter user variables (ignore internals starting with "_") ----
49
+ ns = {key: value for key, value in ns.items() if not key.startswith("_")}
50
+
51
+ # Validate status
52
+ if reset :
53
+ user_inputs.current_group = reset if isinstance(reset, str) else None
54
+ user_inputs.cache = None
55
+
56
+ # Case when user_inputs is on top : cache = None
57
+ if user_inputs.cache is None :
58
+ user_inputs.cache = ns
59
+ return
60
+
61
+ # Case when user_inputs is at bottom : cache = dict
62
+ else :
63
+ updated = { key: value for key, value in ns.items() if key not in user_inputs.cache or user_inputs.cache[key] is not value}
64
+ values = {key: value for key, value in updated.items() if not key.endswith('_')}
65
+ comments = {key: value for key, value in updated.items() if key.endswith('_')}
66
+
67
+ # Group values
68
+ if user_inputs.current_group is not None :
69
+ user_inputs.groups_values[user_inputs.current_group] = values
70
+ user_inputs.groups_comments[user_inputs.current_group] = comments
71
+
72
+ # End
73
+ user_inputs.current_group = None
74
+ user_inputs.cache = None
75
+ return values
76
+
77
+ user_inputs.cache = None
78
+ user_inputs.current_group = None
79
+ user_inputs.groups_values = {}
80
+ user_inputs.groups_comments = {}
81
+
82
+
83
+
84
+ # %% Test function run
85
+ if __name__ == "__main__":
86
+ from corelp import test
87
+ test(__file__)
@@ -0,0 +1,26 @@
1
+ {
2
+ "Section": {
3
+ "date": "2025-08-28",
4
+ "description": "This class defines decorator instances allowing to create section functions.",
5
+ "module": "modules/Section_LP/Section",
6
+ "object": "Section"
7
+ },
8
+ "main": {
9
+ "date": "2025-08-28",
10
+ "description": "This function can decorate the main function of a script.",
11
+ "module": "modules/main_LP/main",
12
+ "object": "main"
13
+ },
14
+ "print": {
15
+ "date": "2025-08-27",
16
+ "description": "This function overrides python built in print function to add functionnalities.",
17
+ "module": "modules/print_LP/print",
18
+ "object": "print"
19
+ },
20
+ "user_inputs": {
21
+ "date": "2025-11-30",
22
+ "description": "Gets last user inputs dictionnary from global variables.",
23
+ "module": "modules/user_inputs_LP/user_inputs",
24
+ "object": "user_inputs"
25
+ }
26
+ }
File without changes
File without changes
@@ -0,0 +1 @@
1
+ {}