_element.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. # -*- test-case-name: twisted.web.test.test_template -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. import itertools
  5. from typing import (
  6. TYPE_CHECKING,
  7. Any,
  8. Callable,
  9. List,
  10. Optional,
  11. TypeVar,
  12. Union,
  13. overload,
  14. )
  15. from zope.interface import implementer
  16. from twisted.web.error import (
  17. MissingRenderMethod,
  18. MissingTemplateLoader,
  19. UnexposedMethodError,
  20. )
  21. from twisted.web.iweb import IRenderable, IRequest, ITemplateLoader
  22. if TYPE_CHECKING:
  23. from twisted.web.template import Flattenable, Tag
  24. T = TypeVar("T")
  25. _Tc = TypeVar("_Tc", bound=Callable[..., object])
  26. class Expose:
  27. """
  28. Helper for exposing methods for various uses using a simple decorator-style
  29. callable.
  30. Instances of this class can be called with one or more functions as
  31. positional arguments. The names of these functions will be added to a list
  32. on the class object of which they are methods.
  33. """
  34. def __call__(self, f: _Tc, /, *funcObjs: Callable[..., object]) -> _Tc:
  35. """
  36. Add one or more functions to the set of exposed functions.
  37. This is a way to declare something about a class definition, similar to
  38. L{zope.interface.implementer}. Use it like this::
  39. magic = Expose('perform extra magic')
  40. class Foo(Bar):
  41. def twiddle(self, x, y):
  42. ...
  43. def frob(self, a, b):
  44. ...
  45. magic(twiddle, frob)
  46. Later you can query the object::
  47. aFoo = Foo()
  48. magic.get(aFoo, 'twiddle')(x=1, y=2)
  49. The call to C{get} will fail if the name it is given has not been
  50. exposed using C{magic}.
  51. @param funcObjs: One or more function objects which will be exposed to
  52. the client.
  53. @return: The first of C{funcObjs}.
  54. """
  55. for fObj in itertools.chain([f], funcObjs):
  56. exposedThrough: List[Expose] = getattr(fObj, "exposedThrough", [])
  57. exposedThrough.append(self)
  58. setattr(fObj, "exposedThrough", exposedThrough)
  59. return f
  60. _nodefault = object()
  61. @overload
  62. def get(self, instance: object, methodName: str) -> Callable[..., Any]:
  63. ...
  64. @overload
  65. def get(
  66. self, instance: object, methodName: str, default: T
  67. ) -> Union[Callable[..., Any], T]:
  68. ...
  69. def get(
  70. self, instance: object, methodName: str, default: object = _nodefault
  71. ) -> object:
  72. """
  73. Retrieve an exposed method with the given name from the given instance.
  74. @raise UnexposedMethodError: Raised if C{default} is not specified and
  75. there is no exposed method with the given name.
  76. @return: A callable object for the named method assigned to the given
  77. instance.
  78. """
  79. method = getattr(instance, methodName, None)
  80. exposedThrough = getattr(method, "exposedThrough", [])
  81. if self not in exposedThrough:
  82. if default is self._nodefault:
  83. raise UnexposedMethodError(self, methodName)
  84. return default
  85. return method
  86. def exposer(thunk: Callable[..., object]) -> Expose:
  87. expose = Expose()
  88. expose.__doc__ = thunk.__doc__
  89. return expose
  90. @exposer
  91. def renderer() -> None:
  92. """
  93. Decorate with L{renderer} to use methods as template render directives.
  94. For example::
  95. class Foo(Element):
  96. @renderer
  97. def twiddle(self, request, tag):
  98. return tag('Hello, world.')
  99. <div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
  100. <span t:render="twiddle" />
  101. </div>
  102. Will result in this final output::
  103. <div>
  104. <span>Hello, world.</span>
  105. </div>
  106. """
  107. @implementer(IRenderable)
  108. class Element:
  109. """
  110. Base for classes which can render part of a page.
  111. An Element is a renderer that can be embedded in a stan document and can
  112. hook its template (from the loader) up to render methods.
  113. An Element might be used to encapsulate the rendering of a complex piece of
  114. data which is to be displayed in multiple different contexts. The Element
  115. allows the rendering logic to be easily re-used in different ways.
  116. Element returns render methods which are registered using
  117. L{twisted.web._element.renderer}. For example::
  118. class Menu(Element):
  119. @renderer
  120. def items(self, request, tag):
  121. ....
  122. Render methods are invoked with two arguments: first, the
  123. L{twisted.web.http.Request} being served and second, the tag object which
  124. "invoked" the render method.
  125. @ivar loader: The factory which will be used to load documents to
  126. return from C{render}.
  127. """
  128. loader: Optional[ITemplateLoader] = None
  129. def __init__(self, loader: Optional[ITemplateLoader] = None):
  130. if loader is not None:
  131. self.loader = loader
  132. def lookupRenderMethod(
  133. self, name: str
  134. ) -> Callable[[Optional[IRequest], "Tag"], "Flattenable"]:
  135. """
  136. Look up and return the named render method.
  137. """
  138. method = renderer.get(self, name, None)
  139. if method is None:
  140. raise MissingRenderMethod(self, name)
  141. return method
  142. def render(self, request: Optional[IRequest]) -> "Flattenable":
  143. """
  144. Implement L{IRenderable} to allow one L{Element} to be embedded in
  145. another's template or rendering output.
  146. (This will simply load the template from the C{loader}; when used in a
  147. template, the flattening engine will keep track of this object
  148. separately as the object to lookup renderers on and call
  149. L{Element.renderer} to look them up. The resulting object from this
  150. method is not directly associated with this L{Element}.)
  151. """
  152. loader = self.loader
  153. if loader is None:
  154. raise MissingTemplateLoader(self)
  155. return loader.load()