0
# Metaprogramming Tools
1
2
Advanced metaclasses, function signature manipulation, delegation patterns, and dynamic code generation utilities for building flexible and extensible APIs. The meta module provides powerful tools for creating sophisticated object-oriented designs and dynamic programming patterns.
3
4
## Capabilities
5
6
### Metaclasses for Enhanced Object Creation
7
8
Specialized metaclasses that modify class creation and object initialization behavior.
9
10
```python { .api }
11
class FixSigMeta(type):
12
"""
13
Metaclass that fixes function signatures on classes overriding __new__.
14
15
Automatically sets __signature__ attribute based on __init__ method
16
to ensure proper introspection and IDE support for classes that
17
override __new__ but define parameters in __init__.
18
19
Usage:
20
class MyClass(metaclass=FixSigMeta):
21
def __init__(self, param1, param2): ...
22
def __new__(cls, *args, **kwargs): ...
23
24
# MyClass.__signature__ automatically reflects __init__ signature
25
"""
26
27
def __new__(cls, name, bases, dict): ...
28
29
class PrePostInitMeta(FixSigMeta):
30
"""
31
Metaclass that calls optional __pre_init__ and __post_init__ methods.
32
33
Automatically calls __pre_init__ before __init__ and __post_init__
34
after __init__ if these methods are defined. Provides hooks for
35
setup and cleanup logic around object initialization.
36
37
Execution order:
38
1. __new__ called
39
2. __pre_init__ called (if defined)
40
3. __init__ called
41
4. __post_init__ called (if defined)
42
43
Usage:
44
class MyClass(metaclass=PrePostInitMeta):
45
def __pre_init__(self, *args, **kwargs): ...
46
def __init__(self, *args, **kwargs): ...
47
def __post_init__(self, *args, **kwargs): ...
48
"""
49
50
def __call__(cls, *args, **kwargs): ...
51
52
class AutoInit(metaclass=PrePostInitMeta):
53
"""
54
Base class that automatically calls super().__init__ in __pre_init__.
55
56
Eliminates need for subclasses to remember to call super().__init__()
57
by automatically handling parent class initialization.
58
59
Usage:
60
class Parent(AutoInit):
61
def __init__(self, x): self.x = x
62
63
class Child(Parent):
64
def __init__(self, x, y): # No need to call super().__init__
65
self.y = y
66
"""
67
68
def __pre_init__(self, *args, **kwargs): ...
69
70
class NewChkMeta(FixSigMeta):
71
"""
72
Metaclass to avoid recreating objects passed to constructor.
73
74
If an object of the same class is passed to the constructor
75
with no additional arguments, returns the object unchanged
76
rather than creating a new instance.
77
78
Usage:
79
class MyClass(metaclass=NewChkMeta): pass
80
81
obj1 = MyClass()
82
obj2 = MyClass(obj1) # Returns obj1, not a new instance
83
obj3 = MyClass(obj1, extra_arg=True) # Creates new instance
84
"""
85
86
def __call__(cls, x=None, *args, **kwargs): ...
87
88
class BypassNewMeta(FixSigMeta):
89
"""
90
Metaclass that casts objects to class type instead of creating new instances.
91
92
If object is of _bypass_type, changes its __class__ to the target class
93
rather than creating a new instance. Useful for wrapping existing objects.
94
95
Class can define:
96
- _bypass_type: Type to cast from
97
- _new_meta: Custom creation method
98
99
Usage:
100
class StringWrapper(metaclass=BypassNewMeta):
101
_bypass_type = str
102
def upper_method(self): return self.upper()
103
104
wrapped = StringWrapper("hello") # Casts str to StringWrapper
105
print(wrapped.upper_method()) # "HELLO"
106
"""
107
108
def __call__(cls, x=None, *args, **kwargs): ...
109
```
110
111
### Function Signature Manipulation
112
113
Tools for analyzing, modifying, and working with function signatures dynamically.
114
115
```python { .api }
116
def test_sig(f, expected_signature):
117
"""
118
Test that function has expected signature string.
119
120
Utility for testing that function signatures match expected format,
121
useful for validating decorator effects and signature modifications.
122
123
Parameters:
124
- f: function to test
125
- expected_signature: str, expected signature format
126
127
Raises:
128
AssertionError: If signatures don't match
129
"""
130
131
def empty2none(param):
132
"""
133
Convert Parameter.empty to None for cleaner handling.
134
135
Helper function to normalize inspect.Parameter.empty values
136
to None for easier processing and comparison.
137
138
Parameters:
139
- param: inspect.Parameter value
140
141
Returns:
142
None if param is Parameter.empty, otherwise param unchanged
143
"""
144
145
def anno_dict(func):
146
"""
147
Get function annotations as dict with empty values normalized.
148
149
Extracts __annotations__ from function and converts Parameter.empty
150
values to None for cleaner processing.
151
152
Parameters:
153
- func: function to get annotations from
154
155
Returns:
156
dict: Annotations with empty values normalized to None
157
"""
158
159
def use_kwargs_dict(keep=False, **kwargs):
160
"""
161
Decorator to replace **kwargs in signature with specific parameters.
162
163
Modifies function signature to show specific keyword parameters
164
instead of generic **kwargs, improving IDE support and documentation.
165
166
Parameters:
167
- keep: bool, whether to keep **kwargs in addition to named params
168
- **kwargs: parameter names and defaults to add to signature
169
170
Returns:
171
Decorator function
172
173
Usage:
174
@use_kwargs_dict(param1=None, param2="default")
175
def func(**kwargs): ...
176
# Signature shows func(*, param1=None, param2='default')
177
"""
178
179
def use_kwargs(names, keep=False):
180
"""
181
Decorator to replace **kwargs with list of parameter names.
182
183
Similar to use_kwargs_dict but takes parameter names as list
184
without default values.
185
186
Parameters:
187
- names: list of str, parameter names to add
188
- keep: bool, whether to keep **kwargs
189
190
Returns:
191
Decorator function
192
"""
193
194
def delegates(to=None, keep=False, but=None):
195
"""
196
Decorator to delegate function parameters from another function.
197
198
Copies parameters from target function to decorated function,
199
enabling parameter forwarding while maintaining proper signatures
200
for IDE support and introspection.
201
202
Parameters:
203
- to: callable, function to delegate parameters from
204
- keep: bool, keep original parameters in addition to delegated ones
205
- but: str|list, parameter names to exclude from delegation
206
207
Returns:
208
Decorator that modifies function signature
209
210
Usage:
211
def target_func(a, b=1, c=2): pass
212
213
@delegates(target_func)
214
def wrapper(**kwargs):
215
return target_func(**kwargs)
216
# wrapper now has signature: wrapper(a, b=1, c=2)
217
"""
218
219
def method(func):
220
"""
221
Convert function to method by adding self parameter.
222
223
Modifies function signature to include 'self' as first parameter,
224
useful for dynamically creating methods.
225
226
Parameters:
227
- func: function to convert to method
228
229
Returns:
230
Function with method signature (including self)
231
"""
232
233
def funcs_kwargs(as_method=False):
234
"""
235
Get kwargs specification for function parameters.
236
237
Analyzes function parameters and returns specification
238
for **kwargs handling and parameter forwarding.
239
240
Parameters:
241
- as_method: bool, treat as method (skip self parameter)
242
243
Returns:
244
dict: Parameter specification for kwargs processing
245
"""
246
```
247
248
### Dynamic Code Generation
249
250
Utilities for generating and modifying code at runtime.
251
252
```python { .api }
253
def _mk_param(name, default=None):
254
"""
255
Create inspect.Parameter for signature construction.
256
257
Helper function to create Parameter objects for building
258
function signatures programmatically.
259
260
Parameters:
261
- name: str, parameter name
262
- default: default value for parameter
263
264
Returns:
265
inspect.Parameter: Configured parameter object
266
"""
267
268
def _rm_self(signature):
269
"""
270
Remove 'self' parameter from function signature.
271
272
Utility to convert method signatures to function signatures
273
by removing the self parameter.
274
275
Parameters:
276
- signature: inspect.Signature with self parameter
277
278
Returns:
279
inspect.Signature: New signature without self parameter
280
"""
281
```
282
283
## Usage Examples
284
285
### Creating Enhanced Classes with Metaclasses
286
287
```python
288
from fastcore.meta import FixSigMeta, PrePostInitMeta, AutoInit
289
290
# Class with proper signature introspection
291
class DataContainer(metaclass=FixSigMeta):
292
def __init__(self, data, transform=None, validate=True):
293
self.data = data
294
self.transform = transform
295
self.validate = validate
296
297
def __new__(cls, *args, **kwargs):
298
# Custom object creation logic
299
instance = super().__new__(cls)
300
return instance
301
302
# Signature is properly reflected for IDE support
303
import inspect
304
print(inspect.signature(DataContainer))
305
# <Signature (data, transform=None, validate=True)>
306
307
# Class with initialization hooks
308
class ConfigurableClass(metaclass=PrePostInitMeta):
309
def __pre_init__(self, *args, **kwargs):
310
print("Setting up configuration...")
311
self.setup_config()
312
313
def __init__(self, name, value):
314
self.name = name
315
self.value = value
316
317
def __post_init__(self, *args, **kwargs):
318
print("Finalizing setup...")
319
self.validate()
320
321
def setup_config(self):
322
self.config = {"initialized": True}
323
324
def validate(self):
325
assert hasattr(self, 'name')
326
assert hasattr(self, 'value')
327
328
obj = ConfigurableClass("test", 42)
329
# Prints: Setting up configuration...
330
# Prints: Finalizing setup...
331
332
# Simplified inheritance with AutoInit
333
class BaseProcessor(AutoInit):
334
def __init__(self, name):
335
self.name = name
336
self.processed_count = 0
337
338
class AdvancedProcessor(BaseProcessor):
339
def __init__(self, name, algorithm):
340
# No need to call super().__init__() - handled automatically
341
self.algorithm = algorithm
342
self.features = []
343
344
processor = AdvancedProcessor("nlp", "transformer")
345
print(processor.name) # "nlp" - inherited properly
346
```
347
348
### Object Identity and Casting Patterns
349
350
```python
351
from fastcore.meta import NewChkMeta, BypassNewMeta
352
353
# Avoid unnecessary object recreation
354
class SmartContainer(metaclass=NewChkMeta):
355
def __init__(self, data=None):
356
self.data = data or []
357
358
def add(self, item):
359
self.data.append(item)
360
361
container1 = SmartContainer([1, 2, 3])
362
container2 = SmartContainer(container1) # Returns container1, not new instance
363
print(container1 is container2) # True
364
365
container3 = SmartContainer(container1, extra_data=True) # Creates new instance
366
print(container1 is container3) # False
367
368
# Type casting with BypassNewMeta
369
class EnhancedString(str, metaclass=BypassNewMeta):
370
_bypass_type = str
371
372
def word_count(self):
373
return len(self.split())
374
375
def title_case(self):
376
return ' '.join(word.capitalize() for word in self.split())
377
378
# Cast existing string to EnhancedString
379
text = "hello world python programming"
380
enhanced = EnhancedString(text) # Casts str to EnhancedString
381
print(enhanced.word_count()) # 4
382
print(enhanced.title_case()) # "Hello World Python Programming"
383
print(type(enhanced)) # <class 'EnhancedString'>
384
385
# Works with string methods too
386
print(enhanced.upper()) # "HELLO WORLD PYTHON PROGRAMMING"
387
```
388
389
### Function Signature Delegation and Enhancement
390
391
```python
392
from fastcore.meta import delegates, use_kwargs_dict, use_kwargs
393
import matplotlib.pyplot as plt
394
395
# Delegate parameters from matplotlib.pyplot.plot
396
@delegates(plt.plot)
397
def enhanced_plot(*args, title="Enhanced Plot", save_path=None, **kwargs):
398
"""Enhanced plotting with automatic title and save functionality."""
399
fig, ax = plt.subplots()
400
ax.plot(*args, **kwargs)
401
ax.set_title(title)
402
403
if save_path:
404
plt.savefig(save_path)
405
406
return fig, ax
407
408
# Function now has full plt.plot signature plus enhancements
409
import inspect
410
sig = inspect.signature(enhanced_plot)
411
print(sig) # Shows all matplotlib plot parameters
412
413
# Use the enhanced function
414
fig, ax = enhanced_plot([1, 2, 3], [1, 4, 9],
415
linestyle='--', color='red',
416
title="My Data", save_path="plot.png")
417
418
# Replace **kwargs with specific parameters for better IDE support
419
@use_kwargs_dict(method='GET', timeout=30, headers=None)
420
def api_request(url, **kwargs):
421
"""Make API request with specified parameters."""
422
import requests
423
return requests.request(**kwargs, url=url)
424
425
# Signature now shows: api_request(url, *, method='GET', timeout=30, headers=None)
426
427
# Replace **kwargs with parameter names from list
428
@use_kwargs(['host', 'port', 'database', 'username', 'password'])
429
def connect_db(**kwargs):
430
"""Connect to database with specified parameters."""
431
# Connection logic here
432
return f"Connected with {kwargs}"
433
434
# Signature shows all database connection parameters
435
```
436
437
### Advanced Delegation Patterns
438
439
```python
440
from fastcore.meta import delegates, method
441
from functools import partial
442
443
# Complex delegation with exclusions
444
class DataFrameWrapper:
445
def __init__(self, df):
446
self.df = df
447
448
@delegates(lambda self: self.df.groupby, but=['by'])
449
def smart_groupby(self, columns, **kwargs):
450
"""Enhanced groupby with automatic column handling."""
451
if isinstance(columns, str):
452
columns = [columns]
453
return self.df.groupby(columns, **kwargs)
454
455
# Delegate from multiple sources
456
class MLPipeline:
457
@delegates(lambda: sklearn.model_selection.train_test_split,
458
but=['arrays'])
459
@delegates(lambda: sklearn.preprocessing.StandardScaler,
460
keep=True)
461
def prepare_data(self, X, y, scale=True, **kwargs):
462
"""Prepare data with splitting and optional scaling."""
463
from sklearn.model_selection import train_test_split
464
from sklearn.preprocessing import StandardScaler
465
466
# Split data
467
split_kwargs = {k: v for k, v in kwargs.items()
468
if k in ['test_size', 'random_state', 'stratify']}
469
X_train, X_test, y_train, y_test = train_test_split(X, y, **split_kwargs)
470
471
# Optional scaling
472
if scale:
473
scaler_kwargs = {k: v for k, v in kwargs.items()
474
if k in ['copy', 'with_mean', 'with_std']}
475
scaler = StandardScaler(**scaler_kwargs)
476
X_train = scaler.fit_transform(X_train)
477
X_test = scaler.transform(X_test)
478
479
return X_train, X_test, y_train, y_test
480
481
# Dynamic method creation
482
def create_property_method(attr_name):
483
@method
484
def get_property(self):
485
return getattr(self, f"_{attr_name}")
486
487
return get_property
488
489
class DynamicClass:
490
def __init__(self, **kwargs):
491
for key, value in kwargs.items():
492
setattr(self, f"_{key}", value)
493
# Create getter method dynamically
494
setattr(self, f"get_{key}",
495
create_property_method(key).__get__(self, type(self)))
496
497
obj = DynamicClass(name="test", value=42, items=[1, 2, 3])
498
print(obj.get_name()) # "test"
499
print(obj.get_value()) # 42
500
print(obj.get_items()) # [1, 2, 3]
501
```
502
503
### Signature Analysis and Testing
504
505
```python
506
from fastcore.meta import test_sig, anno_dict, empty2none
507
import inspect
508
509
# Test function signatures
510
def sample_function(a: int, b: str = "default", c=None) -> bool:
511
return True
512
513
# Verify signature matches expected format
514
test_sig(sample_function, "(a: int, b: str = 'default', c=None) -> bool")
515
516
# Extract and process annotations
517
annotations = anno_dict(sample_function)
518
print(annotations) # {'a': <class 'int'>, 'b': <class 'str'>, 'c': None, 'return': <class 'bool'>}
519
520
# Signature modification validation
521
def modify_signature(func, new_params):
522
"""Modify function signature and validate result."""
523
sig = inspect.signature(func)
524
params = list(sig.parameters.values())
525
526
# Add new parameters
527
for name, default in new_params.items():
528
param = inspect.Parameter(
529
name,
530
inspect.Parameter.KEYWORD_ONLY,
531
default=default
532
)
533
params.append(param)
534
535
# Create new signature
536
new_sig = sig.replace(parameters=params)
537
func.__signature__ = new_sig
538
return func
539
540
# Test the modification
541
@modify_signature
542
def test_func(x, y=1):
543
pass
544
545
modified = modify_signature(test_func, {'extra1': 'default', 'extra2': None})
546
test_sig(modified, "(x, y=1, *, extra1='default', extra2=None)")
547
548
# Parameter processing
549
def process_parameters(func):
550
"""Analyze function parameters and provide summary."""
551
sig = inspect.signature(func)
552
summary = {
553
'total_params': len(sig.parameters),
554
'required_params': [],
555
'optional_params': [],
556
'annotations': anno_dict(func)
557
}
558
559
for name, param in sig.parameters.items():
560
if param.default is inspect.Parameter.empty:
561
summary['required_params'].append(name)
562
else:
563
summary['optional_params'].append((name, empty2none(param.default)))
564
565
return summary
566
567
def example_func(a, b: int, c="default", d: str = None):
568
pass
569
570
summary = process_parameters(example_func)
571
print(f"Required: {summary['required_params']}") # ['a', 'b']
572
print(f"Optional: {summary['optional_params']}") # [('c', 'default'), ('d', None)]
573
print(f"Annotations: {summary['annotations']}") # {'b': int, 'd': str}
574
```
575
576
### Building Flexible APIs
577
578
```python
579
from fastcore.meta import delegates, use_kwargs_dict, PrePostInitMeta
580
581
# Flexible configuration system
582
class ConfigurableAPI(metaclass=PrePostInitMeta):
583
def __pre_init__(self, *args, **kwargs):
584
self.config = {}
585
self.validators = []
586
587
def __init__(self, base_url, **config):
588
self.base_url = base_url
589
self.config.update(config)
590
591
def __post_init__(self, *args, **kwargs):
592
self.validate_config()
593
594
def validate_config(self):
595
for validator in self.validators:
596
validator(self.config)
597
598
def add_validator(self, validator_func):
599
self.validators.append(validator_func)
600
601
# API client with delegated requests parameters
602
import requests
603
604
class APIClient(ConfigurableAPI):
605
@delegates(requests.get, but=['url'])
606
def get(self, endpoint, **kwargs):
607
url = f"{self.base_url}/{endpoint.lstrip('/')}"
608
return requests.get(url, **kwargs)
609
610
@delegates(requests.post, but=['url'])
611
def post(self, endpoint, **kwargs):
612
url = f"{self.base_url}/{endpoint.lstrip('/')}"
613
return requests.post(url, **kwargs)
614
615
# Plugin system with dynamic method delegation
616
class PluginSystem:
617
def __init__(self):
618
self.plugins = {}
619
620
def register_plugin(self, name, plugin_class):
621
self.plugins[name] = plugin_class
622
623
# Delegate plugin methods to main system
624
for method_name in dir(plugin_class):
625
if not method_name.startswith('_'):
626
method = getattr(plugin_class, method_name)
627
if callable(method):
628
self.add_delegated_method(name, method_name, method)
629
630
@delegates(lambda: None) # Will be updated dynamically
631
def add_delegated_method(self, plugin_name, method_name, method):
632
def wrapper(*args, **kwargs):
633
plugin_instance = self.plugins[plugin_name]()
634
return getattr(plugin_instance, method_name)(*args, **kwargs)
635
636
# Add method with proper delegation
637
wrapper = delegates(method)(wrapper)
638
setattr(self, f"{plugin_name}_{method_name}", wrapper)
639
640
# Usage example
641
class DatabasePlugin:
642
def connect(self, host='localhost', port=5432, database='mydb'):
643
return f"Connected to {host}:{port}/{database}"
644
645
def query(self, sql, params=None, timeout=30):
646
return f"Executed: {sql}"
647
648
system = PluginSystem()
649
system.register_plugin('db', DatabasePlugin)
650
651
# Now system has db_connect and db_query methods with proper signatures
652
result = system.db_connect(host='production.db', port=5433)
653
query_result = system.db_query("SELECT * FROM users", timeout=60)
654
```