Skip to content

Commit

Permalink
inspection: normalise legacy build backend handling
Browse files Browse the repository at this point in the history
This change ensures that when inspection packages for metadata, poetry
correctly handles cases where the legacy build backend is used.

In addition to improving handling of PEP 517 metadata builds, error
handling when reading setup files have also been improved.
  • Loading branch information
abn committed Jul 4, 2020
1 parent d3a34e2 commit 6ecaf28
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 57 deletions.
68 changes: 38 additions & 30 deletions poetry/inspection/info.py
Expand Up @@ -12,7 +12,8 @@

import pkginfo

from pep517.meta import build
from pep517.build import compat_system as pep517_compat_system
from pep517.meta import build as pep517_metadata_build
from poetry.core.factory import Factory
from poetry.core.packages import Package
from poetry.core.packages import ProjectPackage
Expand Down Expand Up @@ -255,19 +256,22 @@ def _from_sdist_file(cls, path): # type: (Path) -> PackageInfo
return info.update(new_info)

@classmethod
def from_setup_py(cls, path): # type: (Union[str, Path]) -> PackageInfo
def from_setup_files(cls, path): # type: (Path) -> PackageInfo
"""
Mechanism to parse package information from a `setup.py` file. This uses the implentation
Mechanism to parse package information from a `setup.[py|cfg]` file. This uses the implementation
at `poetry.utils.setup_reader.SetupReader` in order to parse the file. This is not reliable for
complex setup files and should only attempted as a fallback.
:param path: Path to `setup.py` file
:return:
"""
if not (Path(path) / "setup.py").exists():
if not any((path / f).exists() for f in SetupReader.FILES):
raise PackageInfoError(path)

try:
result = SetupReader.read_from_directory(path)
except Exception:
raise PackageInfoError(path)

result = SetupReader.read_from_directory(Path(path))
python_requires = result["python_requires"]
if python_requires is None:
python_requires = "*"
Expand All @@ -289,14 +293,20 @@ def from_setup_py(cls, path): # type: (Union[str, Path]) -> PackageInfo

requirements = parse_requires(requires)

return cls(
info = cls(
name=result.get("name"),
version=result.get("version"),
summary=result.get("description", ""),
requires_dist=requirements or None,
requires_python=python_requires,
)

if not (info.name and info.version) and not info.requires_dist:
# there is nothing useful here
raise PackageInfoError(path)

return info

@staticmethod
def _find_dist_info(path): # type: (Path) -> Iterator[Path]
"""
Expand All @@ -309,22 +319,20 @@ def _find_dist_info(path): # type: (Path) -> Iterator[Path]
# Sometimes pathlib will fail on recursive symbolic links, so we need to workaround it
# and use the glob module instead. Note that this does not happen with pathlib2
# so it's safe to use it for Python < 3.4.
directories = glob.iglob(Path(path, pattern).as_posix(), recursive=True)
directories = glob.iglob(path.joinpath(pattern).as_posix(), recursive=True)
else:
directories = path.glob(pattern)

for d in directories:
yield Path(d)

@classmethod
def from_metadata(cls, path): # type: (Union[str, Path]) -> Optional[PackageInfo]
def from_metadata(cls, path): # type: (Path) -> Optional[PackageInfo]
"""
Helper method to parse package information from an unpacked metadata directory.
:param path: The metadata directory to parse information from.
"""
path = Path(path)

if path.suffix in {".dist-info", ".egg-info"}:
directories = [path]
else:
Expand Down Expand Up @@ -400,36 +408,37 @@ def _pep517_metadata(cls, path): # type (Path) -> PackageInfo
:param path: Path to package source to build and read metadata for.
"""
pyproject_toml = path.joinpath("pyproject.toml")
setup_py = path.joinpath("setup.py")

system = None
if not pyproject_toml.exists() and setup_py.exists():
# use default PEP-517 backend configuration
system = {
"requires": ["setuptools", "wheel"],
"build-backend": "setuptools.build_meta",
}
info = None
try:
info = cls.from_setup_files(path)
if info.requires_dist is not None:
return info
except PackageInfoError:
pass

try:
with temporary_directory() as tmp_dir:
build(source_dir=path.as_posix(), dest=tmp_dir, system=system)
return cls.from_metadata(tmp_dir)
pep517_metadata_build(
source_dir=path.as_posix(),
dest=tmp_dir,
system=pep517_compat_system(source_dir=path.as_posix()),
)
return cls.from_metadata(Path(tmp_dir))
except subprocess.CalledProcessError:
cls._log("PEP 517 metadata build failed for {}".format(path), "debug")
if setup_py.exists():
if info:
cls._log(
"Falling back to parsing setup.py file for {}".format(path), "debug"
"Falling back to parsed setup.py file for {}".format(path), "debug"
)
return cls.from_setup_py(setup_py)
return info

# if we reach here, everything has failed and all hope is lost
raise PackageInfoError(path)

@classmethod
def from_directory(
cls, path, allow_build=False
): # type: (Union[str, Path], bool) -> PackageInfo
): # type: (Path, bool) -> PackageInfo
"""
Generate package information from a package source directory. When `allow_build` is enabled and
introspection of all available metadata fails, the package is attempted to be build in an isolated
Expand All @@ -438,7 +447,6 @@ def from_directory(
:param path: Path to generate package information from.
:param allow_build: If enabled, as a fallback, build the project to gather metadata.
"""
path = Path(path)
info = cls.from_metadata(path)

if info and info.requires_dist is not None:
Expand All @@ -451,7 +459,7 @@ def from_directory(

try:
if not allow_build:
return cls.from_setup_py(path)
return cls.from_setup_files(path)
return cls._pep517_metadata(path)
except PackageInfoError as e:
if info:
Expand All @@ -460,7 +468,7 @@ def from_directory(
raise e

@classmethod
def from_sdist(cls, path): # type: (Union[Path, pkginfo.SDist]) -> PackageInfo
def from_sdist(cls, path): # type: (Path) -> PackageInfo
"""
Gather package information from an sdist file, packed or unpacked.
Expand Down
23 changes: 0 additions & 23 deletions tests/fixtures/inspection/demo_only_setup/setup.py

This file was deleted.

80 changes: 76 additions & 4 deletions tests/inspection/test_info.py
Expand Up @@ -27,6 +27,39 @@ def demo_wheel(): # type: () -> Path
return FIXTURE_DIR_BASE / "distributions" / "demo-0.1.0-py2.py3-none-any.whl"


@pytest.fixture
def demo_setup(tmp_path): # type: (Path) -> Path
setup_py = tmp_path / "setup.py"
setup_py.write_text(
decode(
"from setuptools import setup; "
'setup(name="demo", '
'version="0.1.0", '
'install_requires=["package"])'
)
)
yield tmp_path


@pytest.fixture
def demo_setup_cfg(tmp_path): # type: (Path) -> Path
setup_cfg = tmp_path / "setup.cfg"
setup_cfg.write_text(
decode(
"\n".join(
[
"[metadata]",
"name = demo",
"version = 0.1.0",
"[options]",
"install_requires = package",
]
)
)
)
yield tmp_path


@pytest.fixture
def demo_setup_complex(tmp_path): # type: (Path) -> Path
setup_py = tmp_path / "setup.py"
Expand All @@ -41,6 +74,15 @@ def demo_setup_complex(tmp_path): # type: (Path) -> Path
yield tmp_path


@pytest.fixture
def demo_setup_complex_pep517_legacy(demo_setup_complex): # type: (Path) -> Path
pyproject_toml = demo_setup_complex / "pyproject.toml"
pyproject_toml.write_text(
decode("[build-system]\n" 'requires = ["setuptools", "wheel"]')
)
yield demo_setup_complex


def demo_check_info(info, requires_dist=None): # type: (PackageInfo, Set[str]) -> None
assert info.name == "demo"
assert info.version == "0.1.0"
Expand Down Expand Up @@ -82,9 +124,15 @@ def test_info_from_requires_txt():


@pytest.mark.skipif(not PY35, reason="Parsing of setup.py is skipped for Python < 3.5")
def test_info_from_setup_py():
info = PackageInfo.from_setup_py(FIXTURE_DIR_INSPECTIONS / "demo_only_setup")
demo_check_info(info)
def test_info_from_setup_py(demo_setup):
info = PackageInfo.from_setup_files(demo_setup)
demo_check_info(info, requires_dist={"package"})


@pytest.mark.skipif(not PY35, reason="Parsing of setup.cfg is skipped for Python < 3.5")
def test_info_from_setup_cfg(demo_setup_cfg):
info = PackageInfo.from_setup_files(demo_setup_cfg)
demo_check_info(info, requires_dist={"package"})


def test_info_no_setup_pkg_info_no_deps():
Expand All @@ -96,14 +144,38 @@ def test_info_no_setup_pkg_info_no_deps():
assert info.requires_dist is None


def test_info_setup_simple(mocker, demo_setup):
pep517_metadata_build = mocker.patch("pep517.meta.build")
info = PackageInfo.from_directory(demo_setup, allow_build=True)
pep517_metadata_build.assert_not_called()
demo_check_info(info, requires_dist={"package"})


@pytest.mark.skipif(not PY35, reason="Parsing of setup.cfg is skipped for Python < 3.5")
def test_info_setup_cfg(mocker, demo_setup_cfg):
pep517_metadata_build = mocker.patch("pep517.meta.build")
info = PackageInfo.from_directory(demo_setup_cfg, allow_build=True)
pep517_metadata_build.assert_not_called()
demo_check_info(info, requires_dist={"package"})


def test_info_setup_complex(demo_setup_complex):
info = PackageInfo.from_directory(demo_setup_complex, allow_build=True)
demo_check_info(info, requires_dist={"package"})


def test_info_setup_complex_pep517_legacy(demo_setup_complex_pep517_legacy):
info = PackageInfo.from_directory(
demo_setup_complex_pep517_legacy, allow_build=True
)
demo_check_info(info, requires_dist={"package"})


@pytest.mark.skipif(not PY35, reason="Parsing of setup.py is skipped for Python < 3.5")
def test_info_setup_complex_disable_build(demo_setup_complex):
def test_info_setup_complex_disable_build(mocker, demo_setup_complex):
pep517_metadata_build = mocker.patch("pep517.meta.build")
info = PackageInfo.from_directory(demo_setup_complex, allow_build=False)
pep517_metadata_build.assert_not_called()
assert info.name == "demo"
assert info.version == "0.1.0"
assert info.requires_dist is None

0 comments on commit 6ecaf28

Please sign in to comment.