55
66from dataclasses import dataclass
77from functools import wraps
8+ from inspect import getabsfile
9+ from inspect import getmodule
810from 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