Coverage for /Users/davegaeddert/Development/dropseed/plain/plain/plain/templates/jinja/defaults.py: 75%

1 import functools

2 from importlib import import_module

3 from pathlib import Path

4

5 from jinja2 import Environment, StrictUndefined

6

7 from plain.packages import packages

8 from plain.runtime import settings

9 from plain.utils.module_loading import import_string, module_has_submodule

10

11 from .filters import default_filters

12 from .globals import default_globals

13

14

15@functools.lru_cache

16 def _get_app_template_dirs():

17 """

18 Return an iterable of paths of directories to load app templates from.

19

20 dirname is the name of the subdirectory containing templates inside

21 installed applications.

22 """

23 dirname = "templates"

24 template_dirs = [

25 Path(package_config.path) / dirname

26 for package_config in packages.get_package_configs()

27 if package_config.path and (Path(package_config.path) / dirname).is_dir()

28 ]

29 # Immutable return value because it will be cached and shared by callers.

30 return tuple(template_dirs)

31

32

33 def _get_installed_extensions() -> tuple[list, dict, dict]:

34 """Automatically load extensions, globals, filters from INSTALLED_PACKAGES jinja module and root jinja module"""

35 extensions = []

36 globals = {}

37 filters = {}

38

39 for package_config in packages.get_package_configs():

40 if module_has_submodule(package_config.module, "jinja"):

41 module = import_module(f"{package_config.name}.jinja")

42 else:

43 continue

44

45 if hasattr(module, "extensions"):

46 extensions.extend(module.extensions)

47

48 if hasattr(module, "globals"):

49 globals.update(module.globals)

50

51 if hasattr(module, "filters"):

52 filters.update(module.filters)

53

54 try:

55 import jinja

56

57 if hasattr(jinja, "extensions"):

58 extensions.extend(jinja.extensions)

59

60 if hasattr(jinja, "globals"):

61 globals.update(jinja.globals)

62

63 if hasattr(jinja, "filters"):

64 filters.update(jinja.filters)

65 except ImportError:

66 pass

67

68 return extensions, globals, filters

69

70

71 def finalize_callable_error(obj):

72 """Prevent direct rendering of a callable (likely just forgotten ()) by raising a TypeError"""

73 if callable(obj):

74 raise TypeError(f"{obj} is callable, did you forget parentheses?")

75

76 # TODO find a way to prevent <object representation> from being rendered

77 # if obj.__class__.__str__ is object.__str__:

78 # raise TypeError(f"{obj} does not have a __str__ method")

79

80 return obj

81

82

83 def get_template_dirs():

84 jinja_templates = Path(__file__).parent / "templates"

85 app_templates = settings.path.parent / "templates"

86 return (jinja_templates, app_templates) + _get_app_template_dirs()

87

88

89 def create_default_environment(include_packages=True, **environment_kwargs):

90 """

91 This default jinja environment, also used by the error rendering and internal views so

92 customization needs to happen by using this function, not settings that hook in internally.

93 """

94 loader = import_string(settings.JINJA_LOADER)(get_template_dirs())

95 kwargs = {

96 "loader": loader,

97 "autoescape": True,

98 "auto_reload": settings.DEBUG,

99 "undefined": StrictUndefined,

100 "finalize": finalize_callable_error,

101 "extensions": ["jinja2.ext.loopcontrols", "jinja2.ext.debug"],

102 }

103 kwargs.update(**environment_kwargs)

104 env = Environment(**kwargs)

105

106 # Load the top-level defaults

107 env.globals.update(default_globals)

108 env.filters.update(default_filters)

109

110 if include_packages:

111 app_extensions, app_globals, app_filters = _get_installed_extensions()

112

113 for extension in app_extensions:

114 env.add_extension(extension)

115

116 env.globals.update(app_globals)

117 env.filters.update(app_filters)

118

119 return env