interface-contract 0.4.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.
- interface_contract-0.4.0/CHANGELOG.md +33 -0
- interface_contract-0.4.0/LICENSE +21 -0
- interface_contract-0.4.0/MANIFEST.in +6 -0
- interface_contract-0.4.0/PKG-INFO +305 -0
- interface_contract-0.4.0/README.md +268 -0
- interface_contract-0.4.0/README.tr.md +156 -0
- interface_contract-0.4.0/interface_contract/__init__.py +68 -0
- interface_contract-0.4.0/interface_contract/_core.py +7 -0
- interface_contract-0.4.0/interface_contract/adapters.py +7 -0
- interface_contract-0.4.0/interface_contract/mypy_plugin.py +68 -0
- interface_contract-0.4.0/interface_contract/py.typed +0 -0
- interface_contract-0.4.0/interface_contract.egg-info/PKG-INFO +305 -0
- interface_contract-0.4.0/interface_contract.egg-info/SOURCES.txt +25 -0
- interface_contract-0.4.0/interface_contract.egg-info/dependency_links.txt +1 -0
- interface_contract-0.4.0/interface_contract.egg-info/requires.txt +10 -0
- interface_contract-0.4.0/interface_contract.egg-info/top_level.txt +2 -0
- interface_contract-0.4.0/pyproject.toml +79 -0
- interface_contract-0.4.0/setup.cfg +4 -0
- interface_contract-0.4.0/strict_interface/__init__.py +85 -0
- interface_contract-0.4.0/strict_interface/_core.py +798 -0
- interface_contract-0.4.0/strict_interface/adapters.py +109 -0
- interface_contract-0.4.0/strict_interface/mypy_plugin.py +5 -0
- interface_contract-0.4.0/strict_interface/py.typed +0 -0
- interface_contract-0.4.0/test_interface_contract.py +201 -0
- interface_contract-0.4.0/test_strict_interface.py +668 -0
- interface_contract-0.4.0/typing_tests/incomplete.py +12 -0
- interface_contract-0.4.0/typing_tests/valid.py +18 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
## 0.4.0 - 2026-08-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- New `interface_contract` import package and `interface-contract` distribution
|
|
10
|
+
name.
|
|
11
|
+
- Opt-in instance-field contracts through `check_attributes=True`.
|
|
12
|
+
- `AttributeSpec`, `attributes_of`, `missing_attributes`, `verify_instance`, and
|
|
13
|
+
`satisfies` inspection and validation APIs.
|
|
14
|
+
- Explicit adapter registries and process-wide adapter helpers.
|
|
15
|
+
- Optional mypy plugin for detecting incomplete implementations statically.
|
|
16
|
+
- English primary documentation and a Turkish companion guide.
|
|
17
|
+
- Dataclass compatibility coverage.
|
|
18
|
+
|
|
19
|
+
### Compatibility
|
|
20
|
+
|
|
21
|
+
- `strict_interface` remains a supported alias with identical public objects.
|
|
22
|
+
- Existing annotation behavior is unchanged unless field checking is explicitly
|
|
23
|
+
enabled.
|
|
24
|
+
|
|
25
|
+
## 0.3.0
|
|
26
|
+
|
|
27
|
+
- Added Python 3.14 support and fail-closed bytecode stub detection.
|
|
28
|
+
- Improved custom metaclass composition.
|
|
29
|
+
|
|
30
|
+
## 0.2.0
|
|
31
|
+
|
|
32
|
+
- Made interface validation automatic at class-definition time.
|
|
33
|
+
- Added `abstract=True` for intentionally incomplete intermediate classes.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ayhan Öztemel
|
|
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,305 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: interface-contract
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Definition-time Python interface contracts with signature-aware runtime validation
|
|
5
|
+
Author: Ayhan Öztemel
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/AyhanOztemel/py_interface
|
|
8
|
+
Project-URL: Repository, https://github.com/AyhanOztemel/py_interface
|
|
9
|
+
Project-URL: Issues, https://github.com/AyhanOztemel/py_interface/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/AyhanOztemel/py_interface/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: interface,protocol,abc,contract,runtime,type-safety
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
30
|
+
Requires-Dist: typeguard>=4; extra == "test"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
33
|
+
Requires-Dist: typeguard>=4; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy==2.3.1; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff==0.16.5; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# interface-contract
|
|
39
|
+
|
|
40
|
+
Strict runtime interface contracts for Python, with definition-time failures and
|
|
41
|
+
signature-aware structural checks.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install interface-contract
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from interface_contract import Interface, default
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Repository(Interface):
|
|
52
|
+
def find(self, item_id: int) -> str: ...
|
|
53
|
+
def save(self, item: str) -> None: ...
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def name(self) -> str: ...
|
|
57
|
+
|
|
58
|
+
@default
|
|
59
|
+
def describe(self) -> str:
|
|
60
|
+
return f"repository<{self.name}>"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SqlRepository(Repository):
|
|
64
|
+
def find(self, item_id: int) -> str:
|
|
65
|
+
return f"row {item_id}"
|
|
66
|
+
|
|
67
|
+
def save(self, item: str) -> None:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def name(self) -> str:
|
|
72
|
+
return "sql"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If `SqlRepository` misses a required member, changes its descriptor kind, or has
|
|
76
|
+
an incompatible signature, class creation raises `InterfaceError`. You do not
|
|
77
|
+
need to wait until an instance is created or a method is called.
|
|
78
|
+
|
|
79
|
+
## Why interface-contract?
|
|
80
|
+
|
|
81
|
+
Python already has `abc.ABC` and `typing.Protocol`; this package targets a
|
|
82
|
+
different boundary: strict runtime validation for plugin systems, application
|
|
83
|
+
architecture, dependency injection, and dynamically loaded code.
|
|
84
|
+
|
|
85
|
+
| Capability | `abc.ABC` | runtime `Protocol` | `interface-contract` |
|
|
86
|
+
|---|---:|---:|---:|
|
|
87
|
+
| Missing method detected at runtime | instantiation | `isinstance` | class definition |
|
|
88
|
+
| Runtime signature validation | no | no | yes |
|
|
89
|
+
| Property/static/class method kind validation | no | no | yes |
|
|
90
|
+
| Signature-aware structural `isinstance` | no | no | yes |
|
|
91
|
+
| Explicit default implementations | concrete method | concrete method | `@default` |
|
|
92
|
+
| Optional instance-field contracts | annotations only | presence only | presence + shallow type check |
|
|
93
|
+
| Runtime adapter registry | no | no | yes |
|
|
94
|
+
|
|
95
|
+
This is not a replacement for static typing. Use mypy or another type checker for
|
|
96
|
+
whole-program analysis, and use interface-contract where runtime boundaries must
|
|
97
|
+
fail loudly and predictably.
|
|
98
|
+
|
|
99
|
+
## Core behavior
|
|
100
|
+
|
|
101
|
+
### Definition-time validation
|
|
102
|
+
|
|
103
|
+
Concrete subclasses are checked as soon as their `class` statement executes.
|
|
104
|
+
Intermediate implementations can opt out until a concrete subclass is ready:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
class BaseRepository(Repository, abstract=True):
|
|
108
|
+
def save(self, item: str) -> None:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class MemoryRepository(BaseRepository):
|
|
113
|
+
def find(self, item_id: int) -> str:
|
|
114
|
+
return "row"
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def name(self) -> str:
|
|
118
|
+
return "memory"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Abstract implementations cannot be instantiated.
|
|
122
|
+
|
|
123
|
+
### Default methods
|
|
124
|
+
|
|
125
|
+
Interface methods normally declare requirements and therefore must have an empty
|
|
126
|
+
body (`...`, `pass`, or a docstring-only body). Mark intentional implementations
|
|
127
|
+
with `@default`:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from interface_contract import Interface, default
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Named(Interface):
|
|
134
|
+
@property
|
|
135
|
+
def name(self) -> str: ...
|
|
136
|
+
|
|
137
|
+
@default
|
|
138
|
+
def display_name(self) -> str:
|
|
139
|
+
return self.name.title()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Structural interfaces
|
|
143
|
+
|
|
144
|
+
Set `structural=True` when inheritance is not under your control:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
class Closable(Interface, structural=True):
|
|
148
|
+
def close(self) -> None: ...
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class FileLike:
|
|
152
|
+
def close(self) -> None:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
assert isinstance(FileLike(), Closable)
|
|
157
|
+
assert issubclass(FileLike, Closable)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Unlike runtime-checkable protocols, the structural check also validates callable
|
|
161
|
+
signatures and descriptor kinds.
|
|
162
|
+
|
|
163
|
+
### Instance-field contracts
|
|
164
|
+
|
|
165
|
+
Field checking is opt-in, preserving compatibility with versions that ignored
|
|
166
|
+
class annotations:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
class UserRecord(Interface, check_attributes=True):
|
|
170
|
+
name: str
|
|
171
|
+
age: int
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class User(UserRecord):
|
|
175
|
+
def __init__(self, name: str, age: int) -> None:
|
|
176
|
+
self.name = name
|
|
177
|
+
self.age = age
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Fields are checked immediately after `__init__`. Standard annotations receive a
|
|
181
|
+
best-effort shallow runtime check; parameter contents such as every item inside
|
|
182
|
+
`list[str]` are intentionally not traversed. `ClassVar` does not declare an
|
|
183
|
+
instance field. Dataclass implementations are supported.
|
|
184
|
+
|
|
185
|
+
For objects that cannot inherit from an interface, use `verify_instance` or
|
|
186
|
+
`satisfies`:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from interface_contract import satisfies, verify_instance
|
|
190
|
+
|
|
191
|
+
verify_instance(User("Ada", 37), UserRecord) # returns the object or raises
|
|
192
|
+
assert satisfies(User("Ada", 37), UserRecord)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Adapters
|
|
196
|
+
|
|
197
|
+
The registry converts an existing type to a target interface and validates the
|
|
198
|
+
result:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from interface_contract import AdapterRegistry
|
|
202
|
+
|
|
203
|
+
registry = AdapterRegistry()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@registry.register(dict, UserRecord)
|
|
207
|
+
def dict_to_user(data: dict[str, object]) -> User:
|
|
208
|
+
return User(name=str(data["name"]), age=int(data["age"]))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
user = registry.adapt({"name": "Ada", "age": 37}, UserRecord)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`adapt`, `can_adapt`, `register_adapter`, and `unregister_adapter` expose a
|
|
215
|
+
process-wide default registry when a dedicated registry is unnecessary.
|
|
216
|
+
|
|
217
|
+
## Optional annotation checks
|
|
218
|
+
|
|
219
|
+
Call signatures are always checked for parameter shape. To also compare available
|
|
220
|
+
parameter and return annotations, enable `check_annotations=True`:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
class Parser(Interface, check_annotations=True):
|
|
224
|
+
def parse(self, value: str) -> int: ...
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Runtime annotation comparison is deliberately conservative. It does not try to
|
|
228
|
+
replace a static type checker.
|
|
229
|
+
|
|
230
|
+
## Mypy integration
|
|
231
|
+
|
|
232
|
+
The package is typed and ships an optional mypy plugin. It lets mypy reject the
|
|
233
|
+
instantiation of incomplete implementations before execution. No extra runtime
|
|
234
|
+
dependency is installed.
|
|
235
|
+
|
|
236
|
+
```toml
|
|
237
|
+
[tool.mypy]
|
|
238
|
+
plugins = ["interface_contract.mypy_plugin"]
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
class Job(Interface):
|
|
243
|
+
def execute(self, payload: str) -> int: ...
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class Incomplete(Job):
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
Incomplete() # mypy: Cannot instantiate abstract class "Incomplete"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The mypy plugin API is itself experimental; runtime validation remains the source
|
|
254
|
+
of truth.
|
|
255
|
+
|
|
256
|
+
## Supported members
|
|
257
|
+
|
|
258
|
+
- regular and async methods
|
|
259
|
+
- properties, including independent getter/setter/deleter requirements
|
|
260
|
+
- static methods and class methods
|
|
261
|
+
- generic interfaces
|
|
262
|
+
- multiple and derived interfaces
|
|
263
|
+
- custom metaclass composition
|
|
264
|
+
- source-less environments such as REPL, `exec`, notebooks, frozen apps, and
|
|
265
|
+
bytecode-only distributions
|
|
266
|
+
|
|
267
|
+
Useful inspection functions include `members_of`, `attributes_of`,
|
|
268
|
+
`missing_members`, `missing_attributes`, `signature_problem`, `verify`, and
|
|
269
|
+
`structurally_implements`.
|
|
270
|
+
|
|
271
|
+
## Backward compatibility
|
|
272
|
+
|
|
273
|
+
The former import path remains fully supported:
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from strict_interface import Interface
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`strict_interface.Interface` and `interface_contract.Interface` are the same
|
|
280
|
+
object. Existing source code does not need an import migration. The PyPI
|
|
281
|
+
distribution name is `interface-contract`; the preferred new import is
|
|
282
|
+
`interface_contract`.
|
|
283
|
+
|
|
284
|
+
Version 0.4.0 is additive except for the distribution rename. Runtime field
|
|
285
|
+
checking only activates when `check_attributes=True` is explicitly selected.
|
|
286
|
+
|
|
287
|
+
## Development
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
python -m pip install -e ".[dev]"
|
|
291
|
+
python -m pytest
|
|
292
|
+
python -m ruff check .
|
|
293
|
+
python -m mypy strict_interface interface_contract typing_tests/valid.py
|
|
294
|
+
python -m build
|
|
295
|
+
python -m twine check dist/*
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
See [README.tr.md](https://github.com/AyhanOztemel/py_interface/blob/main/README.tr.md)
|
|
299
|
+
for Turkish documentation and
|
|
300
|
+
[CHANGELOG.md](https://github.com/AyhanOztemel/py_interface/blob/main/CHANGELOG.md)
|
|
301
|
+
for release notes.
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
MIT
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# interface-contract
|
|
2
|
+
|
|
3
|
+
Strict runtime interface contracts for Python, with definition-time failures and
|
|
4
|
+
signature-aware structural checks.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install interface-contract
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from interface_contract import Interface, default
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Repository(Interface):
|
|
15
|
+
def find(self, item_id: int) -> str: ...
|
|
16
|
+
def save(self, item: str) -> None: ...
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def name(self) -> str: ...
|
|
20
|
+
|
|
21
|
+
@default
|
|
22
|
+
def describe(self) -> str:
|
|
23
|
+
return f"repository<{self.name}>"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SqlRepository(Repository):
|
|
27
|
+
def find(self, item_id: int) -> str:
|
|
28
|
+
return f"row {item_id}"
|
|
29
|
+
|
|
30
|
+
def save(self, item: str) -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
return "sql"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If `SqlRepository` misses a required member, changes its descriptor kind, or has
|
|
39
|
+
an incompatible signature, class creation raises `InterfaceError`. You do not
|
|
40
|
+
need to wait until an instance is created or a method is called.
|
|
41
|
+
|
|
42
|
+
## Why interface-contract?
|
|
43
|
+
|
|
44
|
+
Python already has `abc.ABC` and `typing.Protocol`; this package targets a
|
|
45
|
+
different boundary: strict runtime validation for plugin systems, application
|
|
46
|
+
architecture, dependency injection, and dynamically loaded code.
|
|
47
|
+
|
|
48
|
+
| Capability | `abc.ABC` | runtime `Protocol` | `interface-contract` |
|
|
49
|
+
|---|---:|---:|---:|
|
|
50
|
+
| Missing method detected at runtime | instantiation | `isinstance` | class definition |
|
|
51
|
+
| Runtime signature validation | no | no | yes |
|
|
52
|
+
| Property/static/class method kind validation | no | no | yes |
|
|
53
|
+
| Signature-aware structural `isinstance` | no | no | yes |
|
|
54
|
+
| Explicit default implementations | concrete method | concrete method | `@default` |
|
|
55
|
+
| Optional instance-field contracts | annotations only | presence only | presence + shallow type check |
|
|
56
|
+
| Runtime adapter registry | no | no | yes |
|
|
57
|
+
|
|
58
|
+
This is not a replacement for static typing. Use mypy or another type checker for
|
|
59
|
+
whole-program analysis, and use interface-contract where runtime boundaries must
|
|
60
|
+
fail loudly and predictably.
|
|
61
|
+
|
|
62
|
+
## Core behavior
|
|
63
|
+
|
|
64
|
+
### Definition-time validation
|
|
65
|
+
|
|
66
|
+
Concrete subclasses are checked as soon as their `class` statement executes.
|
|
67
|
+
Intermediate implementations can opt out until a concrete subclass is ready:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
class BaseRepository(Repository, abstract=True):
|
|
71
|
+
def save(self, item: str) -> None:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class MemoryRepository(BaseRepository):
|
|
76
|
+
def find(self, item_id: int) -> str:
|
|
77
|
+
return "row"
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def name(self) -> str:
|
|
81
|
+
return "memory"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Abstract implementations cannot be instantiated.
|
|
85
|
+
|
|
86
|
+
### Default methods
|
|
87
|
+
|
|
88
|
+
Interface methods normally declare requirements and therefore must have an empty
|
|
89
|
+
body (`...`, `pass`, or a docstring-only body). Mark intentional implementations
|
|
90
|
+
with `@default`:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from interface_contract import Interface, default
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Named(Interface):
|
|
97
|
+
@property
|
|
98
|
+
def name(self) -> str: ...
|
|
99
|
+
|
|
100
|
+
@default
|
|
101
|
+
def display_name(self) -> str:
|
|
102
|
+
return self.name.title()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Structural interfaces
|
|
106
|
+
|
|
107
|
+
Set `structural=True` when inheritance is not under your control:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
class Closable(Interface, structural=True):
|
|
111
|
+
def close(self) -> None: ...
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class FileLike:
|
|
115
|
+
def close(self) -> None:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
assert isinstance(FileLike(), Closable)
|
|
120
|
+
assert issubclass(FileLike, Closable)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Unlike runtime-checkable protocols, the structural check also validates callable
|
|
124
|
+
signatures and descriptor kinds.
|
|
125
|
+
|
|
126
|
+
### Instance-field contracts
|
|
127
|
+
|
|
128
|
+
Field checking is opt-in, preserving compatibility with versions that ignored
|
|
129
|
+
class annotations:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
class UserRecord(Interface, check_attributes=True):
|
|
133
|
+
name: str
|
|
134
|
+
age: int
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class User(UserRecord):
|
|
138
|
+
def __init__(self, name: str, age: int) -> None:
|
|
139
|
+
self.name = name
|
|
140
|
+
self.age = age
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Fields are checked immediately after `__init__`. Standard annotations receive a
|
|
144
|
+
best-effort shallow runtime check; parameter contents such as every item inside
|
|
145
|
+
`list[str]` are intentionally not traversed. `ClassVar` does not declare an
|
|
146
|
+
instance field. Dataclass implementations are supported.
|
|
147
|
+
|
|
148
|
+
For objects that cannot inherit from an interface, use `verify_instance` or
|
|
149
|
+
`satisfies`:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from interface_contract import satisfies, verify_instance
|
|
153
|
+
|
|
154
|
+
verify_instance(User("Ada", 37), UserRecord) # returns the object or raises
|
|
155
|
+
assert satisfies(User("Ada", 37), UserRecord)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Adapters
|
|
159
|
+
|
|
160
|
+
The registry converts an existing type to a target interface and validates the
|
|
161
|
+
result:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from interface_contract import AdapterRegistry
|
|
165
|
+
|
|
166
|
+
registry = AdapterRegistry()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@registry.register(dict, UserRecord)
|
|
170
|
+
def dict_to_user(data: dict[str, object]) -> User:
|
|
171
|
+
return User(name=str(data["name"]), age=int(data["age"]))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
user = registry.adapt({"name": "Ada", "age": 37}, UserRecord)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`adapt`, `can_adapt`, `register_adapter`, and `unregister_adapter` expose a
|
|
178
|
+
process-wide default registry when a dedicated registry is unnecessary.
|
|
179
|
+
|
|
180
|
+
## Optional annotation checks
|
|
181
|
+
|
|
182
|
+
Call signatures are always checked for parameter shape. To also compare available
|
|
183
|
+
parameter and return annotations, enable `check_annotations=True`:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
class Parser(Interface, check_annotations=True):
|
|
187
|
+
def parse(self, value: str) -> int: ...
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Runtime annotation comparison is deliberately conservative. It does not try to
|
|
191
|
+
replace a static type checker.
|
|
192
|
+
|
|
193
|
+
## Mypy integration
|
|
194
|
+
|
|
195
|
+
The package is typed and ships an optional mypy plugin. It lets mypy reject the
|
|
196
|
+
instantiation of incomplete implementations before execution. No extra runtime
|
|
197
|
+
dependency is installed.
|
|
198
|
+
|
|
199
|
+
```toml
|
|
200
|
+
[tool.mypy]
|
|
201
|
+
plugins = ["interface_contract.mypy_plugin"]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
class Job(Interface):
|
|
206
|
+
def execute(self, payload: str) -> int: ...
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Incomplete(Job):
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
Incomplete() # mypy: Cannot instantiate abstract class "Incomplete"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The mypy plugin API is itself experimental; runtime validation remains the source
|
|
217
|
+
of truth.
|
|
218
|
+
|
|
219
|
+
## Supported members
|
|
220
|
+
|
|
221
|
+
- regular and async methods
|
|
222
|
+
- properties, including independent getter/setter/deleter requirements
|
|
223
|
+
- static methods and class methods
|
|
224
|
+
- generic interfaces
|
|
225
|
+
- multiple and derived interfaces
|
|
226
|
+
- custom metaclass composition
|
|
227
|
+
- source-less environments such as REPL, `exec`, notebooks, frozen apps, and
|
|
228
|
+
bytecode-only distributions
|
|
229
|
+
|
|
230
|
+
Useful inspection functions include `members_of`, `attributes_of`,
|
|
231
|
+
`missing_members`, `missing_attributes`, `signature_problem`, `verify`, and
|
|
232
|
+
`structurally_implements`.
|
|
233
|
+
|
|
234
|
+
## Backward compatibility
|
|
235
|
+
|
|
236
|
+
The former import path remains fully supported:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from strict_interface import Interface
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`strict_interface.Interface` and `interface_contract.Interface` are the same
|
|
243
|
+
object. Existing source code does not need an import migration. The PyPI
|
|
244
|
+
distribution name is `interface-contract`; the preferred new import is
|
|
245
|
+
`interface_contract`.
|
|
246
|
+
|
|
247
|
+
Version 0.4.0 is additive except for the distribution rename. Runtime field
|
|
248
|
+
checking only activates when `check_attributes=True` is explicitly selected.
|
|
249
|
+
|
|
250
|
+
## Development
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
python -m pip install -e ".[dev]"
|
|
254
|
+
python -m pytest
|
|
255
|
+
python -m ruff check .
|
|
256
|
+
python -m mypy strict_interface interface_contract typing_tests/valid.py
|
|
257
|
+
python -m build
|
|
258
|
+
python -m twine check dist/*
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
See [README.tr.md](https://github.com/AyhanOztemel/py_interface/blob/main/README.tr.md)
|
|
262
|
+
for Turkish documentation and
|
|
263
|
+
[CHANGELOG.md](https://github.com/AyhanOztemel/py_interface/blob/main/CHANGELOG.md)
|
|
264
|
+
for release notes.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|