summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnderson Bravalheri <andersonbravalheri@gmail.com>2022-06-21 10:06:03 +0100
committerGitHub <noreply@github.com>2022-06-21 10:06:03 +0100
commitcdd12f324185d3b4ca07597897e7aec9388cdfa9 (patch)
tree5f7a67a63da2ac61f65fdd550a72e68565ae16ff
parentecdeb225804010e2f68c7ec5d72e39364873324d (diff)
parentd019f498ecd5ad45e58e95df64afd9730090df6a (diff)
downloadpython-setuptools-git-cdd12f324185d3b4ca07597897e7aec9388cdfa9.tar.gz
Add get_output_mapping to build_py and build_ext (#3392)
-rw-r--r--changelog.d/3392.change.rst5
-rw-r--r--setuptools/command/build_ext.py116
-rw-r--r--setuptools/command/build_py.py61
-rw-r--r--setuptools/extension.py3
-rw-r--r--setuptools/tests/test_build_ext.py92
-rw-r--r--setuptools/tests/test_build_py.py242
6 files changed, 423 insertions, 96 deletions
diff --git a/changelog.d/3392.change.rst b/changelog.d/3392.change.rst
new file mode 100644
index 00000000..8ae7fd90
--- /dev/null
+++ b/changelog.d/3392.change.rst
@@ -0,0 +1,5 @@
+Exposed ``get_output_mapping()`` from ``build_py`` and ``build_ext``
+subcommands. This interface is reserved for the use of ``setuptools``
+Extensions and third part packages are explicitly disallowed to calling it.
+However, any implementation overwriting ``build_py`` or ``build_ext`` are
+required to honour this interface.
diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py
index 1719d17a..31ef47bf 100644
--- a/setuptools/command/build_ext.py
+++ b/setuptools/command/build_ext.py
@@ -2,13 +2,16 @@ import os
import sys
import itertools
from importlib.machinery import EXTENSION_SUFFIXES
+from importlib.util import cache_from_source as _compiled_file_name
+from typing import Dict, Iterator, List, Tuple
+
from distutils.command.build_ext import build_ext as _du_build_ext
from distutils.ccompiler import new_compiler
from distutils.sysconfig import customize_compiler, get_config_var
-from distutils.errors import DistutilsError
from distutils import log
-from setuptools.extension import Library
+from setuptools.errors import BaseError
+from setuptools.extension import Extension, Library
try:
# Attempt to use Cython for building extensions, if available
@@ -72,6 +75,9 @@ def get_abi3_suffix():
class build_ext(_build_ext):
+ editable_mode: bool = False
+ inplace: bool = False
+
def run(self):
"""Build extensions in build directory, then copy if --inplace"""
old_inplace, self.inplace = self.inplace, 0
@@ -80,24 +86,61 @@ class build_ext(_build_ext):
if old_inplace:
self.copy_extensions_to_source()
+ def _get_inplace_equivalent(self, build_py, ext: Extension) -> Tuple[str, str]:
+ fullname = self.get_ext_fullname(ext.name)
+ filename = self.get_ext_filename(fullname)
+ modpath = fullname.split('.')
+ package = '.'.join(modpath[:-1])
+ package_dir = build_py.get_package_dir(package)
+ inplace_file = os.path.join(package_dir, os.path.basename(filename))
+ regular_file = os.path.join(self.build_lib, filename)
+ return (inplace_file, regular_file)
+
def copy_extensions_to_source(self):
build_py = self.get_finalized_command('build_py')
for ext in self.extensions:
- fullname = self.get_ext_fullname(ext.name)
- filename = self.get_ext_filename(fullname)
- modpath = fullname.split('.')
- package = '.'.join(modpath[:-1])
- package_dir = build_py.get_package_dir(package)
- dest_filename = os.path.join(package_dir,
- os.path.basename(filename))
- src_filename = os.path.join(self.build_lib, filename)
+ inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext)
# Always copy, even if source is older than destination, to ensure
# that the right extensions for the current Python/platform are
# used.
- build_py.copy_file(src_filename, dest_filename)
+ build_py.copy_file(regular_file, inplace_file)
+
+ if ext._needs_stub:
+ inplace_stub = self._get_equivalent_stub(ext, inplace_file)
+ self._write_stub_file(inplace_stub, ext, compile=True)
+ # Always compile stub and remove the original (leave the cache behind)
+ # (this behaviour was observed in previous iterations of the code)
+
+ def _get_equivalent_stub(self, ext: Extension, output_file: str) -> str:
+ dir_ = os.path.dirname(output_file)
+ _, _, name = ext.name.rpartition(".")
+ return f"{os.path.join(dir_, name)}.py"
+
+ def _get_output_mapping(self) -> Iterator[Tuple[str, str]]:
+ if not self.inplace:
+ return
+
+ build_py = self.get_finalized_command('build_py')
+ opt = self.get_finalized_command('install_lib').optimize or ""
+
+ for ext in self.extensions:
+ inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext)
+ yield (regular_file, inplace_file)
+
if ext._needs_stub:
- self.write_stub(package_dir or os.curdir, ext, True)
+ # This version of `build_ext` always builds artifacts in another dir,
+ # when "inplace=True" is given it just copies them back.
+ # This is done in the `copy_extensions_to_source` function, which
+ # always compile stub files via `_compile_and_remove_stub`.
+ # At the end of the process, a `.pyc` stub file is created without the
+ # corresponding `.py`.
+
+ inplace_stub = self._get_equivalent_stub(ext, inplace_file)
+ regular_stub = self._get_equivalent_stub(ext, regular_file)
+ inplace_cache = _compiled_file_name(inplace_stub, optimization=opt)
+ output_cache = _compiled_file_name(regular_stub, optimization=opt)
+ yield (output_cache, inplace_cache)
def get_ext_filename(self, fullname):
so_ext = os.getenv('SETUPTOOLS_EXT_SUFFIX')
@@ -127,6 +170,7 @@ class build_ext(_build_ext):
self.shlib_compiler = None
self.shlibs = []
self.ext_map = {}
+ self.editable_mode = False
def finalize_options(self):
_build_ext.finalize_options(self)
@@ -157,6 +201,9 @@ class build_ext(_build_ext):
if ltd and use_stubs and os.curdir not in ext.runtime_library_dirs:
ext.runtime_library_dirs.append(os.curdir)
+ if self.editable_mode:
+ self.inplace = True
+
def setup_shlib_compiler(self):
compiler = self.shlib_compiler = new_compiler(
compiler=self.compiler, dry_run=self.dry_run, force=self.force
@@ -197,8 +244,8 @@ class build_ext(_build_ext):
self.compiler = self.shlib_compiler
_build_ext.build_extension(self, ext)
if ext._needs_stub:
- cmd = self.get_finalized_command('build_py').build_lib
- self.write_stub(cmd, ext)
+ build_lib = self.get_finalized_command('build_py').build_lib
+ self.write_stub(build_lib, ext)
finally:
self.compiler = _compiler
@@ -211,8 +258,15 @@ class build_ext(_build_ext):
pkg = '.'.join(ext._full_name.split('.')[:-1] + [''])
return any(pkg + libname in libnames for libname in ext.libraries)
- def get_outputs(self):
- return _build_ext.get_outputs(self) + self.__get_stubs_outputs()
+ def get_outputs(self) -> List[str]:
+ if self.inplace:
+ return list(self.get_output_mapping().keys())
+ return sorted(_build_ext.get_outputs(self) + self.__get_stubs_outputs())
+
+ def get_output_mapping(self) -> Dict[str, str]:
+ """See :class:`setuptools.commands.build.SubCommand`"""
+ mapping = self._get_output_mapping()
+ return dict(sorted(mapping, key=lambda x: x[0]))
def __get_stubs_outputs(self):
# assemble the base name for each extension that needs a stub
@@ -232,12 +286,13 @@ class build_ext(_build_ext):
yield '.pyo'
def write_stub(self, output_dir, ext, compile=False):
- log.info("writing stub loader for %s to %s", ext._full_name,
- output_dir)
- stub_file = (os.path.join(output_dir, *ext._full_name.split('.')) +
- '.py')
+ stub_file = os.path.join(output_dir, *ext._full_name.split('.')) + '.py'
+ self._write_stub_file(stub_file, ext, compile)
+
+ def _write_stub_file(self, stub_file: str, ext: Extension, compile=False):
+ log.info("writing stub loader for %s to %s", ext._full_name, stub_file)
if compile and os.path.exists(stub_file):
- raise DistutilsError(stub_file + " already exists! Please delete.")
+ raise BaseError(stub_file + " already exists! Please delete.")
if not self.dry_run:
f = open(stub_file, 'w')
f.write(
@@ -270,16 +325,19 @@ class build_ext(_build_ext):
)
f.close()
if compile:
- from distutils.util import byte_compile
+ self._compile_and_remove_stub(stub_file)
+
+ def _compile_and_remove_stub(self, stub_file: str):
+ from distutils.util import byte_compile
- byte_compile([stub_file], optimize=0,
+ byte_compile([stub_file], optimize=0,
+ force=True, dry_run=self.dry_run)
+ optimize = self.get_finalized_command('install_lib').optimize
+ if optimize > 0:
+ byte_compile([stub_file], optimize=optimize,
force=True, dry_run=self.dry_run)
- optimize = self.get_finalized_command('install_lib').optimize
- if optimize > 0:
- byte_compile([stub_file], optimize=optimize,
- force=True, dry_run=self.dry_run)
- if os.path.exists(stub_file) and not self.dry_run:
- os.unlink(stub_file)
+ if os.path.exists(stub_file) and not self.dry_run:
+ os.unlink(stub_file)
if use_stubs or os.name == 'nt':
diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py
index 9575cdf8..a2a6fe2c 100644
--- a/setuptools/command/build_py.py
+++ b/setuptools/command/build_py.py
@@ -11,6 +11,8 @@ import itertools
import stat
import warnings
from pathlib import Path
+from typing import Dict, Iterator, List, Optional, Tuple
+
from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
from setuptools.extern.more_itertools import unique_everseen
@@ -28,6 +30,8 @@ class build_py(orig.build_py):
Also, this version of the 'build_py' command allows you to specify both
'py_modules' and 'packages' in the same setup operation.
"""
+ editable_mode: bool = False
+ existing_egg_info_dir: Optional[str] = None #: Private API, internal use only.
def finalize_options(self):
orig.build_py.finalize_options(self)
@@ -50,7 +54,8 @@ class build_py(orig.build_py):
def run(self):
"""Build modules, packages, and copy data files to build directory"""
- if not self.py_modules and not self.packages:
+ # if self.editable_mode or not (self.py_modules and self.packages):
+ if not (self.py_modules or self.packages) or self.editable_mode:
return
if self.py_modules:
@@ -123,16 +128,41 @@ class build_py(orig.build_py):
)
return self.exclude_data_files(package, src_dir, files)
- def build_package_data(self):
- """Copy data files into build directory"""
+ def get_outputs(self, include_bytecode=1) -> List[str]:
+ """See :class:`setuptools.commands.build.SubCommand`"""
+ if self.editable_mode:
+ return list(self.get_output_mapping().keys())
+ return super().get_outputs(include_bytecode)
+
+ def get_output_mapping(self) -> Dict[str, str]:
+ """See :class:`setuptools.commands.build.SubCommand`"""
+ mapping = itertools.chain(
+ self._get_package_data_output_mapping(),
+ self._get_module_mapping(),
+ )
+ return dict(sorted(mapping, key=lambda x: x[0]))
+
+ def _get_module_mapping(self) -> Iterator[Tuple[str, str]]:
+ """Iterate over all modules producing (dest, src) pairs."""
+ for (package, module, module_file) in self.find_all_modules():
+ package = package.split('.')
+ filename = self.get_module_outfile(self.build_lib, package, module)
+ yield (filename, module_file)
+
+ def _get_package_data_output_mapping(self) -> Iterator[Tuple[str, str]]:
+ """Iterate over package data producing (dest, src) pairs."""
for package, src_dir, build_dir, filenames in self.data_files:
for filename in filenames:
target = os.path.join(build_dir, filename)
- self.mkpath(os.path.dirname(target))
srcfile = os.path.join(src_dir, filename)
- outf, copied = self.copy_file(srcfile, target)
- make_writable(target)
- srcfile = os.path.abspath(srcfile)
+ yield (target, srcfile)
+
+ def build_package_data(self):
+ """Copy data files into build directory"""
+ for target, srcfile in self._get_package_data_output_mapping():
+ self.mkpath(os.path.dirname(target))
+ _outf, _copied = self.copy_file(srcfile, target)
+ make_writable(target)
def analyze_manifest(self):
self.manifest_files = mf = {}
@@ -143,10 +173,19 @@ class build_py(orig.build_py):
# Locate package source directory
src_dirs[assert_relative(self.get_package_dir(package))] = package
- self.run_command('egg_info')
+ if (
+ getattr(self, 'existing_egg_info_dir', None)
+ and Path(self.existing_egg_info_dir, "SOURCES.txt").exists()
+ ):
+ manifest = Path(self.existing_egg_info_dir, "SOURCES.txt")
+ files = manifest.read_text(encoding="utf-8").splitlines()
+ else:
+ self.run_command('egg_info')
+ ei_cmd = self.get_finalized_command('egg_info')
+ files = ei_cmd.filelist.files
+
check = _IncludePackageDataAbuse()
- ei_cmd = self.get_finalized_command('egg_info')
- for path in ei_cmd.filelist.files:
+ for path in files:
d, f = os.path.split(assert_relative(path))
prev = None
oldf = f
@@ -200,6 +239,8 @@ class build_py(orig.build_py):
def initialize_options(self):
self.packages_checked = {}
orig.build_py.initialize_options(self)
+ self.editable_mode = False
+ self.existing_egg_info_dir = None
def get_package_dir(self, package):
res = orig.build_py.get_package_dir(self, package)
diff --git a/setuptools/extension.py b/setuptools/extension.py
index 64baf114..b9a2bad3 100644
--- a/setuptools/extension.py
+++ b/setuptools/extension.py
@@ -113,6 +113,9 @@ class Extension(_Extension):
:keyword bool optional:
specifies that a build failure in the extension should not abort the
build process, but simply not install the failing extension.
+
+ :keyword bool py_limited_api:
+ opt-in flag for the usage of :doc:`Python's limited API <python:c-api/stable>`.
"""
def __init__(self, name, sources, *args, **kw):
diff --git a/setuptools/tests/test_build_ext.py b/setuptools/tests/test_build_ext.py
index 3177a2cd..07ebcaf8 100644
--- a/setuptools/tests/test_build_ext.py
+++ b/setuptools/tests/test_build_ext.py
@@ -2,6 +2,7 @@ import os
import sys
import distutils.command.build_ext as orig
from distutils.sysconfig import get_config_var
+from importlib.util import cache_from_source as _compiled_file_name
from jaraco import path
@@ -83,6 +84,97 @@ class TestBuildExt:
finally:
del os.environ['SETUPTOOLS_EXT_SUFFIX']
+ def dist_with_example(self):
+ files = {
+ "src": {"mypkg": {"subpkg": {"ext2.c": ""}}},
+ "c-extensions": {"ext1": {"main.c": ""}},
+ }
+
+ ext1 = Extension("mypkg.ext1", ["c-extensions/ext1/main.c"])
+ ext2 = Extension("mypkg.subpkg.ext2", ["src/mypkg/subpkg/ext2.c"])
+ ext3 = Extension("ext3", ["c-extension/ext3.c"])
+
+ path.build(files)
+ dist = Distribution({
+ "script_name": "%test%",
+ "ext_modules": [ext1, ext2, ext3],
+ "package_dir": {"": "src"},
+ })
+ return dist
+
+ def test_get_outputs(self, tmpdir_cwd, monkeypatch):
+ monkeypatch.setenv('SETUPTOOLS_EXT_SUFFIX', '.mp3') # make test OS-independent
+ monkeypatch.setattr('setuptools.command.build_ext.use_stubs', False)
+ dist = self.dist_with_example()
+
+ # Regular build: get_outputs not empty, but get_output_mappings is empty
+ build_ext = dist.get_command_obj("build_ext")
+ build_ext.editable_mode = False
+ build_ext.ensure_finalized()
+ build_lib = build_ext.build_lib.replace(os.sep, "/")
+ outputs = [x.replace(os.sep, "/") for x in build_ext.get_outputs()]
+ assert outputs == [
+ f"{build_lib}/ext3.mp3",
+ f"{build_lib}/mypkg/ext1.mp3",
+ f"{build_lib}/mypkg/subpkg/ext2.mp3",
+ ]
+ assert build_ext.get_output_mapping() == {}
+
+ # Editable build: get_output_mappings should contain everything in get_outputs
+ dist.reinitialize_command("build_ext")
+ build_ext.editable_mode = True
+ build_ext.ensure_finalized()
+ mapping = {
+ k.replace(os.sep, "/"): v.replace(os.sep, "/")
+ for k, v in build_ext.get_output_mapping().items()
+ }
+ assert mapping == {
+ f"{build_lib}/ext3.mp3": "src/ext3.mp3",
+ f"{build_lib}/mypkg/ext1.mp3": "src/mypkg/ext1.mp3",
+ f"{build_lib}/mypkg/subpkg/ext2.mp3": "src/mypkg/subpkg/ext2.mp3",
+ }
+
+ def test_get_output_mapping_with_stub(self, tmpdir_cwd, monkeypatch):
+ monkeypatch.setenv('SETUPTOOLS_EXT_SUFFIX', '.mp3') # make test OS-independent
+ monkeypatch.setattr('setuptools.command.build_ext.use_stubs', True)
+ dist = self.dist_with_example()
+
+ # Editable build should create compiled stubs (.pyc files only, no .py)
+ build_ext = dist.get_command_obj("build_ext")
+ build_ext.editable_mode = True
+ build_ext.ensure_finalized()
+ for ext in build_ext.extensions:
+ monkeypatch.setattr(ext, "_needs_stub", True)
+
+ build_lib = build_ext.build_lib.replace(os.sep, "/")
+ mapping = {
+ k.replace(os.sep, "/"): v.replace(os.sep, "/")
+ for k, v in build_ext.get_output_mapping().items()
+ }
+
+ def C(file):
+ """Make it possible to do comparisons and tests in a OS-independent way"""
+ return _compiled_file_name(file).replace(os.sep, "/")
+
+ assert mapping == {
+ C(f"{build_lib}/ext3.py"): C("src/ext3.py"),
+ f"{build_lib}/ext3.mp3": "src/ext3.mp3",
+ C(f"{build_lib}/mypkg/ext1.py"): C("src/mypkg/ext1.py"),
+ f"{build_lib}/mypkg/ext1.mp3": "src/mypkg/ext1.mp3",
+ C(f"{build_lib}/mypkg/subpkg/ext2.py"): C("src/mypkg/subpkg/ext2.py"),
+ f"{build_lib}/mypkg/subpkg/ext2.mp3": "src/mypkg/subpkg/ext2.mp3",
+ }
+
+ # Ensure only the compiled stubs are present not the raw .py stub
+ assert f"{build_lib}/mypkg/ext1.py" not in mapping
+ assert f"{build_lib}/mypkg/subpkg/ext2.py" not in mapping
+
+ # Visualize what the cached stub files look like
+ example_stub = C(f"{build_lib}/mypkg/ext1.py")
+ assert example_stub in mapping
+ assert example_stub.startswith(f"{build_lib}/mypkg/__pycache__/ext1")
+ assert example_stub.endswith(".pyc")
+
def test_build_ext_config_handling(tmpdir_cwd):
files = {
diff --git a/setuptools/tests/test_build_py.py b/setuptools/tests/test_build_py.py
index 13fa64de..2b32edbc 100644
--- a/setuptools/tests/test_build_py.py
+++ b/setuptools/tests/test_build_py.py
@@ -1,10 +1,11 @@
import os
import stat
import shutil
+from pathlib import Path
+from unittest.mock import Mock
import pytest
import jaraco.path
-from path import Path
from setuptools import SetuptoolsDeprecationWarning
from setuptools.dist import Distribution
@@ -109,67 +110,194 @@ def test_executable_data(tmpdir_cwd):
"Script is not executable"
-def test_excluded_subpackages(tmp_path):
- files = {
- "setup.cfg": DALS("""
- [metadata]
- name = mypkg
- version = 42
+EXAMPLE_WITH_MANIFEST = {
+ "setup.cfg": DALS("""
+ [metadata]
+ name = mypkg
+ version = 42
- [options]
- include_package_data = True
- packages = find:
+ [options]
+ include_package_data = True
+ packages = find:
- [options.packages.find]
- exclude = *.tests*
- """),
+ [options.packages.find]
+ exclude = *.tests*
+ """),
+ "mypkg": {
+ "__init__.py": "",
+ "resource_file.txt": "",
+ "tests": {
+ "__init__.py": "",
+ "test_mypkg.py": "",
+ "test_file.txt": "",
+ }
+ },
+ "MANIFEST.in": DALS("""
+ global-include *.py *.txt
+ global-exclude *.py[cod]
+ prune dist
+ prune build
+ prune *.egg-info
+ """)
+}
+
+
+def test_excluded_subpackages(tmpdir_cwd):
+ jaraco.path.build(EXAMPLE_WITH_MANIFEST)
+ dist = Distribution({"script_name": "%PEP 517%"})
+ dist.parse_config_files()
+
+ build_py = dist.get_command_obj("build_py")
+ msg = r"Python recognizes 'mypkg\.tests' as an importable package"
+ with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
+ # TODO: To fix #3260 we need some transition period to deprecate the
+ # existing behavior of `include_package_data`. After the transition, we
+ # should remove the warning and fix the behaviour.
+ build_py.finalize_options()
+ build_py.run()
+
+ build_dir = Path(dist.get_command_obj("build_py").build_lib)
+ assert (build_dir / "mypkg/__init__.py").exists()
+ assert (build_dir / "mypkg/resource_file.txt").exists()
+
+ # Setuptools is configured to ignore `mypkg.tests`, therefore the following
+ # files/dirs should not be included in the distribution.
+ for f in [
+ "mypkg/tests/__init__.py",
+ "mypkg/tests/test_mypkg.py",
+ "mypkg/tests/test_file.txt",
+ "mypkg/tests",
+ ]:
+ with pytest.raises(AssertionError):
+ # TODO: Enforce the following assertion once #3260 is fixed
+ # (remove context manager and the following xfail).
+ assert not (build_dir / f).exists()
+
+ pytest.xfail("#3260")
+
+
+@pytest.mark.filterwarnings("ignore::setuptools.SetuptoolsDeprecationWarning")
+def test_existing_egg_info(tmpdir_cwd, monkeypatch):
+ """When provided with the ``existing_egg_info_dir`` attribute, build_py should not
+ attempt to run egg_info again.
+ """
+ # == Pre-condition ==
+ # Generate an egg-info dir
+ jaraco.path.build(EXAMPLE_WITH_MANIFEST)
+ dist = Distribution({"script_name": "%PEP 517%"})
+ dist.parse_config_files()
+ assert dist.include_package_data
+
+ egg_info = dist.get_command_obj("egg_info")
+ dist.run_command("egg_info")
+ egg_info_dir = next(Path(egg_info.egg_base).glob("*.egg-info"))
+ assert egg_info_dir.is_dir()
+
+ # == Setup ==
+ build_py = dist.get_command_obj("build_py")
+ build_py.finalize_options()
+ egg_info = dist.get_command_obj("egg_info")
+ egg_info_run = Mock(side_effect=egg_info.run)
+ monkeypatch.setattr(egg_info, "run", egg_info_run)
+
+ # == Remove caches ==
+ # egg_info is called when build_py looks for data_files, which gets cached.
+ # We need to ensure it is not cached yet, otherwise it may impact on the tests
+ build_py.__dict__.pop('data_files', None)
+ dist.reinitialize_command(egg_info)
+
+ # == Sanity check ==
+ # Ensure that if existing_egg_info is not given, build_py attempts to run egg_info
+ build_py.existing_egg_info_dir = None
+ build_py.run()
+ egg_info_run.assert_called()
+
+ # == Remove caches ==
+ egg_info_run.reset_mock()
+ build_py.__dict__.pop('data_files', None)
+ dist.reinitialize_command(egg_info)
+
+ # == Actual test ==
+ # Ensure that if existing_egg_info_dir is given, egg_info doesn't run
+ build_py.existing_egg_info_dir = egg_info_dir
+ build_py.run()
+ egg_info_run.assert_not_called()
+ assert build_py.data_files
+
+ # Make sure the list of outputs is actually OK
+ outputs = map(lambda x: x.replace(os.sep, "/"), build_py.get_outputs())
+ assert outputs
+ example = str(Path(build_py.build_lib, "mypkg/__init__.py")).replace(os.sep, "/")
+ assert example in outputs
+
+
+EXAMPLE_ARBITRARY_MAPPING = {
+ "pyproject.toml": DALS("""
+ [project]
+ name = "mypkg"
+ version = "42"
+
+ [tool.setuptools]
+ packages = ["mypkg", "mypkg.sub1", "mypkg.sub2", "mypkg.sub2.nested"]
+
+ [tool.setuptools.package-dir]
+ "" = "src"
+ "mypkg.sub2" = "src/mypkg/_sub2"
+ "mypkg.sub2.nested" = "other"
+ """),
+ "src": {
"mypkg": {
"__init__.py": "",
"resource_file.txt": "",
- "tests": {
+ "sub1": {
"__init__.py": "",
- "test_mypkg.py": "",
- "test_file.txt": "",
- }
+ "mod1.py": "",
+ },
+ "_sub2": {
+ "mod2.py": "",
+ },
},
- "MANIFEST.in": DALS("""
- global-include *.py *.txt
- global-exclude *.py[cod]
- prune dist
- prune build
- prune *.egg-info
- """)
- }
+ },
+ "other": {
+ "__init__.py": "",
+ "mod3.py": "",
+ },
+ "MANIFEST.in": DALS("""
+ global-include *.py *.txt
+ global-exclude *.py[cod]
+ """)
+}
+
- with Path(tmp_path):
- jaraco.path.build(files)
- dist = Distribution({"script_name": "%PEP 517%"})
- dist.parse_config_files()
-
- build_py = dist.get_command_obj("build_py")
- msg = r"Python recognizes 'mypkg\.tests' as an importable package"
- with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
- # TODO: To fix #3260 we need some transition period to deprecate the
- # existing behavior of `include_package_data`. After the transition, we
- # should remove the warning and fix the behaviour.
- build_py.finalize_options()
- build_py.run()
-
- build_dir = Path(dist.get_command_obj("build_py").build_lib)
- assert (build_dir / "mypkg/__init__.py").exists()
- assert (build_dir / "mypkg/resource_file.txt").exists()
-
- # Setuptools is configured to ignore `mypkg.tests`, therefore the following
- # files/dirs should not be included in the distribution.
- for f in [
- "mypkg/tests/__init__.py",
- "mypkg/tests/test_mypkg.py",
- "mypkg/tests/test_file.txt",
- "mypkg/tests",
- ]:
- with pytest.raises(AssertionError):
- # TODO: Enforce the following assertion once #3260 is fixed
- # (remove context manager and the following xfail).
- assert not (build_dir / f).exists()
-
- pytest.xfail("#3260")
+def test_get_outputs(tmpdir_cwd):
+ jaraco.path.build(EXAMPLE_ARBITRARY_MAPPING)
+ dist = Distribution({"script_name": "%test%"})
+ dist.parse_config_files()
+
+ build_py = dist.get_command_obj("build_py")
+ build_py.editable_mode = True
+ build_py.ensure_finalized()
+ build_lib = build_py.build_lib.replace(os.sep, "/")
+ outputs = [x.replace(os.sep, "/") for x in build_py.get_outputs()]
+ assert outputs == [
+ f"{build_lib}/mypkg/__init__.py",
+ f"{build_lib}/mypkg/resource_file.txt",
+ f"{build_lib}/mypkg/sub1/__init__.py",
+ f"{build_lib}/mypkg/sub1/mod1.py",
+ f"{build_lib}/mypkg/sub2/mod2.py",
+ f"{build_lib}/mypkg/sub2/nested/__init__.py",
+ f"{build_lib}/mypkg/sub2/nested/mod3.py",
+ ]
+ mapping = {
+ k.replace(os.sep, "/"): v.replace(os.sep, "/")
+ for k, v in build_py.get_output_mapping().items()
+ }
+ assert mapping == {
+ f"{build_lib}/mypkg/__init__.py": "src/mypkg/__init__.py",
+ f"{build_lib}/mypkg/resource_file.txt": "src/mypkg/resource_file.txt",
+ f"{build_lib}/mypkg/sub1/__init__.py": "src/mypkg/sub1/__init__.py",
+ f"{build_lib}/mypkg/sub1/mod1.py": "src/mypkg/sub1/mod1.py",
+ f"{build_lib}/mypkg/sub2/mod2.py": "src/mypkg/_sub2/mod2.py",
+ f"{build_lib}/mypkg/sub2/nested/__init__.py": "other/__init__.py",
+ f"{build_lib}/mypkg/sub2/nested/mod3.py": "other/mod3.py",
+ }