test_loaders.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import os.path
  4. import sys
  5. import tempfile
  6. import types
  7. import unittest
  8. from contextlib import contextmanager
  9. from django.template import Context, TemplateDoesNotExist
  10. from django.template.engine import Engine
  11. from django.test import SimpleTestCase, ignore_warnings, override_settings
  12. from django.utils import six
  13. from django.utils.deprecation import RemovedInDjango20Warning
  14. from django.utils.functional import lazystr
  15. from .utils import TEMPLATE_DIR
  16. try:
  17. import pkg_resources
  18. except ImportError:
  19. pkg_resources = None
  20. class CachedLoaderTests(SimpleTestCase):
  21. def setUp(self):
  22. self.engine = Engine(
  23. dirs=[TEMPLATE_DIR],
  24. loaders=[
  25. ('django.template.loaders.cached.Loader', [
  26. 'django.template.loaders.filesystem.Loader',
  27. ]),
  28. ],
  29. )
  30. def test_get_template(self):
  31. template = self.engine.get_template('index.html')
  32. self.assertEqual(template.origin.name, os.path.join(TEMPLATE_DIR, 'index.html'))
  33. self.assertEqual(template.origin.template_name, 'index.html')
  34. self.assertEqual(template.origin.loader, self.engine.template_loaders[0].loaders[0])
  35. cache = self.engine.template_loaders[0].get_template_cache
  36. self.assertEqual(cache['index.html'], template)
  37. # Run a second time from cache
  38. template = self.engine.get_template('index.html')
  39. self.assertEqual(template.origin.name, os.path.join(TEMPLATE_DIR, 'index.html'))
  40. self.assertEqual(template.origin.template_name, 'index.html')
  41. self.assertEqual(template.origin.loader, self.engine.template_loaders[0].loaders[0])
  42. def test_get_template_missing_debug_off(self):
  43. """
  44. With template debugging disabled, the raw TemplateDoesNotExist class
  45. should be cached when a template is missing. See ticket #26306 and
  46. docstrings in the cached loader for details.
  47. """
  48. self.engine.debug = False
  49. with self.assertRaises(TemplateDoesNotExist):
  50. self.engine.get_template('prod-template-missing.html')
  51. e = self.engine.template_loaders[0].get_template_cache['prod-template-missing.html']
  52. self.assertEqual(e, TemplateDoesNotExist)
  53. def test_get_template_missing_debug_on(self):
  54. """
  55. With template debugging enabled, a TemplateDoesNotExist instance
  56. should be cached when a template is missing.
  57. """
  58. self.engine.debug = True
  59. with self.assertRaises(TemplateDoesNotExist):
  60. self.engine.get_template('debug-template-missing.html')
  61. e = self.engine.template_loaders[0].get_template_cache['debug-template-missing.html']
  62. self.assertIsInstance(e, TemplateDoesNotExist)
  63. self.assertEqual(e.args[0], 'debug-template-missing.html')
  64. @unittest.skipIf(six.PY2, "Python 2 doesn't set extra exception attributes")
  65. def test_cached_exception_no_traceback(self):
  66. """
  67. When a TemplateDoesNotExist instance is cached, the cached instance
  68. should not contain the __traceback__, __context__, or __cause__
  69. attributes that Python sets when raising exceptions.
  70. """
  71. self.engine.debug = True
  72. with self.assertRaises(TemplateDoesNotExist):
  73. self.engine.get_template('no-traceback-in-cache.html')
  74. e = self.engine.template_loaders[0].get_template_cache['no-traceback-in-cache.html']
  75. error_msg = "Cached TemplateDoesNotExist must not have been thrown."
  76. self.assertIsNone(e.__traceback__, error_msg)
  77. self.assertIsNone(e.__context__, error_msg)
  78. self.assertIsNone(e.__cause__, error_msg)
  79. @ignore_warnings(category=RemovedInDjango20Warning)
  80. def test_load_template(self):
  81. loader = self.engine.template_loaders[0]
  82. template, origin = loader.load_template('index.html')
  83. self.assertEqual(template.origin.template_name, 'index.html')
  84. cache = self.engine.template_loaders[0].template_cache
  85. self.assertEqual(cache['index.html'][0], template)
  86. # Run a second time from cache
  87. loader = self.engine.template_loaders[0]
  88. source, name = loader.load_template('index.html')
  89. self.assertEqual(template.origin.template_name, 'index.html')
  90. @ignore_warnings(category=RemovedInDjango20Warning)
  91. def test_load_template_missing(self):
  92. """
  93. #19949 -- TemplateDoesNotExist exceptions should be cached.
  94. """
  95. loader = self.engine.template_loaders[0]
  96. self.assertNotIn('missing.html', loader.template_cache)
  97. with self.assertRaises(TemplateDoesNotExist):
  98. loader.load_template("missing.html")
  99. self.assertEqual(
  100. loader.template_cache["missing.html"],
  101. TemplateDoesNotExist,
  102. "Cached loader failed to cache the TemplateDoesNotExist exception",
  103. )
  104. @ignore_warnings(category=RemovedInDjango20Warning)
  105. def test_load_nonexistent_cached_template(self):
  106. loader = self.engine.template_loaders[0]
  107. template_name = 'nonexistent.html'
  108. # fill the template cache
  109. with self.assertRaises(TemplateDoesNotExist):
  110. loader.find_template(template_name)
  111. with self.assertRaisesMessage(TemplateDoesNotExist, template_name):
  112. loader.get_template(template_name)
  113. def test_templatedir_caching(self):
  114. """
  115. #13573 -- Template directories should be part of the cache key.
  116. """
  117. # Retrieve a template specifying a template directory to check
  118. t1, name = self.engine.find_template('test.html', (os.path.join(TEMPLATE_DIR, 'first'),))
  119. # Now retrieve the same template name, but from a different directory
  120. t2, name = self.engine.find_template('test.html', (os.path.join(TEMPLATE_DIR, 'second'),))
  121. # The two templates should not have the same content
  122. self.assertNotEqual(t1.render(Context({})), t2.render(Context({})))
  123. def test_template_name_leading_dash_caching(self):
  124. """
  125. #26536 -- A leading dash in a template name shouldn't be stripped
  126. from its cache key.
  127. """
  128. self.assertEqual(self.engine.template_loaders[0].cache_key('-template.html', []), '-template.html')
  129. def test_template_name_lazy_string(self):
  130. """
  131. #26603 -- A template name specified as a lazy string should be forced
  132. to text before computing its cache key.
  133. """
  134. self.assertEqual(self.engine.template_loaders[0].cache_key(lazystr('template.html'), []), 'template.html')
  135. @unittest.skipUnless(pkg_resources, 'setuptools is not installed')
  136. class EggLoaderTests(SimpleTestCase):
  137. @contextmanager
  138. def create_egg(self, name, resources):
  139. """
  140. Creates a mock egg with a list of resources.
  141. name: The name of the module.
  142. resources: A dictionary of template names mapped to file-like objects.
  143. """
  144. if six.PY2:
  145. name = name.encode('utf-8')
  146. class MockLoader(object):
  147. pass
  148. class MockProvider(pkg_resources.NullProvider):
  149. def __init__(self, module):
  150. pkg_resources.NullProvider.__init__(self, module)
  151. self.module = module
  152. def _has(self, path):
  153. return path in self.module._resources
  154. def _isdir(self, path):
  155. return False
  156. def get_resource_stream(self, manager, resource_name):
  157. return self.module._resources[resource_name]
  158. def _get(self, path):
  159. return self.module._resources[path].read()
  160. def _fn(self, base, resource_name):
  161. return os.path.normcase(resource_name)
  162. egg = types.ModuleType(name)
  163. egg.__loader__ = MockLoader()
  164. egg.__path__ = ['/some/bogus/path/']
  165. egg.__file__ = '/some/bogus/path/__init__.pyc'
  166. egg._resources = resources
  167. sys.modules[name] = egg
  168. pkg_resources._provider_factories[MockLoader] = MockProvider
  169. try:
  170. yield
  171. finally:
  172. del sys.modules[name]
  173. del pkg_resources._provider_factories[MockLoader]
  174. @classmethod
  175. @ignore_warnings(category=RemovedInDjango20Warning)
  176. def setUpClass(cls):
  177. cls.engine = Engine(loaders=[
  178. 'django.template.loaders.eggs.Loader',
  179. ])
  180. cls.loader = cls.engine.template_loaders[0]
  181. super(EggLoaderTests, cls).setUpClass()
  182. def test_get_template(self):
  183. templates = {
  184. os.path.normcase('templates/y.html'): six.StringIO("y"),
  185. }
  186. with self.create_egg('egg', templates):
  187. with override_settings(INSTALLED_APPS=['egg']):
  188. template = self.engine.get_template("y.html")
  189. self.assertEqual(template.origin.name, 'egg:egg:templates/y.html')
  190. self.assertEqual(template.origin.template_name, 'y.html')
  191. self.assertEqual(template.origin.loader, self.engine.template_loaders[0])
  192. output = template.render(Context({}))
  193. self.assertEqual(output, "y")
  194. @ignore_warnings(category=RemovedInDjango20Warning)
  195. def test_load_template_source(self):
  196. loader = self.engine.template_loaders[0]
  197. templates = {
  198. os.path.normcase('templates/y.html'): six.StringIO("y"),
  199. }
  200. with self.create_egg('egg', templates):
  201. with override_settings(INSTALLED_APPS=['egg']):
  202. source, name = loader.load_template_source('y.html')
  203. self.assertEqual(source.strip(), 'y')
  204. self.assertEqual(name, 'egg:egg:templates/y.html')
  205. def test_non_existing(self):
  206. """
  207. Template loading fails if the template is not in the egg.
  208. """
  209. with self.create_egg('egg', {}):
  210. with override_settings(INSTALLED_APPS=['egg']):
  211. with self.assertRaises(TemplateDoesNotExist):
  212. self.engine.get_template('not-existing.html')
  213. def test_not_installed(self):
  214. """
  215. Template loading fails if the egg is not in INSTALLED_APPS.
  216. """
  217. templates = {
  218. os.path.normcase('templates/y.html'): six.StringIO("y"),
  219. }
  220. with self.create_egg('egg', templates):
  221. with self.assertRaises(TemplateDoesNotExist):
  222. self.engine.get_template('y.html')
  223. class FileSystemLoaderTests(SimpleTestCase):
  224. @classmethod
  225. def setUpClass(cls):
  226. cls.engine = Engine(dirs=[TEMPLATE_DIR])
  227. super(FileSystemLoaderTests, cls).setUpClass()
  228. @contextmanager
  229. def set_dirs(self, dirs):
  230. original_dirs = self.engine.dirs
  231. self.engine.dirs = dirs
  232. try:
  233. yield
  234. finally:
  235. self.engine.dirs = original_dirs
  236. @contextmanager
  237. def source_checker(self, dirs):
  238. loader = self.engine.template_loaders[0]
  239. def check_sources(path, expected_sources):
  240. expected_sources = [os.path.abspath(s) for s in expected_sources]
  241. self.assertEqual(
  242. [origin.name for origin in loader.get_template_sources(path)],
  243. expected_sources,
  244. )
  245. with self.set_dirs(dirs):
  246. yield check_sources
  247. def test_get_template(self):
  248. template = self.engine.get_template('index.html')
  249. self.assertEqual(template.origin.name, os.path.join(TEMPLATE_DIR, 'index.html'))
  250. self.assertEqual(template.origin.template_name, 'index.html')
  251. self.assertEqual(template.origin.loader, self.engine.template_loaders[0])
  252. self.assertEqual(template.origin.loader_name, 'django.template.loaders.filesystem.Loader')
  253. @ignore_warnings(category=RemovedInDjango20Warning)
  254. def test_load_template_source(self):
  255. loader = self.engine.template_loaders[0]
  256. source, name = loader.load_template_source('index.html')
  257. self.assertEqual(source.strip(), 'index')
  258. self.assertEqual(name, os.path.join(TEMPLATE_DIR, 'index.html'))
  259. def test_directory_security(self):
  260. with self.source_checker(['/dir1', '/dir2']) as check_sources:
  261. check_sources('index.html', ['/dir1/index.html', '/dir2/index.html'])
  262. check_sources('/etc/passwd', [])
  263. check_sources('etc/passwd', ['/dir1/etc/passwd', '/dir2/etc/passwd'])
  264. check_sources('../etc/passwd', [])
  265. check_sources('../../../etc/passwd', [])
  266. check_sources('/dir1/index.html', ['/dir1/index.html'])
  267. check_sources('../dir2/index.html', ['/dir2/index.html'])
  268. check_sources('/dir1blah', [])
  269. check_sources('../dir1blah', [])
  270. def test_unicode_template_name(self):
  271. with self.source_checker(['/dir1', '/dir2']) as check_sources:
  272. # UTF-8 bytestrings are permitted.
  273. check_sources(b'\xc3\x85ngstr\xc3\xb6m', ['/dir1/Ångström', '/dir2/Ångström'])
  274. # Unicode strings are permitted.
  275. check_sources('Ångström', ['/dir1/Ångström', '/dir2/Ångström'])
  276. def test_utf8_bytestring(self):
  277. """
  278. Invalid UTF-8 encoding in bytestrings should raise a useful error
  279. """
  280. engine = Engine()
  281. loader = engine.template_loaders[0]
  282. with self.assertRaises(UnicodeDecodeError):
  283. list(loader.get_template_sources(b'\xc3\xc3', ['/dir1']))
  284. def test_unicode_dir_name(self):
  285. with self.source_checker([b'/Stra\xc3\x9fe']) as check_sources:
  286. check_sources('Ångström', ['/Straße/Ångström'])
  287. check_sources(b'\xc3\x85ngstr\xc3\xb6m', ['/Straße/Ångström'])
  288. @unittest.skipUnless(
  289. os.path.normcase('/TEST') == os.path.normpath('/test'),
  290. "This test only runs on case-sensitive file systems.",
  291. )
  292. def test_case_sensitivity(self):
  293. with self.source_checker(['/dir1', '/DIR2']) as check_sources:
  294. check_sources('index.html', ['/dir1/index.html', '/DIR2/index.html'])
  295. check_sources('/DIR1/index.HTML', ['/DIR1/index.HTML'])
  296. def test_file_does_not_exist(self):
  297. with self.assertRaises(TemplateDoesNotExist):
  298. self.engine.get_template('doesnotexist.html')
  299. @unittest.skipIf(
  300. sys.platform == 'win32',
  301. "Python on Windows doesn't have working os.chmod().",
  302. )
  303. def test_permissions_error(self):
  304. with tempfile.NamedTemporaryFile() as tmpfile:
  305. tmpdir = os.path.dirname(tmpfile.name)
  306. tmppath = os.path.join(tmpdir, tmpfile.name)
  307. os.chmod(tmppath, 0o0222)
  308. with self.set_dirs([tmpdir]):
  309. with self.assertRaisesMessage(IOError, 'Permission denied'):
  310. self.engine.get_template(tmpfile.name)
  311. def test_notafile_error(self):
  312. with self.assertRaises(IOError):
  313. self.engine.get_template('first')
  314. class AppDirectoriesLoaderTests(SimpleTestCase):
  315. @classmethod
  316. def setUpClass(cls):
  317. cls.engine = Engine(
  318. loaders=['django.template.loaders.app_directories.Loader'],
  319. )
  320. super(AppDirectoriesLoaderTests, cls).setUpClass()
  321. @override_settings(INSTALLED_APPS=['template_tests'])
  322. def test_get_template(self):
  323. template = self.engine.get_template('index.html')
  324. self.assertEqual(template.origin.name, os.path.join(TEMPLATE_DIR, 'index.html'))
  325. self.assertEqual(template.origin.template_name, 'index.html')
  326. self.assertEqual(template.origin.loader, self.engine.template_loaders[0])
  327. @ignore_warnings(category=RemovedInDjango20Warning)
  328. @override_settings(INSTALLED_APPS=['template_tests'])
  329. def test_load_template_source(self):
  330. loader = self.engine.template_loaders[0]
  331. source, name = loader.load_template_source('index.html')
  332. self.assertEqual(source.strip(), 'index')
  333. self.assertEqual(name, os.path.join(TEMPLATE_DIR, 'index.html'))
  334. @override_settings(INSTALLED_APPS=[])
  335. def test_not_installed(self):
  336. with self.assertRaises(TemplateDoesNotExist):
  337. self.engine.get_template('index.html')
  338. class LocmemLoaderTests(SimpleTestCase):
  339. @classmethod
  340. def setUpClass(cls):
  341. cls.engine = Engine(
  342. loaders=[('django.template.loaders.locmem.Loader', {
  343. 'index.html': 'index',
  344. })],
  345. )
  346. super(LocmemLoaderTests, cls).setUpClass()
  347. def test_get_template(self):
  348. template = self.engine.get_template('index.html')
  349. self.assertEqual(template.origin.name, 'index.html')
  350. self.assertEqual(template.origin.template_name, 'index.html')
  351. self.assertEqual(template.origin.loader, self.engine.template_loaders[0])
  352. @ignore_warnings(category=RemovedInDjango20Warning)
  353. def test_load_template_source(self):
  354. loader = self.engine.template_loaders[0]
  355. source, name = loader.load_template_source('index.html')
  356. self.assertEqual(source.strip(), 'index')
  357. self.assertEqual(name, 'index.html')