hunterMakesPy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hunterMakesPy/__init__.py +16 -0
- hunterMakesPy/_theSSOT.py +40 -0
- hunterMakesPy/coping.py +73 -0
- hunterMakesPy/dataStructures.py +265 -0
- hunterMakesPy/filesystemToolkit.py +114 -0
- hunterMakesPy/parseParameters.py +322 -0
- hunterMakesPy/py.typed +0 -0
- hunterMakesPy/pytestForYourUse.py +327 -0
- hunterMakesPy/theTypes.py +9 -0
- huntermakespy-0.1.0.dist-info/METADATA +38 -0
- huntermakespy-0.1.0.dist-info/RECORD +20 -0
- huntermakespy-0.1.0.dist-info/WHEEL +5 -0
- huntermakespy-0.1.0.dist-info/licenses/LICENSE +407 -0
- huntermakespy-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +39 -0
- tests/test_coping.py +56 -0
- tests/test_dataStructures.py +319 -0
- tests/test_filesystemToolkit.py +43 -0
- tests/test_parseParameters.py +21 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""`import hunterMakesPy as humpy`."""
|
|
2
|
+
from hunterMakesPy.theTypes import identifierDotAttribute as identifierDotAttribute
|
|
3
|
+
|
|
4
|
+
from hunterMakesPy.coping import raiseIfNone as raiseIfNone
|
|
5
|
+
|
|
6
|
+
from hunterMakesPy.parseParameters import (defineConcurrencyLimit as defineConcurrencyLimit, intInnit as intInnit,
|
|
7
|
+
oopsieKwargsie as oopsieKwargsie)
|
|
8
|
+
|
|
9
|
+
from hunterMakesPy.filesystemToolkit import (importLogicalPath2Identifier as importLogicalPath2Identifier,
|
|
10
|
+
importPathFilename2Identifier as importPathFilename2Identifier, makeDirsSafely as makeDirsSafely,
|
|
11
|
+
writeStringToHere as writeStringToHere)
|
|
12
|
+
|
|
13
|
+
from hunterMakesPy.dataStructures import (autoDecodingRLE as autoDecodingRLE, stringItUp as stringItUp,
|
|
14
|
+
updateExtendPolishDictionaryLists as updateExtendPolishDictionaryLists)
|
|
15
|
+
|
|
16
|
+
from hunterMakesPy._theSSOT import settingsPackage
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Primary: settings for this package.
|
|
2
|
+
|
|
3
|
+
Secondary: settings for manufacturing.
|
|
4
|
+
Tertiary: hardcoded values until I implement a dynamic solution.
|
|
5
|
+
"""
|
|
6
|
+
from importlib.util import find_spec
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from tomli import loads as tomli_loads
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
import dataclasses
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from importlib.machinery import ModuleSpec
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
identifierPackagePACKAGING: str = tomli_loads(Path("pyproject.toml").read_text())["project"]["name"]
|
|
17
|
+
except Exception: # noqa: BLE001
|
|
18
|
+
identifierPackagePACKAGING = "hunterMakesPy"
|
|
19
|
+
|
|
20
|
+
def getPathPackageINSTALLING() -> Path:
|
|
21
|
+
"""Return the root directory of the installed package."""
|
|
22
|
+
try:
|
|
23
|
+
moduleSpecification: ModuleSpec | None = find_spec(identifierPackagePACKAGING)
|
|
24
|
+
if moduleSpecification and moduleSpecification.origin:
|
|
25
|
+
pathFilename = Path(moduleSpecification.origin)
|
|
26
|
+
return pathFilename.parent if pathFilename.is_file() else pathFilename
|
|
27
|
+
except ModuleNotFoundError:
|
|
28
|
+
pass
|
|
29
|
+
return Path.cwd()
|
|
30
|
+
|
|
31
|
+
@dataclasses.dataclass
|
|
32
|
+
class PackageSettings:
|
|
33
|
+
fileExtension: str = dataclasses.field(default='.py', metadata={'evaluateWhen': 'installing'})
|
|
34
|
+
"""Default file extension for generated code files."""
|
|
35
|
+
identifierPackage: str = dataclasses.field(default = identifierPackagePACKAGING, metadata={'evaluateWhen': 'packaging'})
|
|
36
|
+
"""Name of this package, used for import paths and configuration."""
|
|
37
|
+
pathPackage: Path = dataclasses.field(default_factory=getPathPackageINSTALLING, metadata={'evaluateWhen': 'installing'})
|
|
38
|
+
"""Absolute path to the installed package directory."""
|
|
39
|
+
|
|
40
|
+
settingsPackage = PackageSettings()
|
hunterMakesPy/coping.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Utility functions for handling `None` values and coping with common programming patterns.
|
|
2
|
+
|
|
3
|
+
(AI generated docstring)
|
|
4
|
+
|
|
5
|
+
This module provides helper functions for defensive programming and error handling, particularly for dealing with `None` values that should not occur in correct program flow.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
TypeSansNone = TypeVar('TypeSansNone')
|
|
11
|
+
|
|
12
|
+
def raiseIfNone(returnTarget: TypeSansNone | None, errorMessage: str | None = None) -> TypeSansNone:
|
|
13
|
+
"""Raise a `ValueError` if the target value is `None`, otherwise return the value: tell the type checker that the return value is not `None`.
|
|
14
|
+
|
|
15
|
+
(AI generated docstring)
|
|
16
|
+
|
|
17
|
+
This is a defensive programming function that converts unexpected `None` values into explicit errors with context. It is useful for asserting that functions that might return `None` have actually returned a meaningful value.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
returnTarget : TypeSansNone | None
|
|
22
|
+
The value to check for `None`. If not `None`, this value is returned unchanged.
|
|
23
|
+
errorMessage : str | None = None
|
|
24
|
+
Custom error message to include in the `ValueError`. If `None`, a default message with debugging hints is used.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
returnTarget : TypeSansNone
|
|
29
|
+
The original `returnTarget` value, guaranteed to not be `None`.
|
|
30
|
+
|
|
31
|
+
Raises
|
|
32
|
+
------
|
|
33
|
+
ValueError
|
|
34
|
+
If `returnTarget` is `None`.
|
|
35
|
+
|
|
36
|
+
Examples
|
|
37
|
+
--------
|
|
38
|
+
Ensure a function result is not `None`:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
def findFirstMatch(listItems: list[str], pattern: str) -> str | None:
|
|
42
|
+
for item in listItems:
|
|
43
|
+
if pattern in item:
|
|
44
|
+
return item
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
listFiles = ['document.txt', 'image.png', 'data.csv']
|
|
48
|
+
filename = raiseIfNone(findFirstMatch(listFiles, '.txt'))
|
|
49
|
+
# Returns 'document.txt'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Handle dictionary lookups with custom error messages:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
configurationMapping = {'host': 'localhost', 'port': 8080}
|
|
56
|
+
host = raiseIfNone(configurationMapping.get('host'),
|
|
57
|
+
"Configuration must include 'host' setting")
|
|
58
|
+
# Returns 'localhost'
|
|
59
|
+
|
|
60
|
+
# This would raise ValueError with custom message:
|
|
61
|
+
# database = raiseIfNone(configurationMapping.get('database'),
|
|
62
|
+
# "Configuration must include 'database' setting")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Thanks
|
|
66
|
+
------
|
|
67
|
+
sobolevn, https://github.com/sobolevn, for the seed of the function. https://github.com/python/typing/discussions/1997#discussioncomment-13108399
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
if returnTarget is None:
|
|
71
|
+
message = errorMessage or 'A function unexpectedly returned `None`. Hint: look at the traceback immediately before `raiseIfNone`.'
|
|
72
|
+
raise ValueError(message)
|
|
73
|
+
return returnTarget
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Provides utilities for string extraction from nested data structures and merges multiple dictionaries containing lists into one dictionary."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator, Mapping
|
|
4
|
+
from numpy import integer
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
from typing import Any
|
|
7
|
+
import more_itertools
|
|
8
|
+
import python_minifier
|
|
9
|
+
import re as regex
|
|
10
|
+
|
|
11
|
+
def autoDecodingRLE(arrayTarget: NDArray[integer[Any]], *, assumeAddSpaces: bool = False) -> str: # noqa: C901, PLR0915
|
|
12
|
+
"""Transform a NumPy array into a compact, self-decoding run-length encoded string representation.
|
|
13
|
+
|
|
14
|
+
This function converts a NumPy array into a string that, when evaluated as Python code,
|
|
15
|
+
recreates the original array structure. The function employs two compression strategies:
|
|
16
|
+
1. Python's `range` syntax for consecutive integer sequences
|
|
17
|
+
2. Multiplication syntax for repeated elements
|
|
18
|
+
|
|
19
|
+
The resulting string representation is designed to be both human-readable and space-efficient,
|
|
20
|
+
especially for large cartesian mappings with repetitive patterns. When this string is used
|
|
21
|
+
as a data source, Python will automatically decode it into Python `list`, which if used as an
|
|
22
|
+
argument to `numpy.array()`, will recreate the original array structure.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
arrayTarget : NDArray[integer[Any]]
|
|
27
|
+
(array2target) The NumPy array to be encoded.
|
|
28
|
+
assumeAddSpaces : bool = False
|
|
29
|
+
(assume2add2spaces) Affects internal length comparison during compression decisions.
|
|
30
|
+
This parameter doesn't directly change output format but influences whether
|
|
31
|
+
`range` or multiplication syntax is preferred in certain cases. The parameter
|
|
32
|
+
exists because the Abstract Syntax Tree (AST) inserts spaces in its string
|
|
33
|
+
representation.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
rleString : str
|
|
38
|
+
(rle2string) A string representation of the array using run-length encoding that,
|
|
39
|
+
when evaluated as Python code, reproduces the original array structure.
|
|
40
|
+
|
|
41
|
+
Notes
|
|
42
|
+
-----
|
|
43
|
+
The "autoDecoding" feature means that the string representation evaluates directly
|
|
44
|
+
to the desired data structure without explicit decompression steps.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
def sliceNDArrayToNestedLists(arraySlice: NDArray[integer[Any]]) -> Any:
|
|
48
|
+
def getLengthOption(optionAsStr: str) -> int:
|
|
49
|
+
# `assumeAddSpaces` characters: `,` 1; `]*` 2 # noqa: ERA001
|
|
50
|
+
return assumeAddSpaces * (optionAsStr.count(',') + optionAsStr.count(']*') * 2) + len(optionAsStr)
|
|
51
|
+
|
|
52
|
+
if arraySlice.ndim > 1:
|
|
53
|
+
axisOfOperation = 0
|
|
54
|
+
return [sliceNDArrayToNestedLists(arraySlice[index]) for index in range(arraySlice.shape[axisOfOperation])]
|
|
55
|
+
if arraySlice.ndim == 1:
|
|
56
|
+
arraySliceAsList: list[int | range] = []
|
|
57
|
+
cache_consecutiveGroup_addMe: dict[Iterator[Any], list[int] | list[range]] = {}
|
|
58
|
+
for consecutiveGroup in more_itertools.consecutive_groups(arraySlice.tolist()):
|
|
59
|
+
if consecutiveGroup in cache_consecutiveGroup_addMe:
|
|
60
|
+
addMe = cache_consecutiveGroup_addMe[consecutiveGroup]
|
|
61
|
+
else:
|
|
62
|
+
ImaSerious: list[int] = list(consecutiveGroup)
|
|
63
|
+
ImaRange = [range(ImaSerious[0], ImaSerious[-1] + 1)]
|
|
64
|
+
ImaRangeAsStr = python_minifier.minify(str(ImaRange)).replace('range(0,', 'range(').replace('range', '*range')
|
|
65
|
+
|
|
66
|
+
option1 = ImaRange
|
|
67
|
+
option1AsStr = ImaRangeAsStr
|
|
68
|
+
option2 = ImaSerious
|
|
69
|
+
option2AsStr = None
|
|
70
|
+
|
|
71
|
+
# alpha, potential function
|
|
72
|
+
option1AsStr = option1AsStr or python_minifier.minify(str(option1))
|
|
73
|
+
lengthOption1 = getLengthOption(option1AsStr)
|
|
74
|
+
|
|
75
|
+
option2AsStr = option2AsStr or python_minifier.minify(str(option2))
|
|
76
|
+
lengthOption2 = getLengthOption(option2AsStr)
|
|
77
|
+
|
|
78
|
+
if lengthOption1 < lengthOption2:
|
|
79
|
+
addMe = option1
|
|
80
|
+
else:
|
|
81
|
+
addMe = option2
|
|
82
|
+
|
|
83
|
+
cache_consecutiveGroup_addMe[consecutiveGroup] = addMe
|
|
84
|
+
|
|
85
|
+
arraySliceAsList += addMe
|
|
86
|
+
|
|
87
|
+
listRangeAndTuple: list[int | range | tuple[int | range, int]] = []
|
|
88
|
+
cache_malkovichGrouped_addMe: dict[tuple[int | range, int], list[tuple[int | range, int]] | list[int | range]] = {}
|
|
89
|
+
for malkovichGrouped in more_itertools.run_length.encode(arraySliceAsList):
|
|
90
|
+
if malkovichGrouped in cache_malkovichGrouped_addMe:
|
|
91
|
+
addMe = cache_malkovichGrouped_addMe[malkovichGrouped]
|
|
92
|
+
else:
|
|
93
|
+
lengthMalkovich = malkovichGrouped[-1]
|
|
94
|
+
malkovichAsList = list(more_itertools.run_length.decode([malkovichGrouped]))
|
|
95
|
+
malkovichMalkovich = f"[{malkovichGrouped[0]}]*{lengthMalkovich}"
|
|
96
|
+
|
|
97
|
+
option1 = [malkovichGrouped]
|
|
98
|
+
option1AsStr = malkovichMalkovich
|
|
99
|
+
option2 = malkovichAsList
|
|
100
|
+
option2AsStr = None
|
|
101
|
+
|
|
102
|
+
# beta, potential function
|
|
103
|
+
option1AsStr = option1AsStr or python_minifier.minify(str(option1))
|
|
104
|
+
lengthOption1 = getLengthOption(option1AsStr)
|
|
105
|
+
|
|
106
|
+
option2AsStr = option2AsStr or python_minifier.minify(str(option2))
|
|
107
|
+
lengthOption2 = getLengthOption(option2AsStr)
|
|
108
|
+
|
|
109
|
+
if lengthOption1 < lengthOption2:
|
|
110
|
+
addMe = option1
|
|
111
|
+
else:
|
|
112
|
+
addMe = option2
|
|
113
|
+
|
|
114
|
+
cache_malkovichGrouped_addMe[malkovichGrouped] = addMe
|
|
115
|
+
|
|
116
|
+
listRangeAndTuple += addMe
|
|
117
|
+
|
|
118
|
+
return listRangeAndTuple
|
|
119
|
+
return arraySlice
|
|
120
|
+
|
|
121
|
+
arrayAsNestedLists = sliceNDArrayToNestedLists(arrayTarget)
|
|
122
|
+
|
|
123
|
+
arrayAsStr = python_minifier.minify(str(arrayAsNestedLists))
|
|
124
|
+
|
|
125
|
+
patternRegex = regex.compile(
|
|
126
|
+
"(?<!rang)(?:"
|
|
127
|
+
# Pattern 1: Comma ahead, bracket behind # noqa: ERA001
|
|
128
|
+
"(?P<joinAhead>,)\\((?P<malkovich>\\d+),(?P<multiple>\\d+)\\)(?P<bracketBehind>])|"
|
|
129
|
+
# Pattern 2: Bracket or start ahead, comma behind # noqa: ERA001
|
|
130
|
+
"(?P<bracketOrStartAhead>\\[|^.)\\((?P<malkovichmalkovich>\\d+),(?P<multiple_fml>\\d+)\\)(?P<joinBehind>,)|"
|
|
131
|
+
# Pattern 3: Bracket ahead, bracket behind # noqa: ERA001
|
|
132
|
+
"(?P<bracketAhead>\\[)\\((?P<malkovichmalkovichmalkovich>\\d+),(?P<multiple_whatever>\\d+)\\)(?P<bracketBehindbracketBehind>])|"
|
|
133
|
+
# Pattern 4: Comma ahead, comma behind # noqa: ERA001
|
|
134
|
+
"(?P<joinAhead_prayharder>,)\\((?P<malkovichmalkovichmalkovichmalkovich>\\d+),(?P<multiple_prayharder>\\d+)\\)(?P<joinBehind_prayharder>,)"
|
|
135
|
+
")"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def replacementByContext(match: regex.Match[str]) -> str:
|
|
139
|
+
"""Generate replacement string based on context patterns."""
|
|
140
|
+
yourIdentifiersSuck = match.groupdict()
|
|
141
|
+
joinAhead = yourIdentifiersSuck.get('joinAhead') or yourIdentifiersSuck.get('joinAhead_prayharder')
|
|
142
|
+
malkovich = yourIdentifiersSuck.get('malkovich') or yourIdentifiersSuck.get('malkovichmalkovich') or yourIdentifiersSuck.get('malkovichmalkovichmalkovich') or yourIdentifiersSuck.get('malkovichmalkovichmalkovichmalkovich')
|
|
143
|
+
multiple = yourIdentifiersSuck.get('multiple') or yourIdentifiersSuck.get('multiple_fml') or yourIdentifiersSuck.get('multiple_whatever') or yourIdentifiersSuck.get('multiple_prayharder')
|
|
144
|
+
joinBehind = yourIdentifiersSuck.get('joinBehind') or yourIdentifiersSuck.get('joinBehind_prayharder')
|
|
145
|
+
|
|
146
|
+
replaceAhead = "]+[" if joinAhead == "," else "["
|
|
147
|
+
|
|
148
|
+
replaceBehind = "+[" if joinBehind == "," else ""
|
|
149
|
+
|
|
150
|
+
return f"{replaceAhead}{malkovich}]*{multiple}{replaceBehind}"
|
|
151
|
+
|
|
152
|
+
arrayAsStr = patternRegex.sub(replacementByContext, arrayAsStr)
|
|
153
|
+
arrayAsStr = patternRegex.sub(replacementByContext, arrayAsStr)
|
|
154
|
+
|
|
155
|
+
# Replace `range(0,stop)` syntax with `range(stop)` syntax. # noqa: ERA001
|
|
156
|
+
# Add unpack operator `*` for automatic decoding when evaluated.
|
|
157
|
+
return arrayAsStr.replace('range(0,', 'range(').replace('range', '*range')
|
|
158
|
+
|
|
159
|
+
def stringItUp(*scrapPile: Any) -> list[str]: # noqa: C901
|
|
160
|
+
"""Convert, if possible, every element in the input data structure to a string.
|
|
161
|
+
|
|
162
|
+
Order is not preserved or readily predictable.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
*scrapPile : Any
|
|
167
|
+
(scrap2pile) One or more data structures to unpack and convert to strings.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
listStrungUp : list[str]
|
|
172
|
+
(list2strung2up) A `list` of string versions of all convertible elements.
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
scrap = None
|
|
176
|
+
listStrungUp: list[str] = []
|
|
177
|
+
|
|
178
|
+
def drill(KitKat: Any) -> None: # noqa: C901, PLR0912
|
|
179
|
+
match KitKat:
|
|
180
|
+
case str():
|
|
181
|
+
listStrungUp.append(KitKat)
|
|
182
|
+
case bool() | bytearray() | bytes() | complex() | float() | int() | memoryview() | None:
|
|
183
|
+
listStrungUp.append(str(KitKat)) # pyright: ignore [reportUnknownArgumentType]
|
|
184
|
+
case dict():
|
|
185
|
+
for broken, piece in KitKat.items(): # pyright: ignore [reportUnknownVariableType]
|
|
186
|
+
drill(broken)
|
|
187
|
+
drill(piece)
|
|
188
|
+
case list() | tuple() | set() | frozenset() | range():
|
|
189
|
+
for kit in KitKat: # pyright: ignore [reportUnknownVariableType]
|
|
190
|
+
drill(kit)
|
|
191
|
+
case _:
|
|
192
|
+
if hasattr(KitKat, '__iter__'): # Unpack other iterables
|
|
193
|
+
for kat in KitKat:
|
|
194
|
+
drill(kat)
|
|
195
|
+
else:
|
|
196
|
+
try:
|
|
197
|
+
sharingIsCaring = KitKat.__str__()
|
|
198
|
+
listStrungUp.append(sharingIsCaring)
|
|
199
|
+
except AttributeError:
|
|
200
|
+
pass
|
|
201
|
+
except TypeError: # "The error traceback provided indicates that there is an issue when calling the __str__ method on an object that does not have this method properly defined, leading to a TypeError."
|
|
202
|
+
pass
|
|
203
|
+
except:
|
|
204
|
+
print(f"\nWoah! I received '{repr(KitKat)}'.\nTheir report card says, 'Plays well with others: Needs improvement.'\n") # noqa: RUF010, T201
|
|
205
|
+
raise
|
|
206
|
+
try:
|
|
207
|
+
for scrap in scrapPile:
|
|
208
|
+
drill(scrap)
|
|
209
|
+
except RecursionError:
|
|
210
|
+
listStrungUp.append(repr(scrap))
|
|
211
|
+
return listStrungUp
|
|
212
|
+
|
|
213
|
+
def updateExtendPolishDictionaryLists(*dictionaryLists: Mapping[str, list[Any] | set[Any] | tuple[Any, ...]], destroyDuplicates: bool = False, reorderLists: bool = False, killErroneousDataTypes: bool = False) -> dict[str, list[Any]]:
|
|
214
|
+
"""Merge multiple dictionaries containing `list` into a single dictionary.
|
|
215
|
+
|
|
216
|
+
With options to handle duplicates, `list` ordering, and erroneous data types.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
*dictionaryLists : Mapping[str, list[Any] | set[Any] | tuple[Any, ...]]
|
|
221
|
+
(dictionary2lists) Variable number of dictionaries to be merged. If only one dictionary is passed, it will be processed based on the provided options.
|
|
222
|
+
destroyDuplicates : bool = False
|
|
223
|
+
(destroy2duplicates) If `True`, removes duplicate elements from the `list`. Defaults to `False`.
|
|
224
|
+
reorderLists : bool = False
|
|
225
|
+
(reorder2lists) If `True`, sorts the `list`. Defaults to `False`.
|
|
226
|
+
killErroneousDataTypes : bool = False
|
|
227
|
+
(kill2erroneous2data2types) If `True`, skips dictionary keys or dictionary values that cause a `TypeError` during merging. Defaults to `False`.
|
|
228
|
+
|
|
229
|
+
Returns
|
|
230
|
+
-------
|
|
231
|
+
ePluribusUnum : dict[str, list[Any]]
|
|
232
|
+
(e2pluribus2unum) A single dictionary with merged `list` based on the provided options. If only one dictionary is passed,
|
|
233
|
+
it will be cleaned up based on the options.
|
|
234
|
+
|
|
235
|
+
Notes
|
|
236
|
+
-----
|
|
237
|
+
The returned value, `ePluribusUnum`, is a so-called primitive dictionary (`dict`). Furthermore, every dictionary key is a
|
|
238
|
+
so-called primitive string (cf. `str()`) and every dictionary value is a so-called primitive `list` (`list`). If
|
|
239
|
+
`dictionaryLists` has other data types, the data types will not be preserved. That could have unexpected consequences.
|
|
240
|
+
Conversion from the original data type to a `list`, for example, may not preserve the order even if you want the order to be
|
|
241
|
+
preserved.
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
ePluribusUnum: dict[str, list[Any]] = {}
|
|
245
|
+
|
|
246
|
+
for dictionaryListTarget in dictionaryLists:
|
|
247
|
+
for keyName, keyValue in dictionaryListTarget.items():
|
|
248
|
+
try:
|
|
249
|
+
ImaStr = str(keyName)
|
|
250
|
+
ImaList = list(keyValue)
|
|
251
|
+
ePluribusUnum.setdefault(ImaStr, []).extend(ImaList)
|
|
252
|
+
except TypeError: # noqa: PERF203
|
|
253
|
+
if killErroneousDataTypes:
|
|
254
|
+
continue
|
|
255
|
+
else:
|
|
256
|
+
raise
|
|
257
|
+
|
|
258
|
+
if destroyDuplicates:
|
|
259
|
+
for ImaStr, ImaList in ePluribusUnum.items():
|
|
260
|
+
ePluribusUnum[ImaStr] = list(dict.fromkeys(ImaList))
|
|
261
|
+
if reorderLists:
|
|
262
|
+
for ImaStr, ImaList in ePluribusUnum.items():
|
|
263
|
+
ePluribusUnum[ImaStr] = sorted(ImaList)
|
|
264
|
+
|
|
265
|
+
return ePluribusUnum
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""File system and module import utilities.
|
|
2
|
+
|
|
3
|
+
This module provides basic file I/O utilities such as writing tabular data to files, computing canonical relative paths, importing
|
|
4
|
+
callables from modules, and safely creating directories.
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from hunterMakesPy import identifierDotAttribute
|
|
9
|
+
from os import PathLike
|
|
10
|
+
from pathlib import Path, PurePath
|
|
11
|
+
from typing import Any, TYPE_CHECKING, TypeVar
|
|
12
|
+
import contextlib
|
|
13
|
+
import importlib
|
|
14
|
+
import importlib.util
|
|
15
|
+
import io
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
|
|
20
|
+
归个 = TypeVar('归个')
|
|
21
|
+
|
|
22
|
+
def importLogicalPath2Identifier(logicalPathModule: identifierDotAttribute, identifier: str, packageIdentifierIfRelative: str | None = None) -> 归个:
|
|
23
|
+
"""Import an `identifier`, such as a function or `class`, from a module using its logical path.
|
|
24
|
+
|
|
25
|
+
This function imports a module and retrieves a specific attribute (function, class, or other object) from that module.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
logicalPathModule : identifierDotAttribute
|
|
30
|
+
The logical path to the module, using dot notation (e.g., 'scipy.signal.windows').
|
|
31
|
+
identifier : str
|
|
32
|
+
The identifier of the object to retrieve from the module.
|
|
33
|
+
packageIdentifierIfRelative : str | None = None
|
|
34
|
+
The package name to use as the anchor point if `logicalPathModule` is a relative import. `None` means an absolute import.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
identifierImported : 归个
|
|
39
|
+
The identifier (function, class, or object) retrieved from the module.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
moduleImported: ModuleType = importlib.import_module(logicalPathModule, packageIdentifierIfRelative)
|
|
43
|
+
return getattr(moduleImported, identifier)
|
|
44
|
+
|
|
45
|
+
def importPathFilename2Identifier(pathFilename: PathLike[Any] | PurePath, identifier: str, moduleIdentifier: str | None = None) -> 归个:
|
|
46
|
+
"""Load an identifier from a Python file.
|
|
47
|
+
|
|
48
|
+
This function imports a specified Python file as a module, extracts an identifier from it by name, and returns that
|
|
49
|
+
identifier.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
pathFilename : PathLike[Any] | PurePath
|
|
54
|
+
Path to the Python file to import.
|
|
55
|
+
identifier : str
|
|
56
|
+
Name of the identifier to extract from the imported module.
|
|
57
|
+
moduleIdentifier : str | None = None
|
|
58
|
+
Name to use for the imported module. If `None`, the filename stem is used.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
identifierImported : 归个
|
|
63
|
+
The identifier extracted from the imported module.
|
|
64
|
+
|
|
65
|
+
Raises
|
|
66
|
+
------
|
|
67
|
+
ImportError
|
|
68
|
+
If the file cannot be imported or the importlib specification is invalid.
|
|
69
|
+
AttributeError
|
|
70
|
+
If the identifier does not exist in the imported module.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
pathFilename = Path(pathFilename)
|
|
74
|
+
|
|
75
|
+
importlibSpecification = importlib.util.spec_from_file_location(moduleIdentifier or pathFilename.stem, pathFilename)
|
|
76
|
+
if importlibSpecification is None or importlibSpecification.loader is None:
|
|
77
|
+
message = f"I received\n\t`{pathFilename = }`,\n\t`{identifier = }`, and\n\t`{moduleIdentifier = }`.\n\tAfter loading, \n\t`importlibSpecification` {'is `None`' if importlibSpecification is None else 'has a value'} and\n\t`importlibSpecification.loader` is unknown."
|
|
78
|
+
raise ImportError(message)
|
|
79
|
+
|
|
80
|
+
moduleImported_jk_hahaha: ModuleType = importlib.util.module_from_spec(importlibSpecification)
|
|
81
|
+
importlibSpecification.loader.exec_module(moduleImported_jk_hahaha)
|
|
82
|
+
return getattr(moduleImported_jk_hahaha, identifier)
|
|
83
|
+
|
|
84
|
+
def makeDirsSafely(pathFilename: Any) -> None:
|
|
85
|
+
"""Create parent directories for a given path safely.
|
|
86
|
+
|
|
87
|
+
This function attempts to create all necessary parent directories for a given path. If the directory already exists or if
|
|
88
|
+
there's an `OSError` during creation, it will silently continue without raising an exception.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
pathFilename : Any
|
|
93
|
+
A path-like object or file object representing the path for which to create parent directories. If it's an IO stream
|
|
94
|
+
object, no directories will be created.
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
if not isinstance(pathFilename, io.IOBase):
|
|
98
|
+
with contextlib.suppress(OSError):
|
|
99
|
+
Path(pathFilename).parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
|
|
101
|
+
def writeStringToHere(this: str, pathFilename: PathLike[Any] | PurePath) -> None:
|
|
102
|
+
"""Write a string to a file, creating parent directories as needed.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
this : str
|
|
107
|
+
The string content to write to the file.
|
|
108
|
+
pathFilename : PathLike[Any] | PurePath
|
|
109
|
+
The path and filename where the string will be written.
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
pathFilename = Path(pathFilename)
|
|
113
|
+
makeDirsSafely(pathFilename)
|
|
114
|
+
pathFilename.write_text(str(this), encoding='utf-8')
|