Skip to content

Commit f01c023

Browse files
committed
Move Desc to its own file, add helper function to get similar descriptions
1 parent df2fb89 commit f01c023

File tree

7 files changed

+165
-272
lines changed

7 files changed

+165
-272
lines changed

data_prototype/containers.py

Lines changed: 17 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass
43
from typing import (
54
Protocol,
65
Dict,
@@ -10,7 +9,6 @@
109
Union,
1110
Callable,
1211
MutableMapping,
13-
TypeAlias,
1412
)
1513
import uuid
1614

@@ -19,6 +17,8 @@
1917
import numpy as np
2018
import pandas as pd
2119

20+
from .description import Desc, desc_like
21+
2222
from typing import TYPE_CHECKING
2323

2424
if TYPE_CHECKING:
@@ -33,123 +33,6 @@ def __sub__(self, other) -> "_MatplotlibTransform":
3333
...
3434

3535

36-
ShapeSpec: TypeAlias = Tuple[Union[str, int], ...]
37-
38-
39-
@dataclass(frozen=True)
40-
class Desc:
41-
# TODO: sort out how to actually spell this. We need to know:
42-
# - what the number of dimensions is (1d vs 2d vs ...)
43-
# - is this a fixed size dimension (e.g. 2 for xextent)
44-
# - is this a variable size depending on the query (e.g. N)
45-
# - what is the relative size to the other variable values (N vs N+1)
46-
# We are probably going to have to implement a DSL for this (😞)
47-
shape: ShapeSpec
48-
dtype: np.dtype
49-
coordinates: str = "naive"
50-
51-
@staticmethod
52-
def validate_shapes(
53-
specification: dict[str, ShapeSpec | "Desc"],
54-
actual: dict[str, ShapeSpec | "Desc"],
55-
*,
56-
broadcast: bool = False,
57-
) -> None:
58-
"""Validate specified shape relationships against a provided set of shapes.
59-
60-
Shapes provided are tuples of int | str. If a specification calls for an int,
61-
the exact size is expected.
62-
If it is a str, it must be a single capital letter optionally followed by ``+``
63-
or ``-`` an integer value.
64-
The same letter used in the specification must represent the same value in all
65-
appearances. The value may, however, be a variable (with an offset) in the
66-
actual shapes (which does not need to have the same letter).
67-
68-
Shapes may be provided as raw tuples or as ``Desc`` objects.
69-
70-
Parameters
71-
----------
72-
specification: dict[str, ShapeSpec | "Desc"]
73-
The desired shape relationships
74-
actual: dict[str, ShapeSpec | "Desc"]
75-
The shapes to test for compliance
76-
77-
Keyword Parameters
78-
------------------
79-
broadcast: bool
80-
Whether to allow broadcasted shapes to pass (i.e. actual shapes with a ``1``
81-
will not cause exceptions regardless of what the specified shape value is)
82-
83-
Raises
84-
------
85-
KeyError:
86-
If a required field from the specification is missing in the provided actual
87-
values.
88-
ValueError:
89-
If shapes are incompatible in any other way
90-
"""
91-
specvars: dict[str, int | tuple[str, int]] = {}
92-
for fieldname in specification:
93-
spec = specification[fieldname]
94-
if fieldname not in actual:
95-
raise KeyError(
96-
f"Actual is missing {fieldname!r}, required by specification."
97-
)
98-
desc = actual[fieldname]
99-
if isinstance(spec, Desc):
100-
spec = spec.shape
101-
if isinstance(desc, Desc):
102-
desc = desc.shape
103-
if not broadcast:
104-
if len(spec) != len(desc):
105-
raise ValueError(
106-
f"{fieldname!r} shape {desc} incompatible with specification "
107-
f"{spec}."
108-
)
109-
elif len(desc) > len(spec):
110-
raise ValueError(
111-
f"{fieldname!r} shape {desc} incompatible with specification "
112-
f"{spec}."
113-
)
114-
for speccomp, desccomp in zip(spec[::-1], desc[::-1]):
115-
if broadcast and desccomp == 1:
116-
continue
117-
if isinstance(speccomp, str):
118-
specv, specoff = speccomp[0], int(speccomp[1:] or 0)
119-
120-
if isinstance(desccomp, str):
121-
descv, descoff = desccomp[0], int(desccomp[1:] or 0)
122-
entry = (descv, descoff - specoff)
123-
else:
124-
entry = desccomp - specoff
125-
126-
if specv in specvars and entry != specvars[specv]:
127-
raise ValueError(f"Found two incompatible values for {specv!r}")
128-
129-
specvars[specv] = entry
130-
elif speccomp != desccomp:
131-
raise ValueError(
132-
f"{fieldname!r} shape {desc} incompatible with specification "
133-
f"{spec}"
134-
)
135-
return None
136-
137-
@staticmethod
138-
def compatible(a: dict[str, Desc], b: dict[str, Desc]) -> bool:
139-
"""Determine if ``a`` is a valid input for ``b``.
140-
141-
Note: ``a`` _may_ have additional keys.
142-
"""
143-
try:
144-
Desc.validate_shapes(b, a)
145-
except (KeyError, ValueError):
146-
return False
147-
for k, v in b.items():
148-
if a[k].coordinates != v.coordinates:
149-
return False
150-
return True
151-
152-
15336
class DataContainer(Protocol):
15437
def query(
15538
self,
@@ -184,6 +67,7 @@ def query(
18467
This is a key that clients can use to cache down-stream
18568
computations on this data.
18669
"""
70+
...
18771

18872
def describe(self) -> Dict[str, Desc]:
18973
"""
@@ -193,6 +77,7 @@ def describe(self) -> Dict[str, Desc]:
19377
-------
19478
Dict[str, Desc]
19579
"""
80+
...
19681

19782

19883
class NoNewKeys(ValueError):
@@ -312,73 +197,16 @@ def query(
312197
# if hash_key in self._cache:
313198
# return self._cache[hash_key], hash_key
314199

200+
desc = Desc(("N",), np.dtype("f8"))
201+
xy = {"x": desc, "y": desc}
315202
data_lim = graph.evaluator(
316-
{
317-
"x": Desc(
318-
("N",),
319-
np.dtype(
320-
"f8",
321-
),
322-
coordinates="data",
323-
),
324-
"y": Desc(
325-
("N",),
326-
np.dtype(
327-
"f8",
328-
),
329-
coordinates="data",
330-
),
331-
},
332-
{
333-
"x": Desc(
334-
("N",),
335-
np.dtype(
336-
"f8",
337-
),
338-
coordinates=parent_coordinates,
339-
),
340-
"y": Desc(
341-
("N",),
342-
np.dtype(
343-
"f8",
344-
),
345-
coordinates=parent_coordinates,
346-
),
347-
},
203+
desc_like(xy, coordinates="data"),
204+
desc_like(xy, coordinates=parent_coordinates),
348205
).inverse
206+
349207
screen_size = graph.evaluator(
350-
{
351-
"x": Desc(
352-
("N",),
353-
np.dtype(
354-
"f8",
355-
),
356-
coordinates=parent_coordinates,
357-
),
358-
"y": Desc(
359-
("N",),
360-
np.dtype(
361-
"f8",
362-
),
363-
coordinates=parent_coordinates,
364-
),
365-
},
366-
{
367-
"x": Desc(
368-
("N",),
369-
np.dtype(
370-
"f8",
371-
),
372-
coordinates="display",
373-
),
374-
"y": Desc(
375-
("N",),
376-
np.dtype(
377-
"f8",
378-
),
379-
coordinates="display",
380-
),
381-
},
208+
desc_like(xy, coordinates=parent_coordinates),
209+
desc_like(xy, coordinates="display"),
382210
)
383211

384212
screen_dims = screen_size.evaluate({"x": [0, 1], "y": [0, 1]})
@@ -429,39 +257,11 @@ def query(
429257
) -> Tuple[Dict[str, Any], Union[str, int]]:
430258
dmin, dmax = self._full_range
431259

260+
desc = Desc(("N",), np.dtype("f8"))
261+
xy = {"x": desc, "y": desc}
432262
data_lim = graph.evaluator(
433-
{
434-
"x": Desc(
435-
("N",),
436-
np.dtype(
437-
"f8",
438-
),
439-
coordinates="data",
440-
),
441-
"y": Desc(
442-
("N",),
443-
np.dtype(
444-
"f8",
445-
),
446-
coordinates="data",
447-
),
448-
},
449-
{
450-
"x": Desc(
451-
("N",),
452-
np.dtype(
453-
"f8",
454-
),
455-
coordinates=parent_coordinates,
456-
),
457-
"y": Desc(
458-
("N",),
459-
np.dtype(
460-
"f8",
461-
),
462-
coordinates=parent_coordinates,
463-
),
464-
},
263+
desc_like(xy, coordinates="data"),
264+
desc_like(xy, coordinates=parent_coordinates),
465265
).inverse
466266

467267
pts = data_lim.evaluate({"x": (0, 1), "y": (0, 1)})
@@ -493,7 +293,7 @@ def describe(self) -> Dict[str, Desc]:
493293

494294

495295
class SeriesContainer:
496-
_data: pd.DataFrame
296+
_data: pd.Series
497297
_index_name: str
498298
_hash_key: str
499299

@@ -615,7 +415,7 @@ def query(
615415
parent_coordinates: str = "axes",
616416
) -> Tuple[Dict[str, Any], Union[str, int]]:
617417
def hit_some_database():
618-
{}, "1"
418+
return {}, "1"
619419

620420
data, etag = hit_some_database()
621421
return data, etag

0 commit comments

Comments
 (0)