Repository: pypa/setuptools (python)

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.

Pull request description:
Rename find_namepaces_ns to find_namespace_packages
## Summary of changes

Rename find_namepaces_ns to find_namespace_packages

Closes #1421

### Pull Request Checklist
- [x] Changes have tests
- [x] News fragment added in changelog.d. See [documentation](http://setuptools.readthedocs.io/en/latest/developer-guide.html#making-a-pull-request) for details


Patch:
diff --git a/changelog.d/1312.change.rst b/changelog.d/1312.change.rst
index 9314f9e14..e2542f98f 100644
--- a/changelog.d/1312.change.rst
+++ b/changelog.d/1312.change.rst
@@ -1,1 +1,1 @@
-Introduce find_packages_ns() to find PEP 420 namespace packages.
+Introduce find_namespace_packages() to find PEP 420 namespace packages.
diff --git a/changelog.d/1420.change.rst b/changelog.d/1420.change.rst
new file mode 100644
index 000000000..6967adccb
--- /dev/null
+++ b/changelog.d/1420.change.rst
@@ -0,0 +1,1 @@
+Add find_namespace: directive to config parser
diff --git a/docs/setuptools.txt b/docs/setuptools.txt
index 0660e14d5..89f45bd85 100644
--- a/docs/setuptools.txt
+++ b/docs/setuptools.txt
@@ -57,7 +57,7 @@ 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
+* Full support for PEP 420 via ``find_namespace_packages()``, which is also backwards
   compatible to the existing ``find_packages()`` for Python >= 3.3.
 
 .. contents:: **Table of Contents**
@@ -462,18 +462,18 @@ 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
+``find_namespace_packages()``
+-----------------------------
+In Python 3.3+, ``setuptools`` also provides the ``find_namespace_packages`` 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``::
+packages. Here is a minimal setup script using ``find_namespace_packages``::
 
-    from setuptools import setup, find_packages_ns
+    from setuptools import setup, find_namespace_packages
     setup(
         name="HelloWorld",
         version="0.1",
-        packages=find_packages_ns(),
+        packages=find_namespace_packages(),
     )
 
 
@@ -490,16 +490,16 @@ namespace package is quite lenient, so for a project organized like so::
     └── tests
         └── test_mod1.py
 
-A naive ``find_packages_ns()`` would install both ``namespace.mypackage`` and a
+A naive ``find_namespace_packages()`` 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
+    from setuptools import setup, find_namespace_packages
 
     setup(
         name="namespace.mypackage",
         version="0.1",
-        packages=find_packages_ns(include=['namespace.*'])
+        packages=find_namespace_packages(include=['namespace.*'])
     )
 
 Another option is to use the "src" layout, where all package code is placed in
@@ -520,7 +520,7 @@ 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'))
+          packages=find_namespace_packages(where='src'))
 
 .. _PEP 420: https://www.python.org/dev/peps/pep-0420/
 
@@ -2389,8 +2389,8 @@ Metadata and options are set in the config sections of the same name.
 * In some cases, complex values can be provided in dedicated subsections for
   clarity.
 
-* Some keys allow ``file:``, ``attr:``, and ``find:`` directives in order to
-  cover common usecases.
+* Some keys allow ``file:``, ``attr:``, and ``find:`` and ``find_namespace:`` directives in
+  order to cover common usecases.
 
 * Unknown keys are ignored.
 
@@ -2479,7 +2479,7 @@ eager_resources          list-comma
 dependency_links         list-comma
 tests_require            list-semi
 include_package_data     bool
-packages                 find:, list-comma
+packages                 find:, find_namespace:, list-comma
 package_dir              dict
 package_data             section
 exclude_package_data     section
@@ -2489,10 +2489,13 @@ py_modules               list-comma
 
 .. note::
 
-    **packages** - The ``find:`` directive can be further configured
+    **packages** - The ``find:`` and ``find_namespace:`` directive can be further configured
     in a dedicated subsection ``options.packages.find``. This subsection
-    accepts the same keys as the `setuptools.find` function:
+    accepts the same keys as the `setuptools.find_packages` and the
+    `setuptools.find_namespace_packages` function:
     ``where``, ``include``, and ``exclude``.
+    
+    **find_namespace directive** - The ``find_namespace:`` directive is supported since Python >=3.3.
 
 
 Configuration API
diff --git a/setuptools/__init__.py b/setuptools/__init__.py
index e705f0d1a..54309b57f 100644
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -26,7 +26,7 @@ __all__ = [
 ]
 
 if PY3:
-  __all__.append('find_packages_ns')
+  __all__.append('find_namespace_packages')
 
 __version__ = setuptools.version.__version__
 
@@ -118,7 +118,7 @@ class PEP420PackageFinder(PackageFinder):
 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
index 5f908cf12..0da3dbc9c 100644
--- a/setuptools/config.py
+++ b/setuptools/config.py
@@ -8,7 +8,7 @@ from importlib import import_module
 
 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 @@ class ConfigOptionsHandler(ConfigHandler):
         :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
index 54dd7d2b2..5f4a1c295 100644
--- a/setuptools/tests/__init__.py
+++ b/setuptools/tests/__init__.py
@@ -2,5 +2,17 @@ import locale
 
 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
index 19b376336..acf221549 100644
--- a/setuptools/tests/test_config.py
+++ b/setuptools/tests/test_config.py
@@ -4,18 +4,20 @@ from distutils.errors import DistutilsOptionError, DistutilsFileError
 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 @@ class TestOptions:
             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
index 02ae5a946..b08f91c7d 100644
--- a/setuptools/tests/test_find_packages.py
+++ b/setuptools/tests/test_find_packages.py
@@ -7,12 +7,12 @@ import platform
 
 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 @@ class TestFindPackages:
 
     @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 @@ class TestFindPackages:
     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'])