info.py (poetry-1.1.15) | : | info.py (poetry-1.2.0) | ||
---|---|---|---|---|
from __future__ import annotations | ||||
import contextlib | ||||
import functools | ||||
import glob | import glob | |||
import logging | import logging | |||
import os | import os | |||
import tarfile | import tarfile | |||
import zipfile | import zipfile | |||
from typing import Dict | from pathlib import Path | |||
from typing import Iterator | from typing import TYPE_CHECKING | |||
from typing import List | from typing import Any | |||
from typing import Optional | ||||
from typing import Union | ||||
import pkginfo | import pkginfo | |||
from poetry.core.factory import Factory | from poetry.core.factory import Factory | |||
from poetry.core.packages import Package | from poetry.core.packages.dependency import Dependency | |||
from poetry.core.packages import ProjectPackage | from poetry.core.packages.package import Package | |||
from poetry.core.packages import dependency_from_pep_508 | ||||
from poetry.core.pyproject.toml import PyProjectTOML | from poetry.core.pyproject.toml import PyProjectTOML | |||
from poetry.core.utils._compat import PY35 | ||||
from poetry.core.utils._compat import Path | ||||
from poetry.core.utils.helpers import parse_requires | from poetry.core.utils.helpers import parse_requires | |||
from poetry.core.utils.helpers import temporary_directory | from poetry.core.utils.helpers import temporary_directory | |||
from poetry.core.version.markers import InvalidMarker | from poetry.core.version.markers import InvalidMarker | |||
from poetry.utils.env import EnvCommandError | from poetry.utils.env import EnvCommandError | |||
from poetry.utils.env import EnvManager | from poetry.utils.env import ephemeral_environment | |||
from poetry.utils.env import VirtualEnv | ||||
from poetry.utils.setup_reader import SetupReader | from poetry.utils.setup_reader import SetupReader | |||
if TYPE_CHECKING: | ||||
from collections.abc import Callable | ||||
from collections.abc import Iterator | ||||
from contextlib import AbstractContextManager | ||||
from poetry.core.packages.project_package import ProjectPackage | ||||
logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | |||
PEP517_META_BUILD = """\ | PEP517_META_BUILD = """\ | |||
import pep517.build | import build | |||
import pep517.meta | import build.env | |||
import pep517 | ||||
path='{source}' | ||||
system=pep517.build.compat_system(path) | source = '{source}' | |||
pep517.meta.build(source_dir=path, dest='{dest}', system=system) | dest = '{dest}' | |||
with build.env.IsolatedEnvBuilder() as env: | ||||
builder = build.ProjectBuilder( | ||||
srcdir=source, | ||||
scripts_dir=env.scripts_dir, | ||||
python_executable=env.executable, | ||||
runner=pep517.quiet_subprocess_runner, | ||||
) | ||||
env.install(builder.build_system_requires) | ||||
env.install(builder.get_requires_for_build('wheel')) | ||||
builder.metadata_path(dest) | ||||
""" | """ | |||
PEP517_META_BUILD_DEPS = ["pep517===0.8.2", "toml==0.10.1"] | PEP517_META_BUILD_DEPS = ["build===0.7.0", "pep517==0.12.0"] | |||
class PackageInfoError(ValueError): | class PackageInfoError(ValueError): | |||
def __init__( | def __init__(self, path: Path | str, *reasons: BaseException | str) -> None: | |||
self, path, *reasons | reasons = (f"Unable to determine package info for path: {path!s}",) + re | |||
): # type: (Union[Path, str], *Union[BaseException, str]) -> None | asons | |||
reasons = ( | super().__init__("\n\n".join(str(msg).strip() for msg in reasons if msg) | |||
"Unable to determine package info for path: {}".format(str(path)), | ) | |||
) + reasons | ||||
super(PackageInfoError, self).__init__( | ||||
"\n\n".join(str(msg).strip() for msg in reasons if msg) | ||||
) | ||||
class PackageInfo: | class PackageInfo: | |||
def __init__( | def __init__( | |||
self, | self, | |||
name=None, # type: Optional[str] | *, | |||
version=None, # type: Optional[str] | name: str | None = None, | |||
summary=None, # type: Optional[str] | version: str | None = None, | |||
platform=None, # type: Optional[str] | summary: str | None = None, | |||
requires_dist=None, # type: Optional[List[str]] | platform: str | None = None, | |||
requires_python=None, # type: Optional[str] | requires_dist: list[str] | None = None, | |||
files=None, # type: Optional[List[str]] | requires_python: str | None = None, | |||
cache_version=None, # type: Optional[str] | files: list[dict[str, str]] | None = None, | |||
): | yanked: str | bool = False, | |||
cache_version: str | None = None, | ||||
) -> None: | ||||
self.name = name | self.name = name | |||
self.version = version | self.version = version | |||
self.summary = summary | self.summary = summary | |||
self.platform = platform | self.platform = platform | |||
self.requires_dist = requires_dist | self.requires_dist = requires_dist | |||
self.requires_python = requires_python | self.requires_python = requires_python | |||
self.files = files or [] | self.files = files or [] | |||
self.yanked = yanked | ||||
self._cache_version = cache_version | self._cache_version = cache_version | |||
self._source_type = None | self._source_type: str | None = None | |||
self._source_url = None | self._source_url: str | None = None | |||
self._source_reference = None | self._source_reference: str | None = None | |||
@property | @property | |||
def cache_version(self): # type: () -> Optional[str] | def cache_version(self) -> str | None: | |||
return self._cache_version | return self._cache_version | |||
def update(self, other): # type: (PackageInfo) -> PackageInfo | def update(self, other: PackageInfo) -> PackageInfo: | |||
self.name = other.name or self.name | self.name = other.name or self.name | |||
self.version = other.version or self.version | self.version = other.version or self.version | |||
self.summary = other.summary or self.summary | self.summary = other.summary or self.summary | |||
self.platform = other.platform or self.platform | self.platform = other.platform or self.platform | |||
self.requires_dist = other.requires_dist or self.requires_dist | self.requires_dist = other.requires_dist or self.requires_dist | |||
self.requires_python = other.requires_python or self.requires_python | self.requires_python = other.requires_python or self.requires_python | |||
self.files = other.files or self.files | self.files = other.files or self.files | |||
self._cache_version = other.cache_version or self._cache_version | self._cache_version = other.cache_version or self._cache_version | |||
return self | return self | |||
def asdict(self): # type: () -> Dict[str, Optional[Union[str, List[str]]]] | def asdict(self) -> dict[str, Any]: | |||
""" | """ | |||
Helper method to convert package info into a dictionary used for caching . | Helper method to convert package info into a dictionary used for caching . | |||
""" | """ | |||
return { | return { | |||
"name": self.name, | "name": self.name, | |||
"version": self.version, | "version": self.version, | |||
"summary": self.summary, | "summary": self.summary, | |||
"platform": self.platform, | "platform": self.platform, | |||
"requires_dist": self.requires_dist, | "requires_dist": self.requires_dist, | |||
"requires_python": self.requires_python, | "requires_python": self.requires_python, | |||
"files": self.files, | "files": self.files, | |||
"yanked": self.yanked, | ||||
"_cache_version": self._cache_version, | "_cache_version": self._cache_version, | |||
} | } | |||
@classmethod | @classmethod | |||
def load( | def load(cls, data: dict[str, Any]) -> PackageInfo: | |||
cls, data | ||||
): # type: (Dict[str, Optional[Union[str, List[str]]]]) -> PackageInfo | ||||
""" | """ | |||
Helper method to load data from a dictionary produced by `PackageInfo.as dict()`. | Helper method to load data from a dictionary produced by `PackageInfo.as dict()`. | |||
:param data: Data to load. This is expected to be a `dict` object output | :param data: Data to load. This is expected to be a `dict` object output | |||
by `asdict()`. | by | |||
`asdict()`. | ||||
""" | """ | |||
cache_version = data.pop("_cache_version", None) | cache_version = data.pop("_cache_version", None) | |||
return cls(cache_version=cache_version, **data) | return cls(cache_version=cache_version, **data) | |||
@classmethod | ||||
def _log(cls, msg, level="info"): | ||||
"""Internal helper method to log information.""" | ||||
getattr(logger, level)("<debug>{}:</debug> {}".format(cls.__name__, msg) | ||||
) | ||||
def to_package( | def to_package( | |||
self, name=None, extras=None, root_dir=None | self, | |||
): # type: (Optional[str], Optional[List[str]], Optional[Path]) -> Package | name: str | None = None, | |||
extras: list[str] | None = None, | ||||
root_dir: Path | None = None, | ||||
) -> Package: | ||||
""" | """ | |||
Create a new `poetry.core.packages.package.Package` instance using metad | Create a new `poetry.core.packages.package.Package` instance using metad | |||
ata from this instance. | ata from | |||
this instance. | ||||
:param name: Name to use for the package, if not specified name from thi | :param name: Name to use for the package, if not specified name from thi | |||
s instance is used. | s | |||
instance is used. | ||||
:param extras: Extras to activate for this package. | :param extras: Extras to activate for this package. | |||
:param root_dir: Optional root directory to use for the package. If set | :param root_dir: Optional root directory to use for the package. If set | |||
, dependency strings | , | |||
will be parsed relative to this directory. | dependency strings will be parsed relative to this directory. | |||
""" | """ | |||
name = name or self.name | name = name or self.name | |||
if not name: | ||||
raise RuntimeError("Unable to create package with no name") | ||||
if not self.version: | if not self.version: | |||
# The version could not be determined, so we raise an error since it | # The version could not be determined, so we raise an error since it | |||
is mandatory. | is | |||
raise RuntimeError( | # mandatory. | |||
"Unable to retrieve the package version for {}".format(name) | raise RuntimeError(f"Unable to retrieve the package version for {nam | |||
) | e}") | |||
package = Package( | package = Package( | |||
name=name, | name=name, | |||
version=self.version, | version=self.version, | |||
source_type=self._source_type, | source_type=self._source_type, | |||
source_url=self._source_url, | source_url=self._source_url, | |||
source_reference=self._source_reference, | source_reference=self._source_reference, | |||
yanked=self.yanked, | ||||
) | ) | |||
package.description = self.summary | if self.summary is not None: | |||
package.description = self.summary | ||||
package.root_dir = root_dir | package.root_dir = root_dir | |||
package.python_versions = self.requires_python or "*" | package.python_versions = self.requires_python or "*" | |||
package.files = self.files | package.files = self.files | |||
if root_dir or (self._source_type in {"directory"} and self._source_url) | # If this is a local poetry project, we can extract "richer" requirement | |||
: | # information, eg: development requirements etc. | |||
# this is a local poetry project, this means we can extract "richer" | if root_dir is not None: | |||
requirement information | path = root_dir | |||
# eg: development requirements etc. | elif self._source_type == "directory" and self._source_url is not None: | |||
poetry_package = self._get_poetry_package(path=root_dir or self._sou | path = Path(self._source_url) | |||
rce_url) | else: | |||
path = None | ||||
if path is not None: | ||||
poetry_package = self._get_poetry_package(path=path) | ||||
if poetry_package: | if poetry_package: | |||
package.extras = poetry_package.extras | package.extras = poetry_package.extras | |||
package.requires = poetry_package.requires | for dependency in poetry_package.requires: | |||
package.add_dependency(dependency) | ||||
return package | return package | |||
seen_requirements = set() | seen_requirements = set() | |||
for req in self.requires_dist or []: | for req in self.requires_dist or []: | |||
try: | try: | |||
# Attempt to parse the PEP-508 requirement string | # Attempt to parse the PEP-508 requirement string | |||
dependency = dependency_from_pep_508(req, relative_to=root_dir) | dependency = Dependency.create_from_pep_508(req, relative_to=roo t_dir) | |||
except InvalidMarker: | except InvalidMarker: | |||
# Invalid marker, We strip the markers hoping for the best | # Invalid marker, We strip the markers hoping for the best | |||
req = req.split(";")[0] | req = req.split(";")[0] | |||
dependency = dependency_from_pep_508(req, relative_to=root_dir) | dependency = Dependency.create_from_pep_508(req, relative_to=roo t_dir) | |||
except ValueError: | except ValueError: | |||
# Likely unable to parse constraint so we skip it | # Likely unable to parse constraint so we skip it | |||
self._log( | logger.debug( | |||
"Invalid constraint ({}) found in {}-{} dependencies, " | f"Invalid constraint ({req}) found in" | |||
"skipping".format(req, package.name, package.version), | f" {package.name}-{package.version} dependencies, skipping", | |||
level="warning", | ||||
) | ) | |||
continue | continue | |||
if dependency.in_extras: | if dependency.in_extras: | |||
# this dependency is required by an extra package | # this dependency is required by an extra package | |||
for extra in dependency.in_extras: | for extra in dependency.in_extras: | |||
if extra not in package.extras: | if extra not in package.extras: | |||
# this is the first time we encounter this extra for thi | # this is the first time we encounter this extra for thi | |||
s package | s | |||
# package | ||||
package.extras[extra] = [] | package.extras[extra] = [] | |||
package.extras[extra].append(dependency) | package.extras[extra].append(dependency) | |||
req = dependency.to_pep_508(with_extras=True) | req = dependency.to_pep_508(with_extras=True) | |||
if req not in seen_requirements: | if req not in seen_requirements: | |||
package.requires.append(dependency) | package.add_dependency(dependency) | |||
seen_requirements.add(req) | seen_requirements.add(req) | |||
return package | return package | |||
@classmethod | @classmethod | |||
def _from_distribution( | def _from_distribution( | |||
cls, dist | cls, dist: pkginfo.BDist | pkginfo.SDist | pkginfo.Wheel | |||
): # type: (Union[pkginfo.BDist, pkginfo.SDist, pkginfo.Wheel]) -> PackageI | ) -> PackageInfo: | |||
nfo | ||||
""" | """ | |||
Helper method to parse package information from a `pkginfo.Distribution` | Helper method to parse package information from a `pkginfo.Distribution` | |||
instance. | instance. | |||
:param dist: The distribution instance to parse information from. | :param dist: The distribution instance to parse information from. | |||
""" | """ | |||
requirements = None | requirements = None | |||
if dist.requires_dist: | if dist.requires_dist: | |||
requirements = list(dist.requires_dist) | requirements = list(dist.requires_dist) | |||
else: | else: | |||
requires = Path(dist.filename) / "requires.txt" | requires = Path(dist.filename) / "requires.txt" | |||
if requires.exists(): | if requires.exists(): | |||
skipping to change at line 235 | skipping to change at line 263 | |||
requires_dist=requirements, | requires_dist=requirements, | |||
requires_python=dist.requires_python, | requires_python=dist.requires_python, | |||
) | ) | |||
info._source_type = "file" | info._source_type = "file" | |||
info._source_url = Path(dist.filename).resolve().as_posix() | info._source_url = Path(dist.filename).resolve().as_posix() | |||
return info | return info | |||
@classmethod | @classmethod | |||
def _from_sdist_file(cls, path): # type: (Path) -> PackageInfo | def _from_sdist_file(cls, path: Path) -> PackageInfo: | |||
""" | """ | |||
Helper method to parse package information from an sdist file. We attemp | Helper method to parse package information from an sdist file. We attemp | |||
t to first inspect the | t to | |||
file using `pkginfo.SDist`. If this does not provide us with package req | first inspect the file using `pkginfo.SDist`. If this does not provide u | |||
uirements, we extract the | s with | |||
source and handle it as a directory. | package requirements, we extract the source and handle it as a directory | |||
. | ||||
:param path: The sdist file to parse information from. | :param path: The sdist file to parse information from. | |||
""" | """ | |||
info = None | info = None | |||
try: | try: | |||
info = cls._from_distribution(pkginfo.SDist(str(path))) | info = cls._from_distribution(pkginfo.SDist(str(path))) | |||
except ValueError: | except ValueError: | |||
# Unable to determine dependencies | # Unable to determine dependencies | |||
# We pass and go deeper | # We pass and go deeper | |||
pass | pass | |||
else: | else: | |||
if info.requires_dist is not None: | if info.requires_dist is not None: | |||
# we successfully retrieved dependencies from sdist metadata | # we successfully retrieved dependencies from sdist metadata | |||
return info | return info | |||
# Still not dependencies found | # Still not dependencies found | |||
# So, we unpack and introspect | # So, we unpack and introspect | |||
suffix = path.suffix | suffix = path.suffix | |||
context: Callable[ | ||||
[str], AbstractContextManager[zipfile.ZipFile | tarfile.TarFile] | ||||
] | ||||
if suffix == ".zip": | if suffix == ".zip": | |||
context = zipfile.ZipFile | context = zipfile.ZipFile | |||
else: | else: | |||
if suffix == ".bz2": | if suffix == ".bz2": | |||
suffixes = path.suffixes | suffixes = path.suffixes | |||
if len(suffixes) > 1 and suffixes[-2] == ".tar": | if len(suffixes) > 1 and suffixes[-2] == ".tar": | |||
suffix = ".tar.bz2" | suffix = ".tar.bz2" | |||
else: | else: | |||
suffix = ".tar.gz" | suffix = ".tar.gz" | |||
context = tarfile.open | context = tarfile.open | |||
with temporary_directory() as tmp: | with temporary_directory() as tmp_str: | |||
tmp = Path(tmp) | tmp = Path(tmp_str) | |||
with context(path.as_posix()) as archive: | with context(path.as_posix()) as archive: | |||
archive.extractall(tmp.as_posix()) | archive.extractall(tmp.as_posix()) | |||
# a little bit of guess work to determine the directory we care abou t | # a little bit of guess work to determine the directory we care abou t | |||
elements = list(tmp.glob("*")) | elements = list(tmp.glob("*")) | |||
if len(elements) == 1 and elements[0].is_dir(): | if len(elements) == 1 and elements[0].is_dir(): | |||
sdist_dir = elements[0] | sdist_dir = elements[0] | |||
else: | else: | |||
sdist_dir = tmp / path.name.rstrip(suffix) | sdist_dir = tmp / path.name.rstrip(suffix) | |||
skipping to change at line 296 | skipping to change at line 327 | |||
# now this is an unpacked directory we know how to deal with | # now this is an unpacked directory we know how to deal with | |||
new_info = cls.from_directory(path=sdist_dir) | new_info = cls.from_directory(path=sdist_dir) | |||
if not info: | if not info: | |||
return new_info | return new_info | |||
return info.update(new_info) | return info.update(new_info) | |||
@staticmethod | @staticmethod | |||
def has_setup_files(path): # type: (Path) -> bool | def has_setup_files(path: Path) -> bool: | |||
return any((path / f).exists() for f in SetupReader.FILES) | return any((path / f).exists() for f in SetupReader.FILES) | |||
@classmethod | @classmethod | |||
def from_setup_files(cls, path): # type: (Path) -> PackageInfo | def from_setup_files(cls, path: Path) -> PackageInfo: | |||
""" | """ | |||
Mechanism to parse package information from a `setup.[py|cfg]` file. Thi | Mechanism to parse package information from a `setup.[py|cfg]` file. Thi | |||
s uses the implementation | s uses | |||
at `poetry.utils.setup_reader.SetupReader` in order to parse the file. T | the implementation at `poetry.utils.setup_reader.SetupReader` in order t | |||
his is not reliable for | o parse | |||
complex setup files and should only attempted as a fallback. | the file. This is not reliable for complex setup files and should only a | |||
ttempted | ||||
as a fallback. | ||||
:param path: Path to `setup.py` file | :param path: Path to `setup.py` file | |||
""" | """ | |||
if not cls.has_setup_files(path): | if not cls.has_setup_files(path): | |||
raise PackageInfoError( | raise PackageInfoError( | |||
path, "No setup files (setup.py, setup.cfg) was found." | path, "No setup files (setup.py, setup.cfg) was found." | |||
) | ) | |||
try: | try: | |||
result = SetupReader.read_from_directory(path) | result = SetupReader.read_from_directory(path) | |||
except Exception as e: | except Exception as e: | |||
raise PackageInfoError(path, e) | raise PackageInfoError(path, e) | |||
python_requires = result["python_requires"] | python_requires = result["python_requires"] | |||
if python_requires is None: | if python_requires is None: | |||
python_requires = "*" | python_requires = "*" | |||
requires = "" | requires = "".join(dep + "\n" for dep in result["install_requires"]) | |||
for dep in result["install_requires"]: | ||||
requires += dep + "\n" | ||||
if result["extras_require"]: | if result["extras_require"]: | |||
requires += "\n" | requires += "\n" | |||
for extra_name, deps in result["extras_require"].items(): | for extra_name, deps in result["extras_require"].items(): | |||
requires += "[{}]\n".format(extra_name) | requires += f"[{extra_name}]\n" | |||
for dep in deps: | for dep in deps: | |||
requires += dep + "\n" | requires += dep + "\n" | |||
requires += "\n" | requires += "\n" | |||
requirements = parse_requires(requires) | requirements = parse_requires(requires) | |||
info = cls( | info = cls( | |||
name=result.get("name"), | name=result.get("name"), | |||
skipping to change at line 357 | skipping to change at line 386 | |||
if not (info.name and info.version) and not info.requires_dist: | if not (info.name and info.version) and not info.requires_dist: | |||
# there is nothing useful here | # there is nothing useful here | |||
raise PackageInfoError( | raise PackageInfoError( | |||
path, | path, | |||
"No core metadata (name, version, requires-dist) could be retrie ved.", | "No core metadata (name, version, requires-dist) could be retrie ved.", | |||
) | ) | |||
return info | return info | |||
@staticmethod | @staticmethod | |||
def _find_dist_info(path): # type: (Path) -> Iterator[Path] | def _find_dist_info(path: Path) -> Iterator[Path]: | |||
""" | """ | |||
Discover all `*.*-info` directories in a given path. | Discover all `*.*-info` directories in a given path. | |||
:param path: Path to search. | :param path: Path to search. | |||
""" | """ | |||
pattern = "**/*.*-info" | pattern = "**/*.*-info" | |||
if PY35: | # Sometimes pathlib will fail on recursive symbolic links, so we need to | |||
# Sometimes pathlib will fail on recursive symbolic links, so we nee | work | |||
d to workaround it | # around it and use the glob module instead. Note that this does not hap | |||
# and use the glob module instead. Note that this does not happen wi | pen with | |||
th pathlib2 | # pathlib2 so it's safe to use it for Python < 3.4. | |||
# so it's safe to use it for Python < 3.4. | directories = glob.iglob(path.joinpath(pattern).as_posix(), recursive=Tr | |||
directories = glob.iglob(path.joinpath(pattern).as_posix(), recursiv | ue) | |||
e=True) | ||||
else: | ||||
directories = path.glob(pattern) | ||||
for d in directories: | for d in directories: | |||
yield Path(d) | yield Path(d) | |||
@classmethod | @classmethod | |||
def from_metadata(cls, path): # type: (Path) -> Optional[PackageInfo] | def from_metadata(cls, path: Path) -> PackageInfo | None: | |||
""" | """ | |||
Helper method to parse package information from an unpacked metadata dir ectory. | Helper method to parse package information from an unpacked metadata dir ectory. | |||
:param path: The metadata directory to parse information from. | :param path: The metadata directory to parse information from. | |||
""" | """ | |||
if path.suffix in {".dist-info", ".egg-info"}: | if path.suffix in {".dist-info", ".egg-info"}: | |||
directories = [path] | directories = [path] | |||
else: | else: | |||
directories = cls._find_dist_info(path=path) | directories = list(cls._find_dist_info(path=path)) | |||
for directory in directories: | for directory in directories: | |||
try: | try: | |||
if directory.suffix == ".egg-info": | if directory.suffix == ".egg-info": | |||
dist = pkginfo.UnpackedSDist(directory.as_posix()) | dist = pkginfo.UnpackedSDist(directory.as_posix()) | |||
elif directory.suffix == ".dist-info": | elif directory.suffix == ".dist-info": | |||
dist = pkginfo.Wheel(directory.as_posix()) | dist = pkginfo.Wheel(directory.as_posix()) | |||
else: | else: | |||
continue | continue | |||
break | break | |||
except ValueError: | except ValueError: | |||
continue | continue | |||
else: | else: | |||
try: | try: | |||
# handle PKG-INFO in unpacked sdist root | # handle PKG-INFO in unpacked sdist root | |||
dist = pkginfo.UnpackedSDist(path.as_posix()) | dist = pkginfo.UnpackedSDist(path.as_posix()) | |||
except ValueError: | except ValueError: | |||
return | return None | |||
info = cls._from_distribution(dist=dist) | return cls._from_distribution(dist=dist) | |||
if info: | ||||
return info | ||||
@classmethod | @classmethod | |||
def from_package(cls, package): # type: (Package) -> PackageInfo | def from_package(cls, package: Package) -> PackageInfo: | |||
""" | """ | |||
Helper method to inspect a `Package` object, in order to generate packag e info. | Helper method to inspect a `Package` object, in order to generate packag e info. | |||
:param package: This must be a poetry package instance. | :param package: This must be a poetry package instance. | |||
""" | """ | |||
requires = {dependency.to_pep_508() for dependency in package.requires} | requires = {dependency.to_pep_508() for dependency in package.requires} | |||
for extra_requires in package.extras.values(): | for extra_requires in package.extras.values(): | |||
for dependency in extra_requires: | for dependency in extra_requires: | |||
requires.add(dependency.to_pep_508()) | requires.add(dependency.to_pep_508()) | |||
return cls( | return cls( | |||
name=package.name, | name=package.name, | |||
version=str(package.version), | version=str(package.version), | |||
summary=package.description, | summary=package.description, | |||
platform=package.platform, | platform=package.platform, | |||
requires_dist=list(requires), | requires_dist=list(requires), | |||
requires_python=package.python_versions, | requires_python=package.python_versions, | |||
files=package.files, | files=package.files, | |||
yanked=package.yanked_reason if package.yanked else False, | ||||
) | ) | |||
@staticmethod | @staticmethod | |||
def _get_poetry_package(path): # type: (Path) -> Optional[ProjectPackage] | def _get_poetry_package(path: Path) -> ProjectPackage | None: | |||
# Note: we ignore any setup.py file at this step | # Note: we ignore any setup.py file at this step | |||
# TODO: add support for handling non-poetry PEP-517 builds | # TODO: add support for handling non-poetry PEP-517 builds | |||
if PyProjectTOML(path.joinpath("pyproject.toml")).is_poetry_project(): | if PyProjectTOML(path.joinpath("pyproject.toml")).is_poetry_project(): | |||
try: | with contextlib.suppress(RuntimeError): | |||
return Factory().create_poetry(path).package | return Factory().create_poetry(path).package | |||
except RuntimeError: | ||||
return None | ||||
return None | return None | |||
@classmethod | @classmethod | |||
def _pep517_metadata(cls, path): # type (Path) -> PackageInfo | def from_directory(cls, path: Path, disable_build: bool = False) -> PackageI | |||
""" | nfo: | |||
Helper method to use PEP-517 library to build and read package metadata. | ||||
:param path: Path to package source to build and read metadata for. | ||||
""" | ||||
info = None | ||||
try: | ||||
info = cls.from_setup_files(path) | ||||
if all([info.version, info.name, info.requires_dist]): | ||||
return info | ||||
except PackageInfoError: | ||||
pass | ||||
with temporary_directory() as tmp_dir: | ||||
# TODO: cache PEP 517 build environment corresponding to each projec | ||||
t venv | ||||
venv_dir = Path(tmp_dir) / ".venv" | ||||
EnvManager.build_venv(venv_dir.as_posix()) | ||||
venv = VirtualEnv(venv_dir, venv_dir) | ||||
dest_dir = Path(tmp_dir) / "dist" | ||||
dest_dir.mkdir() | ||||
try: | ||||
venv.run_python( | ||||
"-m", | ||||
"pip", | ||||
"install", | ||||
"--disable-pip-version-check", | ||||
"--ignore-installed", | ||||
*PEP517_META_BUILD_DEPS | ||||
) | ||||
venv.run_python( | ||||
"-", | ||||
input_=PEP517_META_BUILD.format( | ||||
source=path.as_posix(), dest=dest_dir.as_posix() | ||||
), | ||||
) | ||||
return cls.from_metadata(dest_dir) | ||||
except EnvCommandError as e: | ||||
# something went wrong while attempting pep517 metadata build | ||||
# fallback to egg_info if setup.py available | ||||
cls._log("PEP517 build failed: {}".format(e), level="debug") | ||||
setup_py = path / "setup.py" | ||||
if not setup_py.exists(): | ||||
raise PackageInfoError( | ||||
path, | ||||
e, | ||||
"No fallback setup.py file was found to generate egg_inf | ||||
o.", | ||||
) | ||||
cwd = Path.cwd() | ||||
os.chdir(path.as_posix()) | ||||
try: | ||||
venv.run_python("setup.py", "egg_info") | ||||
return cls.from_metadata(path) | ||||
except EnvCommandError as fbe: | ||||
raise PackageInfoError( | ||||
path, "Fallback egg_info generation failed.", fbe | ||||
) | ||||
finally: | ||||
os.chdir(cwd.as_posix()) | ||||
if info: | ||||
cls._log( | ||||
"Falling back to parsed setup.py file for {}".format(path), "deb | ||||
ug" | ||||
) | ||||
return info | ||||
# if we reach here, everything has failed and all hope is lost | ||||
raise PackageInfoError(path, "Exhausted all core metadata sources.") | ||||
@classmethod | ||||
def from_directory( | ||||
cls, path, disable_build=False | ||||
): # type: (Path, bool) -> PackageInfo | ||||
""" | """ | |||
Generate package information from a package source directory. If `disabl | Generate package information from a package source directory. If `disabl | |||
e_build` is not `True` and | e_build` | |||
introspection of all available metadata fails, the package is attempted | is not `True` and introspection of all available metadata fails, the pac | |||
to be build in an isolated | kage is | |||
environment so as to generate required metadata. | attempted to be built in an isolated environment so as to generate requi | |||
red | ||||
metadata. | ||||
:param path: Path to generate package information from. | :param path: Path to generate package information from. | |||
:param disable_build: If not `True` and setup reader fails, PEP 517 isol | :param disable_build: If not `True` and setup reader fails, PEP 517 isol | |||
ated build is attempted in | ated | |||
order to gather metadata. | build is attempted in order to gather metadata. | |||
""" | """ | |||
project_package = cls._get_poetry_package(path) | project_package = cls._get_poetry_package(path) | |||
info: PackageInfo | None | ||||
if project_package: | if project_package: | |||
info = cls.from_package(project_package) | info = cls.from_package(project_package) | |||
else: | else: | |||
info = cls.from_metadata(path) | info = cls.from_metadata(path) | |||
if not info or info.requires_dist is None: | if not info or info.requires_dist is None: | |||
try: | try: | |||
if disable_build: | if disable_build: | |||
info = cls.from_setup_files(path) | info = cls.from_setup_files(path) | |||
else: | else: | |||
info = cls._pep517_metadata(path) | info = get_pep517_metadata(path) | |||
except PackageInfoError: | except PackageInfoError: | |||
if not info: | if not info: | |||
raise | raise | |||
# we discovered PkgInfo but no requirements were listed | # we discovered PkgInfo but no requirements were listed | |||
info._source_type = "directory" | info._source_type = "directory" | |||
info._source_url = path.as_posix() | info._source_url = path.as_posix() | |||
return info | return info | |||
@classmethod | @classmethod | |||
def from_sdist(cls, path): # type: (Path) -> PackageInfo | def from_sdist(cls, path: Path) -> PackageInfo: | |||
""" | """ | |||
Gather package information from an sdist file, packed or unpacked. | Gather package information from an sdist file, packed or unpacked. | |||
:param path: Path to an sdist file or unpacked directory. | :param path: Path to an sdist file or unpacked directory. | |||
""" | """ | |||
if path.is_file(): | if path.is_file(): | |||
return cls._from_sdist_file(path=path) | return cls._from_sdist_file(path=path) | |||
# if we get here then it is neither an sdist instance nor a file | # if we get here then it is neither an sdist instance nor a file | |||
# so, we assume this is an directory | # so, we assume this is an directory | |||
return cls.from_directory(path=path) | return cls.from_directory(path=path) | |||
@classmethod | @classmethod | |||
def from_wheel(cls, path): # type: (Path) -> PackageInfo | def from_wheel(cls, path: Path) -> PackageInfo: | |||
""" | """ | |||
Gather package information from a wheel. | Gather package information from a wheel. | |||
:param path: Path to wheel. | :param path: Path to wheel. | |||
""" | """ | |||
try: | try: | |||
return cls._from_distribution(pkginfo.Wheel(str(path))) | return cls._from_distribution(pkginfo.Wheel(str(path))) | |||
except ValueError: | except ValueError: | |||
return PackageInfo() | return PackageInfo() | |||
@classmethod | @classmethod | |||
def from_bdist(cls, path): # type: (Path) -> PackageInfo | def from_bdist(cls, path: Path) -> PackageInfo: | |||
""" | """ | |||
Gather package information from a bdist (wheel etc.). | Gather package information from a bdist (wheel etc.). | |||
:param path: Path to bdist. | :param path: Path to bdist. | |||
""" | """ | |||
if isinstance(path, (pkginfo.BDist, pkginfo.Wheel)): | if isinstance(path, (pkginfo.BDist, pkginfo.Wheel)): | |||
cls._from_distribution(dist=path) | cls._from_distribution(dist=path) | |||
if path.suffix == ".whl": | if path.suffix == ".whl": | |||
return cls.from_wheel(path=path) | return cls.from_wheel(path=path) | |||
try: | try: | |||
return cls._from_distribution(pkginfo.BDist(str(path))) | return cls._from_distribution(pkginfo.BDist(str(path))) | |||
except ValueError as e: | except ValueError as e: | |||
raise PackageInfoError(path, e) | raise PackageInfoError(path, e) | |||
@classmethod | @classmethod | |||
def from_path(cls, path): # type: (Path) -> PackageInfo | def from_path(cls, path: Path) -> PackageInfo: | |||
""" | """ | |||
Gather package information from a given path (bdist, sdist, directory). | Gather package information from a given path (bdist, sdist, directory). | |||
:param path: Path to inspect. | :param path: Path to inspect. | |||
""" | """ | |||
try: | try: | |||
return cls.from_bdist(path=path) | return cls.from_bdist(path=path) | |||
except PackageInfoError: | except PackageInfoError: | |||
return cls.from_sdist(path=path) | return cls.from_sdist(path=path) | |||
@functools.lru_cache(maxsize=None) | ||||
def get_pep517_metadata(path: Path) -> PackageInfo: | ||||
""" | ||||
Helper method to use PEP-517 library to build and read package metadata. | ||||
:param path: Path to package source to build and read metadata for. | ||||
""" | ||||
info = None | ||||
with contextlib.suppress(PackageInfoError): | ||||
info = PackageInfo.from_setup_files(path) | ||||
if all([info.version, info.name, info.requires_dist]): | ||||
return info | ||||
with ephemeral_environment( | ||||
flags={"no-pip": False, "no-setuptools": False, "no-wheel": False} | ||||
) as venv: | ||||
# TODO: cache PEP 517 build environment corresponding to each project ve | ||||
nv | ||||
dest_dir = venv.path.parent / "dist" | ||||
dest_dir.mkdir() | ||||
pep517_meta_build_script = PEP517_META_BUILD.format( | ||||
source=path.as_posix(), dest=dest_dir.as_posix() | ||||
) | ||||
try: | ||||
venv.run_pip( | ||||
"install", | ||||
"--disable-pip-version-check", | ||||
"--ignore-installed", | ||||
*PEP517_META_BUILD_DEPS, | ||||
) | ||||
venv.run( | ||||
"python", | ||||
"-", | ||||
input_=pep517_meta_build_script, | ||||
) | ||||
info = PackageInfo.from_metadata(dest_dir) | ||||
except EnvCommandError as e: | ||||
# something went wrong while attempting pep517 metadata build | ||||
# fallback to egg_info if setup.py available | ||||
logger.debug("PEP517 build failed: %s", e) | ||||
setup_py = path / "setup.py" | ||||
if not setup_py.exists(): | ||||
raise PackageInfoError( | ||||
path, | ||||
e, | ||||
"No fallback setup.py file was found to generate egg_info.", | ||||
) | ||||
cwd = Path.cwd() | ||||
os.chdir(path.as_posix()) | ||||
try: | ||||
venv.run("python", "setup.py", "egg_info") | ||||
info = PackageInfo.from_metadata(path) | ||||
except EnvCommandError as fbe: | ||||
raise PackageInfoError( | ||||
path, "Fallback egg_info generation failed.", fbe | ||||
) | ||||
finally: | ||||
os.chdir(cwd.as_posix()) | ||||
if info: | ||||
logger.debug("Falling back to parsed setup.py file for %s", path) | ||||
return info | ||||
# if we reach here, everything has failed and all hope is lost | ||||
raise PackageInfoError(path, "Exhausted all core metadata sources.") | ||||
End of changes. 67 change blocks. | ||||
225 lines changed or deleted | 173 lines changed or added |