Skip to content

Commit b0f472b

Browse files
authored
Update disinheritance.py
Refined methods to account for specific exemptions in target type MROs through refactoring and addition of methods; improved docstring
1 parent 87ef71b commit b0f472b

File tree

1 file changed

+94
-34
lines changed

1 file changed

+94
-34
lines changed

disinheritance.py

Lines changed: 94 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55

66
from dataclasses import dataclass
77
from functools import wraps
8+
from inspect import getabsfile
9+
from inspect import getmodule
810
from types import MethodType
911

1012

1113
__author__ = 'Stanton K. Nielson'
1214
__license__ = 'Unlicense'
13-
__version__ = '1.0.0'
15+
__version__ = '1.1.0'
1416
__status__ = 'Production'
1517
__date__ = '2025-07-09'
1618

@@ -22,8 +24,25 @@ class disinherit:
2224

2325
"""decorator to remove access to inherited methods/attributes in an
2426
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+
- exemptions are applied in order of argument declaration
29+
30+
- exemptions not available through inheritance or overridden in the
31+
target type are ignored
32+
33+
- functionally required "origin" object methods will be retained
34+
35+
- disinherited methods/attributes are replaced with NotImplemented in
36+
the target type
37+
38+
-> NotImplemented methods/attributes are ignored in dir() calls for
39+
and produce an AttributeError when retrieved from target type
40+
instances
41+
42+
-> provides explicit status in help() call on target type
43+
44+
-> use ensures reversion back to disinheritance if used for
45+
assignment but deleted in instances
2746
"""
2847

2948
def __init__(self, *, exempt: type | MethodType |
@@ -42,56 +61,97 @@ def in_type(cls, target: type, exempt: type | MethodType |
4261
"""disinherits methods/attributes from a target type, except for
4362
required methods and specified exemptions
4463
"""
45-
for i in cls._get_invalid_names(target, exempt):
46-
setattr(target, i, NotImplemented)
64+
mro_map = cls._map_mro(target)
65+
exempt = cls._coerce_exempt(mro_map, exempt)
66+
invalid = cls._get_invalid_names(target, mro_map, exempt)
67+
for name in invalid: setattr(target, name, NotImplemented)
68+
target_map = dict((k, v) for k, v in cls._map_type(target).items()
69+
if k not in invalid)
70+
for exempt_map in exempt.values():
71+
for name, value in exempt_map.items():
72+
if name not in target_map:
73+
setattr(target, name, value)
4774
cls._wrap_dir(target)
4875
cls._wrap_getter(target)
4976
return target
5077

5178
@classmethod
52-
def _coerce_exempt(cls, exempt: list | tuple | set | type) -> list:
79+
def _coerce_exempt(cls, mro_map: dict, exempt: type | MethodType |
80+
list | tuple | set = None) -> dict[str, dict]:
5381
"""internal class method to coerce exemptions for disinheritance
54-
to a set of methods/attributes
82+
to a mapping of methods/attributes
5583
"""
56-
if exempt is None: return set()
84+
if exempt is None: return dict()
5785
elif not isinstance(exempt, (list, tuple, set)):
5886
exempt = [exempt]
59-
exempt, coerced = list(exempt), set()
87+
elif not isinstance(exempt, list):
88+
try: exempt = list(exempt)
89+
except: return dict()
90+
exempt = list(i for i in exempt if hasattr(i, '__objclass__')
91+
or isinstance(i, type))
92+
coerced = dict()
6093
for i in exempt:
6194
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)
95+
coerced[cls._make_type_key(i)] = cls._map_type(i)
96+
else:
97+
key = cls._make_type_key(i.__objclass__)
98+
submap = {i.__name__: i}
99+
try: coerced[key].update(submap)
100+
except: coerced[key] = submap
101+
coerced = dict((k, v) for k, v in coerced.items()
102+
if k in mro_map)
103+
return coerced
67104

68105
@classmethod
69-
def _get_invalid_names(
70-
cls, target: type,
71-
exempt: list | tuple | set | type = None) -> set[str]:
106+
def _get_invalid_names(cls, target: type, mro_map: dict,
107+
exempt: dict) -> set[str]:
72108
"""internal class method to identify names of methods/attributes
73109
considered invalid in the target subclass (unless part of exempted
74110
methods/attributes)
75111
"""
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)
112+
*ancestor_keys, _ = list(mro_map)[1:]
113+
required = set(
114+
i for i in cls._map_type(object) if len(i.strip('_')) > 2)
115+
valid, invalid = set(vars(target)), set()
116+
for key in ancestor_keys:
117+
type_invalid = dict(
118+
(name, value) for name, value in mro_map[key].items()
119+
if all((name not in required, name not in valid,
120+
name not in exempt.get(key, set()),
121+
name != '__dict__')))
92122
invalid |= set(type_invalid)
93123
return invalid
94124

125+
@classmethod
126+
def _make_type_key(cls, target: type) -> str:
127+
"""internal class method to create a nominally unique key for a
128+
target type based on its module file location (or module name)
129+
and its own name; intended to account for conflicting type names
130+
in a target MRO
131+
"""
132+
if not isinstance(target, type):
133+
error = TypeError(f'{repr(target)} not a valid type')
134+
raise error
135+
try:
136+
name = target.__name__
137+
try: source = getabsfile(target)
138+
except: source = getmodule(target).__name__
139+
return f'{source}->{repr(name)}'
140+
except Exception as e:
141+
error = TypeError(
142+
f'cannot determine origin of {repr(target)} as a type')
143+
raise error
144+
return
145+
146+
@classmethod
147+
def _map_mro(cls, target: type) -> dict:
148+
"""internal class method to map the MRO of a target type with
149+
nominally unique key references for types and associated method/
150+
attribute mappings by name and value
151+
"""
152+
return dict((cls._make_type_key(i), cls._map_type(i))
153+
for i in target.mro())
154+
95155
@classmethod
96156
def _map_type(cls, target: type) -> dict:
97157
"""internal class method to return mapping of names and associated
@@ -130,4 +190,4 @@ def __getattribute__(self, name: str):
130190
return result
131191
if __getattribute__ != getter_base:
132192
target.__getattribute__ = __getattribute__
133-
return
193+
return

0 commit comments

Comments
 (0)