Skip to content

Commit 6402e04

Browse files
committed
Add reusable validators (is_ndarray, is_iso8601) and update porting guide
Create ndi.validators.is_ndarray and ndi.validators.is_iso8601 as reusable validation functions following the MATLAB +ndi/+validators/ pattern. Update PYTHON_PORTING_GUIDE Section 4 with new subsection 4a documenting the requirement to centralise custom validators in ndi/validators/ rather than writing inline checks. https://claude.ai/code/session_016oPH9EoxCLcsUSTpgN9iZp
1 parent 524262d commit 6402e04

4 files changed

Lines changed: 109 additions & 1 deletion

File tree

PYTHON_PORTING_GUIDE.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@ To replicate the robustness of the MATLAB `arguments` block, use Pydantic for al
3131
- MATLAB `char` or `string` → Python `str`
3232
- MATLAB `{member1, member2}` → Python `Literal["member1", "member2"]`
3333
- **Coercion:** Allow Pydantic's default behavior of casting (e.g., allowing a string `"1"` or integer `1` to satisfy a `bool` type).
34+
- **Arbitrary Types:** When a function accepts types that Pydantic cannot serialise natively (e.g. `numpy.ndarray`, file-like `IO[bytes]`), pass `config=ConfigDict(arbitrary_types_allowed=True)` to the decorator.
35+
- **Constraints:** Use `Annotated[type, pydantic.Field(...)]` to express MATLAB `arguments`-block constraints such as `mustBePositive` (`gt=0`), `mustBeNonnegative` (`ge=0`), and `mustBeInteger`.
36+
37+
### 4a. Reusable Validators in `ndi.validators`
38+
39+
MATLAB centralises custom validation functions in the `+ndi/+validators/` namespace. Python must do the same in the `ndi/validators/` package.
40+
41+
- **When to create a validator:** Any type check, format check, or constraint that appears (or is likely to appear) in more than one function should be extracted into its own module under `ndi/validators/` instead of being written inline.
42+
- **Naming convention:** MATLAB-originated validators keep their exact MATLAB name (e.g. `mustBeID`). Python-specific validators that have no MATLAB counterpart use `snake_case` prefixed with a descriptive verb (e.g. `is_ndarray`, `is_iso8601`).
43+
- **Signature pattern:** Each validator takes a single value, raises `ValueError` on failure, and returns the validated value unchanged:
44+
```python
45+
def is_ndarray(val: object) -> np.ndarray:
46+
if not isinstance(val, np.ndarray):
47+
raise ValueError("Input must be a numpy.ndarray")
48+
return val
49+
```
50+
- **Registration:** Every new validator must be imported and listed in `ndi/validators/__init__.py` so it is accessible as `ndi.validators.<name>`.
3451

3552
## 5. Error Handling
3653

@@ -85,12 +102,19 @@ Verified coverage of each MATLAB namespace against the Python port. See
85102

86103
| MATLAB | Python | Coverage |
87104
|--------|--------|:--------:|
88-
| 11 functions | 11 functions | **100 %** |
105+
| 11 functions | 11 + 2 | **100 %** |
89106

90107
All 11 MATLAB `arguments`-block validators ported 1:1 with matching
91108
function names. Python equivalents accept Python types (``list`` for
92109
cell array, ``dict`` for struct, ``pd.DataFrame`` for table).
93110

111+
Python adds two reusable validators with no direct MATLAB counterpart:
112+
113+
| Python | Purpose |
114+
|--------|---------|
115+
| `is_ndarray` | Validates value is a `numpy.ndarray` |
116+
| `is_iso8601` | Validates string is parseable ISO 8601 |
117+
94118
### `ndi.util` — Fully Ported
95119

96120
**Verified:** 2026-03-11 against `VH-Lab/NDI-matlab` branch `main`.

src/ndi/validators/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
or via ``@pydantic.validate_call``.
99
"""
1010

11+
from .is_iso8601 import is_iso8601
12+
from .is_ndarray import is_ndarray
1113
from .mustBeCellArrayOfClass import mustBeCellArrayOfClass
1214
from .mustBeCellArrayOfNdiSessions import mustBeCellArrayOfNdiSessions
1315
from .mustBeCellArrayOfNonEmptyCharacterArrays import (
@@ -23,6 +25,8 @@
2325
from .mustMatchRegex import mustMatchRegex
2426

2527
__all__ = [
28+
"is_iso8601",
29+
"is_ndarray",
2630
"mustBeCellArrayOfClass",
2731
"mustBeCellArrayOfNdiSessions",
2832
"mustBeCellArrayOfNonEmptyCharacterArrays",

src/ndi/validators/is_iso8601.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
ndi.validators.is_iso8601
3+
4+
MATLAB equivalent: +ndi/+validators/ (custom; mirrors the ``arguments``
5+
block format check in ``datestamp2datetime.m``)
6+
7+
Validates that a string is a parseable ISO 8601 datestamp.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from datetime import datetime
13+
14+
15+
def is_iso8601(val: object) -> str:
16+
"""Validate that *val* is a parseable ISO 8601 datestamp string.
17+
18+
MATLAB equivalent: format validation in the ``arguments`` block of
19+
``ndi.util.datestamp2datetime`` (input format
20+
``'yyyy-MM-dd''T''HH:mm:ss.SSSXXX'``).
21+
22+
Parameters
23+
----------
24+
val : object
25+
The value to check.
26+
27+
Returns
28+
-------
29+
str
30+
The validated string (unchanged).
31+
32+
Raises
33+
------
34+
ValueError
35+
If *val* is not a string or cannot be parsed as ISO 8601.
36+
"""
37+
if not isinstance(val, str):
38+
raise ValueError("Input must be a string.")
39+
try:
40+
datetime.fromisoformat(val)
41+
except (ValueError, TypeError) as exc:
42+
raise ValueError(f"String is not valid ISO 8601: {val!r}") from exc
43+
return val

src/ndi/validators/is_ndarray.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
ndi.validators.is_ndarray
3+
4+
MATLAB equivalent: +ndi/+validators/ (custom; mirrors ``mustBeA(val, 'numeric')``)
5+
6+
Validates that an input is a numpy ndarray.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import numpy as np
12+
13+
14+
def is_ndarray(val: object) -> np.ndarray:
15+
"""Validate that *val* is a :class:`numpy.ndarray`.
16+
17+
MATLAB equivalent: type checking inside ``arguments`` blocks
18+
(e.g. ``mustBeA(val, 'numeric')``).
19+
20+
Parameters
21+
----------
22+
val : object
23+
The value to check.
24+
25+
Returns
26+
-------
27+
numpy.ndarray
28+
The validated array (unchanged).
29+
30+
Raises
31+
------
32+
ValueError
33+
If *val* is not a ``numpy.ndarray``.
34+
"""
35+
if not isinstance(val, np.ndarray):
36+
raise ValueError("Input must be a numpy.ndarray")
37+
return val

0 commit comments

Comments
 (0)