Skip to content

Commit 30d6f59

Browse files
committed
chore: add type hints to Sturctures
1 parent 2c8ccaa commit 30d6f59

File tree

10 files changed

+107
-77
lines changed

10 files changed

+107
-77
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Write the date in place of the "Unreleased" in the case a new version is release
88
### Changed
99

1010
- Typehint utils collection implementations
11+
- Typehint Structure types
1112

1213
## v0.1.0-b35 (2025-08-20)
1314

tiled/client/base.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from copy import copy, deepcopy
33
from dataclasses import asdict
44
from pathlib import Path
5-
from typing import Dict, List, Union
5+
from typing import Dict, List, Optional, Union
66
from urllib.parse import parse_qs, urlparse
77

88
import json_merge_patch
@@ -11,6 +11,7 @@
1111
from httpx import URL
1212

1313
from tiled.client.context import Context
14+
from tiled.structures.root import Structure
1415

1516
from ..structures.core import STRUCTURE_TYPES, Spec, StructureFamily
1617
from ..structures.data_source import DataSource
@@ -131,8 +132,8 @@ def __init__(
131132
*,
132133
item,
133134
structure_clients,
134-
structure=None,
135-
include_data_sources=False,
135+
structure: Optional[Structure] = None,
136+
include_data_sources: bool = False,
136137
):
137138
self._context = context
138139
self._item = item

tiled/server/schemas.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from pydantic_core import PydanticCustomError
1717
from typing_extensions import Annotated, TypedDict
1818

19+
from tiled.structures.root import Structure
20+
1921
from ..structures.array import ArrayStructure
2022
from ..structures.awkward import AwkwardStructure
2123
from ..structures.core import STRUCTURE_TYPES, StructureFamily
@@ -30,7 +32,7 @@
3032
DataT = TypeVar("DataT")
3133
LinksT = TypeVar("LinksT")
3234
MetaT = TypeVar("MetaT")
33-
StructureT = TypeVar("StructureT")
35+
StructureT = TypeVar("StructureT", bound=Optional[Structure])
3436

3537

3638
MAX_ALLOWED_SPECS = 20
@@ -152,7 +154,7 @@ def from_orm(cls, orm: tiled.catalog.orm.Revision) -> Revision:
152154
class DataSource(BaseModel, Generic[StructureT]):
153155
id: Optional[int] = None
154156
structure_family: StructureFamily
155-
structure: Optional[StructureT]
157+
structure: StructureT
156158
mimetype: Optional[str] = None
157159
parameters: dict = {}
158160
assets: List[Asset] = []

tiled/structures/array.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import enum
22
import os
33
import sys
4+
from collections.abc import Mapping
45
from dataclasses import dataclass
5-
from typing import List, Optional, Tuple, Union
6+
from typing import Any, ClassVar, List, Optional, Tuple, Union
67

78
import numpy
89

10+
# from dtype.descr
11+
FieldDescr = Union[Tuple[str, str], Tuple[str, str, Tuple[int, ...]]]
12+
NumpyDescr = List[FieldDescr]
13+
914

1015
class Endianness(str, enum.Enum):
1116
"""
@@ -57,7 +62,7 @@ class Kind(str, enum.Enum):
5762
object = "O" # Object (i.e. the memory contains a pointer to PyObject)
5863

5964
@classmethod
60-
def _missing_(cls, key):
65+
def _missing_(cls, key: str):
6166
if key == "O":
6267
raise ObjectArrayTypeDisabled(
6368
"Numpy 'object'-type arrays are not enabled by default "
@@ -78,17 +83,19 @@ class BuiltinDtype:
7883
itemsize: int
7984
dt_units: Optional[str] = None
8085

81-
__endianness_map = {
86+
__endianness_map: ClassVar[Mapping[str, str]] = {
8287
">": "big",
8388
"<": "little",
8489
"=": sys.byteorder,
8590
"|": "not_applicable",
8691
}
8792

88-
__endianness_reverse_map = {"big": ">", "little": "<", "not_applicable": "|"}
93+
__endianness_reverse_map: ClassVar[Mapping[str, str]] = {
94+
v: k for k, v in __endianness_map.items() if k != "="
95+
}
8996

9097
@classmethod
91-
def from_numpy_dtype(cls, dtype) -> "BuiltinDtype":
98+
def from_numpy_dtype(cls, dtype: numpy.dtype) -> "BuiltinDtype":
9299
# Extract datetime units from the dtype string representation,
93100
# e.g. `'<M8[ns]'` has `dt_units = '[ns]'`. Count determines the number of base units in a step.
94101
dt_units = None
@@ -106,7 +113,7 @@ def from_numpy_dtype(cls, dtype) -> "BuiltinDtype":
106113
def to_numpy_dtype(self) -> numpy.dtype:
107114
return numpy.dtype(self.to_numpy_str())
108115

109-
def to_numpy_str(self):
116+
def to_numpy_str(self) -> str:
110117
endianness = self.__endianness_reverse_map[self.endianness]
111118
# dtype.itemsize always reports bytes. The format string from the
112119
# numeric types the string format is: {type_code}{byte_count} so we can
@@ -125,7 +132,7 @@ def to_numpy_descr(self):
125132
return self.to_numpy_str()
126133

127134
@classmethod
128-
def from_json(cls, structure):
135+
def from_json(cls, structure: Mapping[str, Any]) -> "BuiltinDtype":
129136
return cls(
130137
kind=Kind(structure["kind"]),
131138
itemsize=structure["itemsize"],
@@ -141,7 +148,7 @@ class Field:
141148
shape: Optional[Tuple[int, ...]]
142149

143150
@classmethod
144-
def from_numpy_descr(cls, field):
151+
def from_numpy_descr(cls, field: FieldDescr) -> "Field":
145152
name, *rest = field
146153
if name == "":
147154
raise ValueError(
@@ -159,7 +166,7 @@ def from_numpy_descr(cls, field):
159166
FType = StructDtype.from_numpy_dtype(numpy.dtype(f_type))
160167
return cls(name=name, dtype=FType, shape=shape)
161168

162-
def to_numpy_descr(self):
169+
def to_numpy_descr(self) -> FieldDescr:
163170
if isinstance(self.dtype, BuiltinDtype):
164171
base = [self.name, self.dtype.to_numpy_str()]
165172
else:
@@ -170,7 +177,7 @@ def to_numpy_descr(self):
170177
return tuple(base + [self.shape])
171178

172179
@classmethod
173-
def from_json(cls, structure):
180+
def from_json(cls, structure: Mapping[str, Any]) -> "Field":
174181
name = structure["name"]
175182
if "fields" in structure["dtype"]:
176183
ftype = StructDtype.from_json(structure["dtype"])
@@ -185,7 +192,7 @@ class StructDtype:
185192
fields: List[Field]
186193

187194
@classmethod
188-
def from_numpy_dtype(cls, dtype):
195+
def from_numpy_dtype(cls, dtype: numpy.dtype) -> "StructDtype":
189196
# subdtypes push extra dimensions into arrays, we should handle these
190197
# a layer up and report an array with bigger dimensions.
191198
if dtype.subdtype is not None:
@@ -198,20 +205,20 @@ def from_numpy_dtype(cls, dtype):
198205
fields=[Field.from_numpy_descr(f) for f in dtype.descr],
199206
)
200207

201-
def to_numpy_dtype(self):
208+
def to_numpy_dtype(self) -> numpy.dtype:
202209
return numpy.dtype(self.to_numpy_descr())
203210

204-
def to_numpy_descr(self):
211+
def to_numpy_descr(self) -> NumpyDescr:
205212
return [f.to_numpy_descr() for f in self.fields]
206213

207-
def max_depth(self):
214+
def max_depth(self) -> int:
208215
return max(
209216
1 if isinstance(f.dtype, BuiltinDtype) else 1 + f.dtype.max_depth()
210217
for f in self.fields
211218
)
212219

213220
@classmethod
214-
def from_json(cls, structure):
221+
def from_json(cls, structure: Mapping[str, Any]) -> "StructDtype":
215222
return cls(
216223
itemsize=structure["itemsize"],
217224
fields=[Field.from_json(f) for f in structure["fields"]],
@@ -227,7 +234,7 @@ class ArrayStructure:
227234
resizable: Union[bool, Tuple[bool, ...]] = False
228235

229236
@classmethod
230-
def from_json(cls, structure):
237+
def from_json(cls, structure: Mapping[str, Any]) -> "ArrayStructure":
231238
if "fields" in structure["data_type"]:
232239
data_type = StructDtype.from_json(structure["data_type"])
233240
else:
@@ -273,7 +280,7 @@ def from_array(cls, array, shape=None, chunks=None, dims=None) -> "ArrayStructur
273280
data_type = StructDtype.from_numpy_dtype(array.dtype)
274281
else:
275282
data_type = BuiltinDtype.from_numpy_dtype(array.dtype)
276-
return ArrayStructure(
283+
return cls(
277284
data_type=data_type,
278285
shape=shape,
279286
chunks=normalized_chunks,

tiled/structures/awkward.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from dataclasses import dataclass
2+
from typing import Any, Iterable, Mapping, Optional
23

34
import awkward
45

@@ -9,11 +10,13 @@ class AwkwardStructure:
910
form: dict
1011

1112
@classmethod
12-
def from_json(cls, structure):
13+
def from_json(cls, structure: Mapping[str, Any]) -> "AwkwardStructure":
1314
return cls(**structure)
1415

1516

16-
def project_form(form, form_keys_touched):
17+
def project_form(
18+
form: awkward.forms.Form, form_keys_touched: Iterable[str]
19+
) -> Optional[awkward.forms.Form]:
1720
# See https://github.com/bluesky/tiled/issues/450
1821
if isinstance(form, awkward.forms.RecordForm):
1922
if form.fields is None:

tiled/structures/core.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from dataclasses import asdict, dataclass
1010
from typing import Dict, Optional
1111

12+
from tiled.structures.root import Structure
13+
1214
from ..utils import OneShotCachedMap
1315

1416

@@ -47,8 +49,7 @@ def dict(self) -> Dict[str, Optional[str]]:
4749
model_dump = dict # For easy interoperability with pydantic 2.x models
4850

4951

50-
# TODO: make type[Structure] after #1036
51-
STRUCTURE_TYPES = OneShotCachedMap[StructureFamily, type](
52+
STRUCTURE_TYPES = OneShotCachedMap[StructureFamily, type[Structure]](
5253
{
5354
StructureFamily.array: lambda: importlib.import_module(
5455
"...structures.array", StructureFamily.__module__

tiled/structures/data_source.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import dataclasses
22
import enum
3-
from typing import Generic, List, Optional, TypeVar
3+
from collections.abc import Mapping
4+
from typing import Any, Generic, List, Optional, TypeVar
5+
6+
from tiled.structures.root import Structure
47

58
from .core import StructureFamily
69

@@ -21,7 +24,7 @@ class Asset:
2124
id: Optional[int] = None
2225

2326

24-
StructureT = TypeVar("StructureT")
27+
StructureT = TypeVar("StructureT", bound=Optional[Structure])
2528

2629

2730
@dataclasses.dataclass
@@ -35,7 +38,7 @@ class DataSource(Generic[StructureT]):
3538
management: Management = Management.writable
3639

3740
@classmethod
38-
def from_json(cls, d):
39-
d = d.copy()
41+
def from_json(cls, structure: Mapping[str, Any]) -> "DataSource":
42+
d = structure.copy()
4043
assets = [Asset(**a) for a in d.pop("assets")]
4144
return cls(assets=assets, **d)

tiled/structures/root.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from collections.abc import Mapping
2+
from typing import Any, Protocol
3+
4+
5+
class Structure(Protocol):
6+
@classmethod
7+
# TODO: When dropping support for Python 3.10 replace with -> Self
8+
def from_json(cls, structure: Mapping[str, Any]) -> "Structure":
9+
...

tiled/structures/sparse.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import enum
2+
from collections.abc import Mapping
23
from dataclasses import dataclass, field
3-
from typing import Optional, Tuple, Union
4+
from typing import Any, Optional, Tuple, Union
45

56
from .array import BuiltinDtype, Endianness, Kind, StructDtype
67

@@ -27,7 +28,7 @@ class COOStructure:
2728
# TODO Include fill_value?
2829

2930
@classmethod
30-
def from_json(cls, structure):
31+
def from_json(cls, structure: Mapping[str, Any]) -> "COOStructure":
3132
data_type = structure.get("data_type", None)
3233
if data_type is not None and "fields" in data_type:
3334
data_type = StructDtype.from_json(data_type)

0 commit comments

Comments
 (0)