dictation 1.0.0__tar.gz

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.
@@ -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.