Skip to content

Commit 296fefe

Browse files
committed
chore: Add types to Structures
1 parent 2c8ccaa commit 296fefe

File tree

10 files changed

+122
-81
lines changed

10 files changed

+122
-81
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: 3 additions & 1 deletion
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=Structure)
3436

3537

3638
MAX_ALLOWED_SPECS = 20

tiled/structures/array.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
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 tiled.structures.root import Structure
11+
12+
# from dtype.descr
13+
FieldDescr = Union[Tuple[str, str], Tuple[str, str, Tuple[int, ...]]]
14+
NumpyDescr = List[FieldDescr]
15+
916

1017
class Endianness(str, enum.Enum):
1118
"""
@@ -57,7 +64,7 @@ class Kind(str, enum.Enum):
5764
object = "O" # Object (i.e. the memory contains a pointer to PyObject)
5865

5966
@classmethod
60-
def _missing_(cls, key):
67+
def _missing_(cls, key: str):
6168
if key == "O":
6269
raise ObjectArrayTypeDisabled(
6370
"Numpy 'object'-type arrays are not enabled by default "
@@ -78,17 +85,19 @@ class BuiltinDtype:
7885
itemsize: int
7986
dt_units: Optional[str] = None
8087

81-
__endianness_map = {
88+
__endianness_map: ClassVar[Mapping[str, str]] = {
8289
">": "big",
8390
"<": "little",
8491
"=": sys.byteorder,
8592
"|": "not_applicable",
8693
}
8794

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

9099
@classmethod
91-
def from_numpy_dtype(cls, dtype) -> "BuiltinDtype":
100+
def from_numpy_dtype(cls, dtype: numpy.dtype) -> "BuiltinDtype":
92101
# Extract datetime units from the dtype string representation,
93102
# e.g. `'<M8[ns]'` has `dt_units = '[ns]'`. Count determines the number of base units in a step.
94103
dt_units = None
@@ -106,7 +115,7 @@ def from_numpy_dtype(cls, dtype) -> "BuiltinDtype":
106115
def to_numpy_dtype(self) -> numpy.dtype:
107116
return numpy.dtype(self.to_numpy_str())
108117

109-
def to_numpy_str(self):
118+
def to_numpy_str(self) -> str:
110119
endianness = self.__endianness_reverse_map[self.endianness]
111120
# dtype.itemsize always reports bytes. The format string from the
112121
# numeric types the string format is: {type_code}{byte_count} so we can
@@ -125,7 +134,7 @@ def to_numpy_descr(self):
125134
return self.to_numpy_str()
126135

127136
@classmethod
128-
def from_json(cls, structure):
137+
def from_json(cls, structure: Mapping[str, Any]) -> "BuiltinDtype":
129138
return cls(
130139
kind=Kind(structure["kind"]),
131140
itemsize=structure["itemsize"],
@@ -141,7 +150,7 @@ class Field:
141150
shape: Optional[Tuple[int, ...]]
142151

143152
@classmethod
144-
def from_numpy_descr(cls, field):
153+
def from_numpy_descr(cls, field: FieldDescr) -> "Field":
145154
name, *rest = field
146155
if name == "":
147156
raise ValueError(
@@ -159,7 +168,7 @@ def from_numpy_descr(cls, field):
159168
FType = StructDtype.from_numpy_dtype(numpy.dtype(f_type))
160169
return cls(name=name, dtype=FType, shape=shape)
161170

162-
def to_numpy_descr(self):
171+
def to_numpy_descr(self) -> FieldDescr:
163172
if isinstance(self.dtype, BuiltinDtype):
164173
base = [self.name, self.dtype.to_numpy_str()]
165174
else:
@@ -170,7 +179,7 @@ def to_numpy_descr(self):
170179
return tuple(base + [self.shape])
171180

172181
@classmethod
173-
def from_json(cls, structure):
182+
def from_json(cls, structure: Mapping[str, Any]) -> "Field":
174183
name = structure["name"]
175184
if "fields" in structure["dtype"]:
176185
ftype = StructDtype.from_json(structure["dtype"])
@@ -185,7 +194,7 @@ class StructDtype:
185194
fields: List[Field]
186195

187196
@classmethod
188-
def from_numpy_dtype(cls, dtype):
197+
def from_numpy_dtype(cls, dtype: numpy.dtype) -> "StructDtype":
189198
# subdtypes push extra dimensions into arrays, we should handle these
190199
# a layer up and report an array with bigger dimensions.
191200
if dtype.subdtype is not None:
@@ -198,36 +207,36 @@ def from_numpy_dtype(cls, dtype):
198207
fields=[Field.from_numpy_descr(f) for f in dtype.descr],
199208
)
200209

201-
def to_numpy_dtype(self):
210+
def to_numpy_dtype(self) -> numpy.dtype:
202211
return numpy.dtype(self.to_numpy_descr())
203212

204-
def to_numpy_descr(self):
213+
def to_numpy_descr(self) -> NumpyDescr:
205214
return [f.to_numpy_descr() for f in self.fields]
206215

207-
def max_depth(self):
216+
def max_depth(self) -> int:
208217
return max(
209218
1 if isinstance(f.dtype, BuiltinDtype) else 1 + f.dtype.max_depth()
210219
for f in self.fields
211220
)
212221

213222
@classmethod
214-
def from_json(cls, structure):
223+
def from_json(cls, structure: Mapping[str, Any]) -> "StructDtype":
215224
return cls(
216225
itemsize=structure["itemsize"],
217226
fields=[Field.from_json(f) for f in structure["fields"]],
218227
)
219228

220229

221230
@dataclass
222-
class ArrayStructure:
231+
class ArrayStructure(Structure):
223232
data_type: Union[BuiltinDtype, StructDtype]
224233
chunks: Tuple[Tuple[int, ...], ...] # tuple-of-tuples-of-ints like ((3,), (3,))
225234
shape: Tuple[int, ...] # tuple of ints like (3, 3)
226235
dims: Optional[Tuple[str, ...]] = None # None or tuple of names like ("x", "y")
227236
resizable: Union[bool, Tuple[bool, ...]] = False
228237

229238
@classmethod
230-
def from_json(cls, structure):
239+
def from_json(cls, structure: Mapping[str, Any]) -> "ArrayStructure":
231240
if "fields" in structure["data_type"]:
232241
data_type = StructDtype.from_json(structure["data_type"])
233242
else:
@@ -273,7 +282,7 @@ def from_array(cls, array, shape=None, chunks=None, dims=None) -> "ArrayStructur
273282
data_type = StructDtype.from_numpy_dtype(array.dtype)
274283
else:
275284
data_type = BuiltinDtype.from_numpy_dtype(array.dtype)
276-
return ArrayStructure(
285+
return cls(
277286
data_type=data_type,
278287
shape=shape,
279288
chunks=normalized_chunks,

tiled/structures/awkward.py

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

34
import awkward
45

6+
from tiled.structures.root import Structure
7+
58

69
@dataclass
7-
class AwkwardStructure:
10+
class AwkwardStructure(Structure):
811
length: int
912
form: dict
1013

1114
@classmethod
12-
def from_json(cls, structure):
15+
def from_json(cls, structure: Mapping[str, Any]) -> "AwkwardStructure":
1316
return cls(**structure)
1417

1518

16-
def project_form(form, form_keys_touched):
19+
def project_form(
20+
form: awkward.forms.Form, form_keys_touched: Iterable[str]
21+
) -> Optional[awkward.forms.Form]:
1722
# See https://github.com/bluesky/tiled/issues/450
1823
if isinstance(form, awkward.forms.RecordForm):
1924
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: 8 additions & 5 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,21 +24,21 @@ class Asset:
2124
id: Optional[int] = None
2225

2326

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

2629

2730
@dataclasses.dataclass
2831
class DataSource(Generic[StructureT]):
2932
structure_family: StructureFamily
30-
structure: StructureT
33+
structure: Optional[StructureT]
3134
id: Optional[int] = None
3235
mimetype: Optional[str] = None
3336
parameters: dict = dataclasses.field(default_factory=dict)
3437
assets: List[Asset] = dataclasses.field(default_factory=list)
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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import dataclasses
2+
from abc import ABC
3+
from collections.abc import Mapping
4+
from typing import Any
5+
6+
7+
@dataclasses.dataclass
8+
class Structure(ABC):
9+
@classmethod
10+
# TODO: When dropping support for Python 3.10 replace with -> Self
11+
def from_json(cls, structure: Mapping[str, Any]) -> "Structure":
12+
...

tiled/structures/sparse.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
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
5+
6+
from tiled.structures.root import Structure
47

58
from .array import BuiltinDtype, Endianness, Kind, StructDtype
69

@@ -12,7 +15,7 @@ class SparseLayout(str, enum.Enum):
1215

1316

1417
@dataclass
15-
class COOStructure:
18+
class COOStructure(Structure):
1619
chunks: Tuple[Tuple[int, ...], ...] # tuple-of-tuples-of-ints like ((3,), (3,))
1720
shape: Tuple[int, ...] # tuple of ints like (3, 3)
1821
data_type: Optional[Union[BuiltinDtype, StructDtype]] = None
@@ -27,7 +30,7 @@ class COOStructure:
2730
# TODO Include fill_value?
2831

2932
@classmethod
30-
def from_json(cls, structure):
33+
def from_json(cls, structure: Mapping[str, Any]) -> "COOStructure":
3134
data_type = structure.get("data_type", None)
3235
if data_type is not None and "fields" in data_type:
3336
data_type = StructDtype.from_json(data_type)

0 commit comments

Comments
 (0)