Skip to content

Commit 87ef71b

Browse files
authored
Add files via upload
1 parent c1d9ded commit 87ef71b

File tree

1 file changed

+133
-0
lines changed

1 file changed

+133
-0
lines changed

disinheritance.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""module for managing inherited methods/attributes in subclassed object
2+
types
3+
"""
4+
5+
6+
from dataclasses import dataclass
7+
from functools import wraps
8+
from types import MethodType
9+
10+
11+
__author__ = 'Stanton K. Nielson'
12+
__license__ = 'Unlicense'
13+
__version__ = '1.0.0'
14+
__status__ = 'Production'
15+
__date__ = '2025-07-09'
16+
17+
18+
__all__ = 'disinherit',
19+
20+
21+
class disinherit:
22+
23+
"""decorator to remove access to inherited methods/attributes in an
24+
object type, except where specified by the exempt keyword argument
25+
- functionally required "progenitor" object methods will be retained
26+
- exemptions not part of MRO are ignored
27+
"""
28+
29+
def __init__(self, *, exempt: type | MethodType |
30+
list[type | MethodType] | tuple[type | MethodType] |
31+
set[type | MethodType] = None):
32+
self.exempt = exempt
33+
return
34+
35+
def __call__(self, target: type):
36+
return self.in_type(target, self.exempt)
37+
38+
@classmethod
39+
def in_type(cls, target: type, exempt: type | MethodType |
40+
list[type | MethodType] | tuple[type | MethodType] |
41+
set[type | MethodType] = None) -> type:
42+
"""disinherits methods/attributes from a target type, except for
43+
required methods and specified exemptions
44+
"""
45+
for i in cls._get_invalid_names(target, exempt):
46+
setattr(target, i, NotImplemented)
47+
cls._wrap_dir(target)
48+
cls._wrap_getter(target)
49+
return target
50+
51+
@classmethod
52+
def _coerce_exempt(cls, exempt: list | tuple | set | type) -> list:
53+
"""internal class method to coerce exemptions for disinheritance
54+
to a set of methods/attributes
55+
"""
56+
if exempt is None: return set()
57+
elif not isinstance(exempt, (list, tuple, set)):
58+
exempt = [exempt]
59+
exempt, coerced = list(exempt), set()
60+
for i in exempt:
61+
if isinstance(i, type):
62+
coerced |= set(cls._map_type(i).values())
63+
else:
64+
try: coerced.add(i)
65+
except: pass
66+
return list(coerced)
67+
68+
@classmethod
69+
def _get_invalid_names(
70+
cls, target: type,
71+
exempt: list | tuple | set | type = None) -> set[str]:
72+
"""internal class method to identify names of methods/attributes
73+
considered invalid in the target subclass (unless part of exempted
74+
methods/attributes)
75+
"""
76+
exempt = cls._coerce_exempt(exempt)
77+
current, *ancestors, progenitor = target.mro()
78+
required = dict(
79+
(k, v) for k, v in cls._map_type(progenitor).items()
80+
if len(k.strip('_')) > 2)
81+
valid, invalid = dict(vars(target)), set()
82+
for ancestor in ancestors:
83+
type_invalid = cls._map_type(ancestor)
84+
type_invalid.pop('__dict__', None)
85+
for k, v in required.items():
86+
type_invalid.pop(k, None)
87+
for k, v in valid.items():
88+
if k in type_invalid and v != type_invalid[k]:
89+
type_invalid.pop(k)
90+
type_invalid = dict((k, v) for k, v in type_invalid.items()
91+
if v is NotImplemented or v not in exempt)
92+
invalid |= set(type_invalid)
93+
return invalid
94+
95+
@classmethod
96+
def _map_type(cls, target: type) -> dict:
97+
"""internal class method to return mapping of names and associated
98+
methods/attributes for a target type
99+
"""
100+
return dict((name, getattr(target, name)) for name in dir(target))
101+
102+
@classmethod
103+
def _wrap_dir(cls, target: type) -> object.__dir__:
104+
"""internal class method to wrap __dir__ in the target type to
105+
prevent return of disinherited methods/attributes
106+
"""
107+
dir_base = target.__dir__
108+
@wraps(dir_base)
109+
def __dir__(self) -> list[str]:
110+
return list(i for i in dir_base(self) if
111+
getattr(self, i, NotImplemented) is not
112+
NotImplemented)
113+
if __dir__ != dir_base: target.__dir__ = __dir__
114+
return
115+
116+
@classmethod
117+
def _wrap_getter(cls, target: type):
118+
"""internal class method to wrap __getattribute__ in the target
119+
type to prevent return of disinherited methods/attributes
120+
"""
121+
getter_base = target.__getattribute__
122+
@wraps(getter_base)
123+
def __getattribute__(self, name: str):
124+
result = getter_base(self, name)
125+
if result is NotImplemented:
126+
error = AttributeError(
127+
f'{repr(type(self).__name__)} object has no '\
128+
f'attribute {repr(name)}')
129+
raise error
130+
return result
131+
if __getattribute__ != getter_base:
132+
target.__getattribute__ = __getattribute__
133+
return

0 commit comments

Comments
 (0)