Repository: pallets/jinja (python)

Issue:
Add searchpath to TemplateNotFound exception message
This was from https://github.com/apache/airflow/issues/23333 where trying to load a template that doesn't exist in the searchpath raises `TemplateNotFound` exception. In this case an absolute path is used which causes issue over concatenation but including `searchpath` might be a useful addition so that user can know under the paths under which the template was searched for and will be helpful during debugging.

https://github.com/pallets/jinja/blob/6f79daafe868b8ef20baecdcb45d12299529ed2e/src/jinja2/loaders.py#L218

Current exception :

```
jinja2.exceptions.TemplateNotFound: /tmp/a/python_template_exts.txt
```

Proposed exception to include searchpath :

```
jinja2.exceptions.TemplateNotFound: /tmp/a/python_template_exts.txt not found in search path ['/opt/airflow/tests/operators', '/']
```

Pull request description:
`FileSystemLoader` include paths in error
<!--
Before opening a PR, open a ticket describing the issue or feature the PR will address. Follow the steps in CONTRIBUTING.rst.

Replace this comment with a description of the change. Describe how it addresses the linked ticket.
-->

<!--
Link to relevant issues or previous PRs, one per line. Use "fixes" to automatically close an issue.
-->

- fixes #1661

<!--
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to each box below.

If only docs were changed, these aren't relevant and can be removed.
-->

Checklist:

- [ ] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [ ] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [ ] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 384c6122..cd2a4ef0 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -35,6 +35,8 @@ Unreleased
 -   Fix `copy`/`pickle` support for the internal ``missing`` object.
     :issue:`2027`
 -   ``Environment.overlay(enable_async)`` is applied correctly. :pr:`2061`
+-   The error message from ``FileSystemLoader`` includes the paths that were
+    searched. :issue:`1661`
 
 
 Version 3.1.4
diff --git a/src/jinja2/loaders.py b/src/jinja2/loaders.py
index d2373e5e..35799584 100644
--- a/src/jinja2/loaders.py
+++ b/src/jinja2/loaders.py
@@ -204,7 +204,12 @@ class FileSystemLoader(BaseLoader):
             if os.path.isfile(filename):
                 break
         else:
-            raise TemplateNotFound(template)
+            plural = "path" if len(self.searchpath) == 1 else "paths"
+            paths_str = ", ".join(repr(p) for p in self.searchpath)
+            raise TemplateNotFound(
+                template,
+                f"{template!r} not found in search {plural}: {paths_str}",
+            )
 
         with open(filename, encoding=self.encoding) as f:
             contents = f.read()


Test patch:
diff --git a/tests/test_loader.py b/tests/test_loader.py
index e0cff672..5a4e1a9d 100644
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -179,6 +179,24 @@ class TestFileSystemLoader:
         t = e.get_template("foo/test.html")
         assert t.filename == str(self.searchpath / "foo" / "test.html")
 
+    def test_error_includes_paths(self, env, filesystem_loader):
+        env.loader = filesystem_loader
+
+        with pytest.raises(TemplateNotFound) as info:
+            env.get_template("missing")
+
+        e_str = str(info.value)
+        assert e_str.startswith("'missing' not found in search path: ")
+
+        filesystem_loader.searchpath.append("other")
+
+        with pytest.raises(TemplateNotFound) as info:
+            env.get_template("missing")
+
+        e_str = str(info.value)
+        assert e_str.startswith("'missing' not found in search paths: ")
+        assert ", 'other'" in e_str
+
 
 class TestModuleLoader:
     archive = None