PackageLoader doesn't depend on setuptools
diff --git a/CHANGES.rst b/CHANGES.rst
index 381f78e..477721f 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -47,6 +47,8 @@
 -   Fix behavior of ``loop`` control variables such as ``length`` and
     ``revindex0`` when looping over a generator. :issue:`459, 751, 794`,
     :pr:`993`
+-   ``PackageLoader`` doesn't depend on setuptools or pkg_resources.
+    :issue:`970`
 
 
 Version 2.10.3
diff --git a/jinja2/loaders.py b/jinja2/loaders.py
index 4c79793..eb5a879 100644
--- a/jinja2/loaders.py
+++ b/jinja2/loaders.py
@@ -9,6 +9,7 @@
     :license: BSD, see LICENSE for more details.
 """
 import os
+import pkgutil
 import sys
 import weakref
 from types import ModuleType
@@ -203,66 +204,110 @@
 
 
 class PackageLoader(BaseLoader):
-    """Load templates from python eggs or packages.  It is constructed with
-    the name of the python package and the path to the templates in that
-    package::
+    """Load templates from a directory in a Python package.
 
-        loader = PackageLoader('mypackage', 'views')
+    :param package_name: Import name of the package that contains the
+        template directory.
+    :param package_path: Directory within the imported package that
+        contains the templates.
+    :param encoding: Encoding of template files.
 
-    If the package path is not given, ``'templates'`` is assumed.
+    The following example looks up templates in the ``pages`` directory
+    within the ``project.ui`` package.
 
-    Per default the template encoding is ``'utf-8'`` which can be changed
-    by setting the `encoding` parameter to something else.  Due to the nature
-    of eggs it's only possible to reload templates if the package was loaded
-    from the file system and not a zip file.
+    .. code-block:: python
+
+        loader = PackageLoader("project.ui", "pages")
+
+    Only packages installed as directories (standard pip behavior) or
+    zip/egg files (less common) are supported. The Python API for
+    introspecting data in packages is too limited to support other
+    installation methods the way this loader requires.
+
+    .. versionchanged:: 2.11.0
+        No longer uses ``setuptools`` as a dependency.
     """
 
-    def __init__(self, package_name, package_path='templates',
-                 encoding='utf-8'):
-        from pkg_resources import DefaultProvider, ResourceManager, \
-                                  get_provider
-        provider = get_provider(package_name)
-        self.encoding = encoding
-        self.manager = ResourceManager()
-        self.filesystem_bound = isinstance(provider, DefaultProvider)
-        self.provider = provider
+    def __init__(self, package_name, package_path="templates", encoding="utf-8"):
+        if package_path == os.path.curdir:
+            package_path = ""
+        elif package_path[:2] == os.path.curdir + os.path.sep:
+            package_path = package_path[2:]
+
+        package_path = os.path.normpath(package_path)
+
+        self.package_name = package_name
         self.package_path = package_path
+        self.encoding = encoding
+
+        self._loader = pkgutil.get_loader(package_name)
+        # Zip loader's archive attribute points at the zip.
+        self._archive = getattr(self._loader, "archive", None)
+        self._template_root = os.path.join(
+            os.path.dirname(self._loader.get_filename(package_name)), package_path
+        ).rstrip(os.path.sep)
 
     def get_source(self, environment, template):
-        pieces = split_template_path(template)
-        p = '/'.join((self.package_path,) + tuple(pieces))
-        if not self.provider.has_resource(p):
-            raise TemplateNotFound(template)
+        p = os.path.join(self._template_root, *split_template_path(template))
 
-        filename = uptodate = None
-        if self.filesystem_bound:
-            filename = self.provider.get_resource_filename(self.manager, p)
-            mtime = path.getmtime(filename)
-            def uptodate():
-                try:
-                    return path.getmtime(filename) == mtime
-                except OSError:
-                    return False
+        if self._archive is None:
+            # Package is a directory.
+            if not os.path.isfile(p):
+                raise TemplateNotFound(template)
 
-        source = self.provider.get_resource_string(self.manager, p)
-        return source.decode(self.encoding), filename, uptodate
+            with open(p, "rb") as f:
+                source = f.read()
+
+            mtime = os.path.getmtime(p)
+
+            def up_to_date():
+                return os.path.isfile(p) and os.path.getmtime(p) == mtime
+        else:
+            # Package is a zip file.
+            try:
+                source = self._loader.get_data(p)
+            except OSError:
+                raise TemplateNotFound(template)
+
+            # Could use the zip's mtime for all template mtimes, but
+            # would need to safely reload the module if it's out of
+            # date, so just report it as always current.
+            up_to_date = None
+
+        return source.decode(self.encoding), p, up_to_date
 
     def list_templates(self):
-        path = self.package_path
-        if path[:2] == './':
-            path = path[2:]
-        elif path == '.':
-            path = ''
-        offset = len(path)
         results = []
-        def _walk(path):
-            for filename in self.provider.resource_listdir(path):
-                fullname = path + '/' + filename
-                if self.provider.resource_isdir(fullname):
-                    _walk(fullname)
-                else:
-                    results.append(fullname[offset:].lstrip('/'))
-        _walk(path)
+
+        if self._archive is None:
+            # Package is a directory.
+            offset = len(self._template_root)
+
+            for dirpath, _, filenames in os.walk(self._template_root):
+                dirpath = dirpath[offset:].lstrip(os.path.sep)
+                results.extend(
+                    os.path.join(dirpath, name).replace(os.path.sep, "/")
+                    for name in filenames
+                )
+        else:
+            if not hasattr(self._loader, "_files"):
+                raise TypeError(
+                    "This zip import does not have the required"
+                    " metadata to list templates."
+                )
+
+            # Package is a zip file.
+            prefix = (
+                self._template_root[len(self._archive):].lstrip(os.path.sep)
+                + os.path.sep
+            )
+            offset = len(prefix)
+
+            for name in self._loader._files.keys():
+                # Find names under the templates directory that aren't directories.
+                if name.startswith(prefix) and name[-1] != os.path.sep:
+                    results.append(name[offset:].replace(os.path.sep, "/"))
+
         results.sort()
         return results
 
diff --git a/tests/res/package.zip b/tests/res/package.zip
new file mode 100644
index 0000000..d4c9ce9
--- /dev/null
+++ b/tests/res/package.zip
Binary files differ
diff --git a/tests/test_loader.py b/tests/test_loader.py
index cb30ebe..3445ba8 100644
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -9,21 +9,24 @@
     :license: BSD, see LICENSE for more details.
 """
 import os
+import shutil
 import sys
 import tempfile
-import shutil
-import pytest
 import weakref
 
-from jinja2 import Environment, loaders
-from jinja2._compat import PYPY, PY2
-from jinja2.loaders import split_template_path
+import pytest
+
+from jinja2 import Environment
+from jinja2 import loaders
+from jinja2 import PackageLoader
+from jinja2._compat import PY2
+from jinja2._compat import PYPY
 from jinja2.exceptions import TemplateNotFound
+from jinja2.loaders import split_template_path
 
 
 @pytest.mark.loaders
 class TestLoaders(object):
-
     def test_dict_loader(self, dict_loader):
         env = Environment(loader=dict_loader)
         tmpl = env.get_template('justdict.html')
@@ -54,7 +57,6 @@
         # This would raise NotADirectoryError if "t2/foo" wasn't skipped.
         e.get_template("foo/test.html")
 
-
     def test_choice_loader(self, choice_loader):
         env = Environment(loader=choice_loader)
         tmpl = env.get_template('justdict.html')
@@ -243,3 +245,52 @@
         assert tmpl1.render() == 'BAR'
         tmpl2 = self.mod_env.get_template('DICT/test.html')
         assert tmpl2.render() == 'DICT_TEMPLATE'
+
+
+@pytest.fixture()
+def package_dir_loader(monkeypatch):
+    monkeypatch.syspath_prepend(os.path.dirname(__file__))
+    return PackageLoader("res")
+
+
+@pytest.mark.parametrize(
+    ("template", "expect"), [("foo/test.html", "FOO"), ("test.html", "BAR")]
+)
+def test_package_dir_source(package_dir_loader, template, expect):
+    source, name, up_to_date = package_dir_loader.get_source(None, template)
+    assert source.rstrip() == expect
+    assert name.endswith(os.path.join(*split_template_path(template)))
+    assert up_to_date()
+
+
+def test_package_dir_list(package_dir_loader):
+    templates = package_dir_loader.list_templates()
+    assert "foo/test.html" in templates
+    assert "test.html" in templates
+
+
+@pytest.fixture()
+def package_zip_loader(monkeypatch):
+    monkeypatch.syspath_prepend(
+        os.path.join(os.path.dirname(__file__), "res", "package.zip")
+    )
+    return PackageLoader("t_pack")
+
+
+@pytest.mark.parametrize(
+    ("template", "expect"), [("foo/test.html", "FOO"), ("test.html", "BAR")]
+)
+def test_package_zip_source(package_zip_loader, template, expect):
+    source, name, up_to_date = package_zip_loader.get_source(None, template)
+    assert source.rstrip() == expect
+    assert name.endswith(os.path.join(*split_template_path(template)))
+    assert up_to_date is None
+
+
+@pytest.mark.xfail(
+    PYPY,
+    reason="PyPy's zipimporter doesn't have a _files attribute.",
+    raises=TypeError,
+)
+def test_package_zip_list(package_zip_loader):
+    assert package_zip_loader.list_templates() == ["foo/test.html", "test.html"]