Repository: pypa/setuptools

Issue:
Refactor rename find_packages_ns to find_namespace_packages
As @pganssle mentioned in #1420, the export `find_packages_ns` is rather obscure and should be renamed to `find_namespace_packages`. This also goes in line with the new `find_namespace:` directive when using `setup.cfg` that was introduced by the PR #1420 as requested by #1419.
Support find_packages_ns from setup.cfg
Now that #1312 has been merged, closing #97, we support the `find_packages_ns` function in `setup.py`. We should also enable this functionality in `setup.cfg`.

I suggest adding a `find_namespace:` directive. The current `find:` directive is implemented [here](https://github.com/pypa/setuptools/blob/cca86c7f1d4040834c3265ccecdd9e21b4036df5/setuptools/config.py#L512), so I think that would probably be the place to add support for  namespace packages.

Brought to my attention by @agronholm  in https://github.com/pypa/setuptools/issues/97#issuecomment-404195899


Discussion:

I can create a PR for this as well if you like.

Resolving patch:
diff --git a/setuptools/__init__.py b/setuptools/__init__.py
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -26,7 +26,7 @@
 ]
 
 if PY3:
-  __all__.append('find_packages_ns')
+  __all__.append('find_namespace_packages')
 
 __version__ = setuptools.version.__version__
 
@@ -118,7 +118,7 @@ def _looks_like_package(path):
 find_packages = PackageFinder.find
 
 if PY3:
-  find_packages_ns = PEP420PackageFinder.find
+  find_namespace_packages = PEP420PackageFinder.find
 
 
 def _install_setup_requires(attrs):
diff --git a/setuptools/config.py b/setuptools/config.py
--- a/setuptools/config.py
+++ b/setuptools/config.py
@@ -8,7 +8,7 @@
 
 from distutils.errors import DistutilsOptionError, DistutilsFileError
 from setuptools.extern.packaging.version import LegacyVersion, parse
-from setuptools.extern.six import string_types
+from setuptools.extern.six import string_types, PY3
 
 
 __metaclass__ = type
@@ -515,16 +515,24 @@ def _parse_packages(self, value):
         :param value:
         :rtype: list
         """
-        find_directive = 'find:'
+        find_directives = ['find:', 'find_namespace:']
+        trimmed_value = value.strip()
 
-        if not value.startswith(find_directive):
+        if not trimmed_value in find_directives:
             return self._parse_list(value)
 
+        findns = trimmed_value == find_directives[1]
+        if findns and not PY3:
+            raise DistutilsOptionError('find_namespace: directive is unsupported on Python < 3.3')
+
         # Read function arguments from a dedicated section.
         find_kwargs = self.parse_section_packages__find(
             self.sections.get('packages.find', {}))
 
-        from setuptools import find_packages
+        if findns:
+            from setuptools import find_namespace_packages as find_packages
+        else:
+            from setuptools import find_packages
 
         return find_packages(**find_kwargs)
 


Test patch:
diff --git a/setuptools/tests/__init__.py b/setuptools/tests/__init__.py
--- a/setuptools/tests/__init__.py
+++ b/setuptools/tests/__init__.py
@@ -2,5 +2,17 @@
 
 import pytest
 
+from setuptools.extern.six import PY2, PY3
+
+
+__all__ = [
+  'fail_on_ascii', 'py2_only', 'py3_only'
+]
+
+
 is_ascii = locale.getpreferredencoding() == 'ANSI_X3.4-1968'
 fail_on_ascii = pytest.mark.xfail(is_ascii, reason="Test fails in this locale")
+
+
+py2_only = pytest.mark.skipif(not PY2, reason="Test runs on Python 2 only")
+py3_only = pytest.mark.skipif(not PY3, reason="Test runs on Python 3 only")
diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py
--- a/setuptools/tests/test_config.py
+++ b/setuptools/tests/test_config.py
@@ -4,18 +4,20 @@
 from mock import patch
 from setuptools.dist import Distribution, _Distribution
 from setuptools.config import ConfigHandler, read_configuration
-
+from . import py2_only, py3_only
 
 class ErrConfigHandler(ConfigHandler):
     """Erroneous handler. Fails to implement required methods."""
 
 
-def make_package_dir(name, base_dir):
+def make_package_dir(name, base_dir, ns=False):
     dir_package = base_dir
     for dir_name in name.split('/'):
         dir_package = dir_package.mkdir(dir_name)
-    init_file = dir_package.join('__init__.py')
-    init_file.write('')
+    init_file = None
+    if not ns:
+      init_file = dir_package.join('__init__.py')
+      init_file.write('')
     return dir_package, init_file
 
 
@@ -596,6 +598,60 @@ def test_find_directive(self, tmpdir):
             assert set(dist.packages) == set(
                 ['fake_package',  'fake_package.sub_two'])
 
+    @py2_only
+    def test_find_namespace_directive_fails_on_py2(self, tmpdir):
+        dir_package, config = fake_env(
+            tmpdir,
+            '[options]\n'
+            'packages = find_namespace:\n'
+        )
+
+        with pytest.raises(DistutilsOptionError):
+            with get_dist(tmpdir) as dist:
+                dist.parse_config_files()
+
+    @py3_only
+    def test_find_namespace_directive(self, tmpdir):
+        dir_package, config = fake_env(
+            tmpdir,
+            '[options]\n'
+            'packages = find_namespace:\n'
+        )
+
+        dir_sub_one, _ = make_package_dir('sub_one', dir_package)
+        dir_sub_two, _ = make_package_dir('sub_two', dir_package, ns=True)
+
+        with get_dist(tmpdir) as dist:
+            assert set(dist.packages) == { 
+                'fake_package', 'fake_package.sub_two', 'fake_package.sub_one'
+            }
+
+        config.write(
+            '[options]\n'
+            'packages = find_namespace:\n'
+            '\n'
+            '[options.packages.find]\n'
+            'where = .\n'
+            'include =\n'
+            '    fake_package.sub_one\n'
+            '    two\n'
+        )
+        with get_dist(tmpdir) as dist:
+            assert dist.packages == ['fake_package.sub_one']
+
+        config.write(
+            '[options]\n'
+            'packages = find_namespace:\n'
+            '\n'
+            '[options.packages.find]\n'
+            'exclude =\n'
+            '    fake_package.sub_one\n'
+        )
+        with get_dist(tmpdir) as dist:
+            assert set(dist.packages) == {
+                'fake_package',  'fake_package.sub_two'
+            }
+
     def test_extras_require(self, tmpdir):
         fake_env(
             tmpdir,
diff --git a/setuptools/tests/test_find_packages.py b/setuptools/tests/test_find_packages.py
--- a/setuptools/tests/test_find_packages.py
+++ b/setuptools/tests/test_find_packages.py
@@ -7,12 +7,12 @@
 
 import pytest
 
+from . import py3_only
+
 from setuptools.extern.six import PY3
 from setuptools import find_packages
-
-py3_only = pytest.mark.xfail(not PY3, reason="Test runs on Python 3 only")
 if PY3:
-  from setuptools import find_packages_ns
+  from setuptools import find_namespace_packages
 
 # modeled after CPython's test.support.can_symlink
 
@@ -156,26 +156,26 @@ def _assert_packages(self, actual, expected):
 
     @py3_only
     def test_pep420_ns_package(self):
-        packages = find_packages_ns(
+        packages = find_namespace_packages(
             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_packages_ns(
+        packages = find_namespace_packages(
             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_packages_ns(self.dist_dir)
+        packages = find_namespace_packages(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_packages_ns(
+        packages = find_namespace_packages(
             self.dist_dir, exclude=['docs', 'pkg.subpkg.assets'])
         self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
 
@@ -183,6 +183,6 @@ def test_regular_package_with_nested_pep420_ns_packages(self):
     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_packages_ns(self.dist_dir)
+        packages = find_namespace_packages(self.dist_dir)
         self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])