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.
@@ -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
+ [![Release](https://img.shields.io/pypi/v/readonlydict?label=Release&color=cornflowerblue&style=flat-square)](https://pypi.org/project/readonlydict/)
46
+ [![Python](https://img.shields.io/pypi/pyversions/readonlydict?label=Python&color=cornflowerblue&style=flat-square)](https://pypi.org/project/readonlydict/)
47
+ [![Downloads](https://img.shields.io/pypi/dm/readonlydict?label=Downloads&color=cornflowerblue&style=flat-square)](https://pepy.tech/project/readonlydict)
48
+ [![DOI](https://img.shields.io/badge/DOI-10.5281/zenodo.19187089-cornflowerblue?style=flat-square)](https://doi.org/10.5281/zenodo.19187089)
49
+ [![Tests](https://img.shields.io/github/actions/workflow/status/astropenguin/readonlydict/tests.yaml?label=Tests&style=flat-square)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.