|
| 1 | +""" |
| 2 | +Allows overriding existing functions and methods with alternative implementations. |
| 3 | +""" |
| 4 | + |
| 5 | +import functools |
| 6 | +from importlib import import_module |
| 7 | + |
| 8 | +from django.conf import settings |
| 9 | + |
| 10 | + |
| 11 | +def pluggable_override(override): |
| 12 | + """ |
| 13 | + This decorator allows overriding any function or method by pointing to an alternative implementation |
| 14 | + with `override` param. |
| 15 | + :param override: path to the alternative function |
| 16 | +
|
| 17 | + Example usage: |
| 18 | +
|
| 19 | + 1. Add this decorator to an existing function `OVERRIDE_TRANSFORM` is the variable name in settings that can be |
| 20 | + used for overriding this function. Remember to add the `OVERRIDE_` prefix to the name to have the consistent |
| 21 | + namespace for the overrides. |
| 22 | + >>> @pluggable_override('OVERRIDE_TRANSFORM') |
| 23 | + ... def transform(value): |
| 24 | + ... return value + 10 |
| 25 | +
|
| 26 | + 2. Prepare an alternative implementation. It will have the same set of arguments as the original function, with the |
| 27 | + `prev_fn` added at the beginning. |
| 28 | + >>> def decrement(prev_fn, value): |
| 29 | + ... if value >= 10: |
| 30 | + ... return value - 1 # Return the decremented value. |
| 31 | + ... else: |
| 32 | + ... return prev_fn(value) - 1 # Call the original `transform` method before decrementing and returning. |
| 33 | +
|
| 34 | + 3. Specify the path in settings (e.g. in `envs/private.py`): |
| 35 | + >>> OVERRIDE_TRANSFORM = 'transform_plugin.decrement' |
| 36 | +
|
| 37 | + You can also chain overrides: |
| 38 | + >>> OVERRIDE_TRANSFORM = [ |
| 39 | + ... 'transform_plugin.decrement', |
| 40 | + ... 'transform_plugin.increment', |
| 41 | + ... ] |
| 42 | +
|
| 43 | + Another example: |
| 44 | +
|
| 45 | + 1. We want to limit access to a Django view (e.g. `common.djangoapps.student.views.dashboard.student_dashboard`) |
| 46 | + to allow only logged in users. To do this add `OVERRIDE_DASHBOARD` to the original function: |
| 47 | + >>> @pluggable_override('OVERRIDE_DASHBOARD') |
| 48 | + ... def student_dashboard(request): |
| 49 | + ... ... # The rest of the implementation is not relevant in this case. |
| 50 | +
|
| 51 | + 2. Prepare an alternative implementation (e.g. in `envs/private.py` to make this example simpler): |
| 52 | + >>> from django.contrib.auth.decorators import login_required |
| 53 | + ... |
| 54 | + ... def dashboard(prev_fn, request): |
| 55 | + ... return login_required(prev_fn)(request) |
| 56 | + ... |
| 57 | + ... OVERRIDE_DASHBOARD = 'lms.envs.private.dashboard' |
| 58 | + """ |
| 59 | + def decorator(f): |
| 60 | + @functools.wraps(f) |
| 61 | + def wrapper(*args, **kwargs): |
| 62 | + prev_fn = f |
| 63 | + |
| 64 | + override_functions = getattr(settings, override, ()) |
| 65 | + |
| 66 | + if isinstance(override_functions, str): |
| 67 | + override_functions = [override_functions] |
| 68 | + |
| 69 | + for impl in override_functions: |
| 70 | + module, function = impl.rsplit('.', 1) |
| 71 | + mod = import_module(module) |
| 72 | + func = getattr(mod, function) |
| 73 | + |
| 74 | + prev_fn = functools.partial(func, prev_fn) |
| 75 | + # Call the last specified function. It can call the previous one, which can call the previous one, etc. |
| 76 | + # (until it reaches the base implementation). It can also return without calling `prev_fn`. |
| 77 | + return prev_fn(*args, **kwargs) |
| 78 | + return wrapper |
| 79 | + return decorator |
0 commit comments