python-backpack 2.0.1__tar.gz → 2.0.3__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.1 → python_backpack-2.0.3}/.github/workflows/codecov_tests.yml +17 -8
  2. {python_backpack-2.0.1 → python_backpack-2.0.3}/.gitignore +1 -0
  3. python_backpack-2.0.3/PKG-INFO +168 -0
  4. {python_backpack-2.0.1 → python_backpack-2.0.3}/README.md +17 -15
  5. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/cache.py +28 -6
  6. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/file_utils.py +26 -18
  7. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/folder_utils.py +17 -7
  8. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/json_metadata.py +5 -2
  9. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/json_user_settings.py +5 -5
  10. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/json_utils.py +7 -7
  11. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/strings.py +9 -15
  12. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/test_utils.py +10 -4
  13. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/version.py +6 -4
  14. python_backpack-2.0.3/pyproject.toml +52 -0
  15. {python_backpack-2.0.1 → python_backpack-2.0.3}/ruff.toml +4 -2
  16. python_backpack-2.0.3/tests/test_file_utils.py +86 -0
  17. python_backpack-2.0.3/tests/test_folder_utils.py +107 -0
  18. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_json_user_settings.py +21 -44
  19. python_backpack-2.0.3/tests/test_json_utils.py +35 -0
  20. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_jsonmd.py +29 -29
  21. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_misc.py +15 -7
  22. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_strings.py +10 -7
  23. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_test_utils.py +12 -14
  24. python_backpack-2.0.3/uv.lock +312 -0
  25. python_backpack-2.0.1/PKG-INFO +0 -7
  26. python_backpack-2.0.1/pyproject.toml +0 -29
  27. python_backpack-2.0.1/tests/test_file_utils.py +0 -71
  28. python_backpack-2.0.1/tests/test_folder_utils.py +0 -105
  29. python_backpack-2.0.1/tests/test_json_utils.py +0 -46
  30. python_backpack-2.0.1/uv.lock +0 -295
  31. {python_backpack-2.0.1 → python_backpack-2.0.3}/LICENSE +0 -0
  32. {python_backpack-2.0.1 → python_backpack-2.0.3}/__init__.py +0 -0
  33. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/__init__.py +0 -0
  34. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/custom_errors.py +0 -0
  35. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/logger.py +0 -0
  36. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack/patterns.py +0 -0
  37. {python_backpack-2.0.1 → python_backpack-2.0.3}/backpack.code-workspace +0 -0
  38. {python_backpack-2.0.1 → python_backpack-2.0.3}/codecov.yml +0 -0
  39. {python_backpack-2.0.1 → python_backpack-2.0.3}/coverage.bat +0 -0
  40. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/__init__.py +0 -0
  41. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_cache.py +0 -0
  42. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_errors.py +0 -0
  43. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/edited.txt +0 -0
  44. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/edited_remove.txt +0 -0
  45. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/locked_file.txt +0 -0
  46. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/origin.txt +0 -0
  47. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/origin_remove.txt +0 -0
  48. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_files/unlocked_file.txt +0 -0
  49. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_json/data.json +0 -0
  50. {python_backpack-2.0.1 → python_backpack-2.0.3}/tests/test_json/data_broken.json +0 -0
  51. {python_backpack-2.0.1 → python_backpack-2.0.3}/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
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-backpack
3
+ Version: 2.0.3
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
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
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+
23
+ [![PyPI Supported Python Versions](https://img.shields.io/pypi/pyversions/python-backpack.svg?style=flat-square&logo=appveyor)](https://pypi.python.org/pypi/python-backpack/)
24
+ [![PyPI version](https://badge.fury.io/py/python-backpack.svg?style=flat-square&logo=appveyor)](https://badge.fury.io/py/python-backpack)
25
+ [![GitHub version](https://badge.fury.io/gh/MaxRocamora%2Fpython-backpack.svg?style=flat-square&logo=appveyor)](https://badge.fury.io/gh/MaxRocamora%2Fpython-backpack)
26
+ [![codecov](https://codecov.io/gh/MaxRocamora/python-backpack/graph/badge.svg?token=6D1xwYdXW2)](https://codecov.io/gh/MaxRocamora/python-backpack)
27
+
28
+
29
+ # Python-Backpack
30
+
31
+ Python-Backpack is a lightweight utility collection for common scripting tasks:
32
+
33
+ - JSON load/save helpers with validation
34
+ - user settings persistence under the OS user directory
35
+ - metadata export/import to JSON files
36
+ - file and folder operations
37
+ - string normalization and case conversion
38
+ - cache decorator with expiration support
39
+ - custom exceptions, logging helper, singleton pattern, and testing helpers
40
+
41
+ ## Compatibility
42
+
43
+ - Python 3.11+
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install python-backpack
49
+ ```
50
+
51
+ ## Package API Reference
52
+
53
+ ### Cache (`backpack.cache`)
54
+
55
+ - `timed_lru_cache(seconds: int, maxsize: int = 128)`
56
+ - `functools.lru_cache` decorator with expiration time.
57
+ - Wrapped calls support `force_clear=True` and `show_log=True`.
58
+
59
+ ### Custom Errors (`backpack.custom_errors`)
60
+
61
+ - `EnvironmentVariableNotFoundError(var_name: str)`
62
+ - Raised when a required environment variable is missing.
63
+ - `ApplicationNotFoundError(app_name: str)`
64
+ - Raised when a required application is not found.
65
+
66
+ ### File Utils (`backpack.file_utils`)
67
+
68
+ - `replace_strings_in_file(ascii_file: str, strings: Sequence[str], new_string: str) -> None`
69
+ - Replaces multiple string occurrences in 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.
72
+ - `file_is_writeable(filepath: str) -> bool`
73
+ - Checks whether a file can be opened for read/write.
74
+ - `get_version_from_filename(filename: str) -> str`
75
+ - Extracts a numeric version token from a filename.
76
+
77
+ ### Folder Utils (`backpack.folder_utils`)
78
+
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`
82
+ - Creates multiple folders.
83
+ - `create_folder(path: str, force_empty: bool = False, verbose: bool = True) -> bool`
84
+ - Creates a folder and optionally clears it if it already exists.
85
+ - `remove_files_in_dir(path: str) -> None`
86
+ - Removes all files and subdirectories inside a directory.
87
+ - `recursive_dir_copy(source_path: str, target_path: str) -> None`
88
+ - Recursively copies files and subfolders from source to target.
89
+
90
+ ### JSON Utils (`backpack.json_utils`)
91
+
92
+ - `json_load(json_file: str) -> dict`
93
+ - Loads JSON data from file with validation and error handling.
94
+ - `json_save(data: object, json_file: str) -> bool`
95
+ - Saves a JSON-serializable value as UTF-8 and returns whether it succeeded.
96
+
97
+ ### JSON Metadata (`backpack.json_metadata`)
98
+
99
+ - `JsonMetaFile(name: str, path: str)`
100
+ - Manages a metadata JSON file with package/system/time information.
101
+ - Main public methods:
102
+ - `has_file() -> bool`
103
+ - `load() -> None`
104
+ - `insert(key: str, value: Any) -> None`
105
+ - `remove(key: str) -> None`
106
+ - `save() -> None`
107
+ - `load_as_class() -> type`
108
+ - `insert_class(_class: type) -> None`
109
+
110
+ ### JSON User Settings (`backpack.json_user_settings`)
111
+
112
+ - `JsonUserSettings(folder: str, name: str)`
113
+ - Saves and loads JSON settings in the current user's home directory.
114
+ - Main public methods:
115
+ - `save_settings(data: dict | None = None) -> bool`
116
+ - `load_settings() -> dict | bool`
117
+
118
+ ### Logger (`backpack.logger`)
119
+
120
+ - `get_logger(name: str) -> logging.Logger`
121
+ - Returns a configured logger with stream handler.
122
+
123
+ ### Patterns (`backpack.patterns`)
124
+
125
+ - `Singleton`
126
+ - Base class implementing singleton behavior via `__new__`.
127
+
128
+ ### Strings (`backpack.strings`)
129
+
130
+ - `normalize_input_string(input_string: str, under_spaces: bool = True, under_hyphen: bool = True, replacer: str = '_') -> str`
131
+ - Keeps alphanumeric/space/hyphen characters and normalizes separators.
132
+ - `begin_or_end_with_numbers(input_string: str) -> bool`
133
+ - Checks whether first or last character is numeric.
134
+ - `begin_with_number(input_string: str) -> bool`
135
+ - Checks whether the first character is numeric.
136
+ - `has_numbers(input_string: str) -> bool`
137
+ - Checks whether any character is numeric.
138
+ - `camelcase_to_snakecase(input_string: str) -> str`
139
+ - Converts CamelCase to snake_case.
140
+ - Handles acronym prefixes, for example `HTTPServer -> http_server`.
141
+
142
+ ### Test Utils (`backpack.test_utils`)
143
+
144
+ - `random_string(length: int = 10) -> str`
145
+ - Generates a random lowercase string.
146
+ - `time_function_decorator(method: Callable[P, R]) -> Callable[P, R]`
147
+ - Decorator that logs execution time while preserving function metadata and return values.
148
+
149
+ ## Quick Example
150
+
151
+ ```python
152
+ from backpack.cache import timed_lru_cache
153
+ from backpack.strings import camelcase_to_snakecase, normalize_input_string
154
+ from backpack.json_utils import json_save, json_load
155
+
156
+
157
+ @timed_lru_cache(seconds=60)
158
+ def expensive_call():
159
+ return {'ok': True}
160
+
161
+
162
+ value = camelcase_to_snakecase('HTTPServer')
163
+ clean = normalize_input_string('Blade Runner-2049')
164
+
165
+ json_save({'value': value, 'clean': clean}, 'data.json')
166
+ data = json_load('data.json')
167
+ ```
168
+
@@ -43,32 +43,34 @@ 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
+ - `get_version_from_filename(filename: str) -> str`
53
+ - Extracts a numeric version token from a filename.
52
54
 
53
55
  ### Folder Utils (`backpack.folder_utils`)
54
56
 
55
- - `browse_folder(folder: str) -> bool`
56
- - Opens a folder in Windows Explorer.
57
- - `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`
58
60
  - Creates multiple folders.
59
- - `create_folder(path: str, force_empty: bool = False, verbose: bool = True)`
61
+ - `create_folder(path: str, force_empty: bool = False, verbose: bool = True) -> bool`
60
62
  - Creates a folder and optionally clears it if it already exists.
61
- - `remove_files_in_dir(path: str)`
63
+ - `remove_files_in_dir(path: str) -> None`
62
64
  - Removes all files and subdirectories inside a directory.
63
- - `recursive_dir_copy(source_path: str, target_path: str)`
65
+ - `recursive_dir_copy(source_path: str, target_path: str) -> None`
64
66
  - Recursively copies files and subfolders from source to target.
65
67
 
66
68
  ### JSON Utils (`backpack.json_utils`)
67
69
 
68
70
  - `json_load(json_file: str) -> dict`
69
71
  - Loads JSON data from file with validation and error handling.
70
- - `json_save(data: dict, json_file: str) -> bool`
71
- - 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.
72
74
 
73
75
  ### JSON Metadata (`backpack.json_metadata`)
74
76
 
@@ -88,7 +90,7 @@ pip install python-backpack
88
90
  - `JsonUserSettings(folder: str, name: str)`
89
91
  - Saves and loads JSON settings in the current user's home directory.
90
92
  - Main public methods:
91
- - `save_settings(data: dict | None = None) -> bool | None`
93
+ - `save_settings(data: dict | None = None) -> bool`
92
94
  - `load_settings() -> dict | bool`
93
95
 
94
96
  ### Logger (`backpack.logger`)
@@ -119,8 +121,8 @@ pip install python-backpack
119
121
 
120
122
  - `random_string(length: int = 10) -> str`
121
123
  - Generates a random lowercase string.
122
- - `time_function_decorator(method: type)`
123
- - 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.
124
126
 
125
127
  ## Quick Example
126
128
 
@@ -132,7 +134,7 @@ from backpack.json_utils import json_save, json_load
132
134
 
133
135
  @timed_lru_cache(seconds=60)
134
136
  def expensive_call():
135
- return {'ok': True}
137
+ return {'ok': True}
136
138
 
137
139
 
138
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,18 @@
4
4
  # https://github.com/MaxRocamora/python-backpack
5
5
  # ----------------------------------------------------------------------------------------
6
6
 
7
- import contextlib
7
+ from collections.abc import Sequence
8
8
 
9
9
  from backpack.logger import get_logger
10
10
 
11
11
  log = get_logger('Python Backpack - FileUtils')
12
12
 
13
13
 
14
- def replace_strings_in_file(ascii_file: str, strings: list, new_string: str) -> None:
14
+ def replace_strings_in_file(
15
+ ascii_file: str,
16
+ strings: Sequence[str],
17
+ new_string: str,
18
+ ) -> None:
15
19
  """Opens ascii file and replaces all occurrences from strings into new_string.
16
20
 
17
21
  In this class we use a full path to avoid use of os.dirname, which
@@ -28,7 +32,7 @@ def replace_strings_in_file(ascii_file: str, strings: list, new_string: str) ->
28
32
 
29
33
  log.info(f'Replacing Strings, Opening File: {ascii_file}')
30
34
 
31
- with open(ascii_file) as f:
35
+ with open(ascii_file, newline='') as f:
32
36
  file_data = f.read()
33
37
  for i in strings:
34
38
  log.info('Finding: %s', i)
@@ -36,13 +40,16 @@ def replace_strings_in_file(ascii_file: str, strings: list, new_string: str) ->
36
40
  log.info('-' * 50)
37
41
  file_data = file_data.replace(i, new_string)
38
42
 
39
- with open(ascii_file, 'w') as f:
43
+ with open(ascii_file, 'w', newline='') as f:
40
44
  f.write(file_data)
41
- f.close()
42
45
  log.info(f'Closing File: {ascii_file}')
43
46
 
44
47
 
45
- def remove_line_from_file(ascii_file: str, strings: list, verbose: bool = False) -> None:
48
+ def remove_line_from_file(
49
+ ascii_file: str,
50
+ strings: Sequence[str],
51
+ verbose: bool = False,
52
+ ) -> None:
46
53
  """Removes given lines from ascii file.
47
54
 
48
55
  Args:
@@ -51,21 +58,22 @@ def remove_line_from_file(ascii_file: str, strings: list, verbose: bool = False)
51
58
  verbose: (bool) if true, prints removed lines
52
59
  """
53
60
 
54
- with open(ascii_file) as f:
55
- file_content = f.read().splitlines()
61
+ retained_lines = []
62
+ with open(ascii_file, newline='') as f:
63
+ file_content = f.readlines()
56
64
 
57
65
  for line in file_content:
66
+ value = line.rstrip('\r\n')
58
67
  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))
65
-
66
- with open(ascii_file, 'w') as f:
67
- contents = '\n'.join(file_content)
68
- f.write(contents)
68
+ log.info(f'Checking line: {value}')
69
+ if value in strings:
70
+ if verbose:
71
+ log.info(f'Removing line: {value}')
72
+ else:
73
+ retained_lines.append(line)
74
+
75
+ with open(ascii_file, 'w', newline='') as f:
76
+ f.writelines(retained_lines)
69
77
 
70
78
 
71
79
  def file_is_writeable(filepath: str) -> bool:
@@ -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: