-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmprop.py
171 lines (138 loc) · 5.47 KB
/
mprop.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# Copyright 2024 Josiah Carlson <josiah.carlson@gmail.com>
# Released under the LGPL 2.1 license
# Honestly, you shouldn't be using this, you should upgrade to Python 3.7+
# and use module properties.
import sys
import threading
import types
__all__ = ['init', 'mproperty']
PY3 = sys.version_info[:2] >= (3, 0)
_dtypes = type if PY3 else (type, types.ClassType)
def isdescriptor(v):
# shortcut for the most common descriptor in Python land
if isinstance(v, property):
return True
# omit descriptor/property definitions
if isinstance(v, _dtypes):
return False
# check for the descriptor protocol
for attr in ('__get__', '__set__', '__delete__'):
if hasattr(v, attr):
return True
return False
def _cleanup(Module, glbls):
items = glbls.items()
if PY3:
items = list(items)
# Directly assign the properties to the Module class so that they behave
# like properties, and remove them from the globals so that they don't
# alias themselves.
tf = type(_cleanup)
for k, v in items:
if isinstance(v, mproperty):
v = property(v.fget, v.fset, v.fdel, v.doc)
if isinstance(v, tf):
del glbls[k]
setattr(Module, k, staticmethod(v))
elif isdescriptor(v):
del glbls[k]
setattr(Module, k, v)
def init(glbls=None):
'''
Initialize the module-level properties/descriptors for the module that
this function is being called from. Or, if a globals dictionary is passed,
initialize the module-level properties/descriptors for the module that
those globals came from.
See the readme for more documentation.
'''
# Pull the module context in which the init function was called.
init = glbls is None
if init:
glbls = sys._getframe(1).f_globals
name = glbls['__name__']
module = sys.modules[name]
if isinstance(sys.modules[name], types.ModuleType):
# Every module must have a new class to ensure that the module itself
# has its own unique properties.
class Module(object):
def __repr__(self):
return "<Module %r>"%(self.__name__,)
module = Module()
# Give the Module object and the Python module the same namespace.
module.__dict__ = glbls
# Keep a reference to the Module so that non-module properties can
# reference properties via _pmodule.PROPERTY .
module._pmodule = module
# Keep a reference to the original module so the original module isn't
# cleaned up after we've replaced the sys.modules entry (cleanup
# mangles the module globals).
module._module = sys.modules[name]
# Replace the original module with this one.
sys.modules[name] = module
else:
# Assume that module is one of our Module classes, fetch it here just
# in case someone is using both init() and @mproperty together
Module = type(module)
# Handle property assignment and global namespace cleanup
_cleanup(Module, glbls)
return module
class auto_init(object):
'''
New magic, uses the system profiler to handle module property cleanup after
module initialization.
'''
__slots__ = 'op', 'gl'
def __init__(self):
self.op = None
self.gl = {}
def add(self, gl):
if id(gl) not in self.gl:
# don't know about this module / globals yet
if not self.gl:
# install the profiler / cleanup handler, but keep the old one
self.op = sys.getprofile()
sys.setprofile(self)
# keep the reference for later
self.gl[id(gl)] = gl
def __call__(self, f, e, a):
if e == 'return' and \
f.f_code.co_name == '<module>' and \
id(f.f_globals) in self.gl:
# initialize one of the mprop modules
g = f.f_globals
m = init(g)
self.gl.pop(id(f.f_globals))
if not self.gl:
# no more mprop modules
sys.setprofile(self.op)
self.op = None
auto_init = auto_init()
class mproperty(object):
'''
Use this descriptor as a decorator in the same way that you would use
'property', but only apply it to module-level functions, and watch as your
module gains properties!
'''
__slots__ = 'fget', 'fset', 'fdel', 'doc'
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
# Make sure we've got a valid function so we can pull its globals.
for func in [fget, fset, fdel]:
if isinstance(func, types.FunctionType):
break
if not isinstance(func, types.FunctionType):
raise TypeError("At least one of fget, fset, or fdel must be a function")
# If we haven't installed the profiler for the module, install it. We
# need it to set up the properties after the module is loaded. Replaces
# the need for init() at the end.
auto_init.add(func.__globals__ if PY3 else func.func_globals)
# Update our references
self.fget = fget
self.fset = fset
self.fdel = fdel
self.doc = doc
def getter(self, get):
return mproperty(get, self.fset, self.fdel, self.doc)
def setter(self, set):
return mproperty(self.fget, set, self.fdel, self.doc)
def deleter(self, delete):
return mproperty(self.fget, self.fset, delete, self.doc)