python-backpack 2.0.2__tar.gz → 2.0.4__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.
Files changed (51) hide show
  1. {python_backpack-2.0.2 → python_backpack-2.0.4}/.github/workflows/codecov_tests.yml +17 -8
  2. {python_backpack-2.0.2 → python_backpack-2.0.4}/.gitignore +1 -0
  3. {python_backpack-2.0.2 → python_backpack-2.0.4}/PKG-INFO +32 -19
  4. {python_backpack-2.0.2 → python_backpack-2.0.4}/README.md +15 -15
  5. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/cache.py +28 -6
  6. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/file_utils.py +52 -39
  7. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/folder_utils.py +17 -7
  8. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/json_metadata.py +5 -2
  9. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/json_user_settings.py +5 -5
  10. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/json_utils.py +7 -7
  11. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/strings.py +9 -15
  12. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/test_utils.py +10 -4
  13. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/version.py +6 -4
  14. python_backpack-2.0.4/pyproject.toml +52 -0
  15. {python_backpack-2.0.2 → python_backpack-2.0.4}/ruff.toml +4 -2
  16. python_backpack-2.0.4/tests/test_file_utils.py +109 -0
  17. python_backpack-2.0.4/tests/test_folder_utils.py +107 -0
  18. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_json_user_settings.py +29 -44
  19. python_backpack-2.0.4/tests/test_json_utils.py +35 -0
  20. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_jsonmd.py +29 -29
  21. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_misc.py +15 -7
  22. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_strings.py +10 -7
  23. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_test_utils.py +12 -14
  24. python_backpack-2.0.4/uv.lock +312 -0
  25. python_backpack-2.0.2/.vscode/settings.json +0 -9
  26. python_backpack-2.0.2/pyproject.toml +0 -30
  27. python_backpack-2.0.2/tests/test_file_utils.py +0 -71
  28. python_backpack-2.0.2/tests/test_folder_utils.py +0 -105
  29. python_backpack-2.0.2/tests/test_json_utils.py +0 -46
  30. python_backpack-2.0.2/uv.lock +0 -295
  31. {python_backpack-2.0.2 → python_backpack-2.0.4}/LICENSE +0 -0
  32. {python_backpack-2.0.2 → python_backpack-2.0.4}/__init__.py +0 -0
  33. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/__init__.py +0 -0
  34. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/custom_errors.py +0 -0
  35. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/logger.py +0 -0
  36. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack/patterns.py +0 -0
  37. {python_backpack-2.0.2 → python_backpack-2.0.4}/backpack.code-workspace +0 -0
  38. {python_backpack-2.0.2 → python_backpack-2.0.4}/codecov.yml +0 -0
  39. {python_backpack-2.0.2 → python_backpack-2.0.4}/coverage.bat +0 -0
  40. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/__init__.py +0 -0
  41. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_cache.py +0 -0
  42. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_errors.py +0 -0
  43. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_files/edited.txt +0 -0
  44. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_files/edited_remove.txt +0 -0
  45. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_files/locked_file.txt +0 -0
  46. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_files/origin.txt +0 -0
  47. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_files/origin_remove.txt +0 -0
  48. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_files/unlocked_file.txt +0 -0
  49. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_json/data.json +0 -0
  50. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_json/data_broken.json +0 -0
  51. {python_backpack-2.0.2 → python_backpack-2.0.4}/tests/test_json/save/data_saved.json +0 -0
@@ -9,7 +9,7 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  strategy:
11
11
  matrix:
12
- python-version: ['3.11']
12
+ python-version: ['3.11', '3.12', '3.13', '3.14']
13
13
 
14
14
  steps:
15
15
  - uses: actions/checkout@v6
@@ -19,20 +19,29 @@ jobs:
19
19
  with:
20
20
  python-version: ${{ matrix.python-version }}
21
21
 
22
- - name: Add path to PYTHONPATH
23
- run: |
24
- echo "PYTHONPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV
25
-
26
22
  - name: Install dependencies
27
23
  run: |
28
24
  python -m pip install --upgrade pip
29
25
  pip install uv
30
26
  uv sync --dev
31
27
 
32
- - name: Run tests with coverage
28
+ - name: Run Ruff
29
+ if: ${{ matrix.python-version == '3.11' }}
30
+ run: uv run ruff check .
31
+
32
+ - name: Run Pyright
33
+ if: ${{ matrix.python-version == '3.11' }}
34
+ run: uv run pyright backpack tests
35
+
36
+ - name: Run tests
37
+ if: ${{ matrix.python-version != '3.11' }}
38
+ run: uv run pytest
39
+
40
+ - name: Run tests with branch coverage
41
+ if: ${{ matrix.python-version == '3.11' }}
33
42
  run: |
34
- uv run coverage run --source=backpack -m pytest
35
- uv run coverage report -m --fail-under=70
43
+ uv run coverage run --branch --source=backpack -m pytest
44
+ uv run coverage report -m --fail-under=99
36
45
  uv run coverage xml
37
46
 
38
47
  - name: Upload coverage.xml
@@ -129,3 +129,4 @@ dmypy.json
129
129
 
130
130
  # test generated files
131
131
  tests/test_json/MD_test.json
132
+ .vscode/settings.json
@@ -1,9 +1,22 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: python-backpack
3
- Version: 2.0.2
4
- Summary: A collection of personal scripts for json, File/Folder Operations, String Validation, Custom Errors, Cache and stuff.
5
- Author: Maximiliano Rocamora
3
+ Version: 2.0.4
4
+ Summary: Lightweight utilities for JSON, filesystem operations, strings, caching, and scripting.
5
+ Project-URL: Homepage, https://github.com/MaxRocamora/python-backpack
6
+ Project-URL: Repository, https://github.com/MaxRocamora/python-backpack.git
7
+ Author-email: Maximiliano Rocamora <maxirocamora@gmail.com>
8
+ License-Expression: GPL-3.0-only
6
9
  License-File: LICENSE
10
+ Keywords: cache,filesystem,json,strings,utilities
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Utilities
7
20
  Requires-Python: >=3.11
8
21
  Description-Content-Type: text/markdown
9
22
 
@@ -52,10 +65,10 @@ pip install python-backpack
52
65
 
53
66
  ### File Utils (`backpack.file_utils`)
54
67
 
55
- - `replace_strings_in_file(ascii_file: str, strings: list, new_string: str) -> None`
68
+ - `replace_strings_in_file(ascii_file: str, strings: Sequence[str], new_string: str) -> None`
56
69
  - Replaces multiple string occurrences in a text file.
57
- - `remove_line_from_file(ascii_file: str, strings: list, verbose: bool = False) -> None`
58
- - Removes exact matching lines from a text file.
70
+ - `remove_line_from_file(ascii_file: str, strings: Sequence[str], verbose: bool = False) -> None`
71
+ - Removes every exact matching line while preserving retained line endings.
59
72
  - `file_is_writeable(filepath: str) -> bool`
60
73
  - Checks whether a file can be opened for read/write.
61
74
  - `get_version_from_filename(filename: str) -> str`
@@ -63,23 +76,23 @@ pip install python-backpack
63
76
 
64
77
  ### Folder Utils (`backpack.folder_utils`)
65
78
 
66
- - `browse_folder(folder: str) -> bool`
67
- - Opens a folder in Windows Explorer.
68
- - `create_folders(folders: list, force_empty: bool = False, verbose: bool = False)`
79
+ - `browse_folder(folder: str | None) -> bool`
80
+ - Opens a valid folder in Windows Explorer and returns `False` if it cannot be opened.
81
+ - `create_folders(folders: Sequence[str], force_empty: bool = False, verbose: bool = False) -> None`
69
82
  - Creates multiple folders.
70
- - `create_folder(path: str, force_empty: bool = False, verbose: bool = True)`
83
+ - `create_folder(path: str, force_empty: bool = False, verbose: bool = True) -> bool`
71
84
  - Creates a folder and optionally clears it if it already exists.
72
- - `remove_files_in_dir(path: str)`
85
+ - `remove_files_in_dir(path: str) -> None`
73
86
  - Removes all files and subdirectories inside a directory.
74
- - `recursive_dir_copy(source_path: str, target_path: str)`
87
+ - `recursive_dir_copy(source_path: str, target_path: str) -> None`
75
88
  - Recursively copies files and subfolders from source to target.
76
89
 
77
90
  ### JSON Utils (`backpack.json_utils`)
78
91
 
79
92
  - `json_load(json_file: str) -> dict`
80
93
  - Loads JSON data from file with validation and error handling.
81
- - `json_save(data: dict, json_file: str) -> bool`
82
- - Saves a dictionary to JSON file.
94
+ - `json_save(data: object, json_file: str) -> bool`
95
+ - Saves a JSON-serializable value as UTF-8 and returns whether it succeeded.
83
96
 
84
97
  ### JSON Metadata (`backpack.json_metadata`)
85
98
 
@@ -99,7 +112,7 @@ pip install python-backpack
99
112
  - `JsonUserSettings(folder: str, name: str)`
100
113
  - Saves and loads JSON settings in the current user's home directory.
101
114
  - Main public methods:
102
- - `save_settings(data: dict | None = None) -> bool | None`
115
+ - `save_settings(data: dict | None = None) -> bool`
103
116
  - `load_settings() -> dict | bool`
104
117
 
105
118
  ### Logger (`backpack.logger`)
@@ -130,8 +143,8 @@ pip install python-backpack
130
143
 
131
144
  - `random_string(length: int = 10) -> str`
132
145
  - Generates a random lowercase string.
133
- - `time_function_decorator(method: type)`
134
- - Decorator that logs execution time.
146
+ - `time_function_decorator(method: Callable[P, R]) -> Callable[P, R]`
147
+ - Decorator that logs execution time while preserving function metadata and return values.
135
148
 
136
149
  ## Quick Example
137
150
 
@@ -143,7 +156,7 @@ from backpack.json_utils import json_save, json_load
143
156
 
144
157
  @timed_lru_cache(seconds=60)
145
158
  def expensive_call():
146
- return {'ok': True}
159
+ return {'ok': True}
147
160
 
148
161
 
149
162
  value = camelcase_to_snakecase('HTTPServer')
@@ -43,10 +43,10 @@ pip install python-backpack
43
43
 
44
44
  ### File Utils (`backpack.file_utils`)
45
45
 
46
- - `replace_strings_in_file(ascii_file: str, strings: list, new_string: str) -> None`
46
+ - `replace_strings_in_file(ascii_file: str, strings: Sequence[str], new_string: str) -> None`
47
47
  - Replaces multiple string occurrences in a text file.
48
- - `remove_line_from_file(ascii_file: str, strings: list, verbose: bool = False) -> None`
49
- - Removes exact matching lines from a text file.
48
+ - `remove_line_from_file(ascii_file: str, strings: Sequence[str], verbose: bool = False) -> None`
49
+ - Removes every exact matching line while preserving retained line endings.
50
50
  - `file_is_writeable(filepath: str) -> bool`
51
51
  - Checks whether a file can be opened for read/write.
52
52
  - `get_version_from_filename(filename: str) -> str`
@@ -54,23 +54,23 @@ pip install python-backpack
54
54
 
55
55
  ### Folder Utils (`backpack.folder_utils`)
56
56
 
57
- - `browse_folder(folder: str) -> bool`
58
- - Opens a folder in Windows Explorer.
59
- - `create_folders(folders: list, force_empty: bool = False, verbose: bool = False)`
57
+ - `browse_folder(folder: str | None) -> bool`
58
+ - Opens a valid folder in Windows Explorer and returns `False` if it cannot be opened.
59
+ - `create_folders(folders: Sequence[str], force_empty: bool = False, verbose: bool = False) -> None`
60
60
  - Creates multiple folders.
61
- - `create_folder(path: str, force_empty: bool = False, verbose: bool = True)`
61
+ - `create_folder(path: str, force_empty: bool = False, verbose: bool = True) -> bool`
62
62
  - Creates a folder and optionally clears it if it already exists.
63
- - `remove_files_in_dir(path: str)`
63
+ - `remove_files_in_dir(path: str) -> None`
64
64
  - Removes all files and subdirectories inside a directory.
65
- - `recursive_dir_copy(source_path: str, target_path: str)`
65
+ - `recursive_dir_copy(source_path: str, target_path: str) -> None`
66
66
  - Recursively copies files and subfolders from source to target.
67
67
 
68
68
  ### JSON Utils (`backpack.json_utils`)
69
69
 
70
70
  - `json_load(json_file: str) -> dict`
71
71
  - Loads JSON data from file with validation and error handling.
72
- - `json_save(data: dict, json_file: str) -> bool`
73
- - Saves a dictionary to JSON file.
72
+ - `json_save(data: object, json_file: str) -> bool`
73
+ - Saves a JSON-serializable value as UTF-8 and returns whether it succeeded.
74
74
 
75
75
  ### JSON Metadata (`backpack.json_metadata`)
76
76
 
@@ -90,7 +90,7 @@ pip install python-backpack
90
90
  - `JsonUserSettings(folder: str, name: str)`
91
91
  - Saves and loads JSON settings in the current user's home directory.
92
92
  - Main public methods:
93
- - `save_settings(data: dict | None = None) -> bool | None`
93
+ - `save_settings(data: dict | None = None) -> bool`
94
94
  - `load_settings() -> dict | bool`
95
95
 
96
96
  ### Logger (`backpack.logger`)
@@ -121,8 +121,8 @@ pip install python-backpack
121
121
 
122
122
  - `random_string(length: int = 10) -> str`
123
123
  - Generates a random lowercase string.
124
- - `time_function_decorator(method: type)`
125
- - Decorator that logs execution time.
124
+ - `time_function_decorator(method: Callable[P, R]) -> Callable[P, R]`
125
+ - Decorator that logs execution time while preserving function metadata and return values.
126
126
 
127
127
  ## Quick Example
128
128
 
@@ -134,7 +134,7 @@ from backpack.json_utils import json_save, json_load
134
134
 
135
135
  @timed_lru_cache(seconds=60)
136
136
  def expensive_call():
137
- return {'ok': True}
137
+ return {'ok': True}
138
138
 
139
139
 
140
140
  value = camelcase_to_snakecase('HTTPServer')
@@ -5,16 +5,33 @@
5
5
  # ----------------------------------------------------------------------------------------
6
6
  from datetime import datetime, timedelta, timezone
7
7
  from functools import lru_cache, wraps
8
- from typing import Any, Callable, TypeVar, cast
8
+ from typing import Any, Callable, Protocol, TypeVar, cast
9
9
 
10
10
  from backpack.logger import get_logger
11
11
 
12
12
  log = get_logger('Python Backpack - Cache')
13
13
 
14
- F = TypeVar('F', bound=Callable[..., Any])
14
+ R = TypeVar('R', covariant=True)
15
15
 
16
16
 
17
- def timed_lru_cache(seconds: int, maxsize: int = 128) -> Callable[[F], F]:
17
+ class TimedCachedCallable(Protocol[R]):
18
+ """Callable returned by timed_lru_cache with cache control kwargs."""
19
+
20
+ def __call__(
21
+ self,
22
+ *args: Any,
23
+ force_clear: bool = False,
24
+ show_log: bool = False,
25
+ **kwargs: Any,
26
+ ) -> R:
27
+ """Call the cached function, optionally forcing a cache clear first."""
28
+ ...
29
+
30
+
31
+ def timed_lru_cache(
32
+ seconds: int,
33
+ maxsize: int = 128,
34
+ ) -> Callable[[Callable[..., R]], TimedCachedCallable[R]]:
18
35
  """Lru_cache with expiration time.
19
36
 
20
37
  Args:
@@ -39,13 +56,18 @@ def timed_lru_cache(seconds: int, maxsize: int = 128) -> Callable[[F], F]:
39
56
 
40
57
  """
41
58
 
42
- def wrapper_cache(func: F) -> F:
59
+ def wrapper_cache(func: Callable[..., R]) -> TimedCachedCallable[R]:
43
60
  cached_func = cast(Any, lru_cache(maxsize=maxsize)(func))
44
61
  cached_func.lifetime = timedelta(seconds=seconds)
45
62
  cached_func.expiration = datetime.now(timezone.utc) + cached_func.lifetime
46
63
 
47
64
  @wraps(func)
48
- def wrapped_func(*args, force_clear: bool = False, show_log: bool = False, **kwargs):
65
+ def wrapped_func(
66
+ *args: Any,
67
+ force_clear: bool = False,
68
+ show_log: bool = False,
69
+ **kwargs: Any,
70
+ ) -> R:
49
71
  """Wrapper function for lru_cache with expiration time.
50
72
 
51
73
  Args:
@@ -66,6 +88,6 @@ def timed_lru_cache(seconds: int, maxsize: int = 128) -> Callable[[F], F]:
66
88
 
67
89
  return cached_func(*args, **kwargs)
68
90
 
69
- return cast(F, wrapped_func)
91
+ return cast(TimedCachedCallable[R], wrapped_func)
70
92
 
71
93
  return wrapper_cache
@@ -4,14 +4,19 @@
4
4
  # https://github.com/MaxRocamora/python-backpack
5
5
  # ----------------------------------------------------------------------------------------
6
6
 
7
- import contextlib
7
+ import re
8
+ from collections.abc import Sequence
8
9
 
9
10
  from backpack.logger import get_logger
10
11
 
11
12
  log = get_logger('Python Backpack - FileUtils')
12
13
 
13
14
 
14
- def replace_strings_in_file(ascii_file: str, strings: list, new_string: str) -> None:
15
+ def replace_strings_in_file(
16
+ ascii_file: str,
17
+ strings: Sequence[str],
18
+ new_string: str,
19
+ ) -> None:
15
20
  """Opens ascii file and replaces all occurrences from strings into new_string.
16
21
 
17
22
  In this class we use a full path to avoid use of os.dirname, which
@@ -28,7 +33,7 @@ def replace_strings_in_file(ascii_file: str, strings: list, new_string: str) ->
28
33
 
29
34
  log.info(f'Replacing Strings, Opening File: {ascii_file}')
30
35
 
31
- with open(ascii_file) as f:
36
+ with open(ascii_file, newline='') as f:
32
37
  file_data = f.read()
33
38
  for i in strings:
34
39
  log.info('Finding: %s', i)
@@ -36,13 +41,16 @@ def replace_strings_in_file(ascii_file: str, strings: list, new_string: str) ->
36
41
  log.info('-' * 50)
37
42
  file_data = file_data.replace(i, new_string)
38
43
 
39
- with open(ascii_file, 'w') as f:
44
+ with open(ascii_file, 'w', newline='') as f:
40
45
  f.write(file_data)
41
- f.close()
42
46
  log.info(f'Closing File: {ascii_file}')
43
47
 
44
48
 
45
- def remove_line_from_file(ascii_file: str, strings: list, verbose: bool = False) -> None:
49
+ def remove_line_from_file(
50
+ ascii_file: str,
51
+ strings: Sequence[str],
52
+ verbose: bool = False,
53
+ ) -> None:
46
54
  """Removes given lines from ascii file.
47
55
 
48
56
  Args:
@@ -51,21 +59,22 @@ def remove_line_from_file(ascii_file: str, strings: list, verbose: bool = False)
51
59
  verbose: (bool) if true, prints removed lines
52
60
  """
53
61
 
54
- with open(ascii_file) as f:
55
- file_content = f.read().splitlines()
62
+ retained_lines = []
63
+ with open(ascii_file, newline='') as f:
64
+ file_content = f.readlines()
56
65
 
57
66
  for line in file_content:
67
+ value = line.rstrip('\r\n')
58
68
  if verbose:
59
- log.info(f'Checking line: {line}')
60
- with contextlib.suppress(ValueError):
61
- if line in strings:
62
- if verbose:
63
- log.info(f'Removing line: {line}')
64
- file_content.pop(file_content.index(line))
69
+ log.info(f'Checking line: {value}')
70
+ if value in strings:
71
+ if verbose:
72
+ log.info(f'Removing line: {value}')
73
+ else:
74
+ retained_lines.append(line)
65
75
 
66
- with open(ascii_file, 'w') as f:
67
- contents = '\n'.join(file_content)
68
- f.write(contents)
76
+ with open(ascii_file, 'w', newline='') as f:
77
+ f.writelines(retained_lines)
69
78
 
70
79
 
71
80
  def file_is_writeable(filepath: str) -> bool:
@@ -86,11 +95,12 @@ def file_is_writeable(filepath: str) -> bool:
86
95
  return False
87
96
 
88
97
 
89
- def get_version_from_filename(filename: str) -> str:
98
+ def get_version_from_filename(filename: str, separator: str | None = None) -> str:
90
99
  """Extracts version from filename.
91
100
 
92
101
  Args:
93
102
  filename: (str) file name to extract version from
103
+ separator: delimiter that must precede the version; when omitted, use supported delimiters
94
104
  Returns:
95
105
  str : version extracted from filename
96
106
 
@@ -99,29 +109,32 @@ def get_version_from_filename(filename: str) -> str:
99
109
  get_version_from_filename('myfile-9.txt') -> '9'
100
110
  get_version_from_filename('myfile.130.txt') -> '130'
101
111
  get_version_from_filename('myfile_v1002.txt') -> '1002'
112
+ get_version_from_filename('XD_shot_0000.0003.ma') -> '0003'
113
+ get_version_from_filename('ANM_shot_0010.0003.ma') -> '0003'
102
114
 
103
115
  """
104
116
 
105
117
  filename_no_ext = filename.rsplit('.', 1)[0]
106
118
 
107
- # guess is the separator is an underscore, dash, dot or v, and the version is the last part before the extension
108
- separators = ['_', '-', '.', 'v']
109
- if not any(sep in filename_no_ext for sep in separators):
110
- log.info(f'No separator found in filename: {filename_no_ext}. Using fallback extraction.')
111
- else:
112
- for sep in separators:
113
- if sep in filename_no_ext:
114
- parts = filename_no_ext.split(sep)
115
- version_part = parts[-1].split('.')[0] # Get the last part before the extension
116
- if version_part.replace('.', '').isdigit(): # Check if it's a valid version number
117
- log.info(
118
- f'Extracted version: {version_part} from filename: {filename} using separator: {sep}'
119
- )
120
- return version_part
121
-
122
- # Fallback: extract from the entire filename
123
- version = filename_no_ext.lstrip('v')
124
- version = version.replace('_', '.').replace('-', '.')
125
- version = version if version.replace('.', '').isdigit() else '0'
126
- log.info(f'Extracted version: {version} from filename: {filename}')
127
- return version
119
+ if separator is not None:
120
+ if not separator:
121
+ raise ValueError('separator must not be empty')
122
+
123
+ version = filename_no_ext.rsplit(separator, 1)[-1]
124
+ if version.isdigit():
125
+ log.info(
126
+ f'Extracted version: {version} from filename: {filename} using separator: {separator}'
127
+ )
128
+ return version
129
+
130
+ log.info(f'No numeric version found in filename: {filename} using separator: {separator}')
131
+ return '0'
132
+
133
+ match = re.search(r'(?:^|[_.-])v?(\d+)$', filename_no_ext)
134
+ if match:
135
+ version = match.group(1)
136
+ log.info(f'Extracted version: {version} from filename: {filename}')
137
+ return version
138
+
139
+ log.info(f'No numeric version found in filename: {filename}')
140
+ return '0'
@@ -7,27 +7,37 @@
7
7
  import os
8
8
  import shutil
9
9
  import subprocess
10
+ from collections.abc import Sequence
10
11
 
11
12
  from backpack.logger import get_logger
12
13
 
13
14
  log = get_logger('Python Backpack - FolderUtils')
14
15
 
15
16
 
16
- def browse_folder(folder: str) -> bool:
17
+ def browse_folder(folder: str | None) -> bool:
17
18
  """Open windows explorer on folder.
18
19
 
19
20
  Args:
20
21
  folder: (string path) folder to open
21
22
  """
22
23
  if folder and os.path.isdir(folder):
23
- subprocess.Popen(f'explorer {os.path.abspath(folder)}')
24
- return True
24
+ try:
25
+ subprocess.Popen(['explorer', os.path.abspath(folder)])
26
+ return True
27
+ except OSError as error:
28
+ log.warning(f'Unable to open folder {folder}')
29
+ log.error(str(error))
30
+ return False
25
31
 
26
32
  log.warning(f'Unable to open folder {folder}')
27
33
  return False
28
34
 
29
35
 
30
- def create_folders(folders: list, force_empty: bool = False, verbose: bool = False):
36
+ def create_folders(
37
+ folders: Sequence[str],
38
+ force_empty: bool = False,
39
+ verbose: bool = False,
40
+ ) -> None:
31
41
  """Creates multiple folders on disc.
32
42
 
33
43
  Args:
@@ -39,7 +49,7 @@ def create_folders(folders: list, force_empty: bool = False, verbose: bool = Fal
39
49
  create_folder(folder, force_empty=force_empty, verbose=verbose)
40
50
 
41
51
 
42
- def create_folder(path: str, force_empty: bool = False, verbose: bool = True):
52
+ def create_folder(path: str, force_empty: bool = False, verbose: bool = True) -> bool:
43
53
  """Creates a folder.
44
54
 
45
55
  Args:
@@ -63,7 +73,7 @@ def create_folder(path: str, force_empty: bool = False, verbose: bool = True):
63
73
  return True
64
74
 
65
75
 
66
- def remove_files_in_dir(path: str):
76
+ def remove_files_in_dir(path: str) -> None:
67
77
  """Clears all content in given directory."""
68
78
  for root, dirs, files in os.walk(path):
69
79
  for f in files:
@@ -72,7 +82,7 @@ def remove_files_in_dir(path: str):
72
82
  shutil.rmtree(os.path.join(root, d))
73
83
 
74
84
 
75
- def recursive_dir_copy(source_path: str, target_path: str):
85
+ def recursive_dir_copy(source_path: str, target_path: str) -> None:
76
86
  """Copy all files src dir to dest dir, including sub-directories.
77
87
 
78
88
  Args:
@@ -3,6 +3,7 @@
3
3
  # Maximiliano Rocamora / maxirocamora@gmail.com
4
4
  # https://github.com/MaxRocamora/python-backpack
5
5
  # ----------------------------------------------------------------------------------------
6
+ import getpass
6
7
  import inspect
7
8
  import os
8
9
  import platform
@@ -97,7 +98,9 @@ class JsonMetaFile:
97
98
  attributes = {}
98
99
  for name in dir(_class):
99
100
  value = getattr(_class, name)
100
- if not name.startswith('__') and not inspect.ismethod(value):
101
+ if not name.startswith('__') and not (
102
+ inspect.isroutine(value) or inspect.isdatadescriptor(value)
103
+ ):
101
104
  attributes[name] = value
102
105
 
103
106
  self._data = attributes
@@ -113,7 +116,7 @@ class JsonMetaFile:
113
116
  'app': os.path.basename(sys.executable),
114
117
  'PC': str(platform.node()),
115
118
  'python_version': sys.version,
116
- 'User': str(os.getenv('username')),
119
+ 'User': getpass.getuser(),
117
120
  'time': self._current_time_metadata(),
118
121
  }
119
122
 
@@ -27,7 +27,7 @@ class JsonUserSettings:
27
27
  folder (str): name of sub folder inside user path. Defaults to 'json_settings'.
28
28
  name (str): name used for the json file. Defaults to 'user_data'.
29
29
  """
30
- self.name = name
30
+ self.filename = name
31
31
  self.folder = folder
32
32
  self._user_data = {}
33
33
  self._verify_path()
@@ -35,7 +35,7 @@ class JsonUserSettings:
35
35
  @property
36
36
  def filepath(self) -> str:
37
37
  """Returns user filepath."""
38
- path = os.path.join(self.os_user_folder, self.folder, f'{self.name}.json')
38
+ path = os.path.join(self.os_user_folder, self.folder, f'{self.filename}.json')
39
39
  return os.path.abspath(path)
40
40
 
41
41
  @property
@@ -60,14 +60,14 @@ class JsonUserSettings:
60
60
 
61
61
  return True
62
62
 
63
- def save_settings(self, data: dict | None = None) -> bool | None:
63
+ def save_settings(self, data: dict | None = None) -> bool:
64
64
  """Saves a dictionary into a json file (os user path).
65
65
 
66
66
  Args:
67
- data (dictionary): info dictionary to save, if not provided,
67
+ data (dict): info dictionary to save, if not provided,
68
68
  saves instead local self.user_data property
69
69
  Returns:
70
- bool | None: True if file was saved, False if error, None if no data to save.
70
+ bool: True if the file was saved, otherwise False.
71
71
  """
72
72
  if data is None:
73
73
  data = self.user_data
@@ -18,32 +18,32 @@ def json_load(json_file: str) -> dict:
18
18
  if not os.path.exists(json_file):
19
19
  raise OSError(f'json_load: File not found: {json_file}.')
20
20
 
21
- with open(json_file) as json_file_opened:
21
+ with open(json_file, encoding='utf-8') as json_file_opened:
22
22
  try:
23
23
  value = json.load(json_file_opened)
24
24
  except ValueError as e:
25
- json_file_opened.close()
26
25
  raise OSError(f'{json_file} \n JSON File issue: {str(e)}') from e
27
26
 
28
27
  return value
29
28
 
30
29
 
31
- def json_save(data: dict, json_file: str) -> bool:
30
+ def json_save(data: object, json_file: str) -> bool:
32
31
  """Saves a dictionary into a json file.
33
32
 
34
33
  Args:
35
- data: (dict) dictionary to save
34
+ data: value to serialize as JSON
36
35
  json_file: (string filepath) json file to save data.
37
36
 
38
37
  Returns:
39
38
  bool (True if success)
40
39
  """
41
40
 
42
- if not os.path.exists(os.path.dirname(json_file)):
43
- os.makedirs(os.path.dirname(json_file), exist_ok=True)
41
+ parent_dir = os.path.dirname(json_file)
42
+ if parent_dir and not os.path.exists(parent_dir):
43
+ os.makedirs(parent_dir, exist_ok=True)
44
44
 
45
45
  try:
46
- with open(json_file, 'w') as f:
46
+ with open(json_file, 'w', encoding='utf-8') as f:
47
47
  json.dump(data, f, sort_keys=True, indent=4)
48
48
  return True
49
49
 
@@ -4,7 +4,6 @@
4
4
  # https://github.com/MaxRocamora/python-backpack
5
5
  # ----------------------------------------------------------------------------------------
6
6
 
7
- import contextlib
8
7
  import re
9
8
 
10
9
 
@@ -29,30 +28,25 @@ def normalize_input_string(
29
28
  str: reformatted string
30
29
  """
31
30
 
32
- # re-encode string to valid chars
33
- with contextlib.suppress(UnicodeDecodeError):
34
- regex = re.sub('[^A-Za-z0-9 _-]+', '', input_string)
35
-
36
- for char in regex:
37
- # replacing " " for replacer
38
- if under_spaces and char == ' ':
39
- regex = regex.replace(' ', replacer)
40
-
41
- # replacing "-" for replacer
42
- if under_hyphen and char == '-':
43
- regex = regex.replace('-', replacer)
31
+ regex = re.sub(r'[^A-Za-z0-9 _-]+', '', input_string)
32
+ if under_spaces:
33
+ regex = regex.replace(' ', replacer)
34
+ if under_hyphen:
35
+ regex = regex.replace('-', replacer)
44
36
 
45
37
  return regex
46
38
 
47
39
 
48
40
  def begin_or_end_with_numbers(input_string: str) -> bool:
49
41
  """Returns true if the input string begins or ends with number."""
50
- return _char_is_number(input_string[0]) or _char_is_number(input_string[-1])
42
+ return bool(input_string) and (
43
+ _char_is_number(input_string[0]) or _char_is_number(input_string[-1])
44
+ )
51
45
 
52
46
 
53
47
  def begin_with_number(input_string: str) -> bool:
54
48
  """Returns true if the input string begins with numbers."""
55
- return _char_is_number(input_string[0])
49
+ return bool(input_string) and _char_is_number(input_string[0])
56
50
 
57
51
 
58
52
  def has_numbers(input_string: str) -> bool: