Skip to content

Commit

Permalink
Move *PackageFinder to the new 'discovery' module
Browse files Browse the repository at this point in the history
Following up the discussion in pypa#2887 and pypa#2329, it seems that setuptools
is moving towards more automatic discovery features.

PackageFinder and PEP420PackageFinder are fundamental pieces of this
puzzle and grouping together them togheter with the code implementing these
new discovery features make a lot of sense.
  • Loading branch information
abravalheri committed Mar 5, 2022
1 parent e8ad85b commit 1ee9625
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 81 deletions.
82 changes: 1 addition & 81 deletions setuptools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Extensions to the 'distutils' for large or complex distributions"""

from fnmatch import fnmatchcase
import functools
import os
import re
Expand All @@ -9,14 +8,14 @@

import distutils.core
from distutils.errors import DistutilsOptionError
from distutils.util import convert_path

from ._deprecation_warning import SetuptoolsDeprecationWarning

import setuptools.version
from setuptools.extension import Extension
from setuptools.dist import Distribution
from setuptools.depends import Require
from setuptools.discovery import PackageFinder, PEP420PackageFinder
from . import monkey
from . import logging

Expand All @@ -37,85 +36,6 @@
bootstrap_install_from = None


class PackageFinder:
"""
Generate a list of all Python packages found within a directory
"""

@classmethod
def find(cls, where='.', exclude=(), include=('*',)):
"""Return a list all Python packages found within directory 'where'
'where' is the root directory which will be searched for packages. It
should be supplied as a "cross-platform" (i.e. URL-style) path; it will
be converted to the appropriate local path syntax.
'exclude' is a sequence of package names to exclude; '*' can be used
as a wildcard in the names, such that 'foo.*' will exclude all
subpackages of 'foo' (but not 'foo' itself).
'include' is a sequence of package names to include. If it's
specified, only the named packages will be included. If it's not
specified, all found packages will be included. 'include' can contain
shell style wildcard patterns just like 'exclude'.
"""

return list(
cls._find_packages_iter(
convert_path(where),
cls._build_filter('ez_setup', '*__pycache__', *exclude),
cls._build_filter(*include),
)
)

@classmethod
def _find_packages_iter(cls, where, exclude, include):
"""
All the packages found in 'where' that pass the 'include' filter, but
not the 'exclude' filter.
"""
for root, dirs, files in os.walk(where, followlinks=True):
# Copy dirs to iterate over it, then empty dirs.
all_dirs = dirs[:]
dirs[:] = []

for dir in all_dirs:
full_path = os.path.join(root, dir)
rel_path = os.path.relpath(full_path, where)
package = rel_path.replace(os.path.sep, '.')

# Skip directory trees that are not valid packages
if '.' in dir or not cls._looks_like_package(full_path):
continue

# Should this package be included?
if include(package) and not exclude(package):
yield package

# Keep searching subdirectories, as there may be more packages
# down there, even if the parent was excluded.
dirs.append(dir)

@staticmethod
def _looks_like_package(path):
"""Does a directory look like a package?"""
return os.path.isfile(os.path.join(path, '__init__.py'))

@staticmethod
def _build_filter(*patterns):
"""
Given a list of patterns, return a callable that will be true only if
the input matches at least one of the patterns.
"""
return lambda name: any(fnmatchcase(name, pat=pat) for pat in patterns)


class PEP420PackageFinder(PackageFinder):
@staticmethod
def _looks_like_package(path):
return True


find_packages = PackageFinder.find
find_namespace_packages = PEP420PackageFinder.find

Expand Down
89 changes: 89 additions & 0 deletions setuptools/discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Automatic discovery for Python modules and packages for inclusion in the
distribution.
"""

import os
from fnmatch import fnmatchcase

import _distutils_hack.override # noqa: F401

from distutils.util import convert_path


class PackageFinder:
"""
Generate a list of all Python packages found within a directory
"""

@classmethod
def find(cls, where='.', exclude=(), include=('*',)):
"""Return a list all Python packages found within directory 'where'
'where' is the root directory which will be searched for packages. It
should be supplied as a "cross-platform" (i.e. URL-style) path; it will
be converted to the appropriate local path syntax.
'exclude' is a sequence of package names to exclude; '*' can be used
as a wildcard in the names, such that 'foo.*' will exclude all
subpackages of 'foo' (but not 'foo' itself).
'include' is a sequence of package names to include. If it's
specified, only the named packages will be included. If it's not
specified, all found packages will be included. 'include' can contain
shell style wildcard patterns just like 'exclude'.
"""

return list(
cls._find_packages_iter(
convert_path(where),
cls._build_filter('ez_setup', '*__pycache__', *exclude),
cls._build_filter(*include),
)
)

@classmethod
def _find_packages_iter(cls, where, exclude, include):
"""
All the packages found in 'where' that pass the 'include' filter, but
not the 'exclude' filter.
"""
for root, dirs, files in os.walk(where, followlinks=True):
# Copy dirs to iterate over it, then empty dirs.
all_dirs = dirs[:]
dirs[:] = []

for dir in all_dirs:
full_path = os.path.join(root, dir)
rel_path = os.path.relpath(full_path, where)
package = rel_path.replace(os.path.sep, '.')

# Skip directory trees that are not valid packages
if '.' in dir or not cls._looks_like_package(full_path):
continue

# Should this package be included?
if include(package) and not exclude(package):
yield package

# Keep searching subdirectories, as there may be more packages
# down there, even if the parent was excluded.
dirs.append(dir)

@staticmethod
def _looks_like_package(path):
"""Does a directory look like a package?"""
return os.path.isfile(os.path.join(path, '__init__.py'))

@staticmethod
def _build_filter(*patterns):
"""
Given a list of patterns, return a callable that will be true only if
the input matches at least one of the patterns.
"""
return lambda name: any(fnmatchcase(name, pat=pat) for pat in patterns)


class PEP420PackageFinder(PackageFinder):
@staticmethod
def _looks_like_package(path):
return True

0 comments on commit 1ee9625

Please sign in to comment.