dictation 1.0.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.
- dictation/__init__.py +528 -0
- dictation/version.txt +1 -0
- dictation-1.0.0.dist-info/LICENSE.md +21 -0
- dictation-1.0.0.dist-info/METADATA +363 -0
- dictation-1.0.0.dist-info/RECORD +8 -0
- dictation-1.0.0.dist-info/WHEEL +5 -0
- dictation-1.0.0.dist-info/top_level.txt +1 -0
- dictation-1.0.0.dist-info/zip-safe +1 -0
dictation/__init__.py
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import copy
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AnnotationError(AttributeError):
|
|
11
|
+
"""Error for alerting the absence of or inability to set the requested annotation"""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class dictation(dict):
|
|
17
|
+
"""Annotated dictionary type supports one or more annotation stored alongside the
|
|
18
|
+
standard dictionary key-value pairs, as well as automatic hierarchical links between
|
|
19
|
+
root and child dictionary nodes allowing for annotations to be defined and gathered
|
|
20
|
+
hierarchically from a given child dictionary node upwards to the root node."""
|
|
21
|
+
|
|
22
|
+
_reserved: list[str] = ["recursive"]
|
|
23
|
+
|
|
24
|
+
class _annotation(object):
|
|
25
|
+
"""The _annotation class holds an annotation value and its recursive state"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, value: object, recursive: bool = True):
|
|
28
|
+
self._value = value
|
|
29
|
+
|
|
30
|
+
if not isinstance(recursive, bool):
|
|
31
|
+
raise TypeError("The 'recursive' argument must have a boolean value!")
|
|
32
|
+
|
|
33
|
+
self._recursive = recursive
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def value(self) -> object | None:
|
|
37
|
+
"""Return the annotation class' stored value"""
|
|
38
|
+
|
|
39
|
+
return self._value
|
|
40
|
+
|
|
41
|
+
@value.setter
|
|
42
|
+
def value(self, value: object):
|
|
43
|
+
"""The annotation value can only be set during class construction"""
|
|
44
|
+
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def recursive(self) -> bool:
|
|
49
|
+
"""Return the annotation class' stored recursive state"""
|
|
50
|
+
|
|
51
|
+
return self._recursive
|
|
52
|
+
|
|
53
|
+
@recursive.setter
|
|
54
|
+
def recursive(self, recursive: bool):
|
|
55
|
+
"""The recursive value can only be set during class construction"""
|
|
56
|
+
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def data(self) -> dict[str, object]:
|
|
61
|
+
"""Provide access to the annotation in the form of a dictionary"""
|
|
62
|
+
|
|
63
|
+
return dict(value=self.value, recursive=self.recursive)
|
|
64
|
+
|
|
65
|
+
def __init__(self, *args, parent: dictation = None, **kwargs):
|
|
66
|
+
"""Initialize a `dictation` instance, which as a subclass of `dict` supports the
|
|
67
|
+
same instantiation patterns as a standard `dict` class for compatibility"""
|
|
68
|
+
|
|
69
|
+
super().__init__()
|
|
70
|
+
|
|
71
|
+
self._special: list[str] = sorted(
|
|
72
|
+
self.__class__._reserved
|
|
73
|
+
+ [attr for attr in dir(self) if not attr.startswith("_")]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self._annotations: dict[str, annotation] = {}
|
|
77
|
+
|
|
78
|
+
self._parent: dictation = None
|
|
79
|
+
|
|
80
|
+
if parent is None:
|
|
81
|
+
pass
|
|
82
|
+
elif isinstance(parent, dictation):
|
|
83
|
+
self._parent = parent
|
|
84
|
+
else:
|
|
85
|
+
raise TypeError(
|
|
86
|
+
"The 'parent' argument, if set, must have a `dictation` instance value!"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Iterate through any args
|
|
90
|
+
for arg in args:
|
|
91
|
+
if isinstance(arg, (dict, dictation)):
|
|
92
|
+
for key, value in arg.items():
|
|
93
|
+
self[key] = value
|
|
94
|
+
|
|
95
|
+
if isinstance(arg, dictation):
|
|
96
|
+
if isinstance(annotations := arg._annotations, dict):
|
|
97
|
+
for name, annotation in annotations.items():
|
|
98
|
+
self._annotations[name] = self._annotation(
|
|
99
|
+
value=annotation.value,
|
|
100
|
+
recursive=annotation.recursive,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if isinstance(parent := arg.parent, dictation):
|
|
104
|
+
self._parent = parent
|
|
105
|
+
|
|
106
|
+
for key, value in kwargs.items():
|
|
107
|
+
self[key] = value
|
|
108
|
+
|
|
109
|
+
def transform(source: object, parent: dictation = None) -> object:
|
|
110
|
+
"""Transform the current dictionary ensuring all subdictionaries are of the
|
|
111
|
+
expected class type and that their parent relationships are set as needed"""
|
|
112
|
+
|
|
113
|
+
if isinstance(source, dict):
|
|
114
|
+
if isinstance(source, dictation):
|
|
115
|
+
if source.parent is None:
|
|
116
|
+
source._parent = parent
|
|
117
|
+
else:
|
|
118
|
+
source = dictation(source, parent=parent)
|
|
119
|
+
|
|
120
|
+
for key, value in source.items():
|
|
121
|
+
source[key] = transform(value, parent=source)
|
|
122
|
+
elif isinstance(source, (list, tuple, set)):
|
|
123
|
+
for index, value in enumerate(source):
|
|
124
|
+
source[index] = transform(value, parent=parent)
|
|
125
|
+
|
|
126
|
+
return source
|
|
127
|
+
|
|
128
|
+
transform(source=self, parent=parent)
|
|
129
|
+
|
|
130
|
+
def __str__(self) -> str:
|
|
131
|
+
return "<dictation(%s)>" % (super().__str__())
|
|
132
|
+
|
|
133
|
+
def __copy__(self):
|
|
134
|
+
"""Supports the creation of shallow copies of dictation class instances"""
|
|
135
|
+
|
|
136
|
+
data = dict()
|
|
137
|
+
|
|
138
|
+
for key, value in self.items():
|
|
139
|
+
data[key] = copy.copy(value)
|
|
140
|
+
|
|
141
|
+
annotations = dict()
|
|
142
|
+
|
|
143
|
+
for key, annotation in self._annotations.items():
|
|
144
|
+
annotations[key] = copy.copy(annotation)
|
|
145
|
+
|
|
146
|
+
return dictation(data).annotate(**annotations)
|
|
147
|
+
|
|
148
|
+
def __deepcopy__(self, memo):
|
|
149
|
+
"""Supports the creation of deep copies of dictation class instances"""
|
|
150
|
+
|
|
151
|
+
data = dict()
|
|
152
|
+
|
|
153
|
+
for key, value in self.items():
|
|
154
|
+
data[key] = copy.deepcopy(value, memo)
|
|
155
|
+
|
|
156
|
+
annotations = dict()
|
|
157
|
+
|
|
158
|
+
for key, annotation in self._annotations.items():
|
|
159
|
+
annotations[key] = copy.deepcopy(annotation, memo)
|
|
160
|
+
|
|
161
|
+
return dictation(data).annotate(**annotations)
|
|
162
|
+
|
|
163
|
+
def __setattr__(self, name: str, value: object):
|
|
164
|
+
"""Supports setting class attributes or annotations via attributes; annotation
|
|
165
|
+
attributes cannot have names starting with underscores as these are reserved for
|
|
166
|
+
class attributes and methods, nor can annotations use the same name as inherent
|
|
167
|
+
class attributes or public methods; using such names will raise an exception."""
|
|
168
|
+
|
|
169
|
+
if not isinstance(name, str):
|
|
170
|
+
raise TypeError("The 'name' argument must have a string value!")
|
|
171
|
+
|
|
172
|
+
if name.startswith("_"):
|
|
173
|
+
super().__setattr__(name, value)
|
|
174
|
+
elif name in self._special:
|
|
175
|
+
if isinstance(attr := getattr(self.__class__, name, None), property):
|
|
176
|
+
super().__setattr__(name, value)
|
|
177
|
+
else:
|
|
178
|
+
raise AnnotationError(
|
|
179
|
+
"Annotations cannot use any of the following reserved word names: %s!"
|
|
180
|
+
% (", ".join(self._special))
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
self.annotate(**{name: value}, recursive=False)
|
|
184
|
+
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
def __getattr__(self, name: str, recursive: bool = False) -> object | None:
|
|
188
|
+
"""Supports obtaining class attributes or annotations; annotation attributes
|
|
189
|
+
cannot have names starting with underscores as these are reserved for class
|
|
190
|
+
attributes and methods. Annotations are searched hierarchically; if a matching
|
|
191
|
+
annotation is present on the current node it will be returned, alternatively all
|
|
192
|
+
parent nodes up to the root node will be searched for a match; if a match cannot
|
|
193
|
+
be found then an AttributeError exception will be raised to note its absence."""
|
|
194
|
+
|
|
195
|
+
if not isinstance(name, str):
|
|
196
|
+
raise TypeError("The 'name' argument must have a string value!")
|
|
197
|
+
|
|
198
|
+
if not isinstance(recursive, bool):
|
|
199
|
+
raise TypeError("The 'recursive' argument must have a boolean value!")
|
|
200
|
+
|
|
201
|
+
if name.startswith("_") or name in self._special:
|
|
202
|
+
return super().__getattribute__(name)
|
|
203
|
+
elif isinstance(annotation := self._annotations.get(name), self._annotation):
|
|
204
|
+
if recursive is False or annotation.recursive is True:
|
|
205
|
+
return annotation.value
|
|
206
|
+
elif isinstance(parent := self._parent, dictation):
|
|
207
|
+
return parent.__getattr__(name=name, recursive=True)
|
|
208
|
+
else:
|
|
209
|
+
raise AttributeError(f"The attribute '{name}' does not exist!")
|
|
210
|
+
|
|
211
|
+
def __delattr__(self, name: str):
|
|
212
|
+
"""Supports deleting previously assigned annotations"""
|
|
213
|
+
|
|
214
|
+
if name.startswith("_") or name in self._special:
|
|
215
|
+
super().__delattr__(name)
|
|
216
|
+
else:
|
|
217
|
+
self.unannotate(name=name)
|
|
218
|
+
|
|
219
|
+
def __setitem__(self, key: object, value: object):
|
|
220
|
+
"""Supports setting a dictionary item via the usual subscript d[k]=v notation"""
|
|
221
|
+
|
|
222
|
+
if isinstance(value, dict) and not isinstance(value, dictation):
|
|
223
|
+
value = dictation(value, parent=self)
|
|
224
|
+
|
|
225
|
+
super().__setitem__(key, value)
|
|
226
|
+
|
|
227
|
+
def __getitem__(self, key: object) -> object:
|
|
228
|
+
"""Supports getting a dictionary item via the usual subscript v=d[k] notation"""
|
|
229
|
+
|
|
230
|
+
value = super().__getitem__(key)
|
|
231
|
+
|
|
232
|
+
if isinstance(value, dict) and not isinstance(value, dictation):
|
|
233
|
+
value = dictation(value, parent=self)
|
|
234
|
+
|
|
235
|
+
return value
|
|
236
|
+
|
|
237
|
+
def __getstate__(self) -> dict:
|
|
238
|
+
"""Supports pickling the `dictation` dictionary instance"""
|
|
239
|
+
|
|
240
|
+
return self.__dict__.copy()
|
|
241
|
+
|
|
242
|
+
def __setstate__(self, state: dict):
|
|
243
|
+
"""Supports un-pickling the `dictation` dictionary instance"""
|
|
244
|
+
|
|
245
|
+
self.__dict__.update(state)
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def parent(self) -> dictation | None:
|
|
249
|
+
"""Return the instance's parent relationship, if one has been assigned"""
|
|
250
|
+
|
|
251
|
+
return self._parent
|
|
252
|
+
|
|
253
|
+
@parent.setter
|
|
254
|
+
def parent(self, parent: dictation):
|
|
255
|
+
"""A dictation instance's parent relationship can only be assigned internally by
|
|
256
|
+
the library to ensure integrity of the hierarchical references and the correct
|
|
257
|
+
determination of any recursively available annotations for child nodes."""
|
|
258
|
+
|
|
259
|
+
raise NotImplementedError("The 'parent' property cannot be set externally!")
|
|
260
|
+
|
|
261
|
+
def get(self, key: object, default: object = None) -> object | None:
|
|
262
|
+
"""Overrides the `dict` class' `get` method to support specifying the default
|
|
263
|
+
value positionally as well as via its keyword argument to improve documentation
|
|
264
|
+
of the code at the call site, e.g. x.get("y", 0) vs x.get("y", default=0)."""
|
|
265
|
+
|
|
266
|
+
return super().get(key, default)
|
|
267
|
+
|
|
268
|
+
def set(self, key: object, value: object) -> dictation:
|
|
269
|
+
"""Provides a `set` method to compliment the built-in `dict` class' `get` method
|
|
270
|
+
which takes the usual key and value as keyword arguments, and assigns the value
|
|
271
|
+
to the current dictionary instance at the provided key. As this method returns
|
|
272
|
+
`self`, calls to `set()` can be chained with calls to other class methods, such
|
|
273
|
+
as the `annotate()` method."""
|
|
274
|
+
|
|
275
|
+
self[key] = value
|
|
276
|
+
|
|
277
|
+
return self
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def annotations(self) -> dict[str, object]:
|
|
281
|
+
"""Obtain the current annotations associated with the current `dictation` node
|
|
282
|
+
as well as any annotations assigned to ancestors that are marked as recursive"""
|
|
283
|
+
|
|
284
|
+
def gather(source: object, annotations: dict, recursive: bool = False):
|
|
285
|
+
"""Recursively gather any annotations assigned to the current node as well
|
|
286
|
+
as any assigned to any ancestor nodes, returning them as a dictionary"""
|
|
287
|
+
|
|
288
|
+
if isinstance(source, dictation):
|
|
289
|
+
for key, annotation in source._annotations.items():
|
|
290
|
+
# If an annotation with the same name has already been encountered
|
|
291
|
+
# do not overwrite it; this ensures that annotations assigned higher
|
|
292
|
+
# in the hierarchy (starting at the root node), do not overwrite any
|
|
293
|
+
# annotations assigned at deeper levels, such as on a child node:
|
|
294
|
+
if not key in annotations:
|
|
295
|
+
if recursive is False or annotation.recursive is True:
|
|
296
|
+
annotations[key] = annotation.value
|
|
297
|
+
|
|
298
|
+
if isinstance(parent := source.parent, dictation):
|
|
299
|
+
gather(source=parent, annotations=annotations, recursive=True)
|
|
300
|
+
|
|
301
|
+
return annotations
|
|
302
|
+
|
|
303
|
+
return gather(source=self, annotations=dict())
|
|
304
|
+
|
|
305
|
+
@annotations.setter
|
|
306
|
+
def annotations(self, annotations: dict):
|
|
307
|
+
"""The `annotations` property setter supports assigning one or more annotations
|
|
308
|
+
to the current `dictation` instance by assigning a dictionary value of key-value
|
|
309
|
+
pairs. The key-value pairs will be applied to the current `dictation` instance
|
|
310
|
+
as additional annotations. If any of the annotation keys overlap with existing
|
|
311
|
+
annotation keys, the new value will replace the old; annotation keys which do
|
|
312
|
+
not match any existing annotation keys for the current instance will be applied
|
|
313
|
+
as new annotations alongside the old. This behaviour is equivalent to the `dict`
|
|
314
|
+
class' `update()` method, which overwrites existing keys and adds any new keys.
|
|
315
|
+
|
|
316
|
+
It is important to note that annotations applied to the current `dictation`
|
|
317
|
+
instance via the `annotations` property will not be available recursively to
|
|
318
|
+
any of the current instance's nested children. Furthermore, if a pre-existing
|
|
319
|
+
recursive annotation was stored on the current instance under a key that matches
|
|
320
|
+
a key provided in the annotations assigned via the property, not only will the
|
|
321
|
+
new value replace the old, but it will no longer be available recursively.
|
|
322
|
+
|
|
323
|
+
In order to control which annotations are applied recursively, one may use the
|
|
324
|
+
`annotate()` method instead which provides this additional level of control."""
|
|
325
|
+
|
|
326
|
+
if not isinstance(annotations, dict):
|
|
327
|
+
raise TypeError("The annotations property must be assigned as a dictionary")
|
|
328
|
+
else:
|
|
329
|
+
for name, value in annotations.items():
|
|
330
|
+
if not isinstance(name, str):
|
|
331
|
+
raise AnnotationError("Annotations must have string names!")
|
|
332
|
+
elif name.startswith("_"):
|
|
333
|
+
raise AnnotationError(
|
|
334
|
+
"Annotations cannot have names starting with underscores!"
|
|
335
|
+
)
|
|
336
|
+
elif name in self._special:
|
|
337
|
+
raise AnnotationError(
|
|
338
|
+
"Annotations cannot use any of the following reserved word names: %s!"
|
|
339
|
+
% (", ".join(self._special))
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
self.annotate(**annotations)
|
|
343
|
+
|
|
344
|
+
def annotate(self, recursive: bool = False, **annotations) -> dictation:
|
|
345
|
+
"""Supports assigning one or more annotations to the current dictation instance,
|
|
346
|
+
specified as key-value pairs. The 'recursive' keyword is reserved for specifying
|
|
347
|
+
if the annotations will be marked as being recursively available for the current
|
|
348
|
+
node and any children; otherwise they are only available to the current node."""
|
|
349
|
+
|
|
350
|
+
if not isinstance(recursive, bool):
|
|
351
|
+
raise TypeError("The 'recursive' argument must have a boolean value!")
|
|
352
|
+
|
|
353
|
+
for name, value in annotations.items():
|
|
354
|
+
if not isinstance(name, str):
|
|
355
|
+
raise AnnotationError("Annotations must have string names!")
|
|
356
|
+
elif name.startswith("_"):
|
|
357
|
+
raise AnnotationError(
|
|
358
|
+
"Annotations cannot have names starting with underscores!"
|
|
359
|
+
)
|
|
360
|
+
elif name in self._special:
|
|
361
|
+
raise AnnotationError(
|
|
362
|
+
"Annotations cannot use any of the following reserved word names: %s!"
|
|
363
|
+
% (", ".join(self._special))
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
self._annotations[name] = self._annotation(
|
|
367
|
+
value=value,
|
|
368
|
+
recursive=recursive,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return self
|
|
372
|
+
|
|
373
|
+
def annotation(
|
|
374
|
+
self,
|
|
375
|
+
name: str,
|
|
376
|
+
default: object = None,
|
|
377
|
+
recursive: bool = True,
|
|
378
|
+
) -> object | None:
|
|
379
|
+
"""Supports recursively obtaining and returning a named annotation value. If the
|
|
380
|
+
named annotation cannot be found, and if a default value has been specified, it
|
|
381
|
+
will be returned, otherwise the method will return the `None` value instead. If
|
|
382
|
+
the named annotation should only be retrieved from the current node and not via
|
|
383
|
+
a recursive lookup, the 'recursive' argument may be set to `False`."""
|
|
384
|
+
|
|
385
|
+
if not isinstance(name, str):
|
|
386
|
+
raise TypeError("The 'name' argument must have a string value!")
|
|
387
|
+
|
|
388
|
+
if not isinstance(recursive, bool):
|
|
389
|
+
raise TypeError("The 'recursive' argument must have a boolean value!")
|
|
390
|
+
|
|
391
|
+
if isinstance(annotation := self._annotations.get(name), self._annotation):
|
|
392
|
+
return annotation.value
|
|
393
|
+
elif recursive is True and isinstance(parent := self._parent, dictation):
|
|
394
|
+
return parent.annotation(name=name, recursive=recursive, default=default)
|
|
395
|
+
elif not default is None:
|
|
396
|
+
return default
|
|
397
|
+
|
|
398
|
+
def unannotate(self, name: str) -> dictation:
|
|
399
|
+
"""Supports removing an annotation assigned directly to the current `dictation`
|
|
400
|
+
node. The `dictation` class does not support removing annotations recursively,
|
|
401
|
+
as annotations are not stored recursively, rather they are stored directly on
|
|
402
|
+
the node to which they are assigned, but each annotation can be marked as being
|
|
403
|
+
available recursively – or not – to any nested children. As such, to remove any
|
|
404
|
+
recursively available annotation that is no longer needed, it must either be
|
|
405
|
+
removed from the node that it was directly assigned to, or it can be reassigned
|
|
406
|
+
to the same node without recursive availability via the `annotate()` method."""
|
|
407
|
+
|
|
408
|
+
if not isinstance(name, str):
|
|
409
|
+
raise TypeError("The 'name' argument must have a string value!")
|
|
410
|
+
|
|
411
|
+
if name in self._annotations:
|
|
412
|
+
del self._annotations[name]
|
|
413
|
+
|
|
414
|
+
return self
|
|
415
|
+
|
|
416
|
+
def data(self, metadata: bool = False, annotations: bool = False):
|
|
417
|
+
"""Supports generating a dictionary representation of a `dictation` instance's
|
|
418
|
+
data, optionally including associated metadata such as any parent references,
|
|
419
|
+
any assigned annotation values, and typing information for each value. This is
|
|
420
|
+
primarily useful for debugging purposes."""
|
|
421
|
+
|
|
422
|
+
if not isinstance(metadata, bool):
|
|
423
|
+
raise TypeError("The 'metadata' argument must have a boolean value!")
|
|
424
|
+
|
|
425
|
+
if not isinstance(annotations, bool):
|
|
426
|
+
raise TypeError("The 'annotations' argument must have a boolean value!")
|
|
427
|
+
|
|
428
|
+
def dump(source: object, metadata: bool = False, annotations: bool = False):
|
|
429
|
+
data = {}
|
|
430
|
+
|
|
431
|
+
if metadata is True:
|
|
432
|
+
data["@type"] = str(type(source))
|
|
433
|
+
|
|
434
|
+
if isinstance(source, dict):
|
|
435
|
+
if isinstance(source, dictation):
|
|
436
|
+
if metadata is True:
|
|
437
|
+
data["@parent"] = str(source.parent) if source.parent else None
|
|
438
|
+
|
|
439
|
+
if annotations is True:
|
|
440
|
+
data["@annotations"] = {
|
|
441
|
+
key: value for key, value in source.annotations.items()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if metadata is True:
|
|
445
|
+
data["@value"] = {}
|
|
446
|
+
|
|
447
|
+
for key, value in source.items():
|
|
448
|
+
data["@value"][key] = dump(
|
|
449
|
+
source=value,
|
|
450
|
+
metadata=metadata,
|
|
451
|
+
annotations=annotations,
|
|
452
|
+
)
|
|
453
|
+
else:
|
|
454
|
+
for key, value in source.items():
|
|
455
|
+
data[key] = dump(
|
|
456
|
+
source=value,
|
|
457
|
+
metadata=metadata,
|
|
458
|
+
annotations=annotations,
|
|
459
|
+
)
|
|
460
|
+
elif isinstance(source, (list, tuple)):
|
|
461
|
+
if metadata is True:
|
|
462
|
+
data["@value"] = []
|
|
463
|
+
|
|
464
|
+
for index, value in enumerate(source):
|
|
465
|
+
data["@value"].append(
|
|
466
|
+
dump(
|
|
467
|
+
source=value,
|
|
468
|
+
metadata=metadata,
|
|
469
|
+
annotations=annotations,
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
else:
|
|
473
|
+
data = []
|
|
474
|
+
|
|
475
|
+
for index, value in enumerate(source):
|
|
476
|
+
data.append(
|
|
477
|
+
dump(
|
|
478
|
+
source=value,
|
|
479
|
+
metadata=metadata,
|
|
480
|
+
annotations=annotations,
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
else:
|
|
484
|
+
if metadata is True:
|
|
485
|
+
data["@value"] = source
|
|
486
|
+
else:
|
|
487
|
+
data = source
|
|
488
|
+
|
|
489
|
+
return data
|
|
490
|
+
|
|
491
|
+
return dump(source=self, metadata=metadata, annotations=annotations)
|
|
492
|
+
|
|
493
|
+
def print(self, indent: int = 0):
|
|
494
|
+
"""Supports generating a print-out of the `dictation` instance's data as well as
|
|
495
|
+
any annotations which can be useful for debugging or visualisation purposes."""
|
|
496
|
+
|
|
497
|
+
if not isinstance(indent, int):
|
|
498
|
+
raise TypeError("The 'indent' argument must have an integer value!")
|
|
499
|
+
|
|
500
|
+
def printer(source: object, indent: int = 0):
|
|
501
|
+
print((" " * indent) + " => %s" % (type(source)))
|
|
502
|
+
|
|
503
|
+
if isinstance(source, dictation):
|
|
504
|
+
print((" " * indent) + " => parent -> %s" % (source.parent))
|
|
505
|
+
print((" " * indent) + " => annotations -> %s" % (source.annotations))
|
|
506
|
+
|
|
507
|
+
if isinstance(source, dict):
|
|
508
|
+
for key, value in source.items():
|
|
509
|
+
print((" " * indent) + " => %s" % (key))
|
|
510
|
+
|
|
511
|
+
if isinstance(value, (dict, list, tuple)):
|
|
512
|
+
printer(value, indent=(indent + 1))
|
|
513
|
+
else:
|
|
514
|
+
print((" " * (indent + 1)) + " => %s" % (value))
|
|
515
|
+
elif isinstance(source, (list, tuple)):
|
|
516
|
+
print((" " * indent) + " => parent -> %s" % (source.parent))
|
|
517
|
+
|
|
518
|
+
for key, index in enumerate(source):
|
|
519
|
+
print((" " * indent) + " => %s" % (index))
|
|
520
|
+
|
|
521
|
+
if isinstance(value, (dict, list, tuple)):
|
|
522
|
+
printer(value, indent=(indent + 1))
|
|
523
|
+
else:
|
|
524
|
+
print((" " * (indent + 1)) + " => %s" % (value))
|
|
525
|
+
else:
|
|
526
|
+
print((" " * indent) + " => %s (%s)" % (source, type(source)))
|
|
527
|
+
|
|
528
|
+
printer(source=self, indent=indent)
|
dictation/version.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright © 2024–2025 Daniel Sissman.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: dictation
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Annotated dictionaries for Python
|
|
5
|
+
Author: Daniel Sissman
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright © 2024–2025 Daniel Sissman.
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Documentation, https://github.com/bluebinary/dictation/blob/main/README.md
|
|
28
|
+
Project-URL: Changelog, https://github.com/bluebinary/dictation/blob/main/CHANGELOG.md
|
|
29
|
+
Project-URL: Repository, https://github.com/bluebinary/dictation
|
|
30
|
+
Project-URL: Issues, https://github.com/bluebinary/dictation/issues
|
|
31
|
+
Keywords: dictionary,dict,annotated dictionary
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
39
|
+
Requires-Python: >=3.9
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
License-File: LICENSE.md
|
|
42
|
+
|
|
43
|
+
# Dictation: Annotated Python Dictionaries
|
|
44
|
+
|
|
45
|
+
The Dictation library compliments Python's built-in `dict` data type by offering a fully
|
|
46
|
+
compatible subclass, `dictation`, which adds support for annotations – a way to carry
|
|
47
|
+
additional metadata within – yet separate from – the data held in the dictionary itself.
|
|
48
|
+
|
|
49
|
+
The `dictation` dictionary type also automatically assigns parent relationships to all
|
|
50
|
+
child dictionaries, which is useful to have access to in some data processing scenarios,
|
|
51
|
+
whether or not these are used in addition to the library's annotation capabilities.
|
|
52
|
+
|
|
53
|
+
The ability to assign annotations, or keep track of parent relationships for child nodes
|
|
54
|
+
of a nested dictionary structure can be useful where one can not modify the structure or
|
|
55
|
+
data held in a dictionary, because doing so could render it incompatible for some uses.
|
|
56
|
+
|
|
57
|
+
As the `dictation` data type is compatible with the built-in `dict` data type, it should
|
|
58
|
+
be usable anywhere an ordinary `dict` instance can be used.
|
|
59
|
+
|
|
60
|
+
The `dictation` library name is a portmanteau of `dic-tionary` and `anno-tation`.
|
|
61
|
+
|
|
62
|
+
### Requirements
|
|
63
|
+
|
|
64
|
+
The Dictation library has been tested with Python 3.9, 3.10, 3.11, 3.12 and 3.13 but may
|
|
65
|
+
work with some earlier versions such as 3.8 but has not been tested against this version
|
|
66
|
+
or any earlier. The library is not compatible with Python 2.* or earlier.
|
|
67
|
+
|
|
68
|
+
### Installation
|
|
69
|
+
|
|
70
|
+
The Dictation library is available from PyPI so may be added to a project's dependencies
|
|
71
|
+
via its `requirements.txt` file or similar by referencing the Dictation library's name,
|
|
72
|
+
`dictation`, or the library may be installed directly into the local runtime environment
|
|
73
|
+
using `pip install` by entering the following command, and following any prompts:
|
|
74
|
+
|
|
75
|
+
$ pip install dictation
|
|
76
|
+
|
|
77
|
+
### Class Methods & Properties
|
|
78
|
+
|
|
79
|
+
The Dictation library's `dictation` class is a subclass of the built-in `dict` class, so
|
|
80
|
+
all of the built-in functionality of `dict` is available, as well as several additional
|
|
81
|
+
class methods and properties as documented below:
|
|
82
|
+
|
|
83
|
+
* `annotate(recursive: bool = False, **kwargs)` – The `annotate()` method can be used to
|
|
84
|
+
assign one or more annotations to the current `dictation` instance provided as key-value
|
|
85
|
+
pairs; these are held separately from and do not interfere with the actual data held in
|
|
86
|
+
the dictionary. The `recursive` keyword is reserved for specifying if the annotations
|
|
87
|
+
provided will be marked as being recursively available for the current node as well as
|
|
88
|
+
for any nested child nodes, when `recursive` is set to `True` – when `recursive` is set
|
|
89
|
+
to `False` (or simply when the `recursive` argument is not specified), the annotations
|
|
90
|
+
will only available for the current node. Additionally, the `annotate()` method returns
|
|
91
|
+
`self` upon completion so calls to `annotate()` can be chained with calls to other class
|
|
92
|
+
methods, such as the `get()` method.
|
|
93
|
+
|
|
94
|
+
* `unannotate(name: str) -> dictation` – The `unannotate()` method provides support for
|
|
95
|
+
removing a named annotation from the current node; it cannot remove any annotations that
|
|
96
|
+
have been inherited from ancestors in the hierarchy; to remove a recursive annotation,
|
|
97
|
+
it must be removed directly from the `dictation` ancestor node it was assigned to.
|
|
98
|
+
|
|
99
|
+
* `annotation(name: str, default: object = None, recursive: bool = True) -> object` –
|
|
100
|
+
The `annotation()` method supports recursively obtaining a named annotation value. If
|
|
101
|
+
the named annotation cannot be found, the `default` value will be returned if one has
|
|
102
|
+
been provided, and if not, the `None` value will be returned.
|
|
103
|
+
|
|
104
|
+
* `annotations (getter) -> dict[str, object]` – The `annotations` getter returns the
|
|
105
|
+
annotations, if any, assigned to the current `dictation` node, or inherited from any
|
|
106
|
+
ancestors, where those annotations were assigned and set as being recursively available.
|
|
107
|
+
|
|
108
|
+
* `annotations (setter) <- dict[str, object]` – The `annotations` setter supports
|
|
109
|
+
assigning one or more annotations, specified as a dictionary of key-value pairs, to the
|
|
110
|
+
current `dictation` node. Annotations applied via the `annotations` setter only apply to
|
|
111
|
+
the current `dictation` node as they are not assigned as being recursively available to
|
|
112
|
+
any child nodes. If an annotation needs to be made available to the current node as well
|
|
113
|
+
as recursively to any child nodes, it must be set via the `annotate()` method instead,
|
|
114
|
+
which provides control over the recursive availability of the annotation being assigned.
|
|
115
|
+
|
|
116
|
+
* `parent (getter) -> dictation | None` – The `parent` property returns a reference to
|
|
117
|
+
the current `dictation` instance's parent, if available, otherwise `None` is returned.
|
|
118
|
+
|
|
119
|
+
* `set(key: object, value: object) -> dictation` – The `set()` method is a compliment to
|
|
120
|
+
the built-in `dict` class' `get` method. The `set()` method accepts the usual key and
|
|
121
|
+
value as method keyword arguments, and assigns the value to the current `dictation`
|
|
122
|
+
instance at the provided key. Additionally, the `set()` method returns `self` upon
|
|
123
|
+
completion, so calls to `set()` can be chained with calls to other class methods, such
|
|
124
|
+
as the `annotate()` method.
|
|
125
|
+
|
|
126
|
+
* `data(metadata: bool = False, annotations: bool = False) -> dict` – The `data()`
|
|
127
|
+
method supports generating a dictionary representation of the dictation's data, as well
|
|
128
|
+
as optionally including associated metadata such as the parent reference, any assigned
|
|
129
|
+
annotation values, and typing information for each value.
|
|
130
|
+
|
|
131
|
+
* `print(indent: int = 0)` – The `print()` method supports generating a print-out of the
|
|
132
|
+
`dictation` instance's data as well as any annotations which can be useful for debugging
|
|
133
|
+
and data visualisation purposes.
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
### Usage Demonstration
|
|
137
|
+
|
|
138
|
+
To use the Dictation library, simply import it and use the library's `dictation` class
|
|
139
|
+
as a replacement of, or compliment to, the built-in `dict` class:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from dictation import dictation
|
|
143
|
+
|
|
144
|
+
# Create a new `dictation` class instance with example data and add a few annotations:
|
|
145
|
+
sample = dictation(a=1, b=2, c=3).annotate(x=4, y=5)
|
|
146
|
+
|
|
147
|
+
# Check that the data held by the `dictation` instance is as expected; note that as
|
|
148
|
+
# the `dictation` class is a subclass of the `dict` class, that the two assertions below
|
|
149
|
+
# comparing against another `dictation` instance as well as a `dict` instance are valid:
|
|
150
|
+
assert sample == dictation(a=1, b=2, c=3)
|
|
151
|
+
assert sample == dict(a=1, b=2, c=3)
|
|
152
|
+
|
|
153
|
+
# Check that the annotation data held by the `dictation` instance is as expected; note
|
|
154
|
+
# that the annotations are held completely separately from the dictionary's data:
|
|
155
|
+
assert sample.annotations == dictation(x=4, y=5)
|
|
156
|
+
assert sample.annotations == dict(x=4, y=5)
|
|
157
|
+
|
|
158
|
+
# Modify the example annotation, "y", and add an annotation, "z", making both recursive:
|
|
159
|
+
sample.annotate(y=0, z=6.789, recursive=True)
|
|
160
|
+
|
|
161
|
+
# Check that the updates to the annotations are as expected:
|
|
162
|
+
assert sample.annotations == dictation(x=4, y=0, z=6.789)
|
|
163
|
+
|
|
164
|
+
# Attempt to obtain a named annotation; this works like the `dict` class' `get` method
|
|
165
|
+
# whereby if the annotation is found, it's value will be returned, otherwise the default
|
|
166
|
+
# value, if specified, will be returned, otherwise `None` will be returned instead:
|
|
167
|
+
assert sample.annotation("z") == 6.789
|
|
168
|
+
|
|
169
|
+
# An annotation named "v" does not exist, so the provided default is returned:
|
|
170
|
+
assert sample.annotation("v", default=8) == 8
|
|
171
|
+
|
|
172
|
+
# An annotation named "v" does not exist, nor is there a default, so `None` is returned:
|
|
173
|
+
assert sample.annotation("v") is None
|
|
174
|
+
|
|
175
|
+
# Add a new child dictionary to the current `dictation` instance, note that the child
|
|
176
|
+
# dictionary will be converted to a new `dictation` instance as will any of its nested
|
|
177
|
+
# child dictionaries:
|
|
178
|
+
sample["d"] = dict(e=5, f=6)
|
|
179
|
+
|
|
180
|
+
assert isinstance(sample["d"], dict)
|
|
181
|
+
assert isinstance(sample["d"], dictation)
|
|
182
|
+
|
|
183
|
+
# Check that the `sample` dictionary has the expected structure and data:
|
|
184
|
+
assert sample == dict(a=1, b=2, c=3, d=dict(e=5, f=6))
|
|
185
|
+
|
|
186
|
+
# Check that the nested dictionary, "d", has the expected annotations; as no annotations
|
|
187
|
+
# have currently been assigned directly to the nested dictionary "d", it will only have
|
|
188
|
+
# inherited recursive annotations assigned to its parent and their parents. As per this
|
|
189
|
+
# example code, it currently means that the inherited annotations consist of "y" and "z"
|
|
190
|
+
# which were assigned to the parent dictionary, `sample`, and as they were both marked
|
|
191
|
+
# as recursive annotations, they are available to any nested children, including to "d":
|
|
192
|
+
assert sample["d"].annotations == dict(y=0, z=6.789)
|
|
193
|
+
|
|
194
|
+
# The `dictation` library also supports assigning annotations as attributes; annotations
|
|
195
|
+
# added as attributes cannot use the same name as any of the class' inherent attributes,
|
|
196
|
+
# properties or methods however; attempting to assign an attribute using the name of an
|
|
197
|
+
# inherent class` attribute will raise an exception. So long as the annotation names are
|
|
198
|
+
# distinct, annotations can easily be assigned and retrieved using attribute accessors:
|
|
199
|
+
sample.greeting = "hello"
|
|
200
|
+
|
|
201
|
+
# Check that the annotation, "greeting", has the expected value
|
|
202
|
+
assert sample.greeting == "hello"
|
|
203
|
+
|
|
204
|
+
# Annotations assigned via attributes are stored identically as annotations assigned to
|
|
205
|
+
# a `dictation` instance in any other way, so they can all be accessed interchangeably:
|
|
206
|
+
assert sample.annotation("greeting") == "hello"
|
|
207
|
+
|
|
208
|
+
# Annotations assigned via the `annotate()` method can also be accessed as attributes:
|
|
209
|
+
assert sample.x == 4
|
|
210
|
+
assert sample.y == 0
|
|
211
|
+
assert sample.z == 6.789
|
|
212
|
+
|
|
213
|
+
# All annotations assigned to a node can be accessed as a dictionary representation via
|
|
214
|
+
# the `annotations` property, which returns a `dict` instance holding the annotations:
|
|
215
|
+
assert sample.annotations == dict(x=4, y=0, z=6.789, greeting="hello")
|
|
216
|
+
|
|
217
|
+
# Regardless of how annotations are assigned to the `dictation` instance, they can be
|
|
218
|
+
# accessed, modified or removed by any of the other methods; for example, annotations
|
|
219
|
+
# can be removed using the `unannotate()` method, which returns `self` on completion
|
|
220
|
+
# so can be chained:
|
|
221
|
+
assert sample.unannotate("y").annotations == dict(x=4, z=6.789, greeting="hello")
|
|
222
|
+
|
|
223
|
+
# The `del` language keyword can also be used to remove previously assigned annotations:
|
|
224
|
+
del sample.z
|
|
225
|
+
|
|
226
|
+
assert sample.annotations == dict(x=4, greeting="hello")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Please Note: Like any subclass of the built-in `dict` type, instances of the `dictation`
|
|
230
|
+
class can not be created directly via Python's dictionary-literal `{...}` syntax, rather
|
|
231
|
+
they must be instantiated using the `dictation` class constructor. One can however wrap
|
|
232
|
+
any `{}` dictionary literal, as well as variables holding a `dict`, with a `dictation`
|
|
233
|
+
class constructor to convert any regular `dict` to a `dictation` class instance:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from dictation import dictation
|
|
237
|
+
|
|
238
|
+
# The Python dictionary-literal syntax can only create `dict` instances:
|
|
239
|
+
sample = {"a": 1, "b": 2, "c": 3}
|
|
240
|
+
assert isinstance(sample, dictation) is False
|
|
241
|
+
assert isinstance(sample, dict) is True
|
|
242
|
+
assert sample == {"a": 1, "b": 2, "c": 3}
|
|
243
|
+
|
|
244
|
+
# So the `dictation` class constructor must be used to create all `dictation` instances,
|
|
245
|
+
# however, the `dictation` constructor can take a dictionary literal as input:
|
|
246
|
+
sample = dictation({"a": 1, "b": 2, "c": 3})
|
|
247
|
+
assert isinstance(sample, dictation) is True
|
|
248
|
+
assert isinstance(sample, dict) is True
|
|
249
|
+
assert sample == {"a": 1, "b": 2, "c": 3}
|
|
250
|
+
|
|
251
|
+
# Furthermore, variables holding regular `dict` values, whether created via the literal
|
|
252
|
+
# syntax or via the `dict` class constructor syntax...
|
|
253
|
+
sample = {"x": 7, "y": 8, "z": 9}
|
|
254
|
+
assert isinstance(sample, dictation) is False
|
|
255
|
+
assert isinstance(sample, dict) is True
|
|
256
|
+
assert sample == {"x": 7, "y": 8, "z": 9}
|
|
257
|
+
|
|
258
|
+
sample = dict(sample)
|
|
259
|
+
assert isinstance(sample, dictation) is False
|
|
260
|
+
assert isinstance(sample, dict) is True
|
|
261
|
+
assert sample == {"x": 7, "y": 8, "z": 9}
|
|
262
|
+
|
|
263
|
+
# ...can be passed to the `dictation` class constructor to cast to a `dictation` class:
|
|
264
|
+
sample = dictation(sample)
|
|
265
|
+
assert isinstance(sample, dictation) is True
|
|
266
|
+
assert isinstance(sample, dict) is True
|
|
267
|
+
assert sample == {"x": 7, "y": 8, "z": 9}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
One may also pass additional key-value pairs to the `dictation` class constructor during
|
|
271
|
+
casting. These additional key-value pairs will overwrite any matching existing keys with
|
|
272
|
+
the newly assigned values, as well as adding new key-value pairs to the dictionary for
|
|
273
|
+
keys that have not yet been defined:
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from dictation import dictation
|
|
277
|
+
|
|
278
|
+
base = dict(a=1, b=2, c=3)
|
|
279
|
+
assert base == dict(a=1, b=2, c=3)
|
|
280
|
+
|
|
281
|
+
sample = dictation(base, c=4, x=7, y=8, z=9) # "c" is being redefined with a value of 4
|
|
282
|
+
|
|
283
|
+
assert sample == dictation(a=1, b=2, c=4, x=7, y=8, z=9)
|
|
284
|
+
assert sample == dict(a=1, b=2, c=4, x=7, y=8, z=9)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Contributing & Local Development
|
|
288
|
+
|
|
289
|
+
To carry out development of the Dictation library, create a fork of the repository from
|
|
290
|
+
the GitHub account, then clone a copy of the fork to the local machine for development
|
|
291
|
+
and testing:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
$ cd path/to/local/development/directory
|
|
295
|
+
$ git clone git@github.com:<username>/dictation.git
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Then create a new feature/development branch, using a descriptive name for the branch:
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
$ cd path/to/local/development/directory/dictation
|
|
302
|
+
$ git checkout -b new_feature_branch
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
#### Code Linting
|
|
306
|
+
|
|
307
|
+
The Dictation library adheres to the code formatting specifications detailed in PEP-8,
|
|
308
|
+
which are verified and applied by the _Black_ code formatting tool. When code changes
|
|
309
|
+
are made to the library, one needs to ensure that the code conforms to these code
|
|
310
|
+
formatting specifications. To simplify this, the provided `Dockerfile` creates an image
|
|
311
|
+
that supports running _Black_ against the latest version of the code, and will report if
|
|
312
|
+
any issues are found. To run the code formatting checks, perform the following commands,
|
|
313
|
+
which will build the Docker image and then run the formatting checks:
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
$ docker compose build
|
|
317
|
+
$ docker compose run black
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
If any code formatting issues are found, they will be reported by _Black_. It is also
|
|
321
|
+
possible to run _Black_ so that it will automatically reformat the affected files; this
|
|
322
|
+
can be achieved as follows, by passing the `--verbose` flag, which allows _Black_ to
|
|
323
|
+
report which files:
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
$ docker compose run black --verbose
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The above will reformat any library source and unit test files that contain formatting
|
|
330
|
+
issues, and will report which changes are made.
|
|
331
|
+
|
|
332
|
+
#### Unit Tests
|
|
333
|
+
|
|
334
|
+
The Dictation library includes a suite of comprehensive unit tests which ensure that the
|
|
335
|
+
library functionality operates as expected. The unit tests were developed with and are
|
|
336
|
+
run via `pytest`.
|
|
337
|
+
|
|
338
|
+
To ensure that the unit tests are run within a predictable runtime environment where all
|
|
339
|
+
of the necessary dependencies are available, a [Docker](https://www.docker.com) image is
|
|
340
|
+
created within which the tests are run. To run the unit tests, ensure Docker and Docker
|
|
341
|
+
Compose are [installed](https://docs.docker.com/engine/install/), and run the commands
|
|
342
|
+
listed below, which will build the Docker image via `docker compose build` and then run
|
|
343
|
+
the tests via `docker compose run tests` – the output of the tests will be displayed:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
$ docker compose build
|
|
347
|
+
$ docker compose run tests
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
To run the unit tests with optional command line arguments being passed to `pytest`,
|
|
351
|
+
append the relevant arguments to the `docker compose run tests` command, as follows, for
|
|
352
|
+
example passing `-v` to enable verbose output or `-s` to print standard output:
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
$ docker compose run tests -v -s
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
See the documentation for [PyTest](https://docs.pytest.org/en/latest/) regarding the
|
|
359
|
+
available optional command line arguments.
|
|
360
|
+
|
|
361
|
+
### Copyright & License Information
|
|
362
|
+
|
|
363
|
+
Copyright © 2024–2025 Daniel Sissman; licensed under the MIT License.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
dictation/__init__.py,sha256=edbANTtc7HvxOu9QifboI3H6pCF5Vc2Sfqyo2qfZh_k,22269
|
|
2
|
+
dictation/version.txt,sha256=klIfw8vZZL3J9YSpkbif3apXVO0cyW1tQkRTOGacEwU,5
|
|
3
|
+
dictation-1.0.0.dist-info/LICENSE.md,sha256=j1XidOCGUhPx7CyXA31uC0XGKDRnvUcZpMp161qHI6g,1077
|
|
4
|
+
dictation-1.0.0.dist-info/METADATA,sha256=ORuUcuQw31fwaHjdO55tTVmLjGv9nxn2YFt0LppBmqE,17591
|
|
5
|
+
dictation-1.0.0.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
|
6
|
+
dictation-1.0.0.dist-info/top_level.txt,sha256=4ChkzqzYWFJx61tY8Tdu8M_ZZGz42k8F7tkp-jP3qTI,10
|
|
7
|
+
dictation-1.0.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
8
|
+
dictation-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dictation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|