Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1312.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Introduce find_packages_ns() to find PEP 420 namespace packages.
64 changes: 64 additions & 0 deletions docs/setuptools.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`_
Expand Down Expand Up @@ -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
=========================
Expand Down
11 changes: 10 additions & 1 deletion setuptools/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,11 +19,15 @@

__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
Expand Down Expand Up @@ -111,6 +117,9 @@ def _looks_like_package(path):

find_packages = PackageFinder.find

if PY3:
find_packages_ns = PEP420PackageFinder.find


def _install_setup_requires(attrs):
# Note: do not use `setuptools.Distribution` directly, as
Expand Down
26 changes: 16 additions & 10 deletions setuptools/tests/test_find_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@

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"
Expand Down Expand Up @@ -153,30 +154,35 @@ def test_symlinked_packages_are_included(self):
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'])