numbox 0.3.4__py3-none-any.whl → 0.4.0__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.

Potentially problematic release.


This version of numbox might be problematic. Click here for more details.

numbox/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.3.4'
1
+ __version__ = '0.4.0'
@@ -1,14 +1,50 @@
1
1
  import warnings
2
2
 
3
+ from abc import ABC, abstractmethod
3
4
  from collections import defaultdict
4
5
  from dataclasses import dataclass, field
5
6
  from typing import (
6
- Any, Callable, Dict, Iterator, List, Mapping, Set, Tuple, TypeAlias, Union
7
+ Any, Callable, Dict, Iterator, List, Mapping, Protocol, Set, Tuple, TypeAlias, TypedDict
7
8
  )
8
9
 
9
10
 
10
- Namespace: TypeAlias = Union['External', 'Variables']
11
- VarSpec: TypeAlias = Dict[str, Callable | Dict[str, str] | str]
11
+ class Namespace(ABC):
12
+ name: str
13
+ variables: Dict
14
+
15
+ @abstractmethod
16
+ def __contains__(self, key: str) -> bool:
17
+ pass
18
+
19
+ @abstractmethod
20
+ def __getitem__(self, key: str) -> 'Variable':
21
+ pass
22
+
23
+ @abstractmethod
24
+ def __iter__(self) -> Iterator['Variable']:
25
+ pass
26
+
27
+
28
+ class Storage(Protocol):
29
+ values: Dict['Variable', 'Value']
30
+ cache: Dict[tuple, 'VarValue']
31
+
32
+ def get(self, variable: 'Variable') -> 'Value':
33
+ """
34
+ Principal access point to the requested variable.
35
+ Instantiates the corresponding value when first
36
+ invoked for the given variable.
37
+ """
38
+
39
+
40
+ class VarSpec(TypedDict, total=False):
41
+ name: str
42
+ inputs: Dict[str, str]
43
+ formula: Callable
44
+ metadata: str
45
+ cacheable: bool
46
+
47
+
12
48
  VarValue: TypeAlias = Any
13
49
 
14
50
 
@@ -24,39 +60,59 @@ QUAL_SEP = "."
24
60
 
25
61
 
26
62
  def make_qual_name(namespace_name: str, var_name: str) -> str:
27
- """ Each `Variable` instance is best contained in a
28
- mapping namespace, with the given `namespace_name`.
29
- This function thereby returns qualified name of the
30
- `Variable` instance. """
63
+ """ Each `Variable` instance is best initialized in
64
+ and owned by a `Namespace` object (such as, instances
65
+ of `External` and `Variables`), with the given
66
+ `namespace_name`.
67
+
68
+ This function thereby returns qualified name of the
69
+ `Variable` instance. """
31
70
  return f"{namespace_name}{QUAL_SEP}{var_name}"
32
71
 
33
72
 
34
- class External:
73
+ class External(Namespace):
35
74
  """
36
- A dictionary that facilitates discovery of required names.
75
+ An 'external' namespace that facilitates discovery
76
+ of requested names.
77
+
37
78
  When requested a `Variable` with the given name via a
38
79
  typical `__getitem__` call, if the `Variable` is not
39
80
  found, it will be created and added to this dictionary.
40
81
  This way the user will be able to infer which variables
41
82
  are required from the external source abstracted by this
42
- dictionary.
83
+ namespace.
43
84
  """
44
85
  def __init__(self, name: str):
45
86
  self.name = name
46
- self._vars = {}
87
+ self.variables = {}
88
+
89
+ def __contains__(self, name: str) -> bool:
90
+ """
91
+ :param name: un-qualified name of the `Variable`
92
+ It is qualified with `self.name` - the name of this
93
+ `External` namespace that the `Variable` belongs to.
94
+ """
95
+ return name in self.variables
47
96
 
48
97
  def __getitem__(self, name):
49
- variable = self._vars.get(name)
98
+ """
99
+ :param name: un-qualified name of the `Variable`.
100
+ Instances of `Variable` that are 'external' should
101
+ be put in `External` namespace indirectly, through
102
+ a call to this method. These `Variable`s will be
103
+ qualified with the name `self.name` of this namespace.
104
+ """
105
+ variable = self.variables.get(name)
50
106
  if variable is None:
51
107
  variable = Variable(
52
108
  name=name,
53
109
  source=self.name
54
110
  )
55
- self._vars[name] = variable
111
+ self.variables[name] = variable
56
112
  return variable
57
113
 
58
114
  def __iter__(self) -> Iterator['Variable']:
59
- return iter(self._vars.values())
115
+ return iter(self.variables.values())
60
116
 
61
117
 
62
118
  @dataclass(frozen=True)
@@ -64,37 +120,28 @@ class Variable:
64
120
  """
65
121
  An instance of `Variable` is anything that can be calculated
66
122
  from the values of the given input dependencies using the
67
- provided formula (i.e., a function).
123
+ provided `formula` (i.e., a Python function).
68
124
 
69
125
  Calculated value can be `None`, that is why a non-calculated
70
126
  value is designated with `_null`.
71
127
 
72
- An instance of `Variable` is best created directly within
73
- the given `Namespace`, that is, when it is instantiated upon
74
- initialization of that `Namespace` and made then available
75
- through a `__getitem__` call to either a collection of
76
- variables created as an instance of `Variables` (see below)
77
- or a non-`Variables` mapping, referred to (or abstracted by),
78
- in general, as an `External` collection. The namespace is
79
- also referred to as the 'source' of the `Variable`.
80
-
81
- Qualified name of a `Variable` incorporates both the name
82
- of the `Variable` and the name of its source / namespace.
83
-
84
- It is therefore recommended to create `Variable` only in
85
- the `External` or `Variables` containers rather than in
86
- isolation.
128
+ An instance of `Variable` is best created within the given
129
+ `Namespace`. For example, when the `Variables` subtype of
130
+ the `Namespace` is instantiated, it gets populated with
131
+ the freshly created `Variable` instances per the `VarSpec`
132
+ specifications passed to it. Or, when the `External` subtype
133
+ of the `Namespace` is queried for the given variable name,
134
+ if a `Variable` with such a name is not already present in
135
+ that external namespace, it will be created and stored there.
87
136
 
88
137
  :param name: name of the `Variable` instance.
89
- :param source: name of the `Variables` or `External` instance
90
- which is namespaces / source of this `Variable`.
138
+ :param source: name of the `Namespace` instance which is
139
+ the namespace / source of this `Variable`.
91
140
  :param inputs: (optional) map from names of the `Variable`
92
141
  inputs (which are names of other `Variable` instances) to
93
- the names of their sources, i.e., names of either
94
- `Variables` or 'External' mapping collections referencing
95
- these variables.
96
- :param formula: (optional) function that calculates value
97
- of this `Variable` from its sources.
142
+ the names of their `Namespace`s.
143
+ :param formula: (optional) function that calculates the
144
+ value of this `Variable` from its sources.
98
145
  :param metadata: any possible metadata associated with
99
146
  this variable.
100
147
  :param cacheable: (default `False`) when `True`, the
@@ -102,6 +149,7 @@ class Variable:
102
149
  calculation by the `id` of the corresponding Python object
103
150
  containing that value. When attempted to recompute with
104
151
  the same inputs, cached value will be returned instead.
152
+ Use sparingly.
105
153
  """
106
154
  name: str
107
155
  source: str = field(default="")
@@ -117,10 +165,14 @@ class Variable:
117
165
  return isinstance(other, Variable) and self.name == other.name and self.source == other.source
118
166
 
119
167
  def qual_name(self) -> str:
168
+ """
169
+ Qualified name of `Variable` incorporates both the name
170
+ of the `Variable` and the name of its source / namespace.
171
+ """
120
172
  return make_qual_name(self.source, self.name)
121
173
 
122
174
 
123
- class Variables:
175
+ class Variables(Namespace):
124
176
  def __init__(
125
177
  self,
126
178
  name: str,
@@ -136,13 +188,14 @@ class Variables:
136
188
  self.name = name
137
189
  self.variables = {variable["name"]: Variable(source=self.name, **variable) for variable in variables}
138
190
 
191
+ def __contains__(self, name: str) -> bool:
192
+ return name in self.variables
193
+
139
194
  def __getitem__(self, variable_name: str) -> Variable:
140
195
  """
141
- :param variable_name: name of the `Variable` to retrieve,
142
- it is expected that `Variables` and `External` all expose
143
- this method that returns an instance of the `Variable` with
144
- the given name in either a `Variables` namespace or an
145
- `External` namespace, respectively.
196
+ :param variable_name: name of the `Variable` to retrieve.
197
+ This is un-qualified name, as it is already looked up in
198
+ this namespace.
146
199
  """
147
200
  return self.variables[variable_name]
148
201
 
@@ -154,6 +207,8 @@ class Variables:
154
207
  class Value:
155
208
  """
156
209
  Value of the corresponding `Variable`.
210
+ Best used when created indirectly by the
211
+ `Values` storage.
157
212
  """
158
213
  variable: Variable
159
214
  value: VarValue | _Null = field(default_factory=lambda: _null)
@@ -178,7 +233,7 @@ class CompiledNode:
178
233
  inputs: List[Variable]
179
234
 
180
235
  def __post_init__(self):
181
- if self.variable.formula and not self.inputs:
236
+ if self.variable.formula and not self.variable.inputs:
182
237
  raise RuntimeError(f"{self.variable} contains formula but no inputs, how come?")
183
238
 
184
239
  def __hash__(self):
@@ -206,18 +261,19 @@ class CompiledGraph:
206
261
  def execute(
207
262
  self,
208
263
  external_values: Dict[str, Dict[str, VarValue]],
209
- values: Values,
264
+ values: Storage,
210
265
  ):
211
266
  """
212
- Main entry point to calculation of compiled graph.
213
- Calculation requires the following inputs:
267
+ Main entry point to calculate values of nodes of the compiled
268
+ graph. Calculation requires the following inputs:
214
269
 
215
270
  :param external_values: actual values of all required external
216
271
  variables, this can be a superset of what is really needed for
217
272
  the calculation. The map is first from the name of the external
218
- source and then from the name of the variable within that
273
+ namespace and then from the name of the variable within that
219
274
  source to the variable's actual value.
220
- :param values: runtime storage of all values, instance of `Values`.
275
+ :param values: runtime storage of all values, e.g., an instance
276
+ of `Values`.
221
277
  """
222
278
  self._assign_external_values(external_values, values)
223
279
  self._calculate(self.ordered_nodes, values)
@@ -225,17 +281,15 @@ class CompiledGraph:
225
281
  def _assign_external_values(
226
282
  self,
227
283
  external_values: Dict[str, Dict[str, VarValue]],
228
- values: Values
284
+ values: Storage
229
285
  ):
230
286
  """
231
287
  For the external variables required for this calculation,
232
- populate their values into the `Values` container.
288
+ populate their values into the `Values` storage.
233
289
 
234
290
  :param external_values: mapping from names of external sources
235
- to dictionary from names of external `Variable`s to their instances,
291
+ to a dictionary from names of external `Variable`s to their values
236
292
  that are needed for the given calculation.
237
- :param external_values: dictionary of `External` name to a mapping
238
- of `Variable` names to their values.
239
293
  :param values: an instance of `Values` storage of all calculated
240
294
  values.
241
295
  """
@@ -266,17 +320,17 @@ class CompiledGraph:
266
320
  return [node for node in self.ordered_nodes if node in affected]
267
321
 
268
322
  @staticmethod
269
- def _calculate(nodes: List[CompiledNode], values: Values):
323
+ def _calculate(nodes: List[CompiledNode], values: Storage):
270
324
  """
271
- Calculate the values of the `Variable`s using their own callables
325
+ Calculate the values of the `Variable`s using their own `formula`
272
326
  by evaluating them as functions of the values of the specified
273
327
  inputs.
274
328
 
275
329
  All inputs need to be calculated first (i.e., to be non-`_null`)
276
330
  before the value of the given `Variable` can be `calculate`d.
277
-
278
- This is possible because the `Variable`s are supplied as a
279
- topologically ordered list `ordered_variables`.
331
+ This is possible because the `Variable`s in the list `nodes` can be
332
+ supplied as a topologically ordered list `self.ordered_variables`,
333
+ or an ordered sub-set thereof (see, e.g., `recompute`).
280
334
  """
281
335
  for node in nodes:
282
336
  if node.variable.formula is None:
@@ -294,21 +348,22 @@ class CompiledGraph:
294
348
  values.cache[cache_key] = result
295
349
  values.get(node.variable).value = result
296
350
 
297
- def recompute(self, changed: Dict[str, Dict[str, VarValue]], values: Values):
351
+ def recompute(self, changed: Dict[str, Dict[str, VarValue]], values: Storage):
298
352
  """
299
- :param changed: dict of sources to names to new values of changed `Variable`s.
353
+ :param changed: dict of sources to names to new values of changed
354
+ `Variable`s coming from either `External` or `Variables` source.
300
355
  :param values: storage of all the `Variable` values.
301
356
  """
302
357
  changed_vars = set()
303
358
  for src, vals in changed.items():
304
359
  for name, val in vals.items():
305
360
  variable = self.required_external_variables.get(src, {}).get(name)
306
- qual = make_qual_name(src, name)
361
+ qual_name = make_qual_name(src, name)
307
362
  if variable is None:
308
363
  try:
309
- variable = next(n.variable for n in self.ordered_nodes if n.variable.qual_name() == qual)
364
+ variable = next(n.variable for n in self.ordered_nodes if n.variable.qual_name() == qual_name)
310
365
  except StopIteration:
311
- warnings.warn(f"{qual} is not in the calculation path, update has no effect.")
366
+ warnings.warn(f"{qual_name} is not in the calculation path, update has no effect.")
312
367
  continue
313
368
  values.get(variable).value = val
314
369
  changed_vars.add(variable)
@@ -332,9 +387,9 @@ class Graph:
332
387
  `External` sources from which `Variable` inputs might
333
388
  be coming from.
334
389
  """
335
- self.external_source_names = external_source_names
336
- self.registry = {}
337
- self.external = {
390
+ self.external_source_names: List[str] = external_source_names
391
+ self.registry: Dict[str, Namespace] = {}
392
+ self.external: Dict[str, External] = {
338
393
  external_source_name: External(external_source_name) for external_source_name in external_source_names
339
394
  }
340
395
  for variables_name, variables_list in variables_lists.items():
@@ -355,6 +410,7 @@ class Graph:
355
410
  else:
356
411
  self.registry[external_name] = external_
357
412
  self.compiled_graphs = {}
413
+ self.reverse_dependencies = None
358
414
 
359
415
  def compile(self, required: List[str] | str) -> CompiledGraph:
360
416
  """
@@ -391,7 +447,7 @@ class Graph:
391
447
  return variables_source
392
448
  raise KeyError(f"Unknown source {source_name}")
393
449
 
394
- def _topological_order(self, required: List | Tuple | str):
450
+ def _topological_order(self, required: List[str] | Tuple[str] | str):
395
451
  """
396
452
  :param required: qualified name(s) of `Variable` instance(s)
397
453
  for which a topological ordering of a DAG is to be determined.
@@ -406,12 +462,13 @@ class Graph:
406
462
  used_external_vars: Set[Variable] = set()
407
463
 
408
464
  def visit(qual_name: str):
465
+ """ DFS traversal of graph nodes. """
409
466
  if qual_name in visited:
410
467
  return
411
468
  if qual_name in visiting:
412
469
  raise RuntimeError(f"Cycle detected at {qual_name}")
413
470
  visiting.add(qual_name)
414
- source_name, variable_name = qual_name.split(QUAL_SEP)
471
+ source_name, variable_name = qual_name.rsplit(QUAL_SEP, 1)
415
472
  source = self._get_source(source_name)
416
473
  variable = source[variable_name]
417
474
  if isinstance(source, External):
@@ -444,13 +501,16 @@ class Graph:
444
501
  Utility to calculate set of qualified names of variables
445
502
  impacted by each of the encountered inputs.
446
503
  """
504
+ if self.reverse_dependencies is not None:
505
+ return self.reverse_dependencies
447
506
  reverse = defaultdict(set)
448
507
  for source_name, source in self.registry.items():
449
508
  for variable in source:
450
- var_qual = make_qual_name(source_name, variable.name)
509
+ qual_name = make_qual_name(source_name, variable.name)
451
510
  for input_name, input_source in variable.inputs.items():
452
511
  input_qual = make_qual_name(input_source, input_name)
453
- reverse[input_qual].add(var_qual)
512
+ reverse[input_qual].add(qual_name)
513
+ self.reverse_dependencies = reverse
454
514
  return reverse
455
515
 
456
516
  def dependents_of(self, qual_names: List[str] | Set[str] | str) -> Set[str]:
@@ -473,13 +533,17 @@ class Graph:
473
533
  stack.append(dep)
474
534
  return result
475
535
 
476
- def explain(self, qual_name: str, direct: bool = True) -> str:
536
+ def explain(self, qual_name: str, right_to_left: bool = True) -> str:
477
537
  """
478
538
  Follow the dependencies chain to explain how the given
479
539
  variable is derived.
540
+
541
+ Uses `metadata` of the `Variable` instances.
542
+
480
543
  :param qual_name: qualified name of the `Variable`.
481
- :param direct: when `True` (default), begin explanation
482
- with `qual_name`.
544
+ :param right_to_left: when `True` (default), begin explanation
545
+ with `qual_name`. That is, move towards the ends of the
546
+ graph.
483
547
  """
484
548
  derived = set()
485
549
  derivation = []
@@ -503,6 +567,6 @@ class Graph:
503
567
  )
504
568
 
505
569
  collect(qual_name)
506
- derivation = reversed(derivation) if direct else derivation
570
+ derivation = reversed(derivation) if right_to_left else derivation
507
571
  derivation_txt = "\n" + "\n".join(derivation)
508
572
  return derivation_txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: numbox
3
- Version: 0.3.4
3
+ Version: 0.4.0
4
4
  Author: Mikhail Goykhman
5
5
  License: MIT License (with Citation Clause)
6
6
 
@@ -1,4 +1,4 @@
1
- numbox/__init__.py,sha256=Yyz9rKJSsP7kA7oh3l-XTxOFzEuuSR9Ly7oYXA1hfFM,22
1
+ numbox/__init__.py,sha256=2eiWQI55fd-roDdkt4Hvl9WzrTJ4xQo33VzFud6D03U,22
2
2
  numbox/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  numbox/core/configurations.py,sha256=0bCmxXL-QMwtvyIDhpXLeT-1KJMf_QpH0wLuEvYLGxQ,68
4
4
  numbox/core/any/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -15,7 +15,7 @@ numbox/core/bindings/utils.py,sha256=OATfF4k8e5oPa9_wlHHkLQhY_DhrPNKYdeeGu9Nj5yg
15
15
  numbox/core/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  numbox/core/proxy/proxy.py,sha256=Wt7yzswDmeQXt0yjcTcnLi2coneowSHWXy_IFpZZJMU,3612
17
17
  numbox/core/variable/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- numbox/core/variable/variable.py,sha256=vmkecU_1_OUJov1cg0bT3fU7zxu1L7cGMBWD3_0cvQM,19621
18
+ numbox/core/variable/variable.py,sha256=nGQL4MG3uWEADtiTBcJ_j8e2beRXHIZyqr6LQGRdqBI,21411
19
19
  numbox/core/work/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  numbox/core/work/builder.py,sha256=d0DRwJoyskp-6tYQyV1VE-v9eX99qJbQJ_FdAFrjuiE,6273
21
21
  numbox/core/work/builder_utils.py,sha256=z8au1x10AwnzQ0_MAbQ6DnKTp3u9HeYZ1jyfkUMYjVg,1213
@@ -35,8 +35,8 @@ numbox/utils/meminfo.py,sha256=ykFi8Vt0WcHI3ztgMwvpn6NqaflDSQGL8tjI01jrzm0,1759
35
35
  numbox/utils/standard.py,sha256=SPsQcyLZw21RaNCdfkIGE_QBaVnMtZjJY4F40_GGuak,347
36
36
  numbox/utils/timer.py,sha256=5_d690Fb3L2axJBRxtoB0qe23exBosNR4qu6cno4QfY,641
37
37
  numbox/utils/void_type.py,sha256=IkZsjNeAIShYJtvWbvERdHnl_mbF1rCRWiM3gp6II8U,404
38
- numbox-0.3.4.dist-info/LICENSE,sha256=ZlmjEDrZFNHpGxy_7HThWRlyBvuOMchK__qgTdiO_Uk,1446
39
- numbox-0.3.4.dist-info/METADATA,sha256=E3a0zMbjM0vjbO2DVN9zaZOuW1D09A20spTbIZK2imM,2996
40
- numbox-0.3.4.dist-info/WHEEL,sha256=WnJ8fYhv8N4SYVK2lLYNI6N0kVATA7b0piVUNvqIIJE,91
41
- numbox-0.3.4.dist-info/top_level.txt,sha256=A67jOkfqidCSYYm6ifjN_WZyIiR1B27fjxv6nNbPvjc,7
42
- numbox-0.3.4.dist-info/RECORD,,
38
+ numbox-0.4.0.dist-info/LICENSE,sha256=ZlmjEDrZFNHpGxy_7HThWRlyBvuOMchK__qgTdiO_Uk,1446
39
+ numbox-0.4.0.dist-info/METADATA,sha256=mQVFsKmleqDQfuWK0z-vOft_wefItozPamN4W4lE1aY,2996
40
+ numbox-0.4.0.dist-info/WHEEL,sha256=WnJ8fYhv8N4SYVK2lLYNI6N0kVATA7b0piVUNvqIIJE,91
41
+ numbox-0.4.0.dist-info/top_level.txt,sha256=A67jOkfqidCSYYm6ifjN_WZyIiR1B27fjxv6nNbPvjc,7
42
+ numbox-0.4.0.dist-info/RECORD,,
File without changes