Skip to content

Commit 007bc31

Browse files
committed
inspection: use pep517 metadata build
This change replaces setup.py explicit execution in favour of pep517 metadata builds. In addition to improving handling of PEP 517 metadata builds, error handling when reading setup files have also been improved.
1 parent 3222fe3 commit 007bc31

File tree

5 files changed

+268
-88
lines changed

5 files changed

+268
-88
lines changed

poetry/inspection/info.py

+111-53
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@
3030

3131
logger = logging.getLogger(__name__)
3232

33+
PEP517_META_BUILD = """\
34+
import pep517.build
35+
import pep517.meta
36+
37+
path='{source}'
38+
system=pep517.build.compat_system(path)
39+
pep517.meta.build(source_dir=path, dest='{dest}', system=system)
40+
"""
41+
42+
PEP517_META_BUILD_DEPS = ["pep517===0.8.2", "toml==0.10.1"]
43+
3344

3445
class PackageInfoError(ValueError):
3546
def __init__(self, path): # type: (Union[Path, str]) -> None
@@ -256,17 +267,27 @@ def _from_sdist_file(cls, path): # type: (Path) -> PackageInfo
256267

257268
return info.update(new_info)
258269

270+
@staticmethod
271+
def has_setup_files(path): # type: (Path) -> bool
272+
return any((path / f).exists() for f in SetupReader.FILES)
273+
259274
@classmethod
260-
def from_setup_py(cls, path): # type: (Union[str, Path]) -> PackageInfo
275+
def from_setup_files(cls, path): # type: (Path) -> PackageInfo
261276
"""
262-
Mechanism to parse package information from a `setup.py` file. This uses the implentation
277+
Mechanism to parse package information from a `setup.[py|cfg]` file. This uses the implementation
263278
at `poetry.utils.setup_reader.SetupReader` in order to parse the file. This is not reliable for
264279
complex setup files and should only attempted as a fallback.
265280
266281
:param path: Path to `setup.py` file
267-
:return:
268282
"""
269-
result = SetupReader.read_from_directory(Path(path))
283+
if not cls.has_setup_files(path):
284+
raise PackageInfoError(path)
285+
286+
try:
287+
result = SetupReader.read_from_directory(path)
288+
except Exception:
289+
raise PackageInfoError(path)
290+
270291
python_requires = result["python_requires"]
271292
if python_requires is None:
272293
python_requires = "*"
@@ -288,14 +309,20 @@ def from_setup_py(cls, path): # type: (Union[str, Path]) -> PackageInfo
288309

289310
requirements = parse_requires(requires)
290311

291-
return cls(
312+
info = cls(
292313
name=result.get("name"),
293314
version=result.get("version"),
294315
summary=result.get("description", ""),
295316
requires_dist=requirements or None,
296317
requires_python=python_requires,
297318
)
298319

320+
if not (info.name and info.version) and not info.requires_dist:
321+
# there is nothing useful here
322+
raise PackageInfoError(path)
323+
324+
return info
325+
299326
@staticmethod
300327
def _find_dist_info(path): # type: (Path) -> Iterator[Path]
301328
"""
@@ -308,22 +335,20 @@ def _find_dist_info(path): # type: (Path) -> Iterator[Path]
308335
# Sometimes pathlib will fail on recursive symbolic links, so we need to workaround it
309336
# and use the glob module instead. Note that this does not happen with pathlib2
310337
# so it's safe to use it for Python < 3.4.
311-
directories = glob.iglob(Path(path, pattern).as_posix(), recursive=True)
338+
directories = glob.iglob(path.joinpath(pattern).as_posix(), recursive=True)
312339
else:
313340
directories = path.glob(pattern)
314341

315342
for d in directories:
316343
yield Path(d)
317344

318345
@classmethod
319-
def from_metadata(cls, path): # type: (Union[str, Path]) -> Optional[PackageInfo]
346+
def from_metadata(cls, path): # type: (Path) -> Optional[PackageInfo]
320347
"""
321348
Helper method to parse package information from an unpacked metadata directory.
322349
323350
:param path: The metadata directory to parse information from.
324351
"""
325-
path = Path(path)
326-
327352
if path.suffix in {".dist-info", ".egg-info"}:
328353
directories = [path]
329354
else:
@@ -392,10 +417,79 @@ def _get_poetry_package(path): # type: (Path) -> Optional[ProjectPackage]
392417
except RuntimeError:
393418
pass
394419

420+
@classmethod
421+
def _pep517_metadata(cls, path): # type (Path) -> PackageInfo
422+
"""
423+
Helper method to use PEP-517 library to build and read package metadata.
424+
425+
:param path: Path to package source to build and read metadata for.
426+
"""
427+
info = None
428+
try:
429+
info = cls.from_setup_files(path)
430+
if info.requires_dist is not None:
431+
return info
432+
except PackageInfoError:
433+
pass
434+
435+
with temporary_directory() as tmp_dir:
436+
# TODO: cache PEP 517 build environment corresponding to each project venv
437+
venv_dir = Path(tmp_dir) / ".venv"
438+
EnvManager.build_venv(venv_dir.as_posix())
439+
venv = VirtualEnv(venv_dir, venv_dir)
440+
441+
dest_dir = Path(tmp_dir) / "dist"
442+
dest_dir.mkdir()
443+
444+
try:
445+
venv.run(
446+
"python",
447+
"-m",
448+
"pip",
449+
"install",
450+
"--disable-pip-version-check",
451+
"--ignore-installed",
452+
*PEP517_META_BUILD_DEPS
453+
)
454+
venv.run(
455+
"python",
456+
"-",
457+
input_=PEP517_META_BUILD.format(
458+
source=path.as_posix(), dest=dest_dir.as_posix()
459+
),
460+
)
461+
return cls.from_metadata(dest_dir)
462+
except EnvCommandError as e:
463+
# something went wrong while attempting pep517 metadata build
464+
# fallback to egg_info if setup.py available
465+
cls._log("PEP517 build failed: {}".format(e), level="debug")
466+
setup_py = path / "setup.py"
467+
if not setup_py.exists():
468+
raise PackageInfoError(path)
469+
470+
cwd = Path.cwd()
471+
os.chdir(path.as_posix())
472+
try:
473+
venv.run("python", "setup.py", "egg_info")
474+
return cls.from_metadata(path)
475+
except EnvCommandError:
476+
raise PackageInfoError(path)
477+
finally:
478+
os.chdir(cwd.as_posix())
479+
480+
if info:
481+
cls._log(
482+
"Falling back to parsed setup.py file for {}".format(path), "debug"
483+
)
484+
return info
485+
486+
# if we reach here, everything has failed and all hope is lost
487+
raise PackageInfoError(path)
488+
395489
@classmethod
396490
def from_directory(
397491
cls, path, allow_build=False
398-
): # type: (Union[str, Path], bool) -> PackageInfo
492+
): # type: (Path, bool) -> PackageInfo
399493
"""
400494
Generate package information from a package source directory. When `allow_build` is enabled and
401495
introspection of all available metadata fails, the package is attempted to be build in an isolated
@@ -404,57 +498,28 @@ def from_directory(
404498
:param path: Path to generate package information from.
405499
:param allow_build: If enabled, as a fallback, build the project to gather metadata.
406500
"""
407-
path = Path(path)
408-
409-
current_dir = os.getcwd()
410-
411501
info = cls.from_metadata(path)
412502

413503
if info and info.requires_dist is not None:
414504
# return only if requirements are discovered
415505
return info
416506

417-
setup_py = path.joinpath("setup.py")
418-
419507
project_package = cls._get_poetry_package(path)
420508
if project_package:
421509
return cls.from_package(project_package)
422510

423-
if not setup_py.exists():
424-
if not allow_build and info:
425-
# we discovered PkgInfo but no requirements were listed
426-
return info
427-
# this means we cannot do anything else here
428-
raise PackageInfoError(path)
429-
430-
if not allow_build:
431-
return cls.from_setup_py(path=path)
432-
433511
try:
434-
# TODO: replace with PEP517
435-
# we need to switch to the correct path in order for egg_info command to work
436-
os.chdir(str(path))
437-
438-
# Execute egg_info
439-
cls._execute_setup()
440-
except EnvCommandError:
441-
cls._log(
442-
"Falling back to parsing setup.py file for {}".format(path), "debug"
443-
)
444-
# egg_info could not be generated, we fallback to ast parser
445-
return cls.from_setup_py(path=path)
446-
else:
447-
info = cls.from_metadata(path)
512+
if not allow_build:
513+
return cls.from_setup_files(path)
514+
return cls._pep517_metadata(path)
515+
except PackageInfoError as e:
448516
if info:
517+
# we discovered PkgInfo but no requirements were listed
449518
return info
450-
finally:
451-
os.chdir(current_dir)
452-
453-
# if we reach here, everything has failed and all hope is lost
454-
raise PackageInfoError(path)
519+
raise e
455520

456521
@classmethod
457-
def from_sdist(cls, path): # type: (Union[Path, pkginfo.SDist]) -> PackageInfo
522+
def from_sdist(cls, path): # type: (Path) -> PackageInfo
458523
"""
459524
Gather package information from an sdist file, packed or unpacked.
460525
@@ -508,10 +573,3 @@ def from_path(cls, path): # type: (Path) -> PackageInfo
508573
return cls.from_bdist(path=path)
509574
except PackageInfoError:
510575
return cls.from_sdist(path=path)
511-
512-
@classmethod
513-
def _execute_setup(cls):
514-
with temporary_directory() as tmp_dir:
515-
EnvManager.build_venv(tmp_dir)
516-
venv = VirtualEnv(Path(tmp_dir), Path(tmp_dir))
517-
venv.run("python", "setup.py", "egg_info")

tests/conftest.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from poetry.config.config import Config as BaseConfig
1212
from poetry.config.dict_config_source import DictConfigSource
13+
from poetry.inspection.info import PackageInfo
1314
from poetry.utils._compat import Path
1415
from poetry.utils.env import EnvManager
1516
from poetry.utils.env import VirtualEnv
@@ -79,8 +80,11 @@ def download_mock(mocker):
7980

8081

8182
@pytest.fixture(autouse=True)
82-
def execute_setup_mock(mocker):
83-
mocker.patch("poetry.inspection.info.PackageInfo._execute_setup")
83+
def pep517_metadata_mock(mocker):
84+
mocker.patch(
85+
"poetry.inspection.info.PackageInfo._pep517_metadata",
86+
return_value=PackageInfo(name="demo", version="0.1.2"),
87+
)
8488

8589

8690
@pytest.fixture

tests/fixtures/inspection/demo_only_setup/setup.py

-23
This file was deleted.

0 commit comments

Comments
 (0)