0
# Metaclass and Decorator Utilities
1
2
Utilities for working with metaclasses and creating decorators that work across Python versions. These functions provide unified interfaces for metaclass usage and decorator creation that handle the syntax differences between Python 2 and 3.
3
4
## Capabilities
5
6
### Metaclass Utilities
7
8
Functions for creating classes with metaclasses in a cross-version compatible way.
9
10
```python { .api }
11
def with_metaclass(meta: type, *bases: type) -> type
12
"""Create a base class with a metaclass."""
13
14
def add_metaclass(metaclass: type) -> Callable[[type], type]
15
"""Class decorator for adding a metaclass."""
16
```
17
18
**Usage Examples:**
19
20
```python
21
import six
22
23
# Method 1: Using with_metaclass for base class creation
24
class MyMeta(type):
25
def __new__(cls, name, bases, dct):
26
# Add custom behavior
27
dct['created_by'] = 'MyMeta'
28
return super(MyMeta, cls).__new__(cls, name, bases, dct)
29
30
# Create base class with metaclass
31
BaseClass = six.with_metaclass(MyMeta, object)
32
33
class MyClass(BaseClass):
34
pass
35
36
print(MyClass.created_by) # "MyMeta"
37
38
# Method 2: Using add_metaclass decorator
39
@six.add_metaclass(MyMeta)
40
class AnotherClass(object):
41
def method(self):
42
return "Hello"
43
44
print(AnotherClass.created_by) # "MyMeta"
45
```
46
47
### Decorator Utilities
48
49
Cross-version decorator utilities including functools.wraps replacement.
50
51
```python { .api }
52
def wraps(wrapped: Callable) -> Callable[[Callable], Callable]
53
"""Decorator to wrap a function with functools.wraps behavior."""
54
```
55
56
This provides `functools.wraps` functionality across Python versions, preserving function metadata when creating decorators.
57
58
**Usage Examples:**
59
60
```python
61
import six
62
63
# Create a decorator using six.wraps
64
def my_decorator(func):
65
@six.wraps(func)
66
def wrapper(*args, **kwargs):
67
print(f"Calling {func.__name__}")
68
result = func(*args, **kwargs)
69
print(f"Finished {func.__name__}")
70
return result
71
return wrapper
72
73
@my_decorator
74
def greet(name):
75
"""Greet someone by name."""
76
return f"Hello, {name}!"
77
78
# Function metadata is preserved
79
print(greet.__name__) # "greet"
80
print(greet.__doc__) # "Greet someone by name."
81
result = greet("Alice") # Prints: Calling greet, Hello, Alice!, Finished greet
82
```
83
84
### Unicode Compatibility Decorator
85
86
Class decorator for Python 2 unicode string compatibility.
87
88
```python { .api }
89
def python_2_unicode_compatible(cls: type) -> type
90
"""Class decorator for unicode compatibility in Python 2."""
91
```
92
93
This decorator allows classes to define `__str__` methods that return unicode strings in Python 2 and regular strings in Python 3.
94
95
**Usage Example:**
96
97
```python
98
import six
99
100
@six.python_2_unicode_compatible
101
class Person:
102
def __init__(self, name, age):
103
self.name = name
104
self.age = age
105
106
def __str__(self):
107
# This will work correctly in both Python 2 and 3
108
return f"Person(name='{self.name}', age={self.age})"
109
110
def __repr__(self):
111
return f"Person({self.name!r}, {self.age!r})"
112
113
# Works with unicode characters in both versions
114
person = Person("José", 30)
115
print(str(person)) # Handles unicode correctly
116
print(repr(person))
117
```
118
119
## Advanced Metaclass Patterns
120
121
### Custom Metaclass with six.with_metaclass
122
123
```python
124
import six
125
126
class SingletonMeta(type):
127
"""Metaclass that creates singleton instances."""
128
_instances = {}
129
130
def __call__(cls, *args, **kwargs):
131
if cls not in cls._instances:
132
cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)
133
return cls._instances[cls]
134
135
# Create singleton base class
136
SingletonBase = six.with_metaclass(SingletonMeta, object)
137
138
class DatabaseConnection(SingletonBase):
139
def __init__(self):
140
self.connection_id = id(self)
141
142
def connect(self):
143
return f"Connected with ID: {self.connection_id}"
144
145
# Test singleton behavior
146
db1 = DatabaseConnection()
147
db2 = DatabaseConnection()
148
print(db1 is db2) # True
149
print(db1.connect()) # Same connection ID
150
```
151
152
### Registry Metaclass Pattern
153
154
```python
155
import six
156
157
class RegistryMeta(type):
158
"""Metaclass that maintains a registry of all created classes."""
159
registry = {}
160
161
def __new__(cls, name, bases, dct):
162
new_class = super(RegistryMeta, cls).__new__(cls, name, bases, dct)
163
if name != 'BaseRegistered': # Skip base class
164
cls.registry[name] = new_class
165
return new_class
166
167
@classmethod
168
def get_class(cls, name):
169
return cls.registry.get(name)
170
171
@classmethod
172
def list_classes(cls):
173
return list(cls.registry.keys())
174
175
# Create base class with registry metaclass
176
BaseRegistered = six.with_metaclass(RegistryMeta, object)
177
178
class WidgetA(BaseRegistered):
179
pass
180
181
class WidgetB(BaseRegistered):
182
pass
183
184
# Access registry
185
print(RegistryMeta.list_classes()) # ['WidgetA', 'WidgetB']
186
widget_class = RegistryMeta.get_class('WidgetA')
187
widget = widget_class()
188
```
189
190
### Attribute Validation Metaclass
191
192
```python
193
import six
194
195
class ValidatedMeta(type):
196
"""Metaclass that adds attribute validation."""
197
198
def __new__(cls, name, bases, dct):
199
# Find validation rules
200
validators = {}
201
for key, value in list(dct.items()):
202
if key.startswith('validate_'):
203
attr_name = key[9:] # Remove 'validate_' prefix
204
validators[attr_name] = value
205
del dct[key] # Remove validator from class dict
206
207
# Create the class
208
new_class = super(ValidatedMeta, cls).__new__(cls, name, bases, dct)
209
new_class._validators = validators
210
211
# Override __setattr__ if not already defined
212
if '__setattr__' not in dct:
213
new_class.__setattr__ = cls._validated_setattr
214
215
return new_class
216
217
@staticmethod
218
def _validated_setattr(self, name, value):
219
if hasattr(self.__class__, '_validators') and name in self.__class__._validators:
220
validator = self.__class__._validators[name]
221
validator(self, value)
222
object.__setattr__(self, name, value)
223
224
# Use the validation metaclass
225
@six.add_metaclass(ValidatedMeta)
226
class Person:
227
def __init__(self, name, age):
228
self.name = name
229
self.age = age
230
231
def validate_age(self, value):
232
if not isinstance(value, int) or value < 0:
233
raise ValueError("Age must be a non-negative integer")
234
235
def validate_name(self, value):
236
if not isinstance(value, six.string_types) or len(value) == 0:
237
raise ValueError("Name must be a non-empty string")
238
239
# Test validation
240
person = Person("Alice", 30)
241
person.age = 25 # OK
242
try:
243
person.age = -5 # Raises ValueError
244
except ValueError as e:
245
print(f"Validation error: {e}")
246
```
247
248
## Common Usage Patterns
249
250
```python
251
import six
252
253
# Abstract base class pattern with metaclass
254
class AbstractMeta(type):
255
"""Metaclass for creating abstract base classes."""
256
257
def __new__(cls, name, bases, dct):
258
# Collect abstract methods
259
abstract_methods = set()
260
for base in bases:
261
if hasattr(base, '_abstract_methods'):
262
abstract_methods.update(base._abstract_methods)
263
264
for key, value in dct.items():
265
if getattr(value, '_abstract', False):
266
abstract_methods.add(key)
267
elif key in abstract_methods:
268
abstract_methods.discard(key) # Implemented
269
270
new_class = super(AbstractMeta, cls).__new__(cls, name, bases, dct)
271
new_class._abstract_methods = frozenset(abstract_methods)
272
return new_class
273
274
def __call__(cls, *args, **kwargs):
275
if cls._abstract_methods:
276
raise TypeError(f"Can't instantiate abstract class {cls.__name__} "
277
f"with abstract methods {', '.join(cls._abstract_methods)}")
278
return super(AbstractMeta, cls).__call__(*args, **kwargs)
279
280
def abstract_method(func):
281
"""Decorator to mark methods as abstract."""
282
func._abstract = True
283
return func
284
285
# Create abstract base class
286
@six.add_metaclass(AbstractMeta)
287
class Shape:
288
@abstract_method
289
def area(self):
290
pass
291
292
@abstract_method
293
def perimeter(self):
294
pass
295
296
def describe(self):
297
return f"Shape with area {self.area()} and perimeter {self.perimeter()}"
298
299
class Rectangle(Shape):
300
def __init__(self, width, height):
301
self.width = width
302
self.height = height
303
304
def area(self):
305
return self.width * self.height
306
307
def perimeter(self):
308
return 2 * (self.width + self.height)
309
310
# Usage
311
rect = Rectangle(5, 3)
312
print(rect.describe()) # Shape with area 15 and perimeter 16
313
314
# This would raise TypeError:
315
# shape = Shape() # Can't instantiate abstract class
316
317
# Mixin pattern with metaclass support
318
class LoggingMixin:
319
"""Mixin that adds logging capabilities."""
320
321
def log(self, message):
322
class_name = self.__class__.__name__
323
six.print_(f"[{class_name}] {message}")
324
325
@six.add_metaclass(ValidatedMeta)
326
class LoggedPerson(LoggingMixin):
327
def __init__(self, name, age):
328
self.name = name
329
self.age = age
330
self.log(f"Created person: {name}, age {age}")
331
332
def validate_age(self, value):
333
if not isinstance(value, int) or value < 0:
334
self.log(f"Invalid age validation: {value}")
335
raise ValueError("Age must be a non-negative integer")
336
337
def celebrate_birthday(self):
338
old_age = self.age
339
self.age += 1
340
self.log(f"Happy birthday! Age changed from {old_age} to {self.age}")
341
342
# Usage
343
person = LoggedPerson("Bob", 25) # [LoggedPerson] Created person: Bob, age 25
344
person.celebrate_birthday() # [LoggedPerson] Happy birthday! Age changed from 25 to 26
345
```