Repository: pypa/setuptools (python)

Issue:
find_packages() doesn't find PEP 420 packages
Originally reported by: **gwideman (Bitbucket: [gwideman](http://bitbucket.org/gwideman), GitHub: [gwideman](http://github.com/gwideman))**

---

```
#!python

setup(...
    packages=find_packages(..)
    )
```

on the developer machine will fail to find packages that lack a `__init__.py` file, as is allowed in Python 3.3. However, such packages listed explicitly: packages=['mypkg'] do appear to get included and later installed.

Note: When testing this, before each test be sure to delete all generated metadata, including that which setup may previously have placed in the original source directory, as it seems that setup may use metadata created on a previous run in order to include files.

This is part of a general problem reported in issue #83, but I've logged it separately as it's specifically about setuptools.

---
- Bitbucket: https://bitbucket.org/pypa/setuptools/issue/97


Pull request description:
fix #97 PEP420: find_packages()
make find_packages() find also PEP420 namespace packages when supported


Patch:
diff --git a/changelog.d/1312.change.rst b/changelog.d/1312.change.rst
new file mode 100644
index 000000000..9314f9e14
--- /dev/null
+++ b/changelog.d/1312.change.rst
@@ -0,0 +1,1 @@
+Introduce find_packages_ns() to find PEP 420 namespace packages.
diff --git a/docs/setuptools.txt b/docs/setuptools.txt
index c82dc5113..9a088254c 100644
--- a/docs/setuptools.txt
+++ b/docs/setuptools.txt
@@ -59,6 +59,9 @@ Feature Highlights:
 * Create extensible applications and frameworks that automatically discover
   extensions, using simple "entry points" declared in a project's setup script.
 
+* Full support for PEP 420 via ``find_packages_ns()``, which is also backwards
+  compatible to the existing ``find_packages()`` for Python >= 3.3.
+
 .. contents:: **Table of Contents**
 
 .. _ez_setup.py: `bootstrap module`_
@@ -462,6 +465,67 @@ argument in your setup script.  Especially since it frees you from having to
 remember to modify your setup script whenever your project grows additional
 top-level packages or subpackages.
 
+``find_packages_ns()``
+----------------------
+In Python 3.3+, ``setuptools`` also provides the ``find_packages_ns`` variant
+of ``find_packages``, which has the same function signature as
+``find_packages``, but works with `PEP 420`_ compliant implicit namespace
+packages. Here is a minimal setup script using ``find_packages_ns``::
+
+    from setuptools import setup, find_packages_ns
+    setup(
+        name="HelloWorld",
+        version="0.1",
+        packages=find_packages_ns(),
+    )
+
+
+Keep in mind that according to PEP 420, you may have to either re-organize your
+codebase a bit or define a few exclusions, as the definition of an implicit
+namespace package is quite lenient, so for a project organized like so::
+
+
+    ├── namespace
+    │   └── mypackage
+    │       ├── __init__.py
+    │       └── mod1.py
+    ├── setup.py
+    └── tests
+        └── test_mod1.py
+
+A naive ``find_packages_ns()`` would install both ``namespace.mypackage`` and a
+top-level package called ``tests``! One way to avoid this problem is to use the
+``include`` keyword to whitelist the packages to include, like so::
+
+    from setuptools import setup, find_packages_ns
+
+    setup(
+        name="namespace.mypackage",
+        version="0.1",
+        packages=find_packages_ns(include=['namespace.*'])
+    )
+
+Another option is to use the "src" layout, where all package code is placed in
+the ``src`` directory, like so::
+
+
+    ├── setup.py
+    ├── src
+    │   └── namespace
+    │       └── mypackage
+    │           ├── __init__.py
+    │           └── mod1.py
+    └── tests
+        └── test_mod1.py
+
+With this layout, the package directory is specified as ``src``, as such::
+
+    setup(name="namespace.mypackage",
+          version="0.1",
+          package_dir={'': 'src'},
+          packages=find_packages_ns(where='src'))
+
+.. _PEP 420: https://www.python.org/dev/peps/pep-0420/
 
 Automatic Script Creation
 =========================
diff --git a/setuptools/__init__.py b/setuptools/__init__.py
index ce55ec351..e705f0d1a 100644
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -1,12 +1,14 @@
 """Extensions to the 'distutils' for large or complex distributions"""
 
 import os
+import sys
 import functools
 import distutils.core
 import distutils.filelist
 from distutils.util import convert_path
 from fnmatch import fnmatchcase
 
+from setuptools.extern.six import PY3
 from setuptools.extern.six.moves import filter, map
 
 import setuptools.version
@@ -17,11 +19,15 @@ from . import monkey
 
 __metaclass__ = type
 
+
 __all__ = [
     'setup', 'Distribution', 'Feature', 'Command', 'Extension', 'Require',
-    'find_packages',
+    'find_packages'
 ]
 
+if PY3:
+  __all__.append('find_packages_ns')
+
 __version__ = setuptools.version.__version__
 
 bootstrap_install_from = None
@@ -111,6 +117,9 @@ class PEP420PackageFinder(PackageFinder):
 
 find_packages = PackageFinder.find
 
+if PY3:
+  find_packages_ns = PEP420PackageFinder.find
+
 
 def _install_setup_requires(attrs):
     # Note: do not use `setuptools.Distribution` directly, as


Test patch:
diff --git a/setuptools/tests/test_find_packages.py b/setuptools/tests/test_find_packages.py
index a6023de9d..02ae5a946 100644
--- a/setuptools/tests/test_find_packages.py
+++ b/setuptools/tests/test_find_packages.py
@@ -7,14 +7,15 @@ import platform
 
 import pytest
 
-import setuptools
+from setuptools.extern.six import PY3
 from setuptools import find_packages
 
-find_420_packages = setuptools.PEP420PackageFinder.find
+py3_only = pytest.mark.xfail(not PY3, reason="Test runs on Python 3 only")
+if PY3:
+  from setuptools import find_packages_ns
 
 # modeled after CPython's test.support.can_symlink
 
-
 def can_symlink():
     TESTFN = tempfile.mktemp()
     symlink_path = TESTFN + "can_symlink"
@@ -153,30 +154,35 @@ class TestFindPackages:
     def _assert_packages(self, actual, expected):
         assert set(actual) == set(expected)
 
+    @py3_only
     def test_pep420_ns_package(self):
-        packages = find_420_packages(
+        packages = find_packages_ns(
             self.dist_dir, include=['pkg*'], exclude=['pkg.subpkg.assets'])
         self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
 
+    @py3_only
     def test_pep420_ns_package_no_includes(self):
-        packages = find_420_packages(
+        packages = find_packages_ns(
             self.dist_dir, exclude=['pkg.subpkg.assets'])
         self._assert_packages(packages, ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg'])
 
+    @py3_only
     def test_pep420_ns_package_no_includes_or_excludes(self):
-        packages = find_420_packages(self.dist_dir)
-        expected = [
-            'docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg', 'pkg.subpkg.assets']
+        packages = find_packages_ns(self.dist_dir)
+        expected = ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg', 'pkg.subpkg.assets']
         self._assert_packages(packages, expected)
 
+    @py3_only
     def test_regular_package_with_nested_pep420_ns_packages(self):
         self._touch('__init__.py', self.pkg_dir)
-        packages = find_420_packages(
+        packages = find_packages_ns(
             self.dist_dir, exclude=['docs', 'pkg.subpkg.assets'])
         self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
 
+    @py3_only
     def test_pep420_ns_package_no_non_package_dirs(self):
         shutil.rmtree(self.docs_dir)
         shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets'))
-        packages = find_420_packages(self.dist_dir)
+        packages = find_packages_ns(self.dist_dir)
         self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
+