hunterMakesPy 0.2.1__tar.gz → 0.2.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 (26) hide show
  1. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/PKG-INFO +5 -6
  2. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/README.md +1 -1
  3. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/coping.py +29 -25
  4. huntermakespy-0.2.4/hunterMakesPy/dataStructures.py +278 -0
  5. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/parseParameters.py +28 -24
  6. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/tests/test_dataStructures.py +115 -120
  7. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy.egg-info/PKG-INFO +5 -6
  8. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy.egg-info/requires.txt +0 -3
  9. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/pyproject.toml +2 -3
  10. huntermakespy-0.2.1/hunterMakesPy/dataStructures.py +0 -268
  11. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/LICENSE +0 -0
  12. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/__init__.py +0 -0
  13. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/_theSSOT.py +0 -0
  14. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/filesystemToolkit.py +0 -0
  15. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/py.typed +0 -0
  16. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/pytestForYourUse.py +0 -0
  17. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/tests/__init__.py +0 -0
  18. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/tests/conftest.py +0 -0
  19. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/tests/test_coping.py +0 -0
  20. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/tests/test_filesystemToolkit.py +0 -0
  21. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/tests/test_parseParameters.py +0 -0
  22. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy/theTypes.py +0 -0
  23. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy.egg-info/SOURCES.txt +0 -0
  24. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy.egg-info/dependency_links.txt +0 -0
  25. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/hunterMakesPy.egg-info/top_level.txt +0 -0
  26. {huntermakespy-0.2.1 → huntermakespy-0.2.4}/setup.cfg +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hunterMakesPy
3
- Version: 0.2.1
3
+ Version: 0.2.4
4
4
  Summary: Easy Python functions making making functional Python functions easier.
5
5
  Author-email: Hunter Hogan <HunterHogan@pm.me>
6
6
  License: CC-BY-NC-4.0
7
7
  Project-URL: Donate, https://www.patreon.com/integrated
8
- Project-URL: Homepage, https://github.com/hunterhogan/
9
- Project-URL: Issues, https://github.com/hunterhogan/
10
- Project-URL: Repository, https://github.com/hunterhogan/
8
+ Project-URL: Homepage, https://github.com/hunterhogan/hunterMakesPy
9
+ Project-URL: Issues, https://github.com/hunterhogan/hunterMakesPy/issues
10
+ Project-URL: Repository, https://github.com/hunterhogan/hunterMakesPy
11
11
  Keywords: attribute loading,concurrency limit,configuration,defensive programming,dictionary merging,directory creation,dynamic import,error propagation,file system utilities,input validation,integer parsing,module loading,nested data structures,package settings,parameter validation,pytest,string extraction,test utilities
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Environment :: Console
@@ -33,7 +33,6 @@ License-File: LICENSE
33
33
  Requires-Dist: charset_normalizer
34
34
  Requires-Dist: more_itertools
35
35
  Requires-Dist: numpy
36
- Requires-Dist: python_minifier; python_version < "3.14"
37
36
  Provides-Extra: development
38
37
  Requires-Dist: mypy; extra == "development"
39
38
  Requires-Dist: pyupgrade; extra == "development"
@@ -174,4 +173,4 @@ Coding One Step at a Time:
174
173
  2. Write good code.
175
174
  3. When revising, write better code.
176
175
 
177
- [![CC-BY-NC-4.0](https://github.com/hunterhogan/hunterMakesPy/blob/main/CC-BY-NC-4.0.svg)](https://creativecommons.org/licenses/by-nc/4.0/)
176
+ [![CC-BY-NC-4.0](https://github.com/hunterhogan/hunterMakesPy/blob/main/CC-BY-NC-4.0.png)](https://creativecommons.org/licenses/by-nc/4.0/)
@@ -128,4 +128,4 @@ Coding One Step at a Time:
128
128
  2. Write good code.
129
129
  3. When revising, write better code.
130
130
 
131
- [![CC-BY-NC-4.0](https://github.com/hunterhogan/hunterMakesPy/blob/main/CC-BY-NC-4.0.svg)](https://creativecommons.org/licenses/by-nc/4.0/)
131
+ [![CC-BY-NC-4.0](https://github.com/hunterhogan/hunterMakesPy/blob/main/CC-BY-NC-4.0.png)](https://creativecommons.org/licenses/by-nc/4.0/)
@@ -36,14 +36,14 @@ class PackageSettings:
36
36
  package identifiers and installation paths if they are not passed to the `class` constructor. Python `dataclasses` are easy to
37
37
  subtype and extend.
38
38
 
39
- Parameters
39
+ Attributes
40
40
  ----------
41
41
  identifierPackageFALLBACK : str = ''
42
42
  Fallback package identifier used only during initialization when automatic discovery fails.
43
- pathPackage : Path = Path()
44
- Absolute path to the installed package directory. Automatically resolved from `identifierPackage` if not provided.
45
43
  identifierPackage : str = ''
46
- Canonical name of the package. Automatically extracted from `pyproject.toml`.
44
+ Canonical name of the package. Automatically extracted from "pyproject.toml".
45
+ pathPackage : Path = getPathPackageINSTALLING(identifierPackage)
46
+ Absolute path to the installed package directory. Automatically resolved from `identifierPackage` if not provided.
47
47
  fileExtension : str = '.py'
48
48
  Default file extension.
49
49
 
@@ -85,34 +85,39 @@ class PackageSettings:
85
85
  if self.pathPackage == Path() and self.identifierPackage:
86
86
  self.pathPackage = getPathPackageINSTALLING(self.identifierPackage)
87
87
 
88
- def raiseIfNone(returnTarget: TypeSansNone | None, errorMessage: str | None = None) -> TypeSansNone:
89
- """Raise a `ValueError` if the target value is `None`, otherwise return the value: tell the type checker that the return value is not `None`.
90
-
91
- (AI generated docstring)
88
+ def raiseIfNone(expression: TypeSansNone | None, errorMessage: str | None = None) -> TypeSansNone:
89
+ """Convert the `expression` return annotation from '`cerPytainty | None`' to '`cerPytainty`' because `expression` cannot be `None`; `raise` an `Exception` if you're wrong.
92
90
 
93
- 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.
91
+ The Python interpreter evaluates `expression` to a value: think of a function call or an attribute access. You can use
92
+ `raiseIfNone` for fail early defensive programming. I use it, however, to cure type-checker-nihilism: that's when "or `None`"
93
+ return types cause your type checker to repeatedly say, "You can't do that because the value might be `None`."
94
94
 
95
95
  Parameters
96
96
  ----------
97
- returnTarget : TypeSansNone | None
98
- The value to check for `None`. If not `None`, this value is returned unchanged.
99
- errorMessage : str | None = None
100
- Custom error message to include in the `ValueError`. If `None`, a default message with debugging hints is used.
97
+ expression : TypeSansNone | None
98
+ Python code with a return type that is a `union` of `None` and `TypeSansNone`, which is a stand-in for one or more other types.
99
+ errorMessage : str | None = 'A function unexpectedly returned `None`. Hint: look at the traceback immediately before `raiseIfNone`.'
100
+ Custom error message for the `ValueError` `Exception` if `expression` is `None`.
101
101
 
102
102
  Returns
103
103
  -------
104
- returnTarget : TypeSansNone
105
- The original `returnTarget` value, guaranteed to not be `None`.
104
+ contentment : TypeSansNone
105
+ The value returned by `expression`, but guaranteed to not be `None`.
106
106
 
107
107
  Raises
108
108
  ------
109
109
  ValueError
110
- If `returnTarget` is `None`.
110
+ If the value returned by `expression` is `None`.
111
111
 
112
112
  Examples
113
113
  --------
114
- Ensure a function result is not `None`:
114
+ Basic usage with attribute access:
115
+ ```python
116
+ annotation = raiseIfNone(ast_arg.annotation)
117
+ # Raises ValueError if ast_arg.annotation is None
118
+ ```
115
119
 
120
+ Function return value validation:
116
121
  ```python
117
122
  def findFirstMatch(listItems: list[str], pattern: str) -> str | None:
118
123
  for item in listItems:
@@ -122,28 +127,27 @@ def raiseIfNone(returnTarget: TypeSansNone | None, errorMessage: str | None = No
122
127
 
123
128
  listFiles = ['document.txt', 'image.png', 'data.csv']
124
129
  filename = raiseIfNone(findFirstMatch(listFiles, '.txt'))
125
- # Returns 'document.txt'
130
+ # Returns 'document.txt' when match exists
126
131
  ```
127
132
 
128
- Handle dictionary lookups with custom error messages:
129
-
133
+ Dictionary value retrieval with custom message:
130
134
  ```python
131
135
  configurationMapping = {'host': 'localhost', 'port': 8080}
132
136
  host = raiseIfNone(configurationMapping.get('host'),
133
137
  "Configuration must include 'host' setting")
134
- # Returns 'localhost'
138
+ # Returns 'localhost' when key exists
135
139
 
136
140
  # This would raise ValueError with custom message:
137
141
  # database = raiseIfNone(configurationMapping.get('database'),
138
- # "Configuration must include 'database' setting")
142
+ # "Configuration must include 'database' setting")
139
143
  ```
140
144
 
141
145
  Thanks
142
146
  ------
143
- sobolevn, https://github.com/sobolevn, for the seed of the function. https://github.com/python/typing/discussions/1997#discussioncomment-13108399
147
+ sobolevn, https://github.com/sobolevn, for the seed of this function. https://github.com/python/typing/discussions/1997#discussioncomment-13108399
144
148
 
145
149
  """
146
- if returnTarget is None:
150
+ if expression is None:
147
151
  message = errorMessage or 'A function unexpectedly returned `None`. Hint: look at the traceback immediately before `raiseIfNone`.'
148
152
  raise ValueError(message)
149
- return returnTarget
153
+ return expression
@@ -0,0 +1,278 @@
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 re as regex
9
+
10
+ def removeExtraWhitespace(text: str) -> str:
11
+ """Remove extra whitespace from string representation of Python data structures.
12
+
13
+ This function replaces python_minifier.minify() for the specific use case of
14
+ minimizing string representations of lists, tuples, ranges, etc. It removes
15
+ spaces after commas, around brackets and parentheses.
16
+ """
17
+ # Remove spaces after commas
18
+ text = regex.sub(r',\s+', ',', text)
19
+ # Remove spaces after opening brackets/parens
20
+ text = regex.sub(r'([\[\(])\s+', r'\1', text)
21
+ # Remove spaces before closing brackets/parens
22
+ return regex.sub(r'\s+([\]\)])', r'\1', text)
23
+
24
+ def autoDecodingRLE(arrayTarget: NDArray[integer[Any]], *, assumeAddSpaces: bool = False) -> str:
25
+ """Transform a NumPy array into a compact, self-decoding run-length encoded string representation.
26
+
27
+ This function converts a NumPy array into a string that, when evaluated as Python code,
28
+ recreates the original array structure. The function employs two compression strategies:
29
+ 1. Python's `range` syntax for consecutive integer sequences
30
+ 2. Multiplication syntax for repeated elements
31
+
32
+ The resulting string representation is designed to be both human-readable and space-efficient,
33
+ especially for large cartesian mappings with repetitive patterns. When this string is used
34
+ as a data source, Python will automatically decode it into Python `list`, which if used as an
35
+ argument to `numpy.array()`, will recreate the original array structure.
36
+
37
+ Parameters
38
+ ----------
39
+ arrayTarget : NDArray[integer[Any]]
40
+ (array2target) The NumPy array to be encoded.
41
+ assumeAddSpaces : bool = False
42
+ (assume2add2spaces) Affects internal length comparison during compression decisions.
43
+ This parameter doesn't directly change output format but influences whether
44
+ `range` or multiplication syntax is preferred in certain cases. The parameter
45
+ exists because the Abstract Syntax Tree (AST) inserts spaces in its string
46
+ representation.
47
+
48
+ Returns
49
+ -------
50
+ rleString : str
51
+ (rle2string) A string representation of the array using run-length encoding that,
52
+ when evaluated as Python code, reproduces the original array structure.
53
+
54
+ Notes
55
+ -----
56
+ The "autoDecoding" feature means that the string representation evaluates directly
57
+ to the desired data structure without explicit decompression steps.
58
+
59
+ """
60
+ def sliceNDArrayToNestedLists(arraySlice: NDArray[integer[Any]]) -> Any:
61
+ def getLengthOption(optionAsStr: str) -> int:
62
+ """`assumeAddSpaces` characters: `,` 1; `]*` 2."""
63
+ return assumeAddSpaces * (optionAsStr.count(',') + optionAsStr.count(']*') * 2) + len(optionAsStr)
64
+
65
+ if arraySlice.ndim > 1:
66
+ axisOfOperation = 0
67
+ return [sliceNDArrayToNestedLists(arraySlice[index]) for index in range(arraySlice.shape[axisOfOperation])]
68
+ if arraySlice.ndim == 1:
69
+ arraySliceAsList: list[int | range] = []
70
+ cache_consecutiveGroup_addMe: dict[Iterator[Any], list[int] | list[range]] = {}
71
+ for consecutiveGroup in more_itertools.consecutive_groups(arraySlice.tolist()):
72
+ if consecutiveGroup in cache_consecutiveGroup_addMe:
73
+ addMe = cache_consecutiveGroup_addMe[consecutiveGroup]
74
+ else:
75
+ ImaSerious: list[int] = list(consecutiveGroup)
76
+ ImaRange = [range(ImaSerious[0], ImaSerious[-1] + 1)]
77
+ ImaRangeAsStr = removeExtraWhitespace(str(ImaRange)).replace('range(0,', 'range(').replace('range', '*range')
78
+
79
+ option1 = ImaRange
80
+ option1AsStr = ImaRangeAsStr
81
+ option2 = ImaSerious
82
+ option2AsStr = None
83
+
84
+ # alpha, potential function
85
+ option1AsStr = option1AsStr or removeExtraWhitespace(str(option1))
86
+ lengthOption1 = getLengthOption(option1AsStr)
87
+
88
+ option2AsStr = option2AsStr or removeExtraWhitespace(str(option2))
89
+ lengthOption2 = getLengthOption(option2AsStr)
90
+
91
+ if lengthOption1 < lengthOption2:
92
+ addMe = option1
93
+ else:
94
+ addMe = option2
95
+
96
+ cache_consecutiveGroup_addMe[consecutiveGroup] = addMe
97
+
98
+ arraySliceAsList += addMe
99
+
100
+ listRangeAndTuple: list[int | range | tuple[int | range, int]] = []
101
+ cache_malkovichGrouped_addMe: dict[tuple[int | range, int], list[tuple[int | range, int]] | list[int | range]] = {}
102
+ for malkovichGrouped in more_itertools.run_length.encode(arraySliceAsList):
103
+ if malkovichGrouped in cache_malkovichGrouped_addMe:
104
+ addMe = cache_malkovichGrouped_addMe[malkovichGrouped]
105
+ else:
106
+ lengthMalkovich = malkovichGrouped[-1]
107
+ malkovichAsList = list(more_itertools.run_length.decode([malkovichGrouped]))
108
+ malkovichMalkovich = f"[{malkovichGrouped[0]}]*{lengthMalkovich}"
109
+
110
+ option1 = [malkovichGrouped]
111
+ option1AsStr = malkovichMalkovich
112
+ option2 = malkovichAsList
113
+ option2AsStr = None
114
+
115
+ # beta, potential function
116
+ option1AsStr = option1AsStr or removeExtraWhitespace(str(option1))
117
+ lengthOption1 = getLengthOption(option1AsStr)
118
+
119
+ option2AsStr = option2AsStr or removeExtraWhitespace(str(option2))
120
+ lengthOption2 = getLengthOption(option2AsStr)
121
+
122
+ if lengthOption1 < lengthOption2:
123
+ addMe = option1
124
+ else:
125
+ addMe = option2
126
+
127
+ cache_malkovichGrouped_addMe[malkovichGrouped] = addMe
128
+
129
+ listRangeAndTuple += addMe
130
+
131
+ return listRangeAndTuple
132
+ return arraySlice
133
+
134
+ arrayAsNestedLists = sliceNDArrayToNestedLists(arrayTarget)
135
+
136
+ arrayAsStr = removeExtraWhitespace(str(arrayAsNestedLists))
137
+
138
+ patternRegex = regex.compile(
139
+ "(?<!rang)(?:"
140
+ # Pattern 1: Comma ahead, bracket behind # noqa: ERA001
141
+ "(?P<joinAhead>,)\\((?P<malkovich>\\d+),(?P<multiply>\\d+)\\)(?P<bracketBehind>])|"
142
+ # Pattern 2: Bracket or start ahead, comma behind # noqa: ERA001
143
+ "(?P<bracketOrStartAhead>\\[|^.)\\((?P<malkovichMalkovich>\\d+),(?P<multiplyIDK>\\d+)\\)(?P<joinBehind>,)|"
144
+ # Pattern 3: Bracket ahead, bracket behind # noqa: ERA001
145
+ "(?P<bracketAhead>\\[)\\((?P<malkovichMalkovichMalkovich>\\d+),(?P<multiply_whatever>\\d+)\\)(?P<bracketBehindBracketBehind>])|"
146
+ # Pattern 4: Comma ahead, comma behind # noqa: ERA001
147
+ "(?P<joinAheadJoinAhead>,)\\((?P<malkovichMalkovichMalkovichMalkovich>\\d+),(?P<multiplyOrSomething>\\d+)\\)(?P<joinBehindJoinBehind>,)"
148
+ ")"
149
+ )
150
+
151
+ def replacementByContext(match: regex.Match[str]) -> str:
152
+ """Generate replacement string based on context patterns."""
153
+ elephino = match.groupdict()
154
+ joinAhead = elephino.get('joinAhead') or elephino.get('joinAheadJoinAhead')
155
+ malkovich = elephino.get('malkovich') or elephino.get('malkovichMalkovich') or elephino.get('malkovichMalkovichMalkovich') or elephino.get('malkovichMalkovichMalkovichMalkovich')
156
+ multiply = elephino.get('multiply') or elephino.get('multiplyIDK') or elephino.get('multiply_whatever') or elephino.get('multiplyOrSomething')
157
+ joinBehind = elephino.get('joinBehind') or elephino.get('joinBehindJoinBehind')
158
+
159
+ replaceAhead = "]+[" if joinAhead == "," else "["
160
+
161
+ replaceBehind = "+[" if joinBehind == "," else ""
162
+
163
+ return f"{replaceAhead}{malkovich}]*{multiply}{replaceBehind}"
164
+
165
+ arrayAsStr = patternRegex.sub(replacementByContext, arrayAsStr)
166
+ arrayAsStr = patternRegex.sub(replacementByContext, arrayAsStr)
167
+
168
+ # Replace `range(0,stop)` syntax with `range(stop)` syntax. # noqa: ERA001
169
+ # Add unpack operator `*` for automatic decoding when evaluated.
170
+ return arrayAsStr.replace('range(0,', 'range(').replace('range', '*range')
171
+
172
+ def stringItUp(*scrapPile: Any) -> list[str]:
173
+ """Convert, if possible, every element in the input data structure to a string.
174
+
175
+ Order is not preserved or readily predictable.
176
+
177
+ Parameters
178
+ ----------
179
+ *scrapPile : Any
180
+ (scrap2pile) One or more data structures to unpack and convert to strings.
181
+
182
+ Returns
183
+ -------
184
+ listStrungUp : list[str]
185
+ (list2strung2up) A `list` of string versions of all convertible elements.
186
+
187
+ """
188
+ scrap = None
189
+ listStrungUp: list[str] = []
190
+
191
+ def drill(KitKat: Any) -> None:
192
+ match KitKat:
193
+ case str():
194
+ listStrungUp.append(KitKat)
195
+ case bool() | bytearray() | bytes() | complex() | float() | int() | memoryview() | None:
196
+ listStrungUp.append(str(KitKat)) # pyright: ignore [reportUnknownArgumentType]
197
+ case dict():
198
+ for broken, piece in KitKat.items(): # pyright: ignore [reportUnknownVariableType]
199
+ drill(broken)
200
+ drill(piece)
201
+ case list() | tuple() | set() | frozenset() | range():
202
+ for kit in KitKat: # pyright: ignore [reportUnknownVariableType]
203
+ drill(kit)
204
+ case _:
205
+ if hasattr(KitKat, '__iter__'): # Unpack other iterables
206
+ for kat in KitKat:
207
+ drill(kat)
208
+ else:
209
+ try:
210
+ sharingIsCaring = KitKat.__str__()
211
+ listStrungUp.append(sharingIsCaring)
212
+ except AttributeError:
213
+ pass
214
+ 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."
215
+ pass
216
+ except:
217
+ print(f"\nWoah! I received '{repr(KitKat)}'.\nTheir report card says, 'Plays well with others: Needs improvement.'\n") # noqa: T201
218
+ raise
219
+ try:
220
+ for scrap in scrapPile:
221
+ drill(scrap)
222
+ except RecursionError:
223
+ listStrungUp.append(repr(scrap))
224
+ return listStrungUp
225
+
226
+ def updateExtendPolishDictionaryLists(*dictionaryLists: Mapping[str, list[Any] | set[Any] | tuple[Any, ...]], destroyDuplicates: bool = False, reorderLists: bool = False, killErroneousDataTypes: bool = False) -> dict[str, list[Any]]:
227
+ """Merge multiple dictionaries containing `list` into a single dictionary.
228
+
229
+ With options to handle duplicates, `list` ordering, and erroneous data types.
230
+
231
+ Parameters
232
+ ----------
233
+ *dictionaryLists : Mapping[str, list[Any] | set[Any] | tuple[Any, ...]]
234
+ (dictionary2lists) Variable number of dictionaries to be merged. If only one dictionary is passed, it will be processed based on the provided options.
235
+ destroyDuplicates : bool = False
236
+ (destroy2duplicates) If `True`, removes duplicate elements from the `list`. Defaults to `False`.
237
+ reorderLists : bool = False
238
+ (reorder2lists) If `True`, sorts the `list`. Defaults to `False`.
239
+ killErroneousDataTypes : bool = False
240
+ (kill2erroneous2data2types) If `True`, skips dictionary keys or dictionary values that cause a `TypeError` during merging. Defaults to `False`.
241
+
242
+ Returns
243
+ -------
244
+ ePluribusUnum : dict[str, list[Any]]
245
+ (e2pluribus2unum) A single dictionary with merged `list` based on the provided options. If only one dictionary is passed,
246
+ it will be cleaned up based on the options.
247
+
248
+ Notes
249
+ -----
250
+ The returned value, `ePluribusUnum`, is a so-called primitive dictionary (`dict`). Furthermore, every dictionary key is a
251
+ so-called primitive string (cf. `str()`) and every dictionary value is a so-called primitive `list` (`list`). If
252
+ `dictionaryLists` has other data types, the data types will not be preserved. That could have unexpected consequences.
253
+ Conversion from the original data type to a `list`, for example, may not preserve the order even if you want the order to be
254
+ preserved.
255
+
256
+ """
257
+ ePluribusUnum: dict[str, list[Any]] = {}
258
+
259
+ for dictionaryListTarget in dictionaryLists:
260
+ for keyName, keyValue in dictionaryListTarget.items():
261
+ try:
262
+ ImaStr = str(keyName)
263
+ ImaList = list(keyValue)
264
+ ePluribusUnum.setdefault(ImaStr, []).extend(ImaList)
265
+ except TypeError:
266
+ if killErroneousDataTypes:
267
+ continue
268
+ else:
269
+ raise
270
+
271
+ if destroyDuplicates:
272
+ for ImaStr, ImaList in ePluribusUnum.items():
273
+ ePluribusUnum[ImaStr] = list(dict.fromkeys(ImaList))
274
+ if reorderLists:
275
+ for ImaStr, ImaList in ePluribusUnum.items():
276
+ ePluribusUnum[ImaStr] = sorted(ImaList)
277
+
278
+ return ePluribusUnum
@@ -75,7 +75,7 @@ def _constructErrorMessage(context: ErrorMessageContext, parameterName: str, par
75
75
 
76
76
  return "".join(messageParts)
77
77
 
78
- def defineConcurrencyLimit(*, limit: bool | float | int | None, cpuTotal: int = multiprocessing.cpu_count()) -> int: # noqa: C901, PYI041
78
+ def defineConcurrencyLimit(*, limit: bool | float | int | None, cpuTotal: int = multiprocessing.cpu_count()) -> int:
79
79
  """Determine the concurrency limit based on the provided parameter.
80
80
 
81
81
  Tests for this function can be run with:
@@ -84,8 +84,7 @@ def defineConcurrencyLimit(*, limit: bool | float | int | None, cpuTotal: int =
84
84
  Parameters
85
85
  ----------
86
86
  limit : bool | float | int | None
87
- Whether and how to limit CPU usage. Accepts `True`/`False`, an integer count, or a fraction of total CPUs. Positive and
88
- negative values have different behaviors, see code for details.
87
+ Whether and how to limit CPU usage. See notes and examples for details how to describe the options to your users.
89
88
  cpuTotal : int = multiprocessing.cpu_count()
90
89
  The total number of CPUs available in the system. Default is `multiprocessing.cpu_count()`.
91
90
 
@@ -96,35 +95,40 @@ def defineConcurrencyLimit(*, limit: bool | float | int | None, cpuTotal: int =
96
95
 
97
96
  Notes
98
97
  -----
99
- If you want to be extra nice to your users, consider using `hunterMakesPy.oopsieKwargsie()` to handle malformed inputs. For
100
- example:
98
+ Consider using `hunterMakesPy.oopsieKwargsie()` to handle malformed inputs. For example:
101
99
 
102
100
  ```
103
101
  if not (CPUlimit is None or isinstance(CPUlimit, (bool, int, float))):
104
102
  CPUlimit = oopsieKwargsie(CPUlimit)
105
103
  ```
106
104
 
107
- Examples
108
- --------
109
- Example parameters:
110
- CPUlimit: bool | float | int | None
111
- CPUlimit: bool | float | int | None = None
105
+ Example parameters
106
+ ------------------
107
+ ```python
108
+ CPUlimit: bool | float | int | None
109
+ CPUlimit: bool | float | int | None = None
110
+ ```
112
111
 
113
- Example docstring:
114
- ```python
112
+ Example docstring
113
+ -----------------
114
+ ```python
115
115
 
116
- Arguments:
117
- CPUlimit: bool | float | int | None
118
- whether and how to limit the CPU usage. See notes for details.
116
+ Arguments
117
+ ---------
118
+ CPUlimit: bool | float | int | None
119
+ Whether and how to limit the the number of available processors used by the function. See notes for details.
119
120
 
120
- Limits on CPU usage `CPUlimit`:
121
- - `False`, `None`, or `0`: No limits on CPU usage; uses all available CPUs. All other values will potentially limit CPU usage.
122
- - `True`: Yes, limit the CPU usage; limits to 1 CPU.
123
- - Integer `>= 1`: Limits usage to the specified number of CPUs.
124
- - Decimal value (`float`) between 0 and 1: Fraction of total CPUs to use.
125
- - Decimal value (`float`) between -1 and 0: Fraction of CPUs to *not* use.
126
- - Integer `<= -1`: Subtract the absolute value from total CPUs.
127
- ```
121
+ Notes
122
+ -----
123
+ Limits on CPU usage, `CPUlimit`:
124
+ - `False`, `None`, or `0`: No limits on processor usage; uses all available processors. All other values will potentially limit processor usage.
125
+ - `True`: Yes, limit the processor usage; limits to 1 processor.
126
+ - `int >= 1`: The maximum number of available processors to use.
127
+ - `0 < float < 1`: The maximum number of processors to use expressed as a fraction of available processors.
128
+ - `-1 < float < 0`: The number of processors to *not* use expressed as a fraction of available processors.
129
+ - `int <= -1`: The number of available processors to *not* use.
130
+ - If the value of `CPUlimit` is a `float` greater than 1 or less than -1, the function truncates the value to an `int` with the same sign as the `float`.
131
+ ```
128
132
 
129
133
  """
130
134
  concurrencyLimit = cpuTotal
@@ -160,7 +164,7 @@ def defineConcurrencyLimit(*, limit: bool | float | int | None, cpuTotal: int =
160
164
  return max(int(concurrencyLimit), 1)
161
165
 
162
166
  # ruff: noqa: TRY301
163
- def intInnit(listInt_Allegedly: Iterable[Any], parameterName: str | None = None, parameterType: type[Any] | None = None) -> list[int]: # noqa: C901, PLR0912, PLR0915
167
+ def intInnit(listInt_Allegedly: Iterable[Any], parameterName: str | None = None, parameterType: type[Any] | None = None) -> list[int]:
164
168
  """Validate and convert input values to a `list` of integers.
165
169
 
166
170
  Accepts various numeric types and attempts to convert them into integers while providing descriptive error messages. This
@@ -2,7 +2,7 @@
2
2
  from collections.abc import Callable, Iterable, Iterator
3
3
  from decimal import Decimal
4
4
  from fractions import Fraction
5
- from hunterMakesPy import stringItUp, updateExtendPolishDictionaryLists
5
+ from hunterMakesPy import autoDecodingRLE, stringItUp, updateExtendPolishDictionaryLists
6
6
  from hunterMakesPy.tests.conftest import standardizedEqualTo
7
7
  from numpy.typing import NDArray
8
8
  from typing import Any, Literal
@@ -11,9 +11,6 @@ import numpy
11
11
  import pytest
12
12
  import sys
13
13
 
14
- if sys.version_info < (3, 14):
15
- from hunterMakesPy import autoDecodingRLE
16
-
17
14
  class CustomIterable:
18
15
  def __init__(self, items: Iterable[Any]) -> None: self.items = items
19
16
  def __iter__(self) -> Iterator[Any]: return iter(self.items)
@@ -99,27 +96,26 @@ def testUpdateExtendPolishDictionaryLists(description: str, value_dictionaryList
99
96
 
100
97
  # ruff: noqa: RUF005
101
98
 
102
- if sys.version_info < (3, 14):
103
- @pytest.mark.parametrize("description,value_arrayTarget,expected", [
104
- ("One range", numpy.array(list(range(50,60))), "[*range(50,60)]"),
105
- ("Value, range", numpy.array([123]+list(range(71,81))), "[123,*range(71,81)]"),
106
- ("range, value", numpy.array(list(range(91,97))+[101]), "[*range(91,97),101]"),
107
- ("Value, range, value", numpy.array([151]+list(range(163,171))+[181]), "[151,*range(163,171),181]"),
108
- ("Repeat values", numpy.array([191, 191, 191]), "[191]*3"),
109
- ("Value with repeat", numpy.array([211, 223, 223, 223]), "[211]+[223]*3"),
110
- ("Range with repeat", numpy.array(list(range(251,257))+[271, 271, 271]), "[*range(251,257)]+[271]*3"),
111
- ("Value, range, repeat", numpy.array([281]+list(range(291,297))+[307, 307]), "[281,*range(291,297)]+[307]*2"),
112
- ("repeat, value", numpy.array([313, 313, 313, 331, 331, 349]), "[313]*3+[331]*2+[349]"),
113
- ("repeat, range", numpy.array([373, 373, 373]+list(range(383,389))), "[373]*3+[*range(383,389)]"),
114
- ("repeat, range, value", numpy.array(7*[401]+list(range(409,415))+[421]), "[401]*7+[*range(409,415),421]"),
115
- ("Repeated primes", numpy.array([431, 431, 431, 443, 443, 457]), "[431]*3+[443]*2+[457]"),
116
- ("Two Ranges", numpy.array(list(range(461,471))+list(range(479,487))), "[*range(461,471),*range(479,487)]"),
117
- ("2D array primes", numpy.array([[491, 499, 503], [509, 521, 523]]), "[[491,499,503],[509,521,523]]"),
118
- ("3D array primes", numpy.array([[[541, 547], [557, 563]], [[569, 571], [577, 587]]]), "[[[541,547],[557,563]],[[569,571],[577,587]]]"),
119
- ], ids=lambda x: x if isinstance(x, str) else "")
120
- def testAutoDecodingRLE(description: str, value_arrayTarget: NDArray[numpy.integer[Any]], expected: str) -> None:
121
- """Test autoDecodingRLE with various input arrays."""
122
- standardizedEqualTo(expected, autoDecodingRLE, value_arrayTarget)
99
+ @pytest.mark.parametrize("description,value_arrayTarget,expected", [
100
+ ("One range", numpy.array(list(range(50,60))), "[*range(50,60)]"),
101
+ ("Value, range", numpy.array([123]+list(range(71,81))), "[123,*range(71,81)]"),
102
+ ("range, value", numpy.array(list(range(91,97))+[101]), "[*range(91,97),101]"),
103
+ ("Value, range, value", numpy.array([151]+list(range(163,171))+[181]), "[151,*range(163,171),181]"),
104
+ ("Repeat values", numpy.array([191, 191, 191]), "[191]*3"),
105
+ ("Value with repeat", numpy.array([211, 223, 223, 223]), "[211]+[223]*3"),
106
+ ("Range with repeat", numpy.array(list(range(251,257))+[271, 271, 271]), "[*range(251,257)]+[271]*3"),
107
+ ("Value, range, repeat", numpy.array([281]+list(range(291,297))+[307, 307]), "[281,*range(291,297)]+[307]*2"),
108
+ ("repeat, value", numpy.array([313, 313, 313, 331, 331, 349]), "[313]*3+[331]*2+[349]"),
109
+ ("repeat, range", numpy.array([373, 373, 373]+list(range(383,389))), "[373]*3+[*range(383,389)]"),
110
+ ("repeat, range, value", numpy.array(7*[401]+list(range(409,415))+[421]), "[401]*7+[*range(409,415),421]"),
111
+ ("Repeated primes", numpy.array([431, 431, 431, 443, 443, 457]), "[431]*3+[443]*2+[457]"),
112
+ ("Two Ranges", numpy.array(list(range(461,471))+list(range(479,487))), "[*range(461,471),*range(479,487)]"),
113
+ ("2D array primes", numpy.array([[491, 499, 503], [509, 521, 523]]), "[[491,499,503],[509,521,523]]"),
114
+ ("3D array primes", numpy.array([[[541, 547], [557, 563]], [[569, 571], [577, 587]]]), "[[[541,547],[557,563]],[[569,571],[577,587]]]"),
115
+ ], ids=lambda x: x if isinstance(x, str) else "")
116
+ def testAutoDecodingRLE(description: str, value_arrayTarget: NDArray[numpy.integer[Any]], expected: str) -> None:
117
+ """Test autoDecodingRLE with various input arrays."""
118
+ standardizedEqualTo(expected, autoDecodingRLE, value_arrayTarget)
123
119
 
124
120
  # Helper functions for generating RLE test data
125
121
  def generateCartesianMapping(dimensions: tuple[int, int], formula: Callable[[int, int], int]) -> NDArray[Any]:
@@ -228,98 +224,97 @@ def generateAlternatingColumns(dimensions: tuple[int, int], blockSize: int = 1)
228
224
 
229
225
  return generateCartesianMapping(dimensions, columnFormula)
230
226
 
231
- if sys.version_info < (3, 14):
232
- @pytest.mark.parametrize("description,value_arrayTarget", [
233
- # Basic test cases with simple patterns
234
- ("Simple range", numpy.array(list(range(50,60)))),
235
-
236
- # Chessboard patterns
237
- ("Small chessboard", generateChessboard((8, 8))),
238
-
239
- # Alternating columns - creates patterns with good RLE opportunities
240
- ("Alternating columns", generateAlternatingColumns((5, 20), 2)),
241
-
242
- # Step pattern - creates horizontal runs
243
- ("Step pattern", generateStepPattern((6, 30), 3)),
244
-
245
- # Repeating zones - creates horizontal bands
246
- ("Repeating zones", generateRepeatingZones((40, 40), 8)),
247
-
248
- # Tile pattern - creates complex repeating regions
249
- ("Tile pattern", generateTilePattern((15, 15), 5)),
250
-
251
- # Signed quadratic function - includes negative values
252
- ("Signed quadratic", generateSignedQuadraticFunction((10, 10))),
253
-
254
- # Prime modulo matrix - periodic patterns
255
- ("Prime modulo", generatePrimeModuloMatrix((12, 12), 7)),
256
-
257
- # Wave pattern - smooth gradients
258
- ("Wave pattern", generateWavePattern((20, 20))),
259
-
260
- # Spiral pattern - complex pattern with good RLE potential
261
- ("Spiral pattern", generateSpiralPattern((15, 15), 2)),
262
- ], ids=lambda x: x if isinstance(x, str) else "")
263
- def testAutoDecodingRLEWithRealisticData(description: str, value_arrayTarget: NDArray[numpy.integer[Any]]) -> None:
264
- """Test autoDecodingRLE with more realistic data patterns."""
265
- # Here we test the function behavior rather than expected string output
266
- resultRLE = autoDecodingRLE(value_arrayTarget)
267
-
268
- # Test that the result is a valid string
269
- assert isinstance(resultRLE, str)
270
-
271
- # Test that the result contains the expected syntax elements
272
- assert "[" in resultRLE, f"Result should contain list syntax: {resultRLE}"
273
- assert "]" in resultRLE, f"Result should contain list syntax: {resultRLE}"
274
-
275
- # Check that the result is more compact than the raw string representation
276
- rawStrLength = len(str(value_arrayTarget.tolist()))
277
- encodedLength = len(resultRLE)
278
- assert encodedLength <= rawStrLength, f"Encoded string ({encodedLength}) should be shorter than raw string ({rawStrLength})"
279
-
280
- @pytest.mark.parametrize("description,addSpaces", [
281
- ("With spaces", True),
282
- ("Without spaces", False),
283
- ], ids=lambda x: x if isinstance(x, str) else "")
284
- def testAutoDecodingRLEWithSpaces(description: str, addSpaces: bool) -> None:
285
- """Test that the addSpaces parameter affects the internal comparison logic.
286
-
287
- Note: addSpaces doesn't directly change the output format, it just changes
288
- the comparison when measuring the length of the string representation.
289
- The feature exists because `ast` inserts spaces in its string representation.
290
- """
291
- # Create a pattern that has repeated sequences to trigger the RLE logic
292
- arrayTarget = generateRepeatingZones((10, 10), 2)
293
-
294
- # Test both configurations
295
- resultWithSpacesFlag = autoDecodingRLE(arrayTarget, assumeAddSpaces=addSpaces)
296
- resultNoSpacesFlag = autoDecodingRLE(arrayTarget, assumeAddSpaces=False)
297
-
298
- # When addSpaces=True, the internal length comparisons change
299
- # but the actual output format doesn't necessarily differ
300
- # Just verify the function runs without errors in both cases
301
- assert isinstance(resultWithSpacesFlag, str)
302
- assert isinstance(resultNoSpacesFlag, str)
303
-
304
- def testAutoDecodingRLELargeCartesianMapping() -> None:
305
- """Test autoDecodingRLE with a large (100x100) cartesian mapping."""
306
- dimensions = (100, 100)
307
-
308
- # Generate a large cartesian mapping with a complex pattern
309
- def complexFormula(x: int, y: int) -> int:
310
- return ((x * 17) % 11 + (y * 13) % 7) % 10
311
-
312
- arrayMapping = generateCartesianMapping(dimensions, complexFormula)
313
-
314
- # Verify the function works with large arrays
315
- resultRLE = autoDecodingRLE(arrayMapping)
316
-
317
- # The result should be a valid string representation
318
- assert isinstance(resultRLE, str)
319
- assert "[" in resultRLE
320
- assert "]" in resultRLE
321
-
322
- # The RLE encoding should be more compact than the raw representation
323
- rawStrLength = len(str(arrayMapping.tolist()))
324
- encodedLength = len(resultRLE)
325
- assert encodedLength <= rawStrLength, f"RLE encoded string ({encodedLength}) should be shorter than raw string ({rawStrLength})"
227
+ @pytest.mark.parametrize("description,value_arrayTarget", [
228
+ # Basic test cases with simple patterns
229
+ ("Simple range", numpy.array(list(range(50,60)))),
230
+
231
+ # Chessboard patterns
232
+ ("Small chessboard", generateChessboard((8, 8))),
233
+
234
+ # Alternating columns - creates patterns with good RLE opportunities
235
+ ("Alternating columns", generateAlternatingColumns((5, 20), 2)),
236
+
237
+ # Step pattern - creates horizontal runs
238
+ ("Step pattern", generateStepPattern((6, 30), 3)),
239
+
240
+ # Repeating zones - creates horizontal bands
241
+ ("Repeating zones", generateRepeatingZones((40, 40), 8)),
242
+
243
+ # Tile pattern - creates complex repeating regions
244
+ ("Tile pattern", generateTilePattern((15, 15), 5)),
245
+
246
+ # Signed quadratic function - includes negative values
247
+ ("Signed quadratic", generateSignedQuadraticFunction((10, 10))),
248
+
249
+ # Prime modulo matrix - periodic patterns
250
+ ("Prime modulo", generatePrimeModuloMatrix((12, 12), 7)),
251
+
252
+ # Wave pattern - smooth gradients
253
+ ("Wave pattern", generateWavePattern((20, 20))),
254
+
255
+ # Spiral pattern - complex pattern with good RLE potential
256
+ ("Spiral pattern", generateSpiralPattern((15, 15), 2)),
257
+ ], ids=lambda x: x if isinstance(x, str) else "")
258
+ def testAutoDecodingRLEWithRealisticData(description: str, value_arrayTarget: NDArray[numpy.integer[Any]]) -> None:
259
+ """Test autoDecodingRLE with more realistic data patterns."""
260
+ # Here we test the function behavior rather than expected string output
261
+ resultRLE = autoDecodingRLE(value_arrayTarget)
262
+
263
+ # Test that the result is a valid string
264
+ assert isinstance(resultRLE, str)
265
+
266
+ # Test that the result contains the expected syntax elements
267
+ assert "[" in resultRLE, f"Result should contain list syntax: {resultRLE}"
268
+ assert "]" in resultRLE, f"Result should contain list syntax: {resultRLE}"
269
+
270
+ # Check that the result is more compact than the raw string representation
271
+ rawStrLength = len(str(value_arrayTarget.tolist()))
272
+ encodedLength = len(resultRLE)
273
+ assert encodedLength <= rawStrLength, f"Encoded string ({encodedLength}) should be shorter than raw string ({rawStrLength})"
274
+
275
+ @pytest.mark.parametrize("description,addSpaces", [
276
+ ("With spaces", True),
277
+ ("Without spaces", False),
278
+ ], ids=lambda x: x if isinstance(x, str) else "")
279
+ def testAutoDecodingRLEWithSpaces(description: str, addSpaces: bool) -> None:
280
+ """Test that the addSpaces parameter affects the internal comparison logic.
281
+
282
+ Note: addSpaces doesn't directly change the output format, it just changes
283
+ the comparison when measuring the length of the string representation.
284
+ The feature exists because `ast` inserts spaces in its string representation.
285
+ """
286
+ # Create a pattern that has repeated sequences to trigger the RLE logic
287
+ arrayTarget = generateRepeatingZones((10, 10), 2)
288
+
289
+ # Test both configurations
290
+ resultWithSpacesFlag = autoDecodingRLE(arrayTarget, assumeAddSpaces=addSpaces)
291
+ resultNoSpacesFlag = autoDecodingRLE(arrayTarget, assumeAddSpaces=False)
292
+
293
+ # When addSpaces=True, the internal length comparisons change
294
+ # but the actual output format doesn't necessarily differ
295
+ # Just verify the function runs without errors in both cases
296
+ assert isinstance(resultWithSpacesFlag, str)
297
+ assert isinstance(resultNoSpacesFlag, str)
298
+
299
+ def testAutoDecodingRLELargeCartesianMapping() -> None:
300
+ """Test autoDecodingRLE with a large (100x100) cartesian mapping."""
301
+ dimensions = (100, 100)
302
+
303
+ # Generate a large cartesian mapping with a complex pattern
304
+ def complexFormula(x: int, y: int) -> int:
305
+ return ((x * 17) % 11 + (y * 13) % 7) % 10
306
+
307
+ arrayMapping = generateCartesianMapping(dimensions, complexFormula)
308
+
309
+ # Verify the function works with large arrays
310
+ resultRLE = autoDecodingRLE(arrayMapping)
311
+
312
+ # The result should be a valid string representation
313
+ assert isinstance(resultRLE, str)
314
+ assert "[" in resultRLE
315
+ assert "]" in resultRLE
316
+
317
+ # The RLE encoding should be more compact than the raw representation
318
+ rawStrLength = len(str(arrayMapping.tolist()))
319
+ encodedLength = len(resultRLE)
320
+ assert encodedLength <= rawStrLength, f"RLE encoded string ({encodedLength}) should be shorter than raw string ({rawStrLength})"
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hunterMakesPy
3
- Version: 0.2.1
3
+ Version: 0.2.4
4
4
  Summary: Easy Python functions making making functional Python functions easier.
5
5
  Author-email: Hunter Hogan <HunterHogan@pm.me>
6
6
  License: CC-BY-NC-4.0
7
7
  Project-URL: Donate, https://www.patreon.com/integrated
8
- Project-URL: Homepage, https://github.com/hunterhogan/
9
- Project-URL: Issues, https://github.com/hunterhogan/
10
- Project-URL: Repository, https://github.com/hunterhogan/
8
+ Project-URL: Homepage, https://github.com/hunterhogan/hunterMakesPy
9
+ Project-URL: Issues, https://github.com/hunterhogan/hunterMakesPy/issues
10
+ Project-URL: Repository, https://github.com/hunterhogan/hunterMakesPy
11
11
  Keywords: attribute loading,concurrency limit,configuration,defensive programming,dictionary merging,directory creation,dynamic import,error propagation,file system utilities,input validation,integer parsing,module loading,nested data structures,package settings,parameter validation,pytest,string extraction,test utilities
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Environment :: Console
@@ -33,7 +33,6 @@ License-File: LICENSE
33
33
  Requires-Dist: charset_normalizer
34
34
  Requires-Dist: more_itertools
35
35
  Requires-Dist: numpy
36
- Requires-Dist: python_minifier; python_version < "3.14"
37
36
  Provides-Extra: development
38
37
  Requires-Dist: mypy; extra == "development"
39
38
  Requires-Dist: pyupgrade; extra == "development"
@@ -174,4 +173,4 @@ Coding One Step at a Time:
174
173
  2. Write good code.
175
174
  3. When revising, write better code.
176
175
 
177
- [![CC-BY-NC-4.0](https://github.com/hunterhogan/hunterMakesPy/blob/main/CC-BY-NC-4.0.svg)](https://creativecommons.org/licenses/by-nc/4.0/)
176
+ [![CC-BY-NC-4.0](https://github.com/hunterhogan/hunterMakesPy/blob/main/CC-BY-NC-4.0.png)](https://creativecommons.org/licenses/by-nc/4.0/)
@@ -2,9 +2,6 @@ charset_normalizer
2
2
  more_itertools
3
3
  numpy
4
4
 
5
- [:python_version < "3.14"]
6
- python_minifier
7
-
8
5
  [development]
9
6
  mypy
10
7
  pyupgrade
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hunterMakesPy"
3
- version = "0.2.1"
3
+ version = "0.2.4"
4
4
  description = "Easy Python functions making making functional Python functions easier."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -46,12 +46,11 @@ classifiers = [
46
46
  "Topic :: Utilities",
47
47
  "Typing :: Typed",
48
48
  ]
49
- urls = { Donate = "https://www.patreon.com/integrated", Homepage = "https://github.com/hunterhogan/", Issues = "https://github.com/hunterhogan/", Repository = "https://github.com/hunterhogan/" }
49
+ urls = { Donate = "https://www.patreon.com/integrated", Homepage = "https://github.com/hunterhogan/hunterMakesPy", Issues = "https://github.com/hunterhogan/hunterMakesPy/issues", Repository = "https://github.com/hunterhogan/hunterMakesPy" }
50
50
  dependencies = [
51
51
  "charset_normalizer",
52
52
  "more_itertools",
53
53
  "numpy",
54
- "python_minifier; python_version < '3.14'",
55
54
  ]
56
55
  optional-dependencies = { development = [
57
56
  "mypy",
@@ -1,268 +0,0 @@
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 re as regex
9
- import sys
10
-
11
- if sys.version_info < (3, 14):
12
- import python_minifier
13
-
14
- def autoDecodingRLE(arrayTarget: NDArray[integer[Any]], *, assumeAddSpaces: bool = False) -> str: # noqa: C901, PLR0915
15
- """Transform a NumPy array into a compact, self-decoding run-length encoded string representation.
16
-
17
- This function converts a NumPy array into a string that, when evaluated as Python code,
18
- recreates the original array structure. The function employs two compression strategies:
19
- 1. Python's `range` syntax for consecutive integer sequences
20
- 2. Multiplication syntax for repeated elements
21
-
22
- The resulting string representation is designed to be both human-readable and space-efficient,
23
- especially for large cartesian mappings with repetitive patterns. When this string is used
24
- as a data source, Python will automatically decode it into Python `list`, which if used as an
25
- argument to `numpy.array()`, will recreate the original array structure.
26
-
27
- Parameters
28
- ----------
29
- arrayTarget : NDArray[integer[Any]]
30
- (array2target) The NumPy array to be encoded.
31
- assumeAddSpaces : bool = False
32
- (assume2add2spaces) Affects internal length comparison during compression decisions.
33
- This parameter doesn't directly change output format but influences whether
34
- `range` or multiplication syntax is preferred in certain cases. The parameter
35
- exists because the Abstract Syntax Tree (AST) inserts spaces in its string
36
- representation.
37
-
38
- Returns
39
- -------
40
- rleString : str
41
- (rle2string) A string representation of the array using run-length encoding that,
42
- when evaluated as Python code, reproduces the original array structure.
43
-
44
- Notes
45
- -----
46
- The "autoDecoding" feature means that the string representation evaluates directly
47
- to the desired data structure without explicit decompression steps.
48
-
49
- """
50
- def sliceNDArrayToNestedLists(arraySlice: NDArray[integer[Any]]) -> Any:
51
- def getLengthOption(optionAsStr: str) -> int:
52
- """`assumeAddSpaces` characters: `,` 1; `]*` 2."""
53
- return assumeAddSpaces * (optionAsStr.count(',') + optionAsStr.count(']*') * 2) + len(optionAsStr)
54
-
55
- if arraySlice.ndim > 1:
56
- axisOfOperation = 0
57
- return [sliceNDArrayToNestedLists(arraySlice[index]) for index in range(arraySlice.shape[axisOfOperation])]
58
- if arraySlice.ndim == 1:
59
- arraySliceAsList: list[int | range] = []
60
- cache_consecutiveGroup_addMe: dict[Iterator[Any], list[int] | list[range]] = {}
61
- for consecutiveGroup in more_itertools.consecutive_groups(arraySlice.tolist()):
62
- if consecutiveGroup in cache_consecutiveGroup_addMe:
63
- addMe = cache_consecutiveGroup_addMe[consecutiveGroup]
64
- else:
65
- ImaSerious: list[int] = list(consecutiveGroup)
66
- ImaRange = [range(ImaSerious[0], ImaSerious[-1] + 1)]
67
- ImaRangeAsStr = python_minifier.minify(str(ImaRange)).replace('range(0,', 'range(').replace('range', '*range')
68
-
69
- option1 = ImaRange
70
- option1AsStr = ImaRangeAsStr
71
- option2 = ImaSerious
72
- option2AsStr = None
73
-
74
- # alpha, potential function
75
- option1AsStr = option1AsStr or python_minifier.minify(str(option1))
76
- lengthOption1 = getLengthOption(option1AsStr)
77
-
78
- option2AsStr = option2AsStr or python_minifier.minify(str(option2))
79
- lengthOption2 = getLengthOption(option2AsStr)
80
-
81
- if lengthOption1 < lengthOption2:
82
- addMe = option1
83
- else:
84
- addMe = option2
85
-
86
- cache_consecutiveGroup_addMe[consecutiveGroup] = addMe
87
-
88
- arraySliceAsList += addMe
89
-
90
- listRangeAndTuple: list[int | range | tuple[int | range, int]] = []
91
- cache_malkovichGrouped_addMe: dict[tuple[int | range, int], list[tuple[int | range, int]] | list[int | range]] = {}
92
- for malkovichGrouped in more_itertools.run_length.encode(arraySliceAsList):
93
- if malkovichGrouped in cache_malkovichGrouped_addMe:
94
- addMe = cache_malkovichGrouped_addMe[malkovichGrouped]
95
- else:
96
- lengthMalkovich = malkovichGrouped[-1]
97
- malkovichAsList = list(more_itertools.run_length.decode([malkovichGrouped]))
98
- malkovichMalkovich = f"[{malkovichGrouped[0]}]*{lengthMalkovich}"
99
-
100
- option1 = [malkovichGrouped]
101
- option1AsStr = malkovichMalkovich
102
- option2 = malkovichAsList
103
- option2AsStr = None
104
-
105
- # beta, potential function
106
- option1AsStr = option1AsStr or python_minifier.minify(str(option1))
107
- lengthOption1 = getLengthOption(option1AsStr)
108
-
109
- option2AsStr = option2AsStr or python_minifier.minify(str(option2))
110
- lengthOption2 = getLengthOption(option2AsStr)
111
-
112
- if lengthOption1 < lengthOption2:
113
- addMe = option1
114
- else:
115
- addMe = option2
116
-
117
- cache_malkovichGrouped_addMe[malkovichGrouped] = addMe
118
-
119
- listRangeAndTuple += addMe
120
-
121
- return listRangeAndTuple
122
- return arraySlice
123
-
124
- arrayAsNestedLists = sliceNDArrayToNestedLists(arrayTarget)
125
-
126
- arrayAsStr = python_minifier.minify(str(arrayAsNestedLists))
127
-
128
- patternRegex = regex.compile(
129
- "(?<!rang)(?:"
130
- # Pattern 1: Comma ahead, bracket behind # noqa: ERA001
131
- "(?P<joinAhead>,)\\((?P<malkovich>\\d+),(?P<multiply>\\d+)\\)(?P<bracketBehind>])|"
132
- # Pattern 2: Bracket or start ahead, comma behind # noqa: ERA001
133
- "(?P<bracketOrStartAhead>\\[|^.)\\((?P<malkovichMalkovich>\\d+),(?P<multiplyIDK>\\d+)\\)(?P<joinBehind>,)|"
134
- # Pattern 3: Bracket ahead, bracket behind # noqa: ERA001
135
- "(?P<bracketAhead>\\[)\\((?P<malkovichMalkovichMalkovich>\\d+),(?P<multiply_whatever>\\d+)\\)(?P<bracketBehindBracketBehind>])|"
136
- # Pattern 4: Comma ahead, comma behind # noqa: ERA001
137
- "(?P<joinAheadJoinAhead>,)\\((?P<malkovichMalkovichMalkovichMalkovich>\\d+),(?P<multiplyOrSomething>\\d+)\\)(?P<joinBehindJoinBehind>,)"
138
- ")"
139
- )
140
-
141
- def replacementByContext(match: regex.Match[str]) -> str:
142
- """Generate replacement string based on context patterns."""
143
- elephino = match.groupdict()
144
- joinAhead = elephino.get('joinAhead') or elephino.get('joinAheadJoinAhead')
145
- malkovich = elephino.get('malkovich') or elephino.get('malkovichMalkovich') or elephino.get('malkovichMalkovichMalkovich') or elephino.get('malkovichMalkovichMalkovichMalkovich')
146
- multiply = elephino.get('multiply') or elephino.get('multiplyIDK') or elephino.get('multiply_whatever') or elephino.get('multiplyOrSomething')
147
- joinBehind = elephino.get('joinBehind') or elephino.get('joinBehindJoinBehind')
148
-
149
- replaceAhead = "]+[" if joinAhead == "," else "["
150
-
151
- replaceBehind = "+[" if joinBehind == "," else ""
152
-
153
- return f"{replaceAhead}{malkovich}]*{multiply}{replaceBehind}"
154
-
155
- arrayAsStr = patternRegex.sub(replacementByContext, arrayAsStr)
156
- arrayAsStr = patternRegex.sub(replacementByContext, arrayAsStr)
157
-
158
- # Replace `range(0,stop)` syntax with `range(stop)` syntax. # noqa: ERA001
159
- # Add unpack operator `*` for automatic decoding when evaluated.
160
- return arrayAsStr.replace('range(0,', 'range(').replace('range', '*range')
161
-
162
- def stringItUp(*scrapPile: Any) -> list[str]: # noqa: C901
163
- """Convert, if possible, every element in the input data structure to a string.
164
-
165
- Order is not preserved or readily predictable.
166
-
167
- Parameters
168
- ----------
169
- *scrapPile : Any
170
- (scrap2pile) One or more data structures to unpack and convert to strings.
171
-
172
- Returns
173
- -------
174
- listStrungUp : list[str]
175
- (list2strung2up) A `list` of string versions of all convertible elements.
176
-
177
- """
178
- scrap = None
179
- listStrungUp: list[str] = []
180
-
181
- def drill(KitKat: Any) -> None: # noqa: C901, PLR0912
182
- match KitKat:
183
- case str():
184
- listStrungUp.append(KitKat)
185
- case bool() | bytearray() | bytes() | complex() | float() | int() | memoryview() | None:
186
- listStrungUp.append(str(KitKat)) # pyright: ignore [reportUnknownArgumentType]
187
- case dict():
188
- for broken, piece in KitKat.items(): # pyright: ignore [reportUnknownVariableType]
189
- drill(broken)
190
- drill(piece)
191
- case list() | tuple() | set() | frozenset() | range():
192
- for kit in KitKat: # pyright: ignore [reportUnknownVariableType]
193
- drill(kit)
194
- case _:
195
- if hasattr(KitKat, '__iter__'): # Unpack other iterables
196
- for kat in KitKat:
197
- drill(kat)
198
- else:
199
- try:
200
- sharingIsCaring = KitKat.__str__()
201
- listStrungUp.append(sharingIsCaring)
202
- except AttributeError:
203
- pass
204
- 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."
205
- pass
206
- except:
207
- print(f"\nWoah! I received '{repr(KitKat)}'.\nTheir report card says, 'Plays well with others: Needs improvement.'\n") # noqa: RUF010, T201
208
- raise
209
- try:
210
- for scrap in scrapPile:
211
- drill(scrap)
212
- except RecursionError:
213
- listStrungUp.append(repr(scrap))
214
- return listStrungUp
215
-
216
- def updateExtendPolishDictionaryLists(*dictionaryLists: Mapping[str, list[Any] | set[Any] | tuple[Any, ...]], destroyDuplicates: bool = False, reorderLists: bool = False, killErroneousDataTypes: bool = False) -> dict[str, list[Any]]:
217
- """Merge multiple dictionaries containing `list` into a single dictionary.
218
-
219
- With options to handle duplicates, `list` ordering, and erroneous data types.
220
-
221
- Parameters
222
- ----------
223
- *dictionaryLists : Mapping[str, list[Any] | set[Any] | tuple[Any, ...]]
224
- (dictionary2lists) Variable number of dictionaries to be merged. If only one dictionary is passed, it will be processed based on the provided options.
225
- destroyDuplicates : bool = False
226
- (destroy2duplicates) If `True`, removes duplicate elements from the `list`. Defaults to `False`.
227
- reorderLists : bool = False
228
- (reorder2lists) If `True`, sorts the `list`. Defaults to `False`.
229
- killErroneousDataTypes : bool = False
230
- (kill2erroneous2data2types) If `True`, skips dictionary keys or dictionary values that cause a `TypeError` during merging. Defaults to `False`.
231
-
232
- Returns
233
- -------
234
- ePluribusUnum : dict[str, list[Any]]
235
- (e2pluribus2unum) A single dictionary with merged `list` based on the provided options. If only one dictionary is passed,
236
- it will be cleaned up based on the options.
237
-
238
- Notes
239
- -----
240
- The returned value, `ePluribusUnum`, is a so-called primitive dictionary (`dict`). Furthermore, every dictionary key is a
241
- so-called primitive string (cf. `str()`) and every dictionary value is a so-called primitive `list` (`list`). If
242
- `dictionaryLists` has other data types, the data types will not be preserved. That could have unexpected consequences.
243
- Conversion from the original data type to a `list`, for example, may not preserve the order even if you want the order to be
244
- preserved.
245
-
246
- """
247
- ePluribusUnum: dict[str, list[Any]] = {}
248
-
249
- for dictionaryListTarget in dictionaryLists:
250
- for keyName, keyValue in dictionaryListTarget.items():
251
- try:
252
- ImaStr = str(keyName)
253
- ImaList = list(keyValue)
254
- ePluribusUnum.setdefault(ImaStr, []).extend(ImaList)
255
- except TypeError:
256
- if killErroneousDataTypes:
257
- continue
258
- else:
259
- raise
260
-
261
- if destroyDuplicates:
262
- for ImaStr, ImaList in ePluribusUnum.items():
263
- ePluribusUnum[ImaStr] = list(dict.fromkeys(ImaList))
264
- if reorderLists:
265
- for ImaStr, ImaList in ePluribusUnum.items():
266
- ePluribusUnum[ImaStr] = sorted(ImaList)
267
-
268
- return ePluribusUnum
File without changes
File without changes