Skip to content

Commit 18f73ad

Browse files
author
Andy Babic
committed
Provide a mechanism for adding forms and other complex django objects into template contexts
1 parent e1773c3 commit 18f73ad

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

pattern_library/object_utils.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import functools
2+
import inspect
3+
from importlib import import_module
4+
from typing import Any, Callable, Dict, Type
5+
6+
from django.conf import settings
7+
from django.http.request import HttpRequest
8+
9+
10+
@functools.lru_cache(maxsize=None)
11+
def import_from_path(path: str) -> Type:
12+
module_path, object_name = path.rsplit(".", 1)
13+
module = import_module(module_path)
14+
return getattr(module, object_name)
15+
16+
17+
@functools.lru_cache(maxsize=None)
18+
def accepts_kwarg(func: Callable, kwarg: str):
19+
"""
20+
Returns a boolean indicating whether the callable ``func`` has a
21+
signature that accepts the keyword argument `kwarg`
22+
"""
23+
signature = inspect.signature(func)
24+
try:
25+
signature.bind_partial(**{kwarg: None})
26+
return True
27+
except TypeError:
28+
return False
29+
30+
31+
@functools.lru_cache(maxsize=None)
32+
def get_custom_factories():
33+
return getattr(settings, "PATTERN_LIBRARY_CUSTOM_FACTORIES", {})
34+
35+
36+
def simple_object_factory(
37+
klass: Type, definition: Dict[str, Any], request: HttpRequest, **kwargs
38+
):
39+
"""
40+
Creates an instance of type ``klass`` using the remaining key/value pairs
41+
from the ``definition`` and the current ``request``. Any key/value pairs
42+
NOT accepted by the classes ``__init__`` method are set as attributes on
43+
the object after initialisation.
44+
"""
45+
init_kwargs = {}
46+
attributes = {}
47+
for key, value in definition.items():
48+
if accepts_kwarg(klass.__init__, key):
49+
init_kwargs[key] = value
50+
else:
51+
attributes[key] = value
52+
if accepts_kwarg(klass.__init__, "request"):
53+
init_kwargs["request"] = request
54+
obj = klass(**init_kwargs)
55+
for attr, value in attributes.items():
56+
setattr(obj, attr, value)
57+
return klass(**definition)
58+
59+
60+
def make_object(
61+
definition: Dict[str, Any],
62+
context: Dict[str, Any],
63+
request: HttpRequest,
64+
template_name: str,
65+
) -> Any:
66+
"""
67+
Converts an object defintion to an instance of the relevant class,
68+
by importing the class, finding a suitable 'factory' function and
69+
passing everything on to it.
70+
"""
71+
classname = definition.pop("klass")
72+
custom_factories = get_custom_factories()
73+
if classname in custom_factories:
74+
factory = import_from_path(custom_factories[classname])
75+
else:
76+
factory = simple_object_factory
77+
return factory(
78+
import_from_path(classname),
79+
definition=definition,
80+
context=context,
81+
request=request,
82+
template_name=template_name,
83+
)
84+
85+
86+
def get_common_objects(
87+
context: Dict[str, Any], request: HttpRequest, template_name: str
88+
) -> Dict[str, Any]:
89+
"""
90+
Returns a dictionary of objects to be added to all pattern template
91+
contexts, as defined by the ``PATTERN_LIBRARY_COMMON_OBJECTS`` setting.
92+
93+
All arguments passed to this method are for 'passing through' to object
94+
factories only. ``inject_python_objects()`` is responsible for working
95+
the return value into the template context.
96+
"""
97+
objects = {}
98+
for key, value in getattr(settings, "PATTERN_LIBRARY_COMMON_OBJECTS", {}).items():
99+
definition = {}
100+
if isinstance(value, str):
101+
definition["klass"] = value
102+
if isinstance(value, dict):
103+
definition.update(value)
104+
if definition:
105+
objects[key] = make_object(definition, context, request, template_name)
106+
return objects
107+
108+
109+
def replace_object_definitions(
110+
search_dict: Dict[str, Any],
111+
context: Dict[str, Any],
112+
request: HttpRequest,
113+
template_name: str,
114+
) -> None:
115+
"""
116+
Recursively evaluates ``search_dict``, looking for dictionaries that
117+
look like object definitions, and replacing those defintions with
118+
instances of the relevant class.
119+
"""
120+
for key, value in dict(search_dict).items():
121+
if isinstance(value, dict):
122+
replace_object_definitions(value, context, request, template_name)
123+
if "klass" in value:
124+
search_dict[key] = make_object(value, context, request, template_name)
125+
126+
127+
def inject_python_objects(
128+
context: Dict[str, Any],
129+
request: HttpRequest,
130+
template_name: str,
131+
) -> None:
132+
context.update(get_common_objects(context, request, template_name))
133+
replace_object_definitions(context, context, request, template_name)

pattern_library/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
get_pattern_context_var_name, get_pattern_template_suffix, get_sections
1717
)
1818
from pattern_library.exceptions import TemplateIsNotPattern
19+
from pattern_library.object_utils import inject_python_objects
1920

2021

2122
def path_to_section():
@@ -194,6 +195,7 @@ def render_pattern(request, template_name, allow_non_patterns=False):
194195

195196
context = get_pattern_context(template_name)
196197
context[get_pattern_context_var_name()] = True
198+
inject_python_objects(context, request, template_name)
197199
return render_to_string(template_name, request=request, context=context)
198200

199201

0 commit comments

Comments
 (0)