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

61 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-16 22:04 -0500

1import functools 

2from importlib import import_module 

3from pathlib import Path 

4 

5from jinja2 import Environment, StrictUndefined 

6 

7from plain.packages import packages 

8from plain.runtime import settings 

9from plain.utils.module_loading import import_string, module_has_submodule 

10 

11from .filters import default_filters 

12from .globals import default_globals 

13 

14 

15@functools.lru_cache 

16def _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 

33def _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 

71def 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 

83def 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 

89def 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