Skip to content

Commit 4cb991e

Browse files
authored
Add files via upload
Migration to PIP-enabled library
1 parent 856875c commit 4cb991e

File tree

3 files changed

+205
-0
lines changed

3 files changed

+205
-0
lines changed

disinheritance/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Python disinheritance library"""
2+
3+
4+
from ._disinherit import *

disinheritance/_disinherit.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""module for managing inherited methods/attributes in subclassed object
2+
types
3+
"""
4+
5+
6+
from functools import wraps
7+
from inspect import getabsfile
8+
from inspect import getmodule
9+
from types import MethodType
10+
11+
12+
__all__ = 'disinherit',
13+
14+
15+
class disinherit:
16+
17+
"""decorator to remove access to inherited methods/attributes in an
18+
object type, except where specified by the exempt keyword argument
19+
20+
- exemptions are applied in order of argument declaration
21+
22+
- exemptions not available through inheritance or overridden in the
23+
target type are ignored
24+
25+
- functionally required "origin" object methods will be retained
26+
27+
- disinherited methods/attributes are replaced with NotImplemented in
28+
the target type
29+
30+
-> NotImplemented methods/attributes are ignored in dir() calls for
31+
and produce an AttributeError when retrieved from target type
32+
instances
33+
34+
-> provides explicit status in help() call on target type
35+
36+
-> use ensures reversion back to disinheritance if used for
37+
assignment but deleted in instances
38+
"""
39+
40+
def __init__(self, *, exempt: type | MethodType |
41+
list[type | MethodType] | tuple[type | MethodType] |
42+
set[type | MethodType] = None):
43+
self.exempt = exempt
44+
return
45+
46+
def __call__(self, target: type):
47+
return self.in_type(target, self.exempt)
48+
49+
@classmethod
50+
def in_type(cls, target: type, exempt: type | MethodType |
51+
list[type | MethodType] | tuple[type | MethodType] |
52+
set[type | MethodType] = None) -> type:
53+
"""disinherits methods/attributes from a target type, except for
54+
required methods and specified exemptions
55+
"""
56+
mro_map = cls._map_mro(target)
57+
exempt = cls._coerce_exempt(mro_map, exempt)
58+
invalid = cls._get_invalid_names(target, mro_map, exempt)
59+
for name in invalid: setattr(target, name, NotImplemented)
60+
target_map = dict((k, v) for k, v in cls._map_type(target).items()
61+
if k not in invalid)
62+
for exempt_map in exempt.values():
63+
for name, value in exempt_map.items():
64+
if name not in target_map:
65+
setattr(target, name, value)
66+
cls._wrap_dir(target)
67+
cls._wrap_getter(target)
68+
return target
69+
70+
@classmethod
71+
def _coerce_exempt(cls, mro_map: dict, exempt: type | MethodType |
72+
list | tuple | set = None) -> dict[str, dict]:
73+
"""internal class method to coerce exemptions for disinheritance
74+
to a mapping of methods/attributes
75+
"""
76+
if exempt is None: return dict()
77+
elif not isinstance(exempt, (list, tuple, set)):
78+
exempt = [exempt]
79+
elif not isinstance(exempt, list):
80+
try: exempt = list(exempt)
81+
except: return dict()
82+
exempt = list(i for i in exempt if hasattr(i, '__objclass__')
83+
or isinstance(i, type))
84+
coerced = dict()
85+
for i in exempt:
86+
if isinstance(i, type):
87+
coerced[cls._make_type_key(i)] = cls._map_type(i)
88+
else:
89+
key = cls._make_type_key(i.__objclass__)
90+
submap = {i.__name__: i}
91+
try: coerced[key].update(submap)
92+
except: coerced[key] = submap
93+
coerced = dict((k, v) for k, v in coerced.items()
94+
if k in mro_map)
95+
return coerced
96+
97+
@classmethod
98+
def _get_invalid_names(cls, target: type, mro_map: dict,
99+
exempt: dict) -> set[str]:
100+
"""internal class method to identify names of methods/attributes
101+
considered invalid in the target subclass (unless part of exempted
102+
methods/attributes)
103+
"""
104+
*ancestor_keys, _ = list(mro_map)[1:]
105+
required = set(
106+
i for i in cls._map_type(object) if len(i.strip('_')) > 2)
107+
valid, invalid = set(vars(target)), set()
108+
for key in ancestor_keys:
109+
type_invalid = dict(
110+
(name, value) for name, value in mro_map[key].items()
111+
if all((name not in required, name not in valid,
112+
name not in exempt.get(key, set()),
113+
name != '__dict__')))
114+
invalid |= set(type_invalid)
115+
return invalid
116+
117+
@classmethod
118+
def _make_type_key(cls, target: type) -> str:
119+
"""internal class method to create a nominally unique key for a
120+
target type based on its module file location (or module name)
121+
and its own name; intended to account for conflicting type names
122+
in a target MRO
123+
"""
124+
if not isinstance(target, type):
125+
error = TypeError(f'{repr(target)} not a valid type')
126+
raise error
127+
try:
128+
name = target.__name__
129+
try: source = getabsfile(target)
130+
except: source = getmodule(target).__name__
131+
return f'{source}->{repr(name)}'
132+
except Exception as e:
133+
error = TypeError(
134+
f'cannot determine origin of {repr(target)} as a type')
135+
raise error
136+
return
137+
138+
@classmethod
139+
def _map_mro(cls, target: type) -> dict:
140+
"""internal class method to map the MRO of a target type with
141+
nominally unique key references for types and associated method/
142+
attribute mappings by name and value
143+
"""
144+
return dict((cls._make_type_key(i), cls._map_type(i))
145+
for i in target.mro())
146+
147+
@classmethod
148+
def _map_type(cls, target: type) -> dict:
149+
"""internal class method to return mapping of names and associated
150+
methods/attributes for a target type
151+
"""
152+
return dict((name, getattr(target, name)) for name in dir(target))
153+
154+
@classmethod
155+
def _wrap_dir(cls, target: type) -> object.__dir__:
156+
"""internal class method to wrap __dir__ in the target type to
157+
prevent return of disinherited methods/attributes
158+
"""
159+
dir_base = target.__dir__
160+
@wraps(dir_base)
161+
def __dir__(self) -> list[str]:
162+
return list(i for i in dir_base(self) if
163+
getattr(self, i, NotImplemented) is not
164+
NotImplemented)
165+
if __dir__ != dir_base: target.__dir__ = __dir__
166+
return
167+
168+
@classmethod
169+
def _wrap_getter(cls, target: type):
170+
"""internal class method to wrap __getattribute__ in the target
171+
type to prevent return of disinherited methods/attributes
172+
"""
173+
getter_base = target.__getattribute__
174+
@wraps(getter_base)
175+
def __getattribute__(self, name: str):
176+
result = getter_base(self, name)
177+
if result is NotImplemented:
178+
error = AttributeError(
179+
f'{repr(type(self).__name__)} object has no '\
180+
f'attribute {repr(name)}')
181+
raise error
182+
return result
183+
if __getattribute__ != getter_base:
184+
target.__getattribute__ = __getattribute__
185+
return

pyproject.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[build-system]
2+
requires = ["setuptools"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "disinheritance"
7+
description = "Direct management of inherited methods/attributes in subclassed object types"
8+
version = "1.1.1"
9+
authors = [
10+
{name = "Stanton K. Nielson"},
11+
]
12+
requires-python = ">= 3.10"
13+
readme = "README.md"
14+
15+
[project.urls]
16+
Homepage = "https://github.com/stannielson/Python-Disinheritance"

0 commit comments

Comments
 (0)