valarray 0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. valarray-0.4/.coveragerc +6 -0
  2. valarray-0.4/.gitignore +8 -0
  3. valarray-0.4/PKG-INFO +698 -0
  4. valarray-0.4/README.md +682 -0
  5. valarray-0.4/assets/images/valarray_logo.svg +915 -0
  6. valarray-0.4/docs/example_code/introduction/issue1_problem.py +24 -0
  7. valarray-0.4/docs/example_code/introduction/issue1_solution.py +44 -0
  8. valarray-0.4/docs/example_code/introduction/issue2_problem.py +31 -0
  9. valarray-0.4/docs/example_code/introduction/issue2_solution.py +66 -0
  10. valarray-0.4/docs/example_code/introduction/issue_3_problem.py +25 -0
  11. valarray-0.4/docs/example_code/introduction/issue_3_solution.py +41 -0
  12. valarray-0.4/pyproject.toml +29 -0
  13. valarray-0.4/requirements.txt +1 -0
  14. valarray-0.4/requirements_dev.txt +5 -0
  15. valarray-0.4/tests/core/classes_for_testing/__init__.py +17 -0
  16. valarray-0.4/tests/core/classes_for_testing/array_and_dtype.py +43 -0
  17. valarray-0.4/tests/core/classes_for_testing/array_type_adapter.py +85 -0
  18. valarray-0.4/tests/core/classes_for_testing/comparisons.py +22 -0
  19. valarray-0.4/tests/core/classes_for_testing/errors_exceptions.py +36 -0
  20. valarray-0.4/tests/core/classes_for_testing/validated_array.py +16 -0
  21. valarray-0.4/tests/core/classes_for_testing/validators.py +47 -0
  22. valarray-0.4/tests/core/errors_exceptions/test_axes_errors.py +26 -0
  23. valarray-0.4/tests/core/errors_exceptions/test_dtype_errors.py +33 -0
  24. valarray-0.4/tests/core/errors_exceptions/test_error_list.py +110 -0
  25. valarray-0.4/tests/core/errors_exceptions/test_exceptions.py +45 -0
  26. valarray-0.4/tests/core/errors_exceptions/test_values_errors.py +75 -0
  27. valarray-0.4/tests/core/test_array.py +65 -0
  28. valarray-0.4/tests/core/test_axes_and_fields.py +22 -0
  29. valarray-0.4/tests/core/test_misc.py +49 -0
  30. valarray-0.4/tests/core/test_utils.py +58 -0
  31. valarray-0.4/tests/core/test_validators.py +105 -0
  32. valarray-0.4/tests/core/validation_functions/field_values/test_ax_and_field_strings.py +85 -0
  33. valarray-0.4/tests/core/validation_functions/field_values/test_val_field_values_core.py +117 -0
  34. valarray-0.4/tests/core/validation_functions/field_values/test_val_field_values_utils.py +154 -0
  35. valarray-0.4/tests/core/validation_functions/test_val_array.py +81 -0
  36. valarray-0.4/tests/core/validation_functions/test_val_array_values.py +51 -0
  37. valarray-0.4/tests/core/validation_functions/test_val_dtype.py +39 -0
  38. valarray-0.4/tests/core/validation_functions/test_val_shape.py +43 -0
  39. valarray-0.4/tests/numpy/common.py +37 -0
  40. valarray-0.4/tests/numpy/test_adapter.py +277 -0
  41. valarray-0.4/tests/numpy/test_numpy_array.py +84 -0
  42. valarray-0.4/tests/numpy/test_numpy_comparisons.py +132 -0
  43. valarray-0.4/tests/numpy/test_numpy_errors_and_exceptions.py +81 -0
  44. valarray-0.4/tests/numpy/test_validation.py +244 -0
  45. valarray-0.4/tests/test_imports.py +101 -0
  46. valarray-0.4/valarray/__init__.py +6 -0
  47. valarray-0.4/valarray/core/__init__.py +27 -0
  48. valarray-0.4/valarray/core/array.py +121 -0
  49. valarray-0.4/valarray/core/array_type_adapter.py +109 -0
  50. valarray-0.4/valarray/core/axes_and_fields.py +127 -0
  51. valarray-0.4/valarray/core/comparisons.py +71 -0
  52. valarray-0.4/valarray/core/errors_exceptions/__init__.py +41 -0
  53. valarray-0.4/valarray/core/errors_exceptions/error_list.py +87 -0
  54. valarray-0.4/valarray/core/errors_exceptions/exceptions.py +75 -0
  55. valarray-0.4/valarray/core/errors_exceptions/generic.py +57 -0
  56. valarray-0.4/valarray/core/errors_exceptions/validation_errors/__init__.py +0 -0
  57. valarray-0.4/valarray/core/errors_exceptions/validation_errors/array_creation.py +14 -0
  58. valarray-0.4/valarray/core/errors_exceptions/validation_errors/axes.py +68 -0
  59. valarray-0.4/valarray/core/errors_exceptions/validation_errors/base.py +26 -0
  60. valarray-0.4/valarray/core/errors_exceptions/validation_errors/dtype.py +60 -0
  61. valarray-0.4/valarray/core/errors_exceptions/validation_errors/values.py +99 -0
  62. valarray-0.4/valarray/core/types/__init__.py +24 -0
  63. valarray-0.4/valarray/core/types/generics.py +40 -0
  64. valarray-0.4/valarray/core/types/other.py +23 -0
  65. valarray-0.4/valarray/core/utils.py +89 -0
  66. valarray-0.4/valarray/core/validation_functions/__init__.py +20 -0
  67. valarray-0.4/valarray/core/validation_functions/array.py +62 -0
  68. valarray-0.4/valarray/core/validation_functions/array_values.py +47 -0
  69. valarray-0.4/valarray/core/validation_functions/dtype.py +44 -0
  70. valarray-0.4/valarray/core/validation_functions/field_values/__init__.py +0 -0
  71. valarray-0.4/valarray/core/validation_functions/field_values/core.py +106 -0
  72. valarray-0.4/valarray/core/validation_functions/field_values/types_and_data_structures.py +90 -0
  73. valarray-0.4/valarray/core/validation_functions/field_values/utils.py +143 -0
  74. valarray-0.4/valarray/core/validation_functions/shape.py +67 -0
  75. valarray-0.4/valarray/core/validation_functions/utils.py +24 -0
  76. valarray-0.4/valarray/core/validators/__init__.py +13 -0
  77. valarray-0.4/valarray/core/validators/base.py +72 -0
  78. valarray-0.4/valarray/core/validators/value_comparisons.py +107 -0
  79. valarray-0.4/valarray/numpy/__init__.py +24 -0
  80. valarray-0.4/valarray/numpy/array.py +63 -0
  81. valarray-0.4/valarray/numpy/array_type_adapter.py +133 -0
  82. valarray-0.4/valarray/numpy/axes_and_fields.py +83 -0
  83. valarray-0.4/valarray/numpy/comparisons.py +135 -0
  84. valarray-0.4/valarray/numpy/errors_exceptions.py +172 -0
  85. valarray-0.4/valarray/numpy/types.py +32 -0
  86. valarray-0.4/valarray/numpy/validation_functions.py +170 -0
  87. valarray-0.4/valarray/numpy/validators.py +35 -0
@@ -0,0 +1,6 @@
1
+ [run]
2
+ omit =
3
+ tests/*
4
+ setup.py
5
+ temp.py
6
+ ut_*
@@ -0,0 +1,8 @@
1
+ *.pyc
2
+ .vscode
3
+ .coverage
4
+ *egg-info
5
+ dist*
6
+
7
+ ut_*
8
+ temp*
valarray-0.4/PKG-INFO ADDED
@@ -0,0 +1,698 @@
1
+ Metadata-Version: 2.4
2
+ Name: valarray
3
+ Version: 0.4
4
+ Summary: Library for validating numpy arrays.
5
+ Project-URL: Homepage, https://codeberg.org/jfranek/valarray
6
+ Author-email: "J. Franek" <franek.j27@email.cz>
7
+ License-Expression: MIT
8
+ Keywords: numpy, validation, array, typing
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.7
14
+ Requires-Dist: numpy>=2
15
+ Description-Content-Type: text/markdown
16
+
17
+ ![](assets/images/valarray_logo.svg)
18
+
19
+ In short, library for validating numpy arrays that also helps with static analysis and documentation. In long, see [Library rationale](#library-rationale).
20
+
21
+ Currently intended primarily as a personal/hobby project (see [Caveats](#caveats))
22
+
23
+ I have gotten away with using it in a professional setting, but YMMV.
24
+
25
+ # Quick start <!-- omit from toc -->
26
+ Install ***valarray*** via pip:
27
+ ```shell
28
+ pip install valarray
29
+ ```
30
+
31
+ Define a validate array class:
32
+ ```python
33
+ import numpy as np
34
+ from valarray.numpy import ValidatedNumpyArray
35
+ from valarray.core.errors_exceptions import ValidationException
36
+
37
+ class ExampleValidatedNumpyArray(ValidatedNumpyArray[np.float32]):
38
+ dtype = "float32"
39
+ schema = ('n', 3)
40
+
41
+ ge=0
42
+ ```
43
+
44
+ Validate a numpy array:
45
+ ```python
46
+ try:
47
+ v_arr = ExampleValidatedNumpyArray(np.array([[1,-2,3], [4,5,-6]]))
48
+ except ValidationException as v_exc:
49
+ print(v_exc)
50
+
51
+
52
+ >>> 'ExampleValidatedNumpyArray' validation failed:
53
+ >>> Incorrect axis sizes: (2, *4*), expected (any, 3).
54
+ >>> Invalid Array Values (>= 0):
55
+ >>> [-2.0, -6.0]
56
+ ```
57
+
58
+ # Table of contents
59
+ - [Table of contents](#table-of-contents)
60
+ - [Library rationale](#library-rationale)
61
+ - [1) Invalid values causing unintended behaviour](#1-invalid-values-causing-unintended-behaviour)
62
+ - [Problem](#problem)
63
+ - [Solution](#solution)
64
+ - [2) Limited support for static analysis](#2-limited-support-for-static-analysis)
65
+ - [Problem](#problem-1)
66
+ - [Solution](#solution-1)
67
+ - [3) Need for explicit documentation](#3-need-for-explicit-documentation)
68
+ - [Problem](#problem-2)
69
+ - [Solution](#solution-2)
70
+ - [Validated Array](#validated-array)
71
+ - [Defining a validated array](#defining-a-validated-array)
72
+ - [Creating a validated array instance](#creating-a-validated-array-instance)
73
+ - [Accessing array values](#accessing-array-values)
74
+ - [Validation functions](#validation-functions)
75
+ - [Array schema](#array-schema)
76
+ - [Field](#field)
77
+ - [Array schema examples](#array-schema-examples)
78
+ - [rectangles](#rectangles)
79
+ - [Validators](#validators)
80
+ - [Defining a validator](#defining-a-validator)
81
+ - [ValidationResult](#validationresult)
82
+ - [Example Validator](#example-validator)
83
+ - [Catching exceptions](#catching-exceptions)
84
+ - [Special exceptions and errors](#special-exceptions-and-errors)
85
+ - [Generic Errors](#generic-errors)
86
+ - [Caveats](#caveats)
87
+
88
+ # Library rationale
89
+ This library aims to help with 3 issues encountered when working with numpy arrays:
90
+
91
+ ## 1) Invalid values causing unintended behaviour
92
+ ### Problem
93
+ Invalid values can cause crashes, or worse, cause silent failures.
94
+
95
+ For example the following code fails silently when attempting to cut patches from image using bounding boxes with invalid coordinates.
96
+ ```python
97
+ import numpy as np
98
+ import numpy.typing as npt
99
+
100
+
101
+ def cut_patches(
102
+ img: npt.NDArray[np.uint8], boxes: npt.NDArray[np.int64]
103
+ ) -> list[npt.NDArray[np.uint8]]:
104
+ patches = []
105
+ for box in boxes:
106
+ patch = img[box[0] : box[2], box[1] : box[3], :]
107
+ patches.append(patch)
108
+
109
+ return patches
110
+
111
+ img_random = np.random.random((400, 400, 3)).astype(np.uint8) * 255
112
+
113
+ boxes_xyxy_invalid = np.array(
114
+ [[-10, 100, 200, 200], [150, 50, 200, 250]], dtype=int
115
+ )
116
+
117
+ patches = cut_patches(img_random, boxes_xyxy_invalid)
118
+
119
+ for patch in patches:
120
+ print(patch.shape)
121
+
122
+ >>> (0, 100, 3) # empty image patch
123
+ >>> (50, 200, 3)
124
+ ```
125
+
126
+ ### Solution
127
+ Validate boxes array first. If errors are encountered, print descriptive error message(s).
128
+
129
+ ```python
130
+ import numpy as np
131
+ import numpy.typing as npt
132
+
133
+ from valarray.core.errors_exceptions import ValidationException
134
+ from valarray.numpy import Field
135
+ from valarray.numpy.array import ValidatedNumpyArray
136
+
137
+ class BoxesXYXY(ValidatedNumpyArray[np.int64]):
138
+ dtype = int
139
+ schema = (
140
+ "n",
141
+ (
142
+ Field(ge=0),
143
+ Field(ge=0),
144
+ Field(ge=0),
145
+ Field(ge=0),
146
+ ),
147
+ )
148
+
149
+ def cut_patches(
150
+ img: npt.NDArray[np.uint8], boxes: npt.NDArray[np.int64]
151
+ ) -> list[npt.NDArray[np.uint8]]:
152
+ patches = []
153
+ for box in boxes:
154
+ patch = img[box[0] : box[2], box[1] : box[3], :]
155
+ patches.append(patch)
156
+
157
+ return patches
158
+
159
+ try:
160
+ img_random = np.random.random((400, 400, 3)).astype(np.uint8) * 255
161
+
162
+ boxes_xyxy_invalid = BoxesXYXY.validate(
163
+ np.array([[-10, 100, 200, 200], [150, 50, 200, 250]], dtype=int)
164
+ )
165
+
166
+ patches = cut_patches(img_random, boxes_xyxy_invalid.array)
167
+
168
+ for patch in patches:
169
+ print(patch.shape)
170
+
171
+ except ValidationException as exc:
172
+ for err in exc.errs:
173
+ print(err.msg)
174
+
175
+ >>> Invalid Field Values (< 0):
176
+ >>> Axis < 1 >: '_sized_4'
177
+ >>> Field < 0 >: [-10]
178
+ ```
179
+
180
+ ## 2) Limited support for static analysis
181
+ ### Problem
182
+ Support for static analysis is limited. Tools can only check whether the datatype is correct, but not shape, values or what those values actually represent.
183
+
184
+ For example, the function to crop patches needs the boxes to be defined by `xmin, ymin, xmax, ymax` but doesn't throw an error if input boxes are defined by `x_center, y_center, width, height` and static analysis tools cannot detect this error using bulit-in numpy types.
185
+ ```python
186
+ import numpy as np
187
+ import numpy.typing as npt
188
+
189
+ def cut_patches(
190
+ img: npt.NDArray[np.uint8], boxes: npt.NDArray[np.int64]
191
+ ) -> list[npt.NDArray[np.uint8]]:
192
+ patches = []
193
+ for box in boxes:
194
+ patch = img[box[0] : box[2], box[1] : box[3], :]
195
+ patches.append(patch)
196
+
197
+ return patches
198
+
199
+ img_random = np.random.random((400, 400, 3)).astype(np.uint8) * 255
200
+
201
+ boxes_xyxy = np.array([[0, 100, 200, 200], [150, 50, 200, 250]], dtype=int)
202
+
203
+ boxes_xywh = np.array([[100, 150, 200, 100], [175, 50, 50, 250]], dtype=int)
204
+
205
+ patches = cut_patches(img_random, boxes_xyxy) # type checker does not complain
206
+
207
+ print("Valid")
208
+ for patch in patches:
209
+ print(patch.shape)
210
+
211
+ patches_inv = cut_patches(img_random, boxes_xywh) # type checker still does not complain
212
+
213
+ print("Invalid")
214
+ for patch in patches_inv:
215
+ print(patch.shape)
216
+
217
+ >>> Valid
218
+ >>> (200, 100, 3)
219
+ >>> (50, 200, 3)
220
+ >>> Invalid
221
+ >>> (100, 0, 3)
222
+ >>> (0, 200, 3)
223
+ ```
224
+
225
+ ### Solution
226
+ `ValidatedNumpyArray` subclasses can represent these two types of boxes arrays, and can be used instead of bare numpy arrays in function/method signatures and such.
227
+ ```python
228
+ import numpy as np
229
+ import numpy.typing as npt
230
+
231
+ from valarray.numpy import Field
232
+ from valarray.numpy.array import ValidatedNumpyArray
233
+
234
+ class BoxesXYXY(ValidatedNumpyArray[np.int64]):
235
+ dtype = int
236
+ schema = (
237
+ "n",
238
+ (
239
+ Field(ge=0),
240
+ Field(ge=0),
241
+ Field(ge=0),
242
+ Field(ge=0),
243
+ ),
244
+ )
245
+
246
+ class BoxesXYWH(ValidatedNumpyArray[np.int64]):
247
+ dtype = int
248
+ schema = (
249
+ "n",
250
+ (
251
+ Field(ge=0),
252
+ Field(ge=0),
253
+ Field(gt=0),
254
+ Field(gt=0),
255
+ ),
256
+ )
257
+
258
+ def cut_patches(
259
+ img: npt.NDArray[np.uint8], boxes: BoxesXYXY
260
+ ) -> list[npt.NDArray[np.uint8]]:
261
+ patches = []
262
+ for box in boxes.array:
263
+ patch = img[box[0] : box[2], box[1] : box[3], :]
264
+ patches.append(patch)
265
+
266
+ return patches
267
+
268
+ img_random = np.random.random((400, 400, 3)).astype(np.uint8) * 255
269
+
270
+ boxes_xyxy = BoxesXYXY.wrap(
271
+ np.array([[0, 100, 200, 200], [150, 50, 200, 250]], dtype=int)
272
+ )
273
+ boxes_xywh = BoxesXYWH.wrap(
274
+ np.array([[100, 150, 200, 100], [175, 50, 50, 250]], dtype=int)
275
+ )
276
+
277
+ patches = cut_patches(img_random, boxes_xyxy) # type checker does not complain
278
+
279
+ print("Valid")
280
+ for patch in patches:
281
+ print(patch.shape)
282
+
283
+ patches_inv = cut_patches(
284
+ img_random, boxes_xywh # type checker reports wrong argument type
285
+ )
286
+
287
+ print("Invalid")
288
+ for patch in patches_inv:
289
+ print(patch.shape)
290
+ ```
291
+
292
+ ## 3) Need for explicit documentation
293
+ ### Problem
294
+ Using built-in numpy types provides only documentation for data types. Shape, values, constraints and what the array represents need to be explicitly documented either in comments or docstrings.
295
+
296
+ If this type of array is used in multiple places / functions, this can cause duplicated documentation.
297
+
298
+ ```python
299
+ import numpy as np
300
+ import numpy.typing as npt
301
+
302
+ def cut_patches(
303
+ img: npt.NDArray[np.uint8], boxes: npt.NDArray[np.int64]
304
+ ) -> list[npt.NDArray[np.uint8]]:
305
+ """Cuts patches from an image.
306
+
307
+ Args:
308
+ img (npt.NDArray[np.uint8]): Source image
309
+ boxes (npt.NDArray[np.int64]): Array of N boxes `xmin, ymin, xmax, ymax` in pixels.
310
+
311
+ Returns:
312
+ list[npt.NDArray[np.uint8]]: List of patches.
313
+ """
314
+ patches = []
315
+ for box in boxes:
316
+ patch = img[box[0] : box[2], box[1] : box[3], :]
317
+ patches.append(patch)
318
+
319
+ return patches
320
+ ```
321
+
322
+ ### Solution
323
+ Defining data type, schema and constraints on a `ValidatedNumpyArray` subclass already implicitly documents them.
324
+
325
+ This can be complemented by adding additional (or summary) documentation in the class docstring.
326
+
327
+ This implicit/explicit documentation can be then accessed from multiple functions via parameter type.
328
+ ```python
329
+ import numpy as np
330
+ import numpy.typing as npt
331
+
332
+ from valarray.numpy import Field
333
+ from valarray.numpy.array import ValidatedNumpyArray
334
+
335
+ class BoxesXYXY(ValidatedNumpyArray[np.int64]):
336
+ """Array of N `xyxy` boxes in pixels."""
337
+
338
+ dtype = int
339
+ schema = (
340
+ "n",
341
+ (
342
+ Field("xmin_px", ge=0),
343
+ Field("ymin_px", ge=0),
344
+ Field("xmax_px", ge=0),
345
+ Field("ymax_px", ge=0),
346
+ ),
347
+ )
348
+
349
+ def cut_patches(
350
+ img: npt.NDArray[np.uint8], boxes: BoxesXYXY
351
+ ) -> list[npt.NDArray[np.uint8]]:
352
+ """Cuts patches from an image.
353
+
354
+ Args:
355
+ img (npt.NDArray[np.uint8]): Source image
356
+ boxes (BoxesXYXY): Boxes to cut patches with.
357
+
358
+ Returns:
359
+ list[npt.NDArray[np.uint8]]: List of patches.
360
+ """
361
+ patches = []
362
+ for box in boxes.array:
363
+ patch = img[box[0] : box[2], box[1] : box[3], :]
364
+ patches.append(patch)
365
+
366
+ return patches
367
+ ```
368
+
369
+
370
+ # Validated Array
371
+ ## Defining a validated array
372
+ Subclass `ValidatedNumpyArray` and define:
373
+ - `dtype` - expected data type specification (such as `float`, `"float64"`, `np.float64`).
374
+ If not specified, data type is not validated.
375
+ For full list of accepted values, see:
376
+ https://numpy.org/doc/stable/reference/arrays.dtypes.html#specifying-and-constructing-data-types
377
+ - `schema` - expected shape specification (of type `valarray.numpy.axes_and_fields.AxesTuple`). For details, see [Array Schema](#array-schema).
378
+ If not specified, shape is not validated (and no field validators are applied).
379
+ - `lt`/`le`/`ge`/`gt`/`eq` - basic array value constraints -> less (or equal) than, greater (or equal) than, equal to
380
+ - `validators` - optional list of validators applied to the whole array. For details, see [Validators](##validators).
381
+
382
+ ```python
383
+ import numpy as np
384
+ from valarray.numpy import ValidatedNumpyArray
385
+
386
+ class ExampleValidatedNumpyArray(ValidatedNumpyArray):
387
+ dtype = np.float32
388
+ schema = ('batch_size', 3, 5)
389
+ ```
390
+
391
+ ## Creating a validated array instance
392
+ There are 4 ways to create a validated array instance:
393
+ - validate an existing array
394
+ ```python
395
+ # using .validate()
396
+ v_arr = ExampleValidatedNumpyArray.validate(np.array([1,2],dtype=np.float32))
397
+ # using .__init__()
398
+ v_arr = ExampleValidatedNumpyArray(np.array([1,2],dtype=np.float32), validate=True)
399
+ ```
400
+ - from an existing array without validation (to be used as a type hint)
401
+ ```python
402
+ # using .wrap()
403
+ v_arr = ExampleValidatedNumpyArray.wrap(np.array([1,2],dtype=np.float32))
404
+ # using .__init__()
405
+ v_arr = ExampleValidatedNumpyArray(np.array([1,2],dtype=np.float32), validate=False)
406
+
407
+ # NOTE: validation can be performed at a later stage using:
408
+ v_arr.validate_array()
409
+ ```
410
+ - from an arbitrary object pased to `np.array` constructor
411
+ (data type of the resulting array is taken from the validated array class definition,
412
+ if no data type is defined, the most appropriate type is chosen by using `np.asarray()`)
413
+ ```python
414
+ v_arr = ExampleValidatedNumpyArray([1,2])
415
+ ```
416
+ - create an empty array
417
+ - ***shape*** inferred from schema or empty if not defined
418
+ - ***dtype*** from validated array class definition or default if not defined
419
+ ```python
420
+ # using .empty()
421
+ v_arr = ExampleValidatedNumpyArray.empty()
422
+ # or __init__
423
+ v_arr = ExampleValidatedNumpyArray(None)
424
+ ```
425
+
426
+ If created from an existing array, there is an option to try to coerce array to the expected data type.
427
+ `CoerceDTypeException` is raised if this fails.
428
+ ```python
429
+ # (only) using .__init__()
430
+ v_arr = ExampleValidatedNumpyArray(np.array([1,2],dtype=np.float32), coerce_dtype=True, validate=True)
431
+ v_arr = ExampleValidatedNumpyArray(np.array([1,2],dtype=np.float32), coerce_dtype=True, validate=False)
432
+ ```
433
+
434
+ ## Accessing array values
435
+ You can access the underlying array using the `.array`/`.a_` property:
436
+ ```python
437
+ arr = v_arr.array
438
+ # or
439
+ arr = v_arr.a_
440
+ ```
441
+ It is recommended to make a copy before performing operations that could invalidate the array:
442
+ ```python
443
+ arr = v_arr.array.copy()
444
+ ```
445
+
446
+ It is also recommended to specify array data type when subclassing `ValidatedNumpyArray` to ensure correct type hint:
447
+ ```python
448
+ class UnspecifiedDataTypeArray(ValidatedNumpyArray): ...
449
+
450
+ arr = UnspecifiedDataTypeArray(...).array # np.typing.NDArray[Unknown]
451
+
452
+ class SpecifiedDataTypeArray(ValidatedNumpyArray[np.float32]): ...
453
+
454
+ arr = SpecifiedDataTypeArray(...).array # np.typing.NDArray[np.float32]
455
+ ```
456
+
457
+ # Validation functions
458
+ Array validation is designed to be modular and composable and validation functions can be used on they own if only runtime validation is required.
459
+ Each validation function returns a list of errors, from which a `ValidationException` can be raised. For details see [Catching exceptions](#catching-exceptions).
460
+ ```python
461
+ from valarray.numpy.validation_functions import validate_*
462
+
463
+ # validate array and get a list of errors (empty if no errors)
464
+ errs = validate_*(arr, ...)
465
+
466
+ # validate array and raise exception if errors are returned
467
+ validate_*(arr, ...).raise_()
468
+ ```
469
+
470
+ There are 4 validation functions:
471
+ - **validate_dtype**
472
+ - Checks that array has the expected datatype.
473
+ - *returns* `NumpyIncorrectDTypeError`
474
+ - **validate_shape**
475
+ - Checks that array has the right number of axes, and that the axes have expected sizes.
476
+ - *returns* `IncorrectAxNumberError` and or `IncorrectAxSizesError`
477
+ - **validate_array_values**
478
+ - Performs an arbitrary check on the values of the whole array using a [Validator](#validators).
479
+ - *returns* `NumpyInvalidArrayValuesError`
480
+ - **validate_field_values**
481
+ - Performs an arbitrary check on the values of selected fields using a [Validator](#validators) defined in [Array Schema](#array-schema).
482
+ - By default expects array to be in the correct shape. If this is not guaranteed, set parameter `check_shape=True`.
483
+ - *returns* `NumpyInvalidFieldValuesError` (and possibly `IncorrectAxNumberError`/`IncorrectAxSizesError` if `check_shape=True`)
484
+
485
+ and a "composite" validation function:
486
+ - **validate_array**
487
+ - performs validation in the following order:
488
+ - `validate_dtype()`
489
+ - `validate_shape()`
490
+ - *returns* `NumpyIncorrectDTypeError`/`IncorrectAxNumberError`/`IncorrectAxSizesError` if any.
491
+ - `validate_array_values()`
492
+ - `validate_field_values()`
493
+ - *returns* `NumpyInvalidArrayValuesError`/`NumpyInvalidFieldValuesError` or no errors.
494
+
495
+
496
+ # Array schema
497
+ Schema defines expected axes, and for each axis its' fields and optionally constraints on the field values.
498
+
499
+ Axes can be defined with:
500
+ - integer size (`6`)
501
+ - name string ('axis_name`)
502
+ - tuple of fields
503
+
504
+ Fields can be defined with:
505
+ - name string ('field_name')
506
+ - instance of `valarray.numpy.Field`
507
+
508
+ ``` python
509
+ from valarray.numpy import Field
510
+
511
+ schema = (
512
+ "axis_0",
513
+ 3,
514
+ ("field_a", Field())
515
+ )
516
+ ```
517
+ ## Field
518
+ Defines (optional) name and value constrints for array field. More specifically:
519
+ - `name` - descriptive name used in error messages (if missing, field index is used instead)
520
+ - `lt`/`le`/`ge`/`gt`/`eq` - basic array value constraints -> less (or equal) than, greater (or equal) than, equal to
521
+ - `validators` - other validators of fields values. For details, see [Validators](##validators).
522
+
523
+ ```python
524
+ from typing import Any
525
+
526
+ from valarray.numpy import Field, NumpyValidator
527
+
528
+ class ExampleNumpyValidator(NumpyValidator[Any]):
529
+ def validate(self, arr):
530
+ return True
531
+
532
+ f1 = Field("example_named_field", ge=0)
533
+ f2 = Field(gt=10, validators=(ExampleNumpyValidator(),))
534
+ ```
535
+
536
+ ## Array schema examples
537
+ ### rectangles
538
+ An array of arbitrary number of rectangles defined by min and max coordinates which has two axes: *n_rects* and *rect*.
539
+ Axis *rect* is has 4 fields: *x_min*,*y_min*,*x_max*,*y_max*, where values must be greater or equal to zero.
540
+
541
+ ``` python
542
+ import numpy as np
543
+
544
+ from valarray.numpy import ValidatedNumpyArray
545
+ from valarray.numpy.axes_and_fields import Field
546
+
547
+ # validated array with schema
548
+ class Rect(ValidatedNumpyArray):
549
+ schema = (
550
+ "n_rects",
551
+ (
552
+ Field("x_min", ge=0),
553
+ Field("y_min", ge=0),
554
+ Field("x_max", ge=0),
555
+ Field("y_max", ge=0),
556
+ ),
557
+ )
558
+
559
+ # example array
560
+ arr = np.array(
561
+ [
562
+ [10, 20, 30, 40],
563
+ [15, 25, 35, 45],
564
+ ],
565
+ )
566
+
567
+ Rect.validate(arr)
568
+ ```
569
+
570
+ # Validators
571
+ Validators are objects that perform arbitrary validation of array or field values defined by user.
572
+
573
+ ## Defining a validator
574
+ Validators must subclass `valarray.numpy.NumpyValidator` Abstract Base Class
575
+ and implement the `.validate()` method that takes an array as an input and results in success/failure of validation using these options:
576
+
577
+ - **on success**:
578
+ - *returns* `valarray.core.ValidationResult(status="OK")`
579
+ - *returns* `True`
580
+ - *returns* `None`
581
+ - **on failure**:
582
+ - *returns* `valarray.core.ValidationResult(status="FAIL")`
583
+ - *returns* `False`
584
+ - *raises* `ValueError`
585
+
586
+ ### ValidationResult
587
+ Contains result status of validation `status="OK"`/`status="FAIL"`
588
+
589
+ Can also optionally contain:
590
+ - message to be added to validation error
591
+ - indices of invalid values
592
+
593
+ Indices use [advanced numpy indexing](https://numpy.org/doc/stable/user/basics.indexing.html#advanced-indexing) and can be either:
594
+ - a boolean array
595
+ - a tuple of integer arrays with length equal to the number of array axes
596
+
597
+ **!** If used for validating field values, it is recommended that validators return ValidationResult with indices.
598
+ Error messages can then properly show which values of which fields caused the validation to fail.
599
+
600
+ ```python
601
+ import numpy as np
602
+ from valarray.core import ValidationResult
603
+
604
+ # 2D array of shape (3,3)
605
+ indices = np.array(
606
+ [
607
+ [False, False, False],
608
+ [True, False, False],
609
+ [False, False, True],
610
+ ]
611
+ )
612
+
613
+ indices = (np.array([0, 1, 1]), np.array([1, 0, 1]))
614
+
615
+ res = ValidationResult(status="FAIL", indices_invalid=indices, msg="Optional error message.")
616
+ ```
617
+
618
+ ### Example Validator
619
+ ```python
620
+ from dataclasses import dataclass
621
+ from typing import Literal
622
+
623
+ import numpy as np
624
+
625
+ from valarray.core import ValidationResult
626
+ from valarray.numpy import NumpyValidator
627
+
628
+ @dataclass
629
+ class ExampleIsEvenValidator(NumpyValidator[np.uint8]):
630
+ method: Literal["boolean", "raise", "result"] = "boolean"
631
+
632
+ def validate(self, arr):
633
+ even = arr % 2 == 0
634
+
635
+ all_even = np.all(even)
636
+
637
+ match self.method:
638
+ case "boolean":
639
+ if all_even:
640
+ return True
641
+
642
+ return False
643
+ case "raise":
644
+ if all_even:
645
+ return None
646
+
647
+ raise ValueError()
648
+ case "result":
649
+ if all_even:
650
+ return ValidationResult("OK")
651
+
652
+ return ValidationResult("FAIL", indices_invalid=~even)
653
+ ```
654
+
655
+ # Catching exceptions
656
+ Failed validation results in `valarray.core.errors_exceptions.ValidationException` being raised containing list of errors responsible (and name of array class if available).
657
+
658
+ Main error types are:
659
+ - `IncorrectDTypeError` - Wrong data type. **\***
660
+ - `IncorrectAxNumberError` - Wrong number of axes
661
+ - `IncorrectAxSizesError` - Ax or axes have wrong size(s)
662
+ - `InvalidArrayValuesError` - Validator applied to the whole array failed. **\***
663
+ - `InvalidFieldValuesError` - Validator applied field(s) failed. **\***
664
+
665
+ **\*** These errors have special variants that ensure proper type hints. See [Generic Errors](#generic-errors).
666
+
667
+ Error list is a special list type that in addition to integer index and slice can be filtered by error type(s).
668
+ ```python
669
+ try:
670
+ ...
671
+ except ValidationException as exc:
672
+ err = exc.errs[0]
673
+
674
+ sliced_errs = exc.errs[:2]
675
+
676
+ dtype_errs = exc.errs[NumpyIncorrectDTypeError]
677
+
678
+ axis_errs = exc.errs[(IncorrectAxSizesError, IncorrectAxNumberError)]
679
+ ```
680
+
681
+ ## Special exceptions and errors
682
+ There are two special subclasses of `ValidationException` with associated validation errors raised during [instantiation](#creating-a-validated-array-instance):
683
+ - `CreateArrayException` -> `CannotCreateArrayError` - Array cannot be created from supplied object.
684
+ - `CoerceDTypeException` -> `CannotCoerceDTypeError` - If array data type cannot be coerced when creating array with `ValidatedArray(coerce_dtype=True)`
685
+
686
+
687
+ ## Generic Errors
688
+ These error types have subclasses ensuring proper type hints:
689
+ - `IncorrectDTypeError` -> `NumpyIncorrectDTypeError`
690
+ - `CannotCoerceDTypeError` -> `NumpyCannotCoerceDTypeError`
691
+ - `InvalidArrayValuesError` -> `NumpyInvalidArrayValuesError`
692
+ - `InvalidFieldValuesError` -> `NumpyInvalidFieldValuesError`
693
+
694
+
695
+ # Caveats
696
+ - I cannot guarantee that the test suite is foolproof ATM as I'm currently the only one testing this library.
697
+ - Library has so far only been tested with `python==3.12` and `numpy==2.4.0`
698
+ - Library isn't tested for performance, use in production only if the primary bottleneck is brain and not hardware.