"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "poetry/inspection/info.py" between
poetry-1.1.15.tar.gz and poetry-1.2.0.tar.gz

About: Poetry is a tool for dependency management and packaging in Python.

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

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)