interface-contract 0.4.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.
- interface_contract/__init__.py +68 -0
- interface_contract/_core.py +7 -0
- interface_contract/adapters.py +7 -0
- interface_contract/mypy_plugin.py +68 -0
- interface_contract/py.typed +0 -0
- interface_contract-0.4.0.dist-info/METADATA +305 -0
- interface_contract-0.4.0.dist-info/RECORD +15 -0
- interface_contract-0.4.0.dist-info/WHEEL +5 -0
- interface_contract-0.4.0.dist-info/licenses/LICENSE +21 -0
- interface_contract-0.4.0.dist-info/top_level.txt +2 -0
- strict_interface/__init__.py +85 -0
- strict_interface/_core.py +798 -0
- strict_interface/adapters.py +109 -0
- strict_interface/mypy_plugin.py +5 -0
- strict_interface/py.typed +0 -0
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
"""Cekirdek uygulama. Genel API icin `strict_interface/__init__.py`ye bakin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import inspect
|
|
7
|
+
import textwrap
|
|
8
|
+
import types
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import (
|
|
11
|
+
Annotated,
|
|
12
|
+
Any,
|
|
13
|
+
ClassVar,
|
|
14
|
+
Literal,
|
|
15
|
+
TypeVar,
|
|
16
|
+
Union,
|
|
17
|
+
get_args,
|
|
18
|
+
get_origin,
|
|
19
|
+
get_type_hints,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_FLAG = "__is_interface__"
|
|
23
|
+
_MEMBERS = "__interface_members__"
|
|
24
|
+
_VERIFIED = "__interface_verified__"
|
|
25
|
+
_STRUCTURAL = "__interface_structural__"
|
|
26
|
+
_DEFAULT = "__interface_default__"
|
|
27
|
+
_ANNOTATED = "__interface_check_annotations__"
|
|
28
|
+
_ABSTRACT = "__interface_abstract__"
|
|
29
|
+
_ATTRIBUTES = "__interface_attributes__"
|
|
30
|
+
_CHECK_ATTRIBUTES = "__interface_check_attributes__"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InterfaceError(TypeError):
|
|
34
|
+
"""Arayuz sozlesmesi ihlal edildiginde atilir."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# --------------------------------------------------------------------------- #
|
|
38
|
+
# Govde bosluk kontrolu
|
|
39
|
+
#
|
|
40
|
+
# Birincil yontem AST: surumler arasi kararli ve okunabilir.
|
|
41
|
+
# Kaynak koda ulasilamadiginda (REPL, exec, notebook, frozen app, sadece .pyc)
|
|
42
|
+
# bytecode'a dusuluyor.
|
|
43
|
+
#
|
|
44
|
+
# Kaynak bulunamazsa kabul edilen bos govdeler calisan yorumlayicida derlenir.
|
|
45
|
+
# Aday govde, bunlardan uretilen tam kod imzalarindan biriyle eslesmelidir.
|
|
46
|
+
# Opcode adlari veya farkli govdelerden birlestirilmis bir izin listesi
|
|
47
|
+
# kullanilmaz. Boylece gercek bir govde parca parca stub komutlarindan olussa
|
|
48
|
+
# bile kabul edilmez; PyPy gibi yorumlayicilar da kendi imzalarini kalibre eder.
|
|
49
|
+
# --------------------------------------------------------------------------- #
|
|
50
|
+
|
|
51
|
+
# Kabul ettigimiz butun bos govde bicimleri. Kod imzalari bunlardan uretilir.
|
|
52
|
+
_STUB_FORMS = (
|
|
53
|
+
"def _s(self): pass",
|
|
54
|
+
"def _s(self): ...",
|
|
55
|
+
"def _s(self): 'dokuman'",
|
|
56
|
+
"def _s(self): raise NotImplementedError",
|
|
57
|
+
"def _s(self): raise NotImplementedError('mesaj')",
|
|
58
|
+
"async def _s(self): pass",
|
|
59
|
+
"async def _s(self): ...",
|
|
60
|
+
"async def _s(self): 'dokuman'",
|
|
61
|
+
"async def _s(self): raise NotImplementedError",
|
|
62
|
+
"async def _s(self): raise NotImplementedError('mesaj')",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Senkron, coroutine ve generator govdeleri birbirinden ayiran davranis bitleri.
|
|
66
|
+
_CODE_FLAGS = inspect.CO_COROUTINE | inspect.CO_GENERATOR | inspect.CO_ASYNC_GENERATOR
|
|
67
|
+
_CodeShape = tuple[bytes, tuple[str, ...], tuple[str, ...], int]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _code_shape(code: Any) -> _CodeShape | None:
|
|
71
|
+
raw = getattr(code, "co_code", None)
|
|
72
|
+
constants = getattr(code, "co_consts", None)
|
|
73
|
+
names = getattr(code, "co_names", None)
|
|
74
|
+
flags = getattr(code, "co_flags", None)
|
|
75
|
+
if (
|
|
76
|
+
not isinstance(raw, bytes)
|
|
77
|
+
or not isinstance(constants, tuple)
|
|
78
|
+
or not isinstance(names, tuple)
|
|
79
|
+
or not isinstance(flags, int)
|
|
80
|
+
):
|
|
81
|
+
return None
|
|
82
|
+
const_shape = tuple(
|
|
83
|
+
"none" if value is None else "ellipsis" if value is Ellipsis else "literal"
|
|
84
|
+
for value in constants
|
|
85
|
+
)
|
|
86
|
+
return raw, const_shape, tuple(map(str, names)), flags & _CODE_FLAGS
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _calibrate_stub_shapes() -> frozenset[_CodeShape]:
|
|
90
|
+
"""Bos govde kod imzalarini calisan yorumlayicidan ogrenir."""
|
|
91
|
+
shapes: set[_CodeShape] = set()
|
|
92
|
+
for source in _STUB_FORMS:
|
|
93
|
+
namespace: dict[str, Any] = {}
|
|
94
|
+
# S102: derlenen kaynak _STUB_FORMS'tan geliyor, disaridan girdi almiyor.
|
|
95
|
+
exec(compile(source, "<strict_interface>", "exec"), namespace) # noqa: S102
|
|
96
|
+
shape = _code_shape(namespace["_s"].__code__)
|
|
97
|
+
if shape is not None:
|
|
98
|
+
shapes.add(shape)
|
|
99
|
+
return frozenset(shapes)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_STUB_SHAPES = _calibrate_stub_shapes()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _ast_is_stub(func: Callable[..., Any]) -> bool | None:
|
|
106
|
+
try:
|
|
107
|
+
source = textwrap.dedent(inspect.getsource(func))
|
|
108
|
+
except (OSError, TypeError):
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
module = ast.parse(source)
|
|
112
|
+
except SyntaxError:
|
|
113
|
+
return None
|
|
114
|
+
if not module.body:
|
|
115
|
+
return None
|
|
116
|
+
node = module.body[0]
|
|
117
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
body = list(node.body)
|
|
121
|
+
if (body and isinstance(body[0], ast.Expr)
|
|
122
|
+
and isinstance(body[0].value, ast.Constant)
|
|
123
|
+
and isinstance(body[0].value.value, str)):
|
|
124
|
+
body = body[1:] # docstring
|
|
125
|
+
if not body:
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
for stmt in body:
|
|
129
|
+
if isinstance(stmt, ast.Pass):
|
|
130
|
+
continue
|
|
131
|
+
if (isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant)
|
|
132
|
+
and stmt.value.value is Ellipsis):
|
|
133
|
+
continue
|
|
134
|
+
if isinstance(stmt, ast.Raise) and stmt.exc is not None:
|
|
135
|
+
exc = stmt.exc
|
|
136
|
+
target = exc.func if isinstance(exc, ast.Call) else exc
|
|
137
|
+
if isinstance(target, ast.Name) and target.id == "NotImplementedError":
|
|
138
|
+
continue
|
|
139
|
+
return False
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _bytecode_is_stub(func: Callable[..., Any]) -> bool:
|
|
144
|
+
code = getattr(func, "__code__", None)
|
|
145
|
+
if code is None:
|
|
146
|
+
return False
|
|
147
|
+
shape = _code_shape(code)
|
|
148
|
+
return shape is not None and shape in _STUB_SHAPES
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def is_stub(func: Callable[..., Any]) -> bool:
|
|
152
|
+
"""Govde `pass`, `...`, sadece docstring veya `raise NotImplementedError` mi?"""
|
|
153
|
+
verdict = _ast_is_stub(func)
|
|
154
|
+
return _bytecode_is_stub(func) if verdict is None else verdict
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# --------------------------------------------------------------------------- #
|
|
158
|
+
# Uyeler
|
|
159
|
+
# --------------------------------------------------------------------------- #
|
|
160
|
+
class Member:
|
|
161
|
+
"""Arayuzun bir uyesi: metot, property, staticmethod veya classmethod."""
|
|
162
|
+
|
|
163
|
+
__slots__ = ("is_async", "is_default", "kind", "name", "owner", "parts")
|
|
164
|
+
|
|
165
|
+
def __init__(self, name: str, kind: str, parts: dict[str, Callable[..., Any]],
|
|
166
|
+
is_async: bool, owner: str, is_default: bool = False) -> None:
|
|
167
|
+
self.name = name
|
|
168
|
+
self.kind = kind
|
|
169
|
+
self.parts = parts # property icin fget/fset/fdel, digerleri icin ""
|
|
170
|
+
self.is_async = is_async
|
|
171
|
+
self.owner = owner
|
|
172
|
+
self.is_default = is_default
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def primary(self) -> Callable[..., Any] | None:
|
|
176
|
+
return self.parts.get("") or self.parts.get("fget")
|
|
177
|
+
|
|
178
|
+
def __repr__(self) -> str:
|
|
179
|
+
flag = " (default)" if self.is_default else ""
|
|
180
|
+
return f"<{self.kind} {self.owner}.{self.name}{flag}>"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class AttributeSpec:
|
|
184
|
+
"""An opt-in instance-attribute requirement declared by an interface."""
|
|
185
|
+
|
|
186
|
+
__slots__ = ("annotation", "name", "owner")
|
|
187
|
+
|
|
188
|
+
def __init__(self, name: str, annotation: Any, owner: str) -> None:
|
|
189
|
+
self.name = name
|
|
190
|
+
self.annotation = annotation
|
|
191
|
+
self.owner = owner
|
|
192
|
+
|
|
193
|
+
def __repr__(self) -> str:
|
|
194
|
+
return f"<attribute {self.owner}.{self.name}: {self.annotation!r}>"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _has_default(raw: Any) -> bool:
|
|
198
|
+
target = raw.__func__ if isinstance(raw, (staticmethod, classmethod)) else raw
|
|
199
|
+
if isinstance(raw, property):
|
|
200
|
+
target = raw.fget
|
|
201
|
+
return bool(getattr(target, _DEFAULT, False))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def classify(name: str, value: Any, owner: str) -> Member | None:
|
|
205
|
+
"""Sinif sozlugundeki bir girdiyi Member'a cevirir; sozlesme disi ise None."""
|
|
206
|
+
is_default = _has_default(value)
|
|
207
|
+
if isinstance(value, staticmethod):
|
|
208
|
+
fn = value.__func__
|
|
209
|
+
return Member(name, "staticmethod", {"": fn},
|
|
210
|
+
inspect.iscoroutinefunction(fn), owner, is_default)
|
|
211
|
+
if isinstance(value, classmethod):
|
|
212
|
+
fn = value.__func__
|
|
213
|
+
return Member(name, "classmethod", {"": fn},
|
|
214
|
+
inspect.iscoroutinefunction(fn), owner, is_default)
|
|
215
|
+
if isinstance(value, property):
|
|
216
|
+
parts = {k: f for k, f in
|
|
217
|
+
(("fget", value.fget), ("fset", value.fset), ("fdel", value.fdel))
|
|
218
|
+
if f is not None}
|
|
219
|
+
return Member(name, "property", parts, False, owner, is_default)
|
|
220
|
+
if inspect.isfunction(value):
|
|
221
|
+
return Member(name, "method", {"": value},
|
|
222
|
+
inspect.iscoroutinefunction(value), owner, is_default)
|
|
223
|
+
return None # sabitler ve sinif degiskenleri sozlesmeye dahil degil
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# --------------------------------------------------------------------------- #
|
|
227
|
+
# Imza uyumu
|
|
228
|
+
# --------------------------------------------------------------------------- #
|
|
229
|
+
_P = inspect.Parameter
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_catch_all(params: list[_P]) -> bool:
|
|
233
|
+
kinds = {p.kind for p in params}
|
|
234
|
+
return _P.VAR_POSITIONAL in kinds and _P.VAR_KEYWORD in kinds
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def signature_problem(declared: Callable[..., Any], impl: Callable[..., Any],
|
|
238
|
+
check_annotations: bool = False) -> str | None:
|
|
239
|
+
"""Imzalar uyumsuzsa aciklama, uyumluysa None dondurur."""
|
|
240
|
+
try:
|
|
241
|
+
dsig = inspect.signature(declared)
|
|
242
|
+
isig = inspect.signature(impl)
|
|
243
|
+
except (TypeError, ValueError):
|
|
244
|
+
return None # C seviyesinde cagrilabilir; imza okunamiyor
|
|
245
|
+
|
|
246
|
+
d = list(dsig.parameters.values())
|
|
247
|
+
i = list(isig.parameters.values())
|
|
248
|
+
|
|
249
|
+
if _is_catch_all(i):
|
|
250
|
+
return None # (*args, **kwargs) esnek imzaya izin ver
|
|
251
|
+
|
|
252
|
+
if len(i) < len(d):
|
|
253
|
+
return (f"beklenen ({', '.join(p.name for p in d)}), "
|
|
254
|
+
f"verilen ({', '.join(p.name for p in i)}) — eksik parametre")
|
|
255
|
+
|
|
256
|
+
# strict=False bilincli: implementasyon fazladan opsiyonel parametre
|
|
257
|
+
# tanimlayabilir, fazlaliklar asagida ayrica denetleniyor.
|
|
258
|
+
for want, got in zip(d, i, strict=False):
|
|
259
|
+
if want.name != got.name:
|
|
260
|
+
return f"parametre adi '{want.name}' olmali, '{got.name}' verilmis"
|
|
261
|
+
if want.kind is not got.kind:
|
|
262
|
+
return f"parametre '{want.name}' turu {want.kind.description} olmali"
|
|
263
|
+
if (check_annotations and want.annotation is not _P.empty
|
|
264
|
+
and got.annotation is not _P.empty
|
|
265
|
+
and want.annotation != got.annotation):
|
|
266
|
+
return (f"parametre '{want.name}' anotasyonu {want.annotation!r} "
|
|
267
|
+
f"olmali, {got.annotation!r} verilmis")
|
|
268
|
+
|
|
269
|
+
for extra in i[len(d):]:
|
|
270
|
+
if extra.default is _P.empty and extra.kind not in (
|
|
271
|
+
_P.VAR_POSITIONAL, _P.VAR_KEYWORD):
|
|
272
|
+
return f"fazladan zorunlu parametre '{extra.name}' — varsayilan deger verin"
|
|
273
|
+
|
|
274
|
+
if (check_annotations and dsig.return_annotation is not inspect.Signature.empty
|
|
275
|
+
and isig.return_annotation is not inspect.Signature.empty
|
|
276
|
+
and dsig.return_annotation != isig.return_annotation):
|
|
277
|
+
return (f"donus anotasyonu {dsig.return_annotation!r} olmali, "
|
|
278
|
+
f"{isig.return_annotation!r} verilmis")
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def member_problem(declared: Member, impl: Member,
|
|
283
|
+
check_annotations: bool = False) -> str | None:
|
|
284
|
+
if impl.kind != declared.kind:
|
|
285
|
+
return f"{declared.kind} olmali, {impl.kind} olarak tanimlanmis"
|
|
286
|
+
if impl.is_async != declared.is_async:
|
|
287
|
+
return ("async def olarak tanimlanmali" if declared.is_async
|
|
288
|
+
else "async olmayan def olarak tanimlanmali")
|
|
289
|
+
for part, decl_fn in declared.parts.items():
|
|
290
|
+
impl_fn = impl.parts.get(part)
|
|
291
|
+
if impl_fn is None:
|
|
292
|
+
label = {"fget": "okuyucu", "fset": "setter", "fdel": "deleter"}.get(part, part)
|
|
293
|
+
return f"property {label}'si eksik"
|
|
294
|
+
problem = signature_problem(decl_fn, impl_fn, check_annotations)
|
|
295
|
+
if problem:
|
|
296
|
+
prefix = f"{part} " if part else ""
|
|
297
|
+
return f"{prefix}imzasi uyusmuyor: {problem}"
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# --------------------------------------------------------------------------- #
|
|
302
|
+
# Metaclass
|
|
303
|
+
# --------------------------------------------------------------------------- #
|
|
304
|
+
def _is_allowed_base(base: type) -> bool:
|
|
305
|
+
"""Arayuzun turetilebilecegi taban mi? (arayuz, object, typing yardimcilari)"""
|
|
306
|
+
if base is object:
|
|
307
|
+
return True
|
|
308
|
+
if getattr(base, "__module__", "") in ("typing", "typing_extensions"):
|
|
309
|
+
return True
|
|
310
|
+
return is_interface(base)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _looks_like_interface(namespace: dict[str, Any]) -> bool:
|
|
314
|
+
"""Govdesi tamamen bos olan bir sinif — muhtemelen arayuz olmasi isteniyordu."""
|
|
315
|
+
found = False
|
|
316
|
+
for key, value in namespace.items():
|
|
317
|
+
if key.startswith("__") and key.endswith("__"):
|
|
318
|
+
continue
|
|
319
|
+
member = classify(key, value, "?")
|
|
320
|
+
if member is None:
|
|
321
|
+
continue
|
|
322
|
+
found = True
|
|
323
|
+
if not all(is_stub(fn) for fn in member.parts.values()):
|
|
324
|
+
return False
|
|
325
|
+
return found
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _with_interface_hint(exc: InterfaceError, cls: type,
|
|
329
|
+
namespace: dict[str, Any]) -> InterfaceError:
|
|
330
|
+
problems = [line for line in str(exc).splitlines() if line.startswith(" - ")]
|
|
331
|
+
if not problems or not all("implement edilmemis" in p for p in problems):
|
|
332
|
+
return exc # imza/tur hatasi var; bu bir arayuz karisikligi degil
|
|
333
|
+
if not _looks_like_interface(namespace):
|
|
334
|
+
return exc
|
|
335
|
+
return InterfaceError(
|
|
336
|
+
f"{exc}\n\n"
|
|
337
|
+
f"Ipucu: {cls.__name__} govdesi tamamen bos. Bunun bir arayuz olmasini "
|
|
338
|
+
f"istiyorsaniz `Interface`i taban olarak listeleyin veya `interface=True` "
|
|
339
|
+
f"verin:\n"
|
|
340
|
+
f" class {cls.__name__}(..., Interface): ...\n"
|
|
341
|
+
f" class {cls.__name__}(..., interface=True): ...\n"
|
|
342
|
+
f"Sozlesmeyi kasten kismen dolduran bir ara sinif ise `abstract=True` verin.")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class InterfaceMeta(type):
|
|
346
|
+
"""Arayuz semantigini tasiyan metaclass.
|
|
347
|
+
|
|
348
|
+
- arayuzler ornekleneemez, hata mesaji anlamlidir
|
|
349
|
+
- bir arayuzden turetilen her sinif **tanim aninda** dogrulanir; kismen
|
|
350
|
+
dolduran ara siniflar icin `abstract=True` verin
|
|
351
|
+
- sinifa sonradan atama yapilirsa dogrulama onbellegi gecersizlenir
|
|
352
|
+
- structural=True verilen arayuzlerde isinstance/issubclass mirassiz calisir
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
def __new__(mcls, name: str, bases: tuple[type, ...], namespace: dict[str, Any],
|
|
356
|
+
*, interface: bool | None = None, structural: bool = False,
|
|
357
|
+
name_prefix: str | None = None, strict_body: bool = True,
|
|
358
|
+
check_annotations: bool = False, abstract: bool = False,
|
|
359
|
+
check_attributes: bool | None = None,
|
|
360
|
+
**kwargs: Any) -> InterfaceMeta:
|
|
361
|
+
cls = super().__new__(mcls, name, bases, dict(namespace), **kwargs)
|
|
362
|
+
|
|
363
|
+
root = globals().get("Interface")
|
|
364
|
+
if interface is None:
|
|
365
|
+
interface = bool(namespace.get(_FLAG)) or (root is not None and root in bases)
|
|
366
|
+
|
|
367
|
+
type.__setattr__(cls, _FLAG, bool(interface))
|
|
368
|
+
type.__setattr__(cls, _VERIFIED, False)
|
|
369
|
+
|
|
370
|
+
# --- implementasyon yolu ------------------------------------------- #
|
|
371
|
+
if not interface:
|
|
372
|
+
if abstract:
|
|
373
|
+
# sozlesmeyi kasten kismen dolduran ara sinif: dogrulama
|
|
374
|
+
# somut alt sinifa ertelenir
|
|
375
|
+
type.__setattr__(cls, _ABSTRACT, True)
|
|
376
|
+
return cls
|
|
377
|
+
if any(is_interface(base) for base in cls.__mro__[1:]):
|
|
378
|
+
try:
|
|
379
|
+
verify(cls)
|
|
380
|
+
except InterfaceError as exc:
|
|
381
|
+
raise _with_interface_hint(exc, cls, namespace) from None
|
|
382
|
+
return cls
|
|
383
|
+
|
|
384
|
+
# --- arayuz yolu ---------------------------------------------------- #
|
|
385
|
+
if check_attributes is None:
|
|
386
|
+
check_attributes = any(
|
|
387
|
+
bool(base.__dict__.get(_CHECK_ATTRIBUTES, False))
|
|
388
|
+
for base in cls.__mro__[1:] if is_interface(base)
|
|
389
|
+
)
|
|
390
|
+
type.__setattr__(cls, _STRUCTURAL, structural)
|
|
391
|
+
type.__setattr__(cls, _ANNOTATED, check_annotations)
|
|
392
|
+
type.__setattr__(cls, _CHECK_ATTRIBUTES, bool(check_attributes))
|
|
393
|
+
|
|
394
|
+
problems: list[str] = []
|
|
395
|
+
if name_prefix and not name.startswith(name_prefix):
|
|
396
|
+
problems.append(f"arayuz adi '{name_prefix}' ile baslamali")
|
|
397
|
+
|
|
398
|
+
members: dict[str, Member] = {}
|
|
399
|
+
attributes: dict[str, AttributeSpec] = {}
|
|
400
|
+
for base in reversed(cls.__mro__[1:]):
|
|
401
|
+
if is_interface(base):
|
|
402
|
+
members.update(base.__dict__.get(_MEMBERS, {}))
|
|
403
|
+
attributes.update(base.__dict__.get(_ATTRIBUTES, {}))
|
|
404
|
+
elif not _is_allowed_base(base):
|
|
405
|
+
problems.append(f"arayuz yalnizca arayuzlerden turetilebilir; "
|
|
406
|
+
f"'{base.__name__}' bir arayuz degil")
|
|
407
|
+
|
|
408
|
+
for key, value in namespace.items():
|
|
409
|
+
if key.startswith("__") and key.endswith("__"):
|
|
410
|
+
if key in ("__init__", "__new__"):
|
|
411
|
+
problems.append(f"arayuz '{key}' tanimlayamaz")
|
|
412
|
+
continue
|
|
413
|
+
member = classify(key, value, name)
|
|
414
|
+
if member is None:
|
|
415
|
+
continue
|
|
416
|
+
if strict_body and not member.is_default:
|
|
417
|
+
for part, fn in member.parts.items():
|
|
418
|
+
if not is_stub(fn):
|
|
419
|
+
where = f"{key}.{part}" if part and part != "fget" else key
|
|
420
|
+
problems.append(
|
|
421
|
+
f"'{where}' govdesi bos olmali (`...`, `pass`, docstring "
|
|
422
|
+
f"veya `raise NotImplementedError`); govdeli metot icin "
|
|
423
|
+
f"@default kullanin")
|
|
424
|
+
members[key] = member
|
|
425
|
+
|
|
426
|
+
if check_attributes:
|
|
427
|
+
raw_annotations = namespace.get("__annotations__", {})
|
|
428
|
+
try:
|
|
429
|
+
resolved_annotations = get_type_hints(cls, include_extras=True)
|
|
430
|
+
except (NameError, TypeError):
|
|
431
|
+
resolved_annotations = {}
|
|
432
|
+
for key, raw_annotation in raw_annotations.items():
|
|
433
|
+
annotation = resolved_annotations.get(key, raw_annotation)
|
|
434
|
+
if key.startswith("_") or get_origin(annotation) is ClassVar:
|
|
435
|
+
continue
|
|
436
|
+
attributes[key] = AttributeSpec(key, annotation, name)
|
|
437
|
+
|
|
438
|
+
if problems:
|
|
439
|
+
raise InterfaceError(
|
|
440
|
+
f"{name} arayuzu gecersiz:\n - " + "\n - ".join(problems))
|
|
441
|
+
|
|
442
|
+
type.__setattr__(cls, _MEMBERS, members)
|
|
443
|
+
type.__setattr__(cls, _ATTRIBUTES, attributes)
|
|
444
|
+
return cls
|
|
445
|
+
|
|
446
|
+
# --- ornekleme -------------------------------------------------------- #
|
|
447
|
+
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
|
|
448
|
+
if cls.__dict__.get(_FLAG, False):
|
|
449
|
+
raise InterfaceError(
|
|
450
|
+
f"{cls.__name__} bir arayuzdur, ornegi olusturulamaz.")
|
|
451
|
+
if cls.__dict__.get(_ABSTRACT, False):
|
|
452
|
+
raise InterfaceError(
|
|
453
|
+
f"{cls.__name__} soyut bir sinif (abstract=True), "
|
|
454
|
+
f"ornegi olusturulamaz.")
|
|
455
|
+
if not cls.__dict__.get(_VERIFIED, False):
|
|
456
|
+
verify(cls)
|
|
457
|
+
instance = super().__call__(*args, **kwargs)
|
|
458
|
+
verify_instance(instance)
|
|
459
|
+
return instance
|
|
460
|
+
|
|
461
|
+
# --- onbellek gecersizleme -------------------------------------------- #
|
|
462
|
+
def __setattr__(cls, key: str, value: Any) -> None:
|
|
463
|
+
type.__setattr__(cls, key, value)
|
|
464
|
+
if key not in (
|
|
465
|
+
_VERIFIED, _MEMBERS, _FLAG, _STRUCTURAL, _ANNOTATED, _ABSTRACT,
|
|
466
|
+
_ATTRIBUTES, _CHECK_ATTRIBUTES,
|
|
467
|
+
):
|
|
468
|
+
_invalidate(cls)
|
|
469
|
+
|
|
470
|
+
def __delattr__(cls, key: str) -> None:
|
|
471
|
+
type.__delattr__(cls, key)
|
|
472
|
+
_invalidate(cls)
|
|
473
|
+
|
|
474
|
+
# --- yapisal tipleme --------------------------------------------------- #
|
|
475
|
+
def __instancecheck__(cls, obj: Any) -> bool:
|
|
476
|
+
if super().__instancecheck__(obj):
|
|
477
|
+
return True
|
|
478
|
+
if not (cls.__dict__.get(_FLAG) and cls.__dict__.get(_STRUCTURAL)):
|
|
479
|
+
return False
|
|
480
|
+
return satisfies(obj, cls)
|
|
481
|
+
|
|
482
|
+
def __subclasscheck__(cls, sub: type) -> bool:
|
|
483
|
+
# super() ile zincirleniyor ki InterfaceMeta baska bir metaclass ile
|
|
484
|
+
# birlestirildiginde (ornegin ABCMeta) onun register() semantigi yasasin.
|
|
485
|
+
if super().__subclasscheck__(sub):
|
|
486
|
+
return True
|
|
487
|
+
if not (cls.__dict__.get(_FLAG) and cls.__dict__.get(_STRUCTURAL)):
|
|
488
|
+
return False
|
|
489
|
+
return structurally_implements(sub, cls)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _invalidate(cls: type) -> None:
|
|
493
|
+
type.__setattr__(cls, _VERIFIED, False)
|
|
494
|
+
for sub in cls.__subclasses__():
|
|
495
|
+
_invalidate(sub)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
# --------------------------------------------------------------------------- #
|
|
499
|
+
# Genel yardimcilar
|
|
500
|
+
# --------------------------------------------------------------------------- #
|
|
501
|
+
def is_interface(obj: Any) -> bool:
|
|
502
|
+
"""Sinifin *kendisi* arayuz mu? Implementasyonlar icin False."""
|
|
503
|
+
return isinstance(obj, type) and obj.__dict__.get(_FLAG, False)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class Interface(metaclass=InterfaceMeta, interface=True):
|
|
507
|
+
"""Tum arayuzlerin kok sinifi.
|
|
508
|
+
|
|
509
|
+
`class IFoo(Interface): ...` seklinde turetin. Dogrudan `Interface`
|
|
510
|
+
listelemeyen alt siniflar implementasyon sayilir; turetilmis bir arayuz
|
|
511
|
+
yaziyorsaniz `Interface`i tekrar listeleyin veya `interface=True` verin:
|
|
512
|
+
|
|
513
|
+
class IAudited(IRepository, Interface): ...
|
|
514
|
+
class IAudited(IRepository, interface=True): ...
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
__slots__ = ()
|
|
518
|
+
|
|
519
|
+
def __init_subclass__(
|
|
520
|
+
cls,
|
|
521
|
+
*,
|
|
522
|
+
interface: bool | None = None,
|
|
523
|
+
structural: bool = False,
|
|
524
|
+
name_prefix: str | None = None,
|
|
525
|
+
strict_body: bool = True,
|
|
526
|
+
check_annotations: bool = False,
|
|
527
|
+
abstract: bool = False,
|
|
528
|
+
check_attributes: bool | None = None,
|
|
529
|
+
**kwargs: Any,
|
|
530
|
+
) -> None:
|
|
531
|
+
"""Document class keywords for type checkers; InterfaceMeta consumes them."""
|
|
532
|
+
super().__init_subclass__(**kwargs)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def is_abstract(obj: Any) -> bool:
|
|
536
|
+
"""`abstract=True` ile tanimlanmis, sozlesmeyi kismen dolduran ara sinif mi?"""
|
|
537
|
+
return isinstance(obj, type) and obj.__dict__.get(_ABSTRACT, False)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def members_of(cls: type) -> dict[str, Member]:
|
|
541
|
+
"""Sinifin uymasi gereken tum arayuz uyeleri (default'lar dahil)."""
|
|
542
|
+
collected: dict[str, Member] = {}
|
|
543
|
+
for base in reversed(getattr(cls, "__mro__", (cls,))):
|
|
544
|
+
if is_interface(base):
|
|
545
|
+
collected.update(base.__dict__.get(_MEMBERS, {}))
|
|
546
|
+
return collected
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def attributes_of(cls: type) -> dict[str, AttributeSpec]:
|
|
550
|
+
"""Return opt-in instance-attribute requirements inherited by ``cls``."""
|
|
551
|
+
collected: dict[str, AttributeSpec] = {}
|
|
552
|
+
for base in reversed(getattr(cls, "__mro__", (cls,))):
|
|
553
|
+
if is_interface(base):
|
|
554
|
+
collected.update(base.__dict__.get(_ATTRIBUTES, {}))
|
|
555
|
+
return collected
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _required(cls: type) -> dict[str, Member]:
|
|
559
|
+
return {n: m for n, m in members_of(cls).items() if not m.is_default}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _find_implementation(cls: type, name: str) -> tuple[Any, type] | None:
|
|
563
|
+
for base in cls.__mro__:
|
|
564
|
+
if is_interface(base):
|
|
565
|
+
continue
|
|
566
|
+
if name in vars(base):
|
|
567
|
+
return vars(base)[name], base
|
|
568
|
+
return None
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def missing_members(cls: type) -> list[str]:
|
|
572
|
+
"""Implement edilmemis zorunlu uyelerin adlari."""
|
|
573
|
+
return [n for n in _required(cls) if _find_implementation(cls, n) is None]
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _concrete_attribute_owner(cls: type, name: str) -> type | None:
|
|
577
|
+
for base in cls.__mro__:
|
|
578
|
+
if is_interface(base):
|
|
579
|
+
continue
|
|
580
|
+
if name in vars(base):
|
|
581
|
+
return base
|
|
582
|
+
return None
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _annotation_matches(value: Any, annotation: Any) -> bool:
|
|
586
|
+
"""Best-effort, shallow runtime check for a standard type annotation."""
|
|
587
|
+
if annotation is Any or annotation is inspect.Signature.empty:
|
|
588
|
+
return True
|
|
589
|
+
if annotation is None:
|
|
590
|
+
return value is None
|
|
591
|
+
if isinstance(annotation, str):
|
|
592
|
+
return True # Forward references need the defining module namespace.
|
|
593
|
+
if isinstance(annotation, TypeVar):
|
|
594
|
+
if annotation.__constraints__:
|
|
595
|
+
return any(_annotation_matches(value, item) for item in annotation.__constraints__)
|
|
596
|
+
return _annotation_matches(value, annotation.__bound__) if annotation.__bound__ else True
|
|
597
|
+
|
|
598
|
+
origin = get_origin(annotation)
|
|
599
|
+
args = get_args(annotation)
|
|
600
|
+
if origin is Annotated:
|
|
601
|
+
return _annotation_matches(value, args[0]) if args else True
|
|
602
|
+
if origin in (Union, types.UnionType):
|
|
603
|
+
return any(_annotation_matches(value, item) for item in args)
|
|
604
|
+
if origin is Literal:
|
|
605
|
+
return value in args
|
|
606
|
+
if origin is ClassVar:
|
|
607
|
+
return True
|
|
608
|
+
if origin is not None:
|
|
609
|
+
try:
|
|
610
|
+
return isinstance(value, origin)
|
|
611
|
+
except TypeError:
|
|
612
|
+
return True
|
|
613
|
+
try:
|
|
614
|
+
return isinstance(value, annotation)
|
|
615
|
+
except TypeError:
|
|
616
|
+
return True
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _instance_attribute_value(obj: Any, name: str) -> tuple[bool, Any]:
|
|
620
|
+
try:
|
|
621
|
+
namespace = vars(obj)
|
|
622
|
+
except TypeError:
|
|
623
|
+
namespace = {}
|
|
624
|
+
if name in namespace:
|
|
625
|
+
return True, namespace[name]
|
|
626
|
+
|
|
627
|
+
owner = _concrete_attribute_owner(type(obj), name)
|
|
628
|
+
if owner is None:
|
|
629
|
+
return False, None
|
|
630
|
+
try:
|
|
631
|
+
return True, getattr(obj, name)
|
|
632
|
+
except AttributeError:
|
|
633
|
+
return False, None
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _attribute_problems(obj: Any, iface_or_impl: type) -> list[str]:
|
|
637
|
+
problems: list[str] = []
|
|
638
|
+
for name, spec in attributes_of(iface_or_impl).items():
|
|
639
|
+
exists, value = _instance_attribute_value(obj, name)
|
|
640
|
+
if not exists:
|
|
641
|
+
problems.append(f"'{name}' instance attribute eksik ({spec.owner} arayuzu)")
|
|
642
|
+
elif not _annotation_matches(value, spec.annotation):
|
|
643
|
+
problems.append(
|
|
644
|
+
f"'{name}' degeri {spec.annotation!r} anotasyonuyla uyusmuyor; "
|
|
645
|
+
f"{type(value).__name__} verildi"
|
|
646
|
+
)
|
|
647
|
+
return problems
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def missing_attributes(obj: Any, iface_or_impl: type | None = None) -> list[str]:
|
|
651
|
+
"""Return missing opt-in instance attributes without raising."""
|
|
652
|
+
contract = type(obj) if iface_or_impl is None else iface_or_impl
|
|
653
|
+
return [
|
|
654
|
+
name for name in attributes_of(contract)
|
|
655
|
+
if not _instance_attribute_value(obj, name)[0]
|
|
656
|
+
]
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def verify_instance(obj: Any, iface: type | None = None) -> Any:
|
|
660
|
+
"""Verify runtime attributes, and optionally structural methods, on ``obj``."""
|
|
661
|
+
contract = type(obj) if iface is None else iface
|
|
662
|
+
if iface is not None and not is_interface(iface):
|
|
663
|
+
raise TypeError(f"{iface!r} bir arayuz degil")
|
|
664
|
+
if iface is not None and not structurally_implements(type(obj), iface, check_attributes=False):
|
|
665
|
+
raise InterfaceError(
|
|
666
|
+
f"{type(obj).__name__} {iface.__name__} arayuzunun metotlarini karsilamiyor."
|
|
667
|
+
)
|
|
668
|
+
problems = _attribute_problems(obj, contract)
|
|
669
|
+
if problems:
|
|
670
|
+
raise InterfaceError(
|
|
671
|
+
f"{type(obj).__name__} instance sozlesmesini karsilamiyor:\n - "
|
|
672
|
+
+ "\n - ".join(problems)
|
|
673
|
+
)
|
|
674
|
+
return obj
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def satisfies(obj: Any, iface: type) -> bool:
|
|
678
|
+
"""Return whether an object structurally satisfies methods and attributes."""
|
|
679
|
+
try:
|
|
680
|
+
verify_instance(obj, iface)
|
|
681
|
+
except (InterfaceError, TypeError, ValueError):
|
|
682
|
+
return False
|
|
683
|
+
return True
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def verify(cls: type) -> type:
|
|
687
|
+
"""Sinifi arayuz sozlesmesine karsi dogrular; uyumsuzsa InterfaceError atar."""
|
|
688
|
+
contract = members_of(cls)
|
|
689
|
+
if not contract and not attributes_of(cls):
|
|
690
|
+
raise InterfaceError(
|
|
691
|
+
f"{cls.__name__} hicbir arayuzden turetilmemis.")
|
|
692
|
+
|
|
693
|
+
check_annotations = any(
|
|
694
|
+
b.__dict__.get(_ANNOTATED, False) for b in cls.__mro__ if is_interface(b))
|
|
695
|
+
|
|
696
|
+
problems: list[str] = []
|
|
697
|
+
for name, declared in contract.items():
|
|
698
|
+
found = _find_implementation(cls, name)
|
|
699
|
+
if found is None:
|
|
700
|
+
if not declared.is_default:
|
|
701
|
+
problems.append(
|
|
702
|
+
f"'{name}' implement edilmemis ({declared.owner} arayuzu)")
|
|
703
|
+
continue
|
|
704
|
+
value, owner = found
|
|
705
|
+
impl = classify(name, value, owner.__name__)
|
|
706
|
+
if impl is None:
|
|
707
|
+
problems.append(f"'{name}' cagrilabilir bir uye degil")
|
|
708
|
+
continue
|
|
709
|
+
problem = member_problem(declared, impl, check_annotations)
|
|
710
|
+
if problem:
|
|
711
|
+
problems.append(f"'{name}' {problem}")
|
|
712
|
+
|
|
713
|
+
if problems:
|
|
714
|
+
raise InterfaceError(
|
|
715
|
+
f"{cls.__name__} arayuz sozlesmesini karsilamiyor:\n - "
|
|
716
|
+
+ "\n - ".join(problems))
|
|
717
|
+
|
|
718
|
+
type.__setattr__(cls, _VERIFIED, True)
|
|
719
|
+
return cls
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def structurally_implements(
|
|
723
|
+
candidate: type,
|
|
724
|
+
iface: type,
|
|
725
|
+
*,
|
|
726
|
+
check_attributes: bool = True,
|
|
727
|
+
) -> bool:
|
|
728
|
+
"""Miras olmaksizin, uye uye uyum kontrolu (Protocol benzeri)."""
|
|
729
|
+
if not is_interface(iface):
|
|
730
|
+
raise TypeError(f"{iface!r} bir arayuz degil")
|
|
731
|
+
for name, declared in members_of(iface).items():
|
|
732
|
+
raw = None
|
|
733
|
+
for base in getattr(candidate, "__mro__", (candidate,)):
|
|
734
|
+
if name in vars(base):
|
|
735
|
+
raw = vars(base)[name]
|
|
736
|
+
break
|
|
737
|
+
if raw is None:
|
|
738
|
+
return False
|
|
739
|
+
impl = classify(name, raw, getattr(candidate, "__name__", "?"))
|
|
740
|
+
if impl is None or member_problem(declared, impl) is not None:
|
|
741
|
+
return False
|
|
742
|
+
if check_attributes:
|
|
743
|
+
for name, spec in attributes_of(iface).items():
|
|
744
|
+
owner = _concrete_attribute_owner(candidate, name)
|
|
745
|
+
if owner is None:
|
|
746
|
+
return False
|
|
747
|
+
raw = vars(owner)[name]
|
|
748
|
+
if not isinstance(raw, property) and not _annotation_matches(raw, spec.annotation):
|
|
749
|
+
return False
|
|
750
|
+
return True
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def implements(cls: type) -> type:
|
|
754
|
+
"""Geriye donuk uyumluluk.
|
|
755
|
+
|
|
756
|
+
Dogrulama artik tanim aninda otomatik yapiliyor; bu dekoratör yalnizca
|
|
757
|
+
niyeti belgelemek isteyenler icin duruyor ve sinifi degistirmeden dondurur.
|
|
758
|
+
"""
|
|
759
|
+
if not cls.__dict__.get(_VERIFIED, False):
|
|
760
|
+
verify(cls)
|
|
761
|
+
return cls
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def default(func: Any) -> Any:
|
|
765
|
+
"""Arayuzde govdeli 'default method' tanimlamaya izin verir (Java 8 benzeri)."""
|
|
766
|
+
target = func
|
|
767
|
+
if isinstance(func, (staticmethod, classmethod)):
|
|
768
|
+
target = func.__func__
|
|
769
|
+
elif isinstance(func, property):
|
|
770
|
+
target = func.fget
|
|
771
|
+
setattr(target, _DEFAULT, True)
|
|
772
|
+
return func
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def interface(cls: type | None = None, **options: Any) -> Any:
|
|
776
|
+
"""Dekoratör stili arayuz tanimi.
|
|
777
|
+
|
|
778
|
+
`class IFoo(Interface)` yazimi tercih edilmelidir. Bu dekoratör sinifi
|
|
779
|
+
InterfaceMeta ile yeniden olusturur; govdede sifir argumanli `super()`
|
|
780
|
+
kullanan bir `@default` metot varsa o metot bozulur (`__class__` hucresi
|
|
781
|
+
eski sinifi gosterir). Arayuzlerin govdesi normalde bos oldugu icin bu
|
|
782
|
+
pratikte yalnizca default metotlari ilgilendirir.
|
|
783
|
+
"""
|
|
784
|
+
def decorate(target: type) -> type:
|
|
785
|
+
namespace = {k: v for k, v in vars(target).items()
|
|
786
|
+
if k not in ("__dict__", "__weakref__")}
|
|
787
|
+
namespace[_FLAG] = True
|
|
788
|
+
bases = tuple(b for b in target.__bases__ if b is not object)
|
|
789
|
+
if not any(is_interface(b) for b in bases):
|
|
790
|
+
bases = (*bases, Interface)
|
|
791
|
+
return InterfaceMeta(target.__name__, bases, namespace,
|
|
792
|
+
interface=True, **options)
|
|
793
|
+
|
|
794
|
+
return decorate(cls) if cls is not None else decorate
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
# Geriye donuk uyumluluk
|
|
798
|
+
interface_implement = implements
|