Annotated dictionaries for Python
Project description
Dictation: Annotated Python Dictionaries
The Dictation library compliments Python's built-in dict data type by offering a fully
compatible subclass, dictation, which adds support for annotations – a way to carry
additional metadata within – yet separate from – the data held in the dictionary itself.
The dictation dictionary type also automatically assigns parent relationships to all
child dictionaries, which is useful to have access to in some data processing scenarios,
whether or not these are used in addition to the library's annotation capabilities.
The ability to assign annotations, or keep track of parent relationships for child nodes of a nested dictionary structure can be useful where one can not modify the structure or data held in a dictionary, because doing so could render it incompatible for some uses.
As the dictation data type is compatible with the built-in dict data type, it should
be usable anywhere an ordinary dict instance can be used.
The dictation library name is a portmanteau of dic-tionary and anno-tation.
Requirements
The Dictation library has been tested with Python 3.9, 3.10, 3.11, 3.12 and 3.13 but may work with some earlier versions such as 3.8 but has not been tested against this version or any earlier. The library is not compatible with Python 2.* or earlier.
Installation
The Dictation library is available from PyPI so may be added to a project's dependencies
via its requirements.txt file or similar by referencing the Dictation library's name,
dictation, or the library may be installed directly into the local runtime environment
using pip install by entering the following command, and following any prompts:
$ pip install dictation
Class Methods & Properties
The Dictation library's dictation class is a subclass of the built-in dict class, so
all of the built-in functionality of dict is available, as well as several additional
class methods and properties as documented below:
-
annotate(recursive: bool = False, **kwargs)– Theannotate()method can be used to assign one or more annotations to the currentdictationinstance provided as key-value pairs; these are held separately from and do not interfere with the actual data held in the dictionary. Therecursivekeyword is reserved for specifying if the annotations provided will be marked as being recursively available for the current node as well as for any nested child nodes, whenrecursiveis set toTrue– whenrecursiveis set toFalse(or simply when therecursiveargument is not specified), the annotations will only available for the current node. Additionally, theannotate()method returnsselfupon completion so calls toannotate()can be chained with calls to other class methods, such as theget()method. -
unannotate(name: str) -> dictation– Theunannotate()method provides support for removing a named annotation from the current node; it cannot remove any annotations that have been inherited from ancestors in the hierarchy; to remove a recursive annotation, it must be removed directly from thedictationancestor node it was assigned to. -
annotation(name: str, default: object = None, recursive: bool = True) -> object– Theannotation()method supports recursively obtaining a named annotation value. If the named annotation cannot be found, thedefaultvalue will be returned if one has been provided, and if not, theNonevalue will be returned. -
annotations (getter) -> dict[str, object]– Theannotationsgetter returns the annotations, if any, assigned to the currentdictationnode, or inherited from any ancestors, where those annotations were assigned and set as being recursively available. -
annotations (setter) <- dict[str, object]– Theannotationssetter supports assigning one or more annotations, specified as a dictionary of key-value pairs, to the currentdictationnode. Annotations applied via theannotationssetter only apply to the currentdictationnode as they are not assigned as being recursively available to any child nodes. If an annotation needs to be made available to the current node as well as recursively to any child nodes, it must be set via theannotate()method instead, which provides control over the recursive availability of the annotation being assigned. -
parent (getter) -> dictation | None– Theparentproperty returns a reference to the currentdictationinstance's parent, if available, otherwiseNoneis returned. -
set(key: object, value: object) -> dictation– Theset()method is a compliment to the built-indictclass'getmethod. Theset()method accepts the usual key and value as method keyword arguments, and assigns the value to the currentdictationinstance at the provided key. Additionally, theset()method returnsselfupon completion, so calls toset()can be chained with calls to other class methods, such as theannotate()method. -
data(metadata: bool = False, annotations: bool = False) -> dict– Thedata()method supports generating a dictionary representation of the dictation's data, as well as optionally including associated metadata such as the parent reference, any assigned annotation values, and typing information for each value. -
print(indent: int = 0)– Theprint()method supports generating a print-out of thedictationinstance's data as well as any annotations which can be useful for debugging and data visualisation purposes.
Usage Demonstration
To use the Dictation library, simply import it and use the library's dictation class
as a replacement of, or compliment to, the built-in dict class:
from dictation import dictation
# Create a new `dictation` class instance with example data and add a few annotations:
sample = dictation(a=1, b=2, c=3).annotate(x=4, y=5)
# Check that the data held by the `dictation` instance is as expected; note that as
# the `dictation` class is a subclass of the `dict` class, that the two assertions below
# comparing against another `dictation` instance as well as a `dict` instance are valid:
assert sample == dictation(a=1, b=2, c=3)
assert sample == dict(a=1, b=2, c=3)
# Check that the annotation data held by the `dictation` instance is as expected; note
# that the annotations are held completely separately from the dictionary's data:
assert sample.annotations == dictation(x=4, y=5)
assert sample.annotations == dict(x=4, y=5)
# Modify the example annotation, "y", and add an annotation, "z", making both recursive:
sample.annotate(y=0, z=6.789, recursive=True)
# Check that the updates to the annotations are as expected:
assert sample.annotations == dictation(x=4, y=0, z=6.789)
# Attempt to obtain a named annotation; this works like the `dict` class' `get` method
# whereby if the annotation is found, it's value will be returned, otherwise the default
# value, if specified, will be returned, otherwise `None` will be returned instead:
assert sample.annotation("z") == 6.789
# An annotation named "v" does not exist, so the provided default is returned:
assert sample.annotation("v", default=8) == 8
# An annotation named "v" does not exist, nor is there a default, so `None` is returned:
assert sample.annotation("v") is None
# Add a new child dictionary to the current `dictation` instance, note that the child
# dictionary will be converted to a new `dictation` instance as will any of its nested
# child dictionaries:
sample["d"] = dict(e=5, f=6)
assert isinstance(sample["d"], dict)
assert isinstance(sample["d"], dictation)
# Check that the `sample` dictionary has the expected structure and data:
assert sample == dict(a=1, b=2, c=3, d=dict(e=5, f=6))
# Check that the nested dictionary, "d", has the expected annotations; as no annotations
# have currently been assigned directly to the nested dictionary "d", it will only have
# inherited recursive annotations assigned to its parent and their parents. As per this
# example code, it currently means that the inherited annotations consist of "y" and "z"
# which were assigned to the parent dictionary, `sample`, and as they were both marked
# as recursive annotations, they are available to any nested children, including to "d":
assert sample["d"].annotations == dict(y=0, z=6.789)
# The `dictation` library also supports assigning annotations as attributes; annotations
# added as attributes cannot use the same name as any of the class' inherent attributes,
# properties or methods however; attempting to assign an attribute using the name of an
# inherent class` attribute will raise an exception. So long as the annotation names are
# distinct, annotations can easily be assigned and retrieved using attribute accessors:
sample.greeting = "hello"
# Check that the annotation, "greeting", has the expected value
assert sample.greeting == "hello"
# Annotations assigned via attributes are stored identically as annotations assigned to
# a `dictation` instance in any other way, so they can all be accessed interchangeably:
assert sample.annotation("greeting") == "hello"
# Annotations assigned via the `annotate()` method can also be accessed as attributes:
assert sample.x == 4
assert sample.y == 0
assert sample.z == 6.789
# All annotations assigned to a node can be accessed as a dictionary representation via
# the `annotations` property, which returns a `dict` instance holding the annotations:
assert sample.annotations == dict(x=4, y=0, z=6.789, greeting="hello")
# Regardless of how annotations are assigned to the `dictation` instance, they can be
# accessed, modified or removed by any of the other methods; for example, annotations
# can be removed using the `unannotate()` method, which returns `self` on completion
# so can be chained:
assert sample.unannotate("y").annotations == dict(x=4, z=6.789, greeting="hello")
# The `del` language keyword can also be used to remove previously assigned annotations:
del sample.z
assert sample.annotations == dict(x=4, greeting="hello")
Please Note: Like any subclass of the built-in dict type, instances of the dictation
class can not be created directly via Python's dictionary-literal {...} syntax, rather
they must be instantiated using the dictation class constructor. One can however wrap
any {} dictionary literal, as well as variables holding a dict, with a dictation
class constructor to convert any regular dict to a dictation class instance:
from dictation import dictation
# The Python dictionary-literal syntax can only create `dict` instances:
sample = {"a": 1, "b": 2, "c": 3}
assert isinstance(sample, dictation) is False
assert isinstance(sample, dict) is True
assert sample == {"a": 1, "b": 2, "c": 3}
# So the `dictation` class constructor must be used to create all `dictation` instances,
# however, the `dictation` constructor can take a dictionary literal as input:
sample = dictation({"a": 1, "b": 2, "c": 3})
assert isinstance(sample, dictation) is True
assert isinstance(sample, dict) is True
assert sample == {"a": 1, "b": 2, "c": 3}
# Furthermore, variables holding regular `dict` values, whether created via the literal
# syntax or via the `dict` class constructor syntax...
sample = {"x": 7, "y": 8, "z": 9}
assert isinstance(sample, dictation) is False
assert isinstance(sample, dict) is True
assert sample == {"x": 7, "y": 8, "z": 9}
sample = dict(sample)
assert isinstance(sample, dictation) is False
assert isinstance(sample, dict) is True
assert sample == {"x": 7, "y": 8, "z": 9}
# ...can be passed to the `dictation` class constructor to cast to a `dictation` class:
sample = dictation(sample)
assert isinstance(sample, dictation) is True
assert isinstance(sample, dict) is True
assert sample == {"x": 7, "y": 8, "z": 9}
One may also pass additional key-value pairs to the dictation class constructor during
casting. These additional key-value pairs will overwrite any matching existing keys with
the newly assigned values, as well as adding new key-value pairs to the dictionary for
keys that have not yet been defined:
from dictation import dictation
base = dict(a=1, b=2, c=3)
assert base == dict(a=1, b=2, c=3)
sample = dictation(base, c=4, x=7, y=8, z=9) # "c" is being redefined with a value of 4
assert sample == dictation(a=1, b=2, c=4, x=7, y=8, z=9)
assert sample == dict(a=1, b=2, c=4, x=7, y=8, z=9)
Contributing & Local Development
To carry out development of the Dictation library, create a fork of the repository from the GitHub account, then clone a copy of the fork to the local machine for development and testing:
$ cd path/to/local/development/directory
$ git clone git@github.com:<username>/dictation.git
Then create a new feature/development branch, using a descriptive name for the branch:
$ cd path/to/local/development/directory/dictation
$ git checkout -b new_feature_branch
Code Linting
The Dictation library adheres to the code formatting specifications detailed in PEP-8,
which are verified and applied by the Black code formatting tool. When code changes
are made to the library, one needs to ensure that the code conforms to these code
formatting specifications. To simplify this, the provided Dockerfile creates an image
that supports running Black against the latest version of the code, and will report if
any issues are found. To run the code formatting checks, perform the following commands,
which will build the Docker image and then run the formatting checks:
$ docker compose build
$ docker compose run black
If any code formatting issues are found, they will be reported by Black. It is also
possible to run Black so that it will automatically reformat the affected files; this
can be achieved as follows, by passing the --verbose flag, which allows Black to
report which files:
$ docker compose run black --verbose
The above will reformat any library source and unit test files that contain formatting issues, and will report which changes are made.
Unit Tests
The Dictation library includes a suite of comprehensive unit tests which ensure that the
library functionality operates as expected. The unit tests were developed with and are
run via pytest.
To ensure that the unit tests are run within a predictable runtime environment where all
of the necessary dependencies are available, a Docker image is
created within which the tests are run. To run the unit tests, ensure Docker and Docker
Compose are installed, and run the commands
listed below, which will build the Docker image via docker compose build and then run
the tests via docker compose run tests – the output of the tests will be displayed:
$ docker compose build
$ docker compose run tests
To run the unit tests with optional command line arguments being passed to pytest,
append the relevant arguments to the docker compose run tests command, as follows, for
example passing -v to enable verbose output or -s to print standard output:
$ docker compose run tests -v -s
See the documentation for PyTest regarding the available optional command line arguments.
Copyright & License Information
Copyright © 2024–2025 Daniel Sissman; licensed under the MIT License.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dictation-1.0.0.tar.gz.
File metadata
- Download URL: dictation-1.0.0.tar.gz
- Upload date:
- Size: 20.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c86125c99c4e441a6a17c3f872a780ff780647187b0bcb7d87ce909ecc18ac4
|
|
| MD5 |
4c434ee13a3cdf3c40b66a803e905f34
|
|
| BLAKE2b-256 |
80f65e673d365fc7b8e775cc841585fc20007d849b711511e281f261e7847ac5
|
Provenance
The following attestation bundles were made for dictation-1.0.0.tar.gz:
Publisher:
python-publish.yml on bluebinary/dictation
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dictation-1.0.0.tar.gz -
Subject digest:
0c86125c99c4e441a6a17c3f872a780ff780647187b0bcb7d87ce909ecc18ac4 - Sigstore transparency entry: 182084468
- Sigstore integration time:
-
Permalink:
bluebinary/dictation@6c91be6caed9113a35b17db8c1bdf620d0c9c197 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/bluebinary
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@6c91be6caed9113a35b17db8c1bdf620d0c9c197 -
Trigger Event:
release
-
Statement type:
File details
Details for the file dictation-1.0.0-py3-none-any.whl.
File metadata
- Download URL: dictation-1.0.0-py3-none-any.whl
- Upload date:
- Size: 12.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e45ff52260fdb87367851cc8317e58d26677b732816171c7abadf2cc16e5d278
|
|
| MD5 |
22673fbc7e42b3a7c99dade2aa5cda8a
|
|
| BLAKE2b-256 |
1eb64965e9ba85badcd2fce8257938263a89b44391ae603148117e52eccb8614
|
Provenance
The following attestation bundles were made for dictation-1.0.0-py3-none-any.whl:
Publisher:
python-publish.yml on bluebinary/dictation
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dictation-1.0.0-py3-none-any.whl -
Subject digest:
e45ff52260fdb87367851cc8317e58d26677b732816171c7abadf2cc16e5d278 - Sigstore transparency entry: 182084473
- Sigstore integration time:
-
Permalink:
bluebinary/dictation@6c91be6caed9113a35b17db8c1bdf620d0c9c197 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/bluebinary
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@6c91be6caed9113a35b17db8c1bdf620d0c9c197 -
Trigger Event:
release
-
Statement type: