readonlydict 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.
readonlydict/__init__.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
__all__ = ["ReadonlyDict"]
|
|
2
|
+
__version__ = "1.0.0"
|
|
3
|
+
|
|
4
|
+
# standard library
|
|
5
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
6
|
+
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|
7
|
+
|
|
8
|
+
# dependencies
|
|
9
|
+
from typing_extensions import Self
|
|
10
|
+
|
|
11
|
+
# type hints
|
|
12
|
+
K = TypeVar("K")
|
|
13
|
+
V = TypeVar("V")
|
|
14
|
+
K2 = TypeVar("K2")
|
|
15
|
+
V2 = TypeVar("V2")
|
|
16
|
+
Tuples = Iterable[tuple[K, V]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ReadonlyDict(Mapping[K, V]):
|
|
20
|
+
"""Drop-in read-only dictionary with typing and runtime compatibility.
|
|
21
|
+
|
|
22
|
+
- ``ReadonlyDict()`` -> New empty read-only dictionary.
|
|
23
|
+
- ``ReadonlyDict(mapping)`` -> New read-only dictionary
|
|
24
|
+
initialized from a mapping object's ``(key, value)`` pairs.
|
|
25
|
+
- ``ReadonlyDict(iterable)`` -> New read-only dictionary
|
|
26
|
+
initialized as if via: ``d = {}; for k, v in iterable: d[k] = v``.
|
|
27
|
+
- ``ReadonlyDict(**kwargs)`` -> New read-only dictionary
|
|
28
|
+
initialized with the ``name=value`` pairs in the keyword argument list.
|
|
29
|
+
For example: ``ReadonlyDict(one=1, two=2)``.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
_data: dict[K, V]
|
|
34
|
+
_hash: int | None
|
|
35
|
+
|
|
36
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
37
|
+
# methods for initialization
|
|
38
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
|
|
41
|
+
@overload
|
|
42
|
+
def __new__(cls, mapping: Mapping[K, V], /) -> Self: ... # pyright: ignore
|
|
43
|
+
@overload
|
|
44
|
+
def __new__(cls, iterable: Tuples[K, V], /) -> Self: ... # pyright: ignore
|
|
45
|
+
@overload
|
|
46
|
+
def __new__(cls, **kwargs: V) -> "ReadonlyDict[str, V]": ...
|
|
47
|
+
|
|
48
|
+
# fmt: off
|
|
49
|
+
@overload
|
|
50
|
+
def __new__(cls, mapping: Mapping[K, V], /, **kwargs: V2) -> "ReadonlyDict[K | str, V | V2]": ...
|
|
51
|
+
@overload
|
|
52
|
+
def __new__(cls, iterable: Tuples[K, V], /, **kwargs: V2) -> "ReadonlyDict[K | str, V | V2]": ...
|
|
53
|
+
# fmt: on
|
|
54
|
+
|
|
55
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> Any: # type: ignore[misc]
|
|
56
|
+
return super().__new__(cls)
|
|
57
|
+
|
|
58
|
+
else:
|
|
59
|
+
|
|
60
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
61
|
+
"""Initialize ``self``. See ``help(type(self))`` for accurate signature."""
|
|
62
|
+
self._data = dict(*args, **kwargs)
|
|
63
|
+
self._hash = None
|
|
64
|
+
|
|
65
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
66
|
+
# methods for immutability
|
|
67
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
68
|
+
def __copy__(self) -> Self:
|
|
69
|
+
"""Return ``self`` for a shallow copy."""
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __hash__(self) -> int:
|
|
73
|
+
"""Return ``hash(self)``."""
|
|
74
|
+
if self._hash is None:
|
|
75
|
+
self._hash = hash(frozenset(self._data.items()))
|
|
76
|
+
|
|
77
|
+
return self._hash
|
|
78
|
+
|
|
79
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
80
|
+
# methods for instantiation
|
|
81
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
82
|
+
def __getitem__(self, key: K, /) -> V:
|
|
83
|
+
"""Return ``self[key]``."""
|
|
84
|
+
return self._data[key]
|
|
85
|
+
|
|
86
|
+
def __iter__(self) -> Iterator[K]:
|
|
87
|
+
"""Return ``iter(self).``"""
|
|
88
|
+
return iter(self._data)
|
|
89
|
+
|
|
90
|
+
def __len__(self) -> int:
|
|
91
|
+
"""Return ``len(self)``."""
|
|
92
|
+
return len(self._data)
|
|
93
|
+
|
|
94
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
95
|
+
# methods to be compatible with built-in dictionary
|
|
96
|
+
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
97
|
+
def copy(self) -> Self:
|
|
98
|
+
"""Return a shallow copy of ``self``."""
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
# fmt: off
|
|
102
|
+
@overload
|
|
103
|
+
@classmethod
|
|
104
|
+
def fromkeys(cls, iterable: Iterable[K2], /) -> "ReadonlyDict[K2, None]": ...
|
|
105
|
+
@overload
|
|
106
|
+
@classmethod
|
|
107
|
+
def fromkeys(cls, iterable: Iterable[K2], value: V2, /) -> "ReadonlyDict[K2, V2]": ...
|
|
108
|
+
# fmt: on
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def fromkeys(cls, iterable: Iterable[Any], value: Any = None, /) -> Any:
|
|
112
|
+
"""Create a new read-only dictionary with keys from ``iterable`` and values set to ``value``."""
|
|
113
|
+
return cls(dict.fromkeys(iterable, value))
|
|
114
|
+
|
|
115
|
+
def __or__(self, other: Mapping[K2, V2], /) -> "ReadonlyDict[K | K2, V | V2]":
|
|
116
|
+
"""Return ``self|value``."""
|
|
117
|
+
if not isinstance(other, Mapping): # pyright: ignore
|
|
118
|
+
return NotImplemented
|
|
119
|
+
|
|
120
|
+
return self.__class__(self._data | dict(other)) # pyright: ignore
|
|
121
|
+
|
|
122
|
+
def __ror__(self, other: Mapping[K2, V2], /) -> dict[K | K2, V | V2]:
|
|
123
|
+
"""Return ``value|self``."""
|
|
124
|
+
if not isinstance(other, Mapping): # pyright: ignore
|
|
125
|
+
return NotImplemented
|
|
126
|
+
|
|
127
|
+
return dict(other) | self._data
|
|
128
|
+
|
|
129
|
+
def __repr__(self) -> str:
|
|
130
|
+
"""Return ``repr(self)``."""
|
|
131
|
+
return f"{self.__class__.__name__}({self._data!r})"
|
|
132
|
+
|
|
133
|
+
def __reversed__(self) -> Iterator[K]:
|
|
134
|
+
"""Return ``reversed(self)``."""
|
|
135
|
+
return reversed(self._data)
|
readonlydict/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: readonlydict
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Drop-in read-only dictionary with 100% typing and runtime compatibility
|
|
5
|
+
Project-URL: homepage, https://astropenguin.github.io/readonlydict
|
|
6
|
+
Project-URL: repository, https://github.com/astropenguin/readonlydict
|
|
7
|
+
Author-email: Akio Taniguchi <a-taniguchi@mail.kitami-it.ac.jp>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Akio Taniguchi
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: dictionary,hashable,immutable,mapping,python,read-only,typing
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
39
|
+
Requires-Python: <4.0,>=3.10
|
|
40
|
+
Requires-Dist: typing-extensions<5,>=4
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# ReadonlyDict
|
|
44
|
+
|
|
45
|
+
[](https://pypi.org/project/readonlydict/)
|
|
46
|
+
[](https://pypi.org/project/readonlydict/)
|
|
47
|
+
[](https://pepy.tech/project/readonlydict)
|
|
48
|
+
[](https://doi.org/10.5281/zenodo.19187089)
|
|
49
|
+
[](https://github.com/astropenguin/readonlydict/actions)
|
|
50
|
+
|
|
51
|
+
Drop-in read-only dictionary with 100% typing and runtime compatibility
|
|
52
|
+
|
|
53
|
+
## Overview: Why ReadonlyDict?
|
|
54
|
+
|
|
55
|
+
This package is built strictly on the following formula: ``ReadonlyDict = (Built-in dictionary) - (In-place features) + (Read-only features)``.
|
|
56
|
+
|
|
57
|
+
- **100% compatibility and zero custom API:** Our goal is to achieve flawless compatibility with Python's built-in dictionary in both static type checking (e.g., [mypy], [Pyright]) and runtime behavior. We simply removed in-place methods (e.g., ``pop()``, ``update()``). We do not introduce any custom methods.
|
|
58
|
+
- **True immutable semantics:** The only additions are those strictly required for a read-only data structure: it is fully hashable (only if all values are hashable), and shallow copies (i.e., ``self.copy()``, ``copy.copy(self)``) simply return itself to save memory.
|
|
59
|
+
- **When to use this package:** If you want extended read-only features, existing packages like [frozendict], [immutabledict], or [immutables] are better choices. However, if your priority is pure compatibility and perfect static type inference, ReadonlyDict should be the optimal choice.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install readonlydict
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Basic Usage
|
|
68
|
+
|
|
69
|
+
It works exactly like a built-in dictionary, but raises an error if you try to modify it.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from readonlydict import ReadonlyDict
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Initialization works just like the built-in dictionary:
|
|
76
|
+
>>> ro = ReadonlyDict(a=0, b=1)
|
|
77
|
+
>>> ro
|
|
78
|
+
ReadonlyDict({'a': 0, 'b': 1})
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# It is fully hashable (can be used as a dictionary key or in a set):
|
|
82
|
+
>>> hash(ro)
|
|
83
|
+
-5925576189957013898
|
|
84
|
+
>>> {ro, ro}
|
|
85
|
+
{ReadonlyDict({'a': 0, 'b': 1})}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Mutation is strictly prohibited (static type checkers will also warn you):
|
|
89
|
+
>>> ro["c"] = 2
|
|
90
|
+
TypeError: 'ReadonlyDict' object does not support item assignment
|
|
91
|
+
>>> ro.update(c=2)
|
|
92
|
+
AttributeError: 'ReadonlyDict' object has no attribute 'update'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Advanced Usage: Subclassing with Type Hints
|
|
96
|
+
|
|
97
|
+
If you want to create your own custom read-only dictionary by subclassing ``ReadonlyDict``, you can maintain static type inference by utilizing ``TYPE_CHECKING`` and ``@overload``.
|
|
98
|
+
Here is the best-practice template for subclassing:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# standard library
|
|
102
|
+
from collections.abc import Iterable, Mapping
|
|
103
|
+
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|
104
|
+
|
|
105
|
+
# dependencies
|
|
106
|
+
from readonlydict import ReadonlyDict
|
|
107
|
+
|
|
108
|
+
# type variables
|
|
109
|
+
K = TypeVar("K")
|
|
110
|
+
K2 = TypeVar("K2")
|
|
111
|
+
V = TypeVar("V")
|
|
112
|
+
V2 = TypeVar("V2")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class CustomDict(ReadonlyDict[K, V]):
|
|
116
|
+
# Modify the return types to guarantee type inference:
|
|
117
|
+
if TYPE_CHECKING:
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
def __new__(cls, **kwargs: V) -> "CustomDict[str, V]": ...
|
|
121
|
+
@overload
|
|
122
|
+
def __new__(cls, mapping: Mapping[K, V], /, **kwargs: V2) -> "CustomDict[K | str, V | V2]": ...
|
|
123
|
+
@overload
|
|
124
|
+
def __new__(cls, iterable: Iterable[tuple[K, V]], /, **kwargs: V2) -> "CustomDict[K | str, V | V2]": ...
|
|
125
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> Any: ... # type: ignore[misc]
|
|
126
|
+
|
|
127
|
+
@overload
|
|
128
|
+
@classmethod
|
|
129
|
+
def fromkeys(cls, iterable: Iterable[K2], /) -> "CustomDict[K2, None]": ...
|
|
130
|
+
@overload
|
|
131
|
+
@classmethod
|
|
132
|
+
def fromkeys(cls, iterable: Iterable[K2], value: V2, /) -> "CustomDict[K2, V2]": ...
|
|
133
|
+
@classmethod
|
|
134
|
+
def fromkeys(cls, *args: Any, **kwargs: Any) -> Any: ...
|
|
135
|
+
|
|
136
|
+
def __or__(self, other: Mapping[K2, V2], /) -> "CustomDict[K | K2, V | V2]": ...
|
|
137
|
+
|
|
138
|
+
# Then add your custom properties or methods:
|
|
139
|
+
@property
|
|
140
|
+
def first(self) -> tuple[K, V]:
|
|
141
|
+
return next(iter(self.items()))
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
[frozendict]: https://github.com/Marco-Sulla/python-frozendict
|
|
145
|
+
[immutabledict]: https://immutabledict.corenting.fr
|
|
146
|
+
[immutables]: https://github.com/MagicStack/immutables
|
|
147
|
+
[mypy]: https://www.mypy-lang.org
|
|
148
|
+
[Pyright]: https://microsoft.github.io/pyright
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
readonlydict/__init__.py,sha256=0ODy0QFj-FyTlRybxAhnRktwwnxJGPmgPoeKblPMt0Q,4862
|
|
2
|
+
readonlydict/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
readonlydict-1.0.0.dist-info/METADATA,sha256=eUA8JPnxYARVZbb1yScPCax50gLgRVi6rYOVEFYR5Ek,6790
|
|
4
|
+
readonlydict-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
5
|
+
readonlydict-1.0.0.dist-info/licenses/LICENSE,sha256=kLOd6dOMVSpEJQdHKtfu2BGrEAXE3dcoOjcjS_kjWIU,1071
|
|
6
|
+
readonlydict-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akio Taniguchi
|
|
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.
|