Skip to content
Open
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 newsfragments/5089.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Strict editable installs now respect Python module exclusions in ``MANIFEST.in``.
42 changes: 28 additions & 14 deletions setuptools/command/build_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class build_py(orig.build_py):
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
editable_mode: bool = False
existing_egg_info_dir: StrPath | None = None #: Private API, internal use only.
_strict_editable: bool = False

def finalize_options(self) -> None:
orig.build_py.finalize_options(self)
Expand Down Expand Up @@ -154,7 +155,17 @@ def get_output_mapping(self) -> dict[str, str]:

def _get_module_mapping(self) -> Iterator[tuple[str, str]]:
"""Iterate over all modules producing (dest, src) pairs."""
manifest = None
if self._strict_editable:
manifest = {
os.path.normcase(os.path.abspath(path))
for path in self._get_manifest_files()
}
for package, module, module_file in self.find_all_modules():
if manifest is not None:
normalized = os.path.normcase(os.path.abspath(module_file))
if normalized not in manifest:
continue
package = package.split('.')
filename = self.get_module_outfile(self.build_lib, package, module)
yield (filename, module_file)
Expand Down Expand Up @@ -183,21 +194,8 @@ def analyze_manifest(self) -> None:
# Locate package source directory
src_dirs[assert_relative(self.get_package_dir(package))] = package

if (
self.existing_egg_info_dir
and Path(self.existing_egg_info_dir, "SOURCES.txt").exists()
):
egg_info_dir = self.existing_egg_info_dir
manifest = Path(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')
egg_info_dir = ei_cmd.egg_info
files = ei_cmd.filelist.files

check = _IncludePackageDataAbuse()
for path in self._filter_build_files(files, egg_info_dir):
for path in self._get_manifest_files():
d, f = os.path.split(assert_relative(path))
prev = None
oldf = f
Expand All @@ -215,6 +213,21 @@ def analyze_manifest(self) -> None:
check.warn(importable)
self.manifest_files.setdefault(src_dirs[d], []).append(path)

def _get_manifest_files(self) -> Iterator[str]:
if (
self.existing_egg_info_dir
and Path(self.existing_egg_info_dir, "SOURCES.txt").exists()
):
egg_info_dir = self.existing_egg_info_dir
manifest = Path(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')
egg_info_dir = ei_cmd.egg_info
files = ei_cmd.filelist.files
yield from self._filter_build_files(files, egg_info_dir)

def _filter_build_files(
self, files: Iterable[str], egg_info: StrPath
) -> Iterator[str]:
Expand Down Expand Up @@ -274,6 +287,7 @@ def initialize_options(self):
orig.build_py.initialize_options(self)
self.editable_mode = False
self.existing_egg_info_dir = None
self._strict_editable = False

def get_package_dir(self, package: str) -> str:
res = orig.build_py.get_package_dir(self, package)
Expand Down
3 changes: 3 additions & 0 deletions setuptools/command/editable_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ def _configure_build(
build_py = cast(build_py_cls, dist.get_command_obj("build_py"))
build_py.compile = False
build_py.existing_egg_info_dir = self._find_egg_info_dir()
build_py._strict_editable = (
self.mode or ""
).lower() == _EditableMode.STRICT.value

self._set_editable_mode()

Expand Down
13 changes: 13 additions & 0 deletions setuptools/tests/test_editable_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,19 @@ def test_strict_install(self, tmp_path, venv):
assert "No such file or directory" in out
assert "resource.not_in_manifest" in out

def test_strict_install_respects_manifest_module_exclusions(self, tmp_path, venv):
files = deepcopy(TestOverallBehaviour.EXAMPLES["src-layout"])
files["MANIFEST.in"] += "\nexclude src/mypkg/mod1.py"
opts = ["--config-settings", "editable-mode=strict"]
install_project("mypkg", venv, tmp_path, files, *opts)

out = venv.run([
"python",
"-c",
"import importlib.util; print(importlib.util.find_spec('mypkg.mod1'))",
])
assert out.strip() == "None"


@pytest.mark.filterwarnings("ignore:.*compat.*:setuptools.SetuptoolsDeprecationWarning")
def test_compat_install(tmp_path, venv):
Expand Down