atdata 0.1.3b3__py3-none-any.whl → 0.2.0a1__py3-none-any.whl

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.
atdata/lens.py CHANGED
@@ -1,4 +1,42 @@
1
- """Lenses between typed datasets"""
1
+ """Lens-based type transformations for datasets.
2
+
3
+ This module implements a lens system for bidirectional transformations between
4
+ different sample types. Lenses enable viewing a dataset through different type
5
+ schemas without duplicating the underlying data.
6
+
7
+ Key components:
8
+
9
+ - ``Lens``: Bidirectional transformation with getter (S -> V) and optional
10
+ putter (V, S -> S)
11
+ - ``LensNetwork``: Global singleton registry for lens transformations
12
+ - ``@lens``: Decorator to create and register lens transformations
13
+
14
+ Lenses support the functional programming concept of composable, well-behaved
15
+ transformations that satisfy lens laws (GetPut and PutGet).
16
+
17
+ Example:
18
+ >>> @packable
19
+ ... class FullData:
20
+ ... name: str
21
+ ... age: int
22
+ ... embedding: NDArray
23
+ ...
24
+ >>> @packable
25
+ ... class NameOnly:
26
+ ... name: str
27
+ ...
28
+ >>> @lens
29
+ ... def name_view(full: FullData) -> NameOnly:
30
+ ... return NameOnly(name=full.name)
31
+ ...
32
+ >>> @name_view.putter
33
+ ... def name_view_put(view: NameOnly, source: FullData) -> FullData:
34
+ ... return FullData(name=view.name, age=source.age,
35
+ ... embedding=source.embedding)
36
+ ...
37
+ >>> ds = Dataset[FullData]("data.tar")
38
+ >>> ds_names = ds.as_type(NameOnly) # Uses registered lens
39
+ """
2
40
 
3
41
  ##
4
42
  # Imports
@@ -39,24 +77,45 @@ type LensPutter[S, V] = Callable[[V, S], S]
39
77
  # Shortcut decorators
40
78
 
41
79
  class Lens( Generic[S, V] ):
42
- """TODO"""
43
-
44
- # @property
45
- # def source_type( self ) -> Type[S]:
46
- # """The source type (S) for the lens; what is put to"""
47
- # # TODO Figure out why linting fails here
48
- # return self.__orig_class__.__args__[0]
49
-
50
- # @property
51
- # def view_type( self ) -> Type[V]:
52
- # """The view type (V) for the lens; what is get'd from"""
53
- # # TODO FIgure out why linting fails here
54
- # return self.__orig_class__.__args__[1]
80
+ """A bidirectional transformation between two sample types.
81
+
82
+ A lens provides a way to view and update data of type ``S`` (source) as if
83
+ it were type ``V`` (view). It consists of a getter that transforms ``S -> V``
84
+ and an optional putter that transforms ``(V, S) -> S``, enabling updates to
85
+ the view to be reflected back in the source.
86
+
87
+ Type Parameters:
88
+ S: The source type, must derive from ``PackableSample``.
89
+ V: The view type, must derive from ``PackableSample``.
90
+
91
+ Example:
92
+ >>> @lens
93
+ ... def name_lens(full: FullData) -> NameOnly:
94
+ ... return NameOnly(name=full.name)
95
+ ...
96
+ >>> @name_lens.putter
97
+ ... def name_lens_put(view: NameOnly, source: FullData) -> FullData:
98
+ ... return FullData(name=view.name, age=source.age)
99
+ """
55
100
 
56
101
  def __init__( self, get: LensGetter[S, V],
57
102
  put: Optional[LensPutter[S, V]] = None
58
103
  ) -> None:
59
- """TODO"""
104
+ """Initialize a lens with a getter and optional putter function.
105
+
106
+ Args:
107
+ get: A function that transforms from source type ``S`` to view type
108
+ ``V``. Must accept exactly one parameter annotated with the
109
+ source type.
110
+ put: An optional function that updates the source based on a modified
111
+ view. Takes a view of type ``V`` and original source of type ``S``,
112
+ and returns an updated source of type ``S``. If not provided, a
113
+ trivial putter is used that ignores updates to the view.
114
+
115
+ Raises:
116
+ AssertionError: If the getter function doesn't have exactly one
117
+ parameter.
118
+ """
60
119
  ##
61
120
 
62
121
  # Check argument validity
@@ -70,11 +129,11 @@ class Lens( Generic[S, V] ):
70
129
  functools.update_wrapper( self, get )
71
130
 
72
131
  self.source_type: Type[PackableSample] = input_types[0].annotation
73
- self.view_type = sig.return_annotation
132
+ self.view_type: Type[PackableSample] = sig.return_annotation
74
133
 
75
134
  # Store the getter
76
135
  self._getter = get
77
-
136
+
78
137
  # Determine and store the putter
79
138
  if put is None:
80
139
  # Trivial putter does not update the source
@@ -86,7 +145,20 @@ class Lens( Generic[S, V] ):
86
145
  #
87
146
 
88
147
  def putter( self, put: LensPutter[S, V] ) -> LensPutter[S, V]:
89
- """TODO"""
148
+ """Decorator to register a putter function for this lens.
149
+
150
+ Args:
151
+ put: A function that takes a view of type ``V`` and source of type
152
+ ``S``, and returns an updated source of type ``S``.
153
+
154
+ Returns:
155
+ The putter function, allowing this to be used as a decorator.
156
+
157
+ Example:
158
+ >>> @my_lens.putter
159
+ ... def my_lens_put(view: ViewType, source: SourceType) -> SourceType:
160
+ ... return SourceType(...)
161
+ """
90
162
  ##
91
163
  self._putter = put
92
164
  return put
@@ -94,107 +166,135 @@ class Lens( Generic[S, V] ):
94
166
  # Methods to actually execute transformations
95
167
 
96
168
  def put( self, v: V, s: S ) -> S:
97
- """TODO"""
169
+ """Update the source based on a modified view.
170
+
171
+ Args:
172
+ v: The modified view of type ``V``.
173
+ s: The original source of type ``S``.
174
+
175
+ Returns:
176
+ An updated source of type ``S`` that reflects changes from the view.
177
+ """
98
178
  return self._putter( v, s )
99
179
 
100
180
  def get( self, s: S ) -> V:
101
- """TODO"""
181
+ """Transform the source into the view type.
182
+
183
+ Args:
184
+ s: The source sample of type ``S``.
185
+
186
+ Returns:
187
+ A view of the source as type ``V``.
188
+ """
102
189
  return self( s )
103
190
 
104
191
  # Convenience to enable calling the lens as its getter
105
-
192
+
106
193
  def __call__( self, s: S ) -> V:
107
- return self._getter( s )
194
+ """Apply the lens transformation (same as ``get()``).
108
195
 
109
- # TODO Figure out how to properly parameterize this
110
- # def _lens_factory[S, V]( register: bool = True ):
111
- # """Register the annotated function `f` as the getter of a sample lens"""
196
+ Args:
197
+ s: The source sample of type ``S``.
112
198
 
113
- # # The actual lens decorator taking a lens getter function to a lens object
114
- # def _decorator( f: LensGetter[S, V] ) -> Lens[S, V]:
115
- # ret = Lens[S, V]( f )
116
- # if register:
117
- # _network.register( ret )
118
- # return ret
119
-
120
- # # Return the lens decorator
121
- # return _decorator
199
+ Returns:
200
+ A view of the source as type ``V``.
201
+ """
202
+ return self._getter( s )
122
203
 
123
- # # For convenience
124
- # lens = _lens_factory
125
204
 
126
205
  def lens( f: LensGetter[S, V] ) -> Lens[S, V]:
206
+ """Decorator to create and register a lens transformation.
207
+
208
+ This decorator converts a getter function into a ``Lens`` object and
209
+ automatically registers it in the global ``LensNetwork`` registry.
210
+
211
+ Args:
212
+ f: A getter function that transforms from source type ``S`` to view
213
+ type ``V``. Must have exactly one parameter with a type annotation.
214
+
215
+ Returns:
216
+ A ``Lens[S, V]`` object that can be called to apply the transformation
217
+ or decorated with ``@lens_name.putter`` to add a putter function.
218
+
219
+ Example:
220
+ >>> @lens
221
+ ... def extract_name(full: FullData) -> NameOnly:
222
+ ... return NameOnly(name=full.name)
223
+ ...
224
+ >>> @extract_name.putter
225
+ ... def extract_name_put(view: NameOnly, source: FullData) -> FullData:
226
+ ... return FullData(name=view.name, age=source.age)
227
+ """
127
228
  ret = Lens[S, V]( f )
128
229
  _network.register( ret )
129
230
  return ret
130
231
 
131
232
 
132
- ##
133
- # Global registry of used lenses
233
+ class LensNetwork:
234
+ """Global registry for lens transformations between sample types.
134
235
 
135
- # _registered_lenses: Dict[LensSignature, Lens] = dict()
136
- # """TODO"""
236
+ This class implements a singleton pattern to maintain a global registry of
237
+ all lenses decorated with ``@lens``. It enables looking up transformations
238
+ between different ``PackableSample`` types.
137
239
 
138
- class LensNetwork:
139
- """TODO"""
240
+ Attributes:
241
+ _instance: The singleton instance of this class.
242
+ _registry: Dictionary mapping ``(source_type, view_type)`` tuples to
243
+ their corresponding ``Lens`` objects.
244
+ """
140
245
 
141
246
  _instance = None
142
247
  """The singleton instance"""
143
248
 
144
249
  def __new__(cls, *args, **kwargs):
250
+ """Ensure only one instance of LensNetwork exists (singleton pattern)."""
145
251
  if cls._instance is None:
146
252
  # If no instance exists, create a new one
147
253
  cls._instance = super().__new__(cls)
148
254
  return cls._instance # Return the existing (or newly created) instance
149
255
 
150
256
  def __init__(self):
257
+ """Initialize the lens registry (only on first instantiation)."""
151
258
  if not hasattr(self, '_initialized'): # Check if already initialized
152
259
  self._registry: Dict[LensSignature, Lens] = dict()
153
260
  self._initialized = True
154
261
 
155
262
  def register( self, _lens: Lens ):
156
- """Set `lens` as the canonical view between its source and view types"""
157
-
158
- # sig = inspect.signature( _lens.get )
159
- # input_types = list( sig.parameters.values() )
160
- # assert len( input_types ) == 1, \
161
- # 'Wrong number of input args for lens: should only have one'
162
-
163
- # input_type = input_types[0].annotation
164
- # print( input_type )
165
- # output_type = sig.return_annotation
166
-
167
- # self._registry[input_type, output_type] = _lens
168
- # print( _lens.source_type )
263
+ """Register a lens as the canonical transformation between two types.
264
+
265
+ Args:
266
+ _lens: The lens to register. Will be stored in the registry under
267
+ the key ``(_lens.source_type, _lens.view_type)``.
268
+
269
+ Note:
270
+ If a lens already exists for the same type pair, it will be
271
+ overwritten.
272
+ """
169
273
  self._registry[_lens.source_type, _lens.view_type] = _lens
170
274
 
171
275
  def transform( self, source: DatasetType, view: DatasetType ) -> Lens:
172
- """TODO"""
173
-
174
- # TODO Handle compositional closure
175
- ret = self._registry.get( (source, view), None )
176
- if ret is None:
177
- raise ValueError( f'No registered lens from source {source} to view {view}' )
178
-
179
- return ret
276
+ """Look up the lens transformation between two sample types.
180
277
 
278
+ Args:
279
+ source: The source sample type (must derive from ``PackableSample``).
280
+ view: The target view type (must derive from ``PackableSample``).
181
281
 
182
- # Create global singleton registry instance
183
- _network = LensNetwork()
282
+ Returns:
283
+ The registered ``Lens`` that transforms from ``source`` to ``view``.
184
284
 
185
- # def lens( f: LensPutter ) -> Lens:
186
- # """Register the annotated function `f` as a sample lens"""
187
- # ##
188
-
189
- # sig = inspect.signature( f )
285
+ Raises:
286
+ ValueError: If no lens has been registered for the given type pair.
190
287
 
191
- # input_types = list( sig.parameters.values() )
192
- # output_type = sig.return_annotation
193
-
194
- # _registered_lenses[]
288
+ Note:
289
+ Currently only supports direct transformations. Compositional
290
+ transformations (chaining multiple lenses) are not yet implemented.
291
+ """
292
+ ret = self._registry.get( (source, view), None )
293
+ if ret is None:
294
+ raise ValueError( f'No registered lens from source {source} to view {view}' )
195
295
 
196
- # f.lens = Lens(
296
+ return ret
197
297
 
198
- # )
199
298
 
200
- # return f
299
+ # Global singleton registry instance
300
+ _network = LensNetwork()