strictabc 1.0.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.
- strictabc-1.0.0/LICENSE +21 -0
- strictabc-1.0.0/PKG-INFO +14 -0
- strictabc-1.0.0/README.rst +11 -0
- strictabc-1.0.0/pyproject.toml +19 -0
- strictabc-1.0.0/setup.cfg +4 -0
- strictabc-1.0.0/src/strictabc/__init__.py +3 -0
- strictabc-1.0.0/src/strictabc/strict.py +152 -0
- strictabc-1.0.0/src/strictabc.egg-info/PKG-INFO +14 -0
- strictabc-1.0.0/src/strictabc.egg-info/SOURCES.txt +10 -0
- strictabc-1.0.0/src/strictabc.egg-info/dependency_links.txt +1 -0
- strictabc-1.0.0/src/strictabc.egg-info/top_level.txt +1 -0
- strictabc-1.0.0/tests/test_strict_abstract.py +79 -0
strictabc-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 taviken
|
|
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.
|
strictabc-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strictabc
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A module containing the implementation of Strict ABC classes and decorators
|
|
5
|
+
Author-email: Trevor Viken <trevor.viken@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/taviken/strictabc
|
|
8
|
+
Project-URL: Issues, https://github.com/taviken/strictabc/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "strictabc"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="Trevor Viken", email="trevor.viken@gmail.com" },
|
|
6
|
+
]
|
|
7
|
+
description = "A module containing the implementation of Strict ABC classes and decorators"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICEN[CS]E*"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/taviken/strictabc"
|
|
19
|
+
Issues = "https://github.com/taviken/strictabc/issues"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the implementation of Strict ABC classes and decorators
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABCMeta, abstractmethod, ABC
|
|
6
|
+
from typing import Mapping, List, Tuple, Iterable, Callable, Any
|
|
7
|
+
from inspect import signature
|
|
8
|
+
from collections import namedtuple
|
|
9
|
+
|
|
10
|
+
_sentinel = object()
|
|
11
|
+
|
|
12
|
+
miss_matched_sigs = namedtuple(
|
|
13
|
+
"miss_matched_sigs", ["method_name", "good_sig", "bad_sig"]
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__strict_signature__ = "__strict_signature__"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StrictAbstractError(AttributeError):
|
|
20
|
+
"""Metaclass for defining Strict Abstract Base Classes (ABCs).
|
|
21
|
+
|
|
22
|
+
Use this metaclass to create an Strict ABC. An ABC can be subclassed
|
|
23
|
+
directly, and then acts as a mix-in class. You can also register
|
|
24
|
+
unrelated concrete classes (even built-in classes) and unrelated
|
|
25
|
+
ABCs as 'virtual subclasses' -- these and their descendants will
|
|
26
|
+
be considered subclasses of the registering ABC by the built-in
|
|
27
|
+
issubclass() function, but the registering ABC won't show up in
|
|
28
|
+
their MRO (Method Resolution Order) nor will method
|
|
29
|
+
implementations defined by the registering ABC be callable (not
|
|
30
|
+
even via super()).
|
|
31
|
+
|
|
32
|
+
This Class will throw an Attribute like error upon detecting un implemented
|
|
33
|
+
methods in the Concrete class at compile time.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self, name: str, missing: List[str], bad_sigs: List[miss_matched_sigs]
|
|
38
|
+
):
|
|
39
|
+
missing_methods = f"Missing methods: {missing}"
|
|
40
|
+
missmatch = f"Missmatched signatures detected: {bad_sigs}"
|
|
41
|
+
msg = f"Errors in <{name}>\n{missing_methods}\n{missmatch}"
|
|
42
|
+
super().__init__(msg)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def strictabstract(funcobj: Callable):
|
|
46
|
+
"""A decorator indicating strict abstract methods.
|
|
47
|
+
|
|
48
|
+
Requires that the metaclass is StrictABCMeta or derived from it. A
|
|
49
|
+
class that has a metaclass derived from StrictABCMeta cannot be
|
|
50
|
+
compiled unless all of its abstract methods are overridden.
|
|
51
|
+
The abstract methods can be called using any of the normal
|
|
52
|
+
'super' call mechanisms. strictabstract() may be used to declare
|
|
53
|
+
abstract methods for properties and descriptors.
|
|
54
|
+
|
|
55
|
+
Usage:
|
|
56
|
+
|
|
57
|
+
class C(metaclass=StrictABCMeta):
|
|
58
|
+
@strictabstract
|
|
59
|
+
def my_abstract_method(self, arg1, arg2, argN):
|
|
60
|
+
...
|
|
61
|
+
"""
|
|
62
|
+
sig = signature(funcobj)
|
|
63
|
+
setattr(funcobj, __strict_signature__, sig)
|
|
64
|
+
funcobj.__isabstractmethod__ = True
|
|
65
|
+
return funcobj
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class StrictABCMeta(ABCMeta):
|
|
69
|
+
def __new__(
|
|
70
|
+
mcls,
|
|
71
|
+
name: str,
|
|
72
|
+
bases: Tuple[object],
|
|
73
|
+
classdict: Mapping[str, object],
|
|
74
|
+
/,
|
|
75
|
+
**kwargs: Mapping[Any, Any],
|
|
76
|
+
):
|
|
77
|
+
|
|
78
|
+
cls = super().__new__(mcls, name, bases, classdict, **kwargs)
|
|
79
|
+
|
|
80
|
+
mcls._check_bases(mcls, bases, classdict)
|
|
81
|
+
|
|
82
|
+
return cls
|
|
83
|
+
|
|
84
|
+
def _check_bases(
|
|
85
|
+
mcls, bases: Iterable[Tuple[object]], classdict: Mapping[str, object]
|
|
86
|
+
):
|
|
87
|
+
for base in bases:
|
|
88
|
+
|
|
89
|
+
# filter on only methods, and are abstract
|
|
90
|
+
base_methods = dict(
|
|
91
|
+
(method_name, method)
|
|
92
|
+
for method_name, method in vars(base).items()
|
|
93
|
+
if callable(method) and method_name in base.__abstractmethods__
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# check for missing and bad signatures
|
|
97
|
+
mcls._check_missmatch(mcls, base_methods, classdict)
|
|
98
|
+
|
|
99
|
+
def _check_missmatch(
|
|
100
|
+
mcls, base_methods: Mapping[str, Callable], classdict: Mapping[str, object]
|
|
101
|
+
):
|
|
102
|
+
missing = []
|
|
103
|
+
bad_sigs = []
|
|
104
|
+
# loop through base classes
|
|
105
|
+
for method_name, method in base_methods.items():
|
|
106
|
+
# grab corresponding concrete method
|
|
107
|
+
concrete_method = classdict.get(method_name, _sentinel)
|
|
108
|
+
|
|
109
|
+
# if no concrete method found, append to missing and continue
|
|
110
|
+
if concrete_method is _sentinel:
|
|
111
|
+
missing.append(method_name)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# get base method signature
|
|
115
|
+
base_sig = getattr(method, __strict_signature__)
|
|
116
|
+
|
|
117
|
+
# get signature fro concrete method
|
|
118
|
+
if isinstance(concrete_method, classmethod):
|
|
119
|
+
# concrete method is classmethod, handle grab sig differently
|
|
120
|
+
concrete_sig = signature(concrete_method.__func__)
|
|
121
|
+
else:
|
|
122
|
+
concrete_sig = signature(concrete_method)
|
|
123
|
+
|
|
124
|
+
# if base and concrete signatures don't match, append to bad sigs
|
|
125
|
+
if concrete_sig != base_sig:
|
|
126
|
+
bad_sig = miss_matched_sigs(
|
|
127
|
+
method_name=classdict["__qualname__"],
|
|
128
|
+
good_sig=base_sig,
|
|
129
|
+
bad_sig=concrete_sig,
|
|
130
|
+
)
|
|
131
|
+
bad_sigs.append(bad_sig)
|
|
132
|
+
continue # this onctinue added in case of future improvement
|
|
133
|
+
|
|
134
|
+
# if either missing or bad sigs found raise exception here
|
|
135
|
+
if missing or bad_sigs:
|
|
136
|
+
raise StrictAbstractError(mcls.__name__, missing, bad_sigs)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class StrictABC(metaclass=StrictABCMeta):
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = [
|
|
144
|
+
"StrictABC",
|
|
145
|
+
"StrictABCMeta",
|
|
146
|
+
"strictabstract",
|
|
147
|
+
"StrictAbstractError",
|
|
148
|
+
# adding regular abc items from package here
|
|
149
|
+
"ABC",
|
|
150
|
+
"ABCMeta",
|
|
151
|
+
"abstractmethod",
|
|
152
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strictabc
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A module containing the implementation of Strict ABC classes and decorators
|
|
5
|
+
Author-email: Trevor Viken <trevor.viken@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/taviken/strictabc
|
|
8
|
+
Project-URL: Issues, https://github.com/taviken/strictabc/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.rst
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/strictabc/__init__.py
|
|
5
|
+
src/strictabc/strict.py
|
|
6
|
+
src/strictabc.egg-info/PKG-INFO
|
|
7
|
+
src/strictabc.egg-info/SOURCES.txt
|
|
8
|
+
src/strictabc.egg-info/dependency_links.txt
|
|
9
|
+
src/strictabc.egg-info/top_level.txt
|
|
10
|
+
tests/test_strict_abstract.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
strictabc
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from strictabc import *
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@pytest.fixture
|
|
6
|
+
def setup():
|
|
7
|
+
class Foo(metaclass=StrictABCMeta):
|
|
8
|
+
@strictabstract
|
|
9
|
+
def bar(self):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@strictabstract
|
|
13
|
+
def baz(self, a):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
class Foo2(StrictABC):
|
|
17
|
+
@strictabstract
|
|
18
|
+
def bar(self):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@strictabstract
|
|
22
|
+
def baz(self, a):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
return Foo, Foo2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_strict_abc_meta(setup):
|
|
29
|
+
|
|
30
|
+
Foo, Foo2 = setup
|
|
31
|
+
|
|
32
|
+
class ConFooPass(Foo):
|
|
33
|
+
def bar(self):
|
|
34
|
+
print("happy")
|
|
35
|
+
|
|
36
|
+
def baz(self, a):
|
|
37
|
+
print(a)
|
|
38
|
+
|
|
39
|
+
assert ConFooPass()
|
|
40
|
+
|
|
41
|
+
class ConFooPassClass(Foo):
|
|
42
|
+
@classmethod
|
|
43
|
+
def bar(self):
|
|
44
|
+
print("happy")
|
|
45
|
+
|
|
46
|
+
def baz(self, a):
|
|
47
|
+
print(a)
|
|
48
|
+
|
|
49
|
+
assert ConFooPassClass()
|
|
50
|
+
|
|
51
|
+
class ConFooPassClass2(Foo2):
|
|
52
|
+
@classmethod
|
|
53
|
+
def bar(self):
|
|
54
|
+
print("happy")
|
|
55
|
+
|
|
56
|
+
def baz(self, a):
|
|
57
|
+
print(a)
|
|
58
|
+
|
|
59
|
+
assert ConFooPassClass2()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_fail_missing(setup):
|
|
63
|
+
Foo, _ = setup
|
|
64
|
+
with pytest.raises(StrictAbstractError):
|
|
65
|
+
|
|
66
|
+
class ConFooFailMissing(Foo):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_fail_missmatch(setup):
|
|
71
|
+
Foo, _ = setup
|
|
72
|
+
with pytest.raises(StrictAbstractError):
|
|
73
|
+
|
|
74
|
+
class ConFooFailSig(Foo):
|
|
75
|
+
def bar(cls):
|
|
76
|
+
print("happy")
|
|
77
|
+
|
|
78
|
+
def baz(self, A):
|
|
79
|
+
print(A)
|