Commit 3b30d2a1 by Sébastien Eustace Committed by GitHub

Use a lightweight core library (#2212)

* Use poetry-core

* Upgrade dependencies

* No longer use Poetry as a build backend but poetry-core instead

* Use the latest version of poetry-core
parent 8224b164
...@@ -42,8 +42,8 @@ So, in your `pyproject.toml` file, add this section if it does not already exist ...@@ -42,8 +42,8 @@ So, in your `pyproject.toml` file, add this section if it does not already exist
```toml ```toml
[build-system] [build-system]
requires = ["poetry>=0.12"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.masonry.api" build-backend = "poetry.core.masonry.api"
``` ```
And use a `tox.ini` configuration file similar to this: And use a `tox.ini` configuration file similar to this:
......
...@@ -274,16 +274,22 @@ If you publish you package on PyPI, they will appear in the `Project Links` sect ...@@ -274,16 +274,22 @@ If you publish you package on PyPI, they will appear in the `Project Links` sect
[PEP-517](https://www.python.org/dev/peps/pep-0517/) introduces a standard way [PEP-517](https://www.python.org/dev/peps/pep-0517/) introduces a standard way
to define alternative build systems to build a Python project. to define alternative build systems to build a Python project.
Poetry is compliant with PEP-517 so if you use Poetry to manage your Python Poetry is compliant with PEP-517, by providing a lightweight core library,
project you should reference it in the `build-system` section of the `pyproject.toml` so if you use Poetry to manage your Python project you should reference
file like so: it in the `build-system` section of the `pyproject.toml` file like so:
```toml ```toml
[build-system] [build-system]
requires = ["poetry>=0.12"] requires = ["poetry_core>=1.0.0"]
build-backend = "poetry.masonry.api" build-backend = "poetry.core.masonry.api"
``` ```
!!!note !!!note
When using the `new` or `init` command this section will be automatically added. When using the `new` or `init` command this section will be automatically added.
!!!note
If your `pyproject.toml` file still references `poetry` directly as a build backend,
you should update it to reference `poetry_core` instead.
...@@ -204,8 +204,13 @@ import sys ...@@ -204,8 +204,13 @@ import sys
import os import os
lib = os.path.normpath(os.path.join(os.path.realpath(__file__), "../..", "lib")) lib = os.path.normpath(os.path.join(os.path.realpath(__file__), "../..", "lib"))
vendors = os.path.join(lib, "poetry", "_vendor")
current_vendors = os.path.join(
vendors, "py{}".format(".".join(str(v) for v in sys.version_info[:2]))
)
sys.path.insert(0, lib) sys.path.insert(0, lib)
sys.path.insert(0, current_vendors)
if __name__ == "__main__": if __name__ == "__main__":
from poetry.console import main from poetry.console import main
......
import os from pkgutil import extend_path
import sys
from .__version__ import __version__ # noqa
__path__ = extend_path(__path__, __name__)
_ROOT = os.path.dirname(os.path.realpath(__file__))
_VENDOR = os.path.join(_ROOT, "_vendor")
_CURRENT_VENDOR = os.path.join(
_VENDOR, "py{}".format(".".join(str(v) for v in sys.version_info[:2]))
)
# Add vendored dependencies to path.
sys.path.insert(0, _CURRENT_VENDOR)
from cleo import Application as BaseApplication from cleo import Application as BaseApplication
from poetry import __version__ from poetry.__version__ import __version__
from .commands.about import AboutCommand from .commands.about import AboutCommand
from .commands.add import AddCommand from .commands.add import AddCommand
......
...@@ -56,7 +56,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba ...@@ -56,7 +56,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
def handle(self): def handle(self):
from poetry.installation.installer import Installer from poetry.installation.installer import Installer
from poetry.semver import parse_constraint from poetry.core.semver import parse_constraint
from tomlkit import inline_table from tomlkit import inline_table
packages = self.argument("name") packages = self.argument("name")
......
...@@ -13,7 +13,7 @@ class BuildCommand(EnvCommand): ...@@ -13,7 +13,7 @@ class BuildCommand(EnvCommand):
] ]
def handle(self): def handle(self):
from poetry.masonry import Builder from poetry.core.masonry import Builder
fmt = "all" fmt = "all"
if self.option("format"): if self.option("format"):
...@@ -26,5 +26,5 @@ class BuildCommand(EnvCommand): ...@@ -26,5 +26,5 @@ class BuildCommand(EnvCommand):
) )
) )
builder = Builder(self.poetry, self.env, self.io) builder = Builder(self.poetry)
builder.build(fmt) builder.build(fmt)
...@@ -29,7 +29,7 @@ class DebugResolveCommand(InitCommand): ...@@ -29,7 +29,7 @@ class DebugResolveCommand(InitCommand):
def handle(self): def handle(self):
from poetry.io.null_io import NullIO from poetry.io.null_io import NullIO
from poetry.packages import ProjectPackage from poetry.core.packages import ProjectPackage
from poetry.puzzle import Solver from poetry.puzzle import Solver
from poetry.repositories.pool import Pool from poetry.repositories.pool import Pool
from poetry.repositories.repository import Repository from poetry.repositories.repository import Repository
......
...@@ -64,7 +64,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -64,7 +64,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
from poetry.layouts import layout from poetry.layouts import layout
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.env import SystemEnv from poetry.utils.env import SystemEnv
from poetry.vcs.git import GitConfig from poetry.core.vcs.git import GitConfig
if (Path.cwd() / "pyproject.toml").exists(): if (Path.cwd() / "pyproject.toml").exists():
self.line("<error>A pyproject.toml file already exists.</error>") self.line("<error>A pyproject.toml file already exists.</error>")
...@@ -367,8 +367,8 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -367,8 +367,8 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
if url_parsed.scheme and url_parsed.netloc: if url_parsed.scheme and url_parsed.netloc:
# Url # Url
if url_parsed.scheme in ["git+https", "git+ssh"]: if url_parsed.scheme in ["git+https", "git+ssh"]:
from poetry.vcs.git import Git from poetry.core.vcs.git import Git
from poetry.vcs.git import ParsedUrl from poetry.core.vcs.git import ParsedUrl
parsed = ParsedUrl.parse(requirement) parsed = ParsedUrl.parse(requirement)
url = Git.normalize_url(requirement) url = Git.normalize_url(requirement)
...@@ -481,7 +481,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -481,7 +481,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
return requires return requires
def _validate_author(self, author, default): def _validate_author(self, author, default):
from poetry.packages.package import AUTHOR_REGEX from poetry.core.packages.package import AUTHOR_REGEX
author = author or default author = author or default
...@@ -498,7 +498,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the ...@@ -498,7 +498,7 @@ The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the
return author return author
def _validate_license(self, license): def _validate_license(self, license):
from poetry.spdx import license_by_id from poetry.core.spdx import license_by_id
if license: if license:
license_by_id(license) license_by_id(license)
......
...@@ -42,7 +42,7 @@ exist it will look for <comment>pyproject.toml</> and do the same. ...@@ -42,7 +42,7 @@ exist it will look for <comment>pyproject.toml</> and do the same.
from clikit.io import NullIO from clikit.io import NullIO
from poetry.installation.installer import Installer from poetry.installation.installer import Installer
from poetry.masonry.builders import EditableBuilder from poetry.masonry.builders import EditableBuilder
from poetry.masonry.utils.module import ModuleOrPackageNotFound from poetry.core.masonry.utils.module import ModuleOrPackageNotFound
installer = Installer( installer = Installer(
self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool self.io, self.env, self.poetry.package, self.poetry.locker, self.poetry.pool
......
...@@ -21,10 +21,10 @@ class NewCommand(Command): ...@@ -21,10 +21,10 @@ class NewCommand(Command):
def handle(self): def handle(self):
from poetry.layouts import layout from poetry.layouts import layout
from poetry.semver import parse_constraint from poetry.core.semver import parse_constraint
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.env import SystemEnv from poetry.utils.env import SystemEnv
from poetry.vcs.git import GitConfig from poetry.core.vcs.git import GitConfig
if self.option("src"): if self.option("src"):
layout_ = layout("src") layout_ = layout("src")
......
...@@ -40,7 +40,7 @@ the config command. ...@@ -40,7 +40,7 @@ the config command.
loggers = ["poetry.masonry.publishing.publisher"] loggers = ["poetry.masonry.publishing.publisher"]
def handle(self): def handle(self):
from poetry.masonry.publishing.publisher import Publisher from poetry.publishing.publisher import Publisher
publisher = Publisher(self.poetry, self.io) publisher = Publisher(self.poetry, self.io)
......
...@@ -54,7 +54,7 @@ class SelfUpdateCommand(Command): ...@@ -54,7 +54,7 @@ class SelfUpdateCommand(Command):
def handle(self): def handle(self):
from poetry.__version__ import __version__ from poetry.__version__ import __version__
from poetry.repositories.pypi_repository import PyPiRepository from poetry.repositories.pypi_repository import PyPiRepository
from poetry.semver import Version from poetry.core.semver import Version
from poetry.utils._compat import Path from poetry.utils._compat import Path
current = Path(__file__) current = Path(__file__)
......
...@@ -35,7 +35,7 @@ lists all packages available.""" ...@@ -35,7 +35,7 @@ lists all packages available."""
def handle(self): def handle(self):
from clikit.utils.terminal import Terminal from clikit.utils.terminal import Terminal
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
from poetry.semver import Version from poetry.core.semver import Version
package = self.argument("package") package = self.argument("package")
...@@ -347,7 +347,7 @@ lists all packages available.""" ...@@ -347,7 +347,7 @@ lists all packages available."""
return selector.find_best_candidate(name, ">={}".format(package.pretty_version)) return selector.find_best_candidate(name, ">={}".format(package.pretty_version))
def get_update_status(self, latest, package): def get_update_status(self, latest, package):
from poetry.semver import parse_constraint from poetry.core.semver import parse_constraint
if latest.full_pretty_version == package.full_pretty_version: if latest.full_pretty_version == package.full_pretty_version:
return "up-to-date" return "up-to-date"
......
...@@ -65,7 +65,7 @@ patch, minor, major, prepatch, preminor, premajor, prerelease. ...@@ -65,7 +65,7 @@ patch, minor, major, prepatch, preminor, premajor, prerelease.
) )
def increment_version(self, version, rule): def increment_version(self, version, rule):
from poetry.semver import Version from poetry.core.semver import Version
try: try:
version = Version.parse(version) version = Version.parse(version)
......
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import shutil
from typing import Dict from typing import Dict
from typing import List
from typing import Optional from typing import Optional
from clikit.api.io.io import IO from clikit.api.io.io import IO
from poetry.core.factory import Factory as BaseFactory
from poetry.core.utils.toml_file import TomlFile
from .config.config import Config from .config.config import Config
from .config.file_config_source import FileConfigSource from .config.file_config_source import FileConfigSource
from .io.null_io import NullIO from .io.null_io import NullIO
from .json import validate_object
from .locations import CONFIG_DIR from .locations import CONFIG_DIR
from .packages.dependency import Dependency
from .packages.locker import Locker from .packages.locker import Locker
from .packages.project_package import ProjectPackage
from .poetry import Poetry from .poetry import Poetry
from .repositories.pypi_repository import PyPiRepository from .repositories.pypi_repository import PyPiRepository
from .spdx import license_by_id
from .utils._compat import Path from .utils._compat import Path
from .utils.toml_file import TomlFile
class Factory: class Factory(BaseFactory):
""" """
Factory class to create various elements needed by Poetry. Factory class to create various elements needed by Poetry.
""" """
...@@ -35,125 +30,17 @@ class Factory: ...@@ -35,125 +30,17 @@ class Factory:
if io is None: if io is None:
io = NullIO() io = NullIO()
poetry_file = self.locate(cwd) base_poetry = super(Factory, self).create_poetry(cwd)
local_config = TomlFile(poetry_file.as_posix()).read() locker = Locker(
if "tool" not in local_config or "poetry" not in local_config["tool"]: base_poetry.file.parent / "poetry.lock", base_poetry.local_config
raise RuntimeError(
"[tool.poetry] section not found in {}".format(poetry_file.name)
) )
local_config = local_config["tool"]["poetry"]
# Checking validity
check_result = self.validate(local_config)
if check_result["errors"]:
message = ""
for error in check_result["errors"]:
message += " - {}\n".format(error)
raise RuntimeError("The Poetry configuration is invalid:\n" + message)
# Load package
name = local_config["name"]
version = local_config["version"]
package = ProjectPackage(name, version, version)
package.root_dir = poetry_file.parent
for author in local_config["authors"]:
package.authors.append(author)
for maintainer in local_config.get("maintainers", []):
package.maintainers.append(maintainer)
package.description = local_config.get("description", "")
package.homepage = local_config.get("homepage")
package.repository_url = local_config.get("repository")
package.documentation_url = local_config.get("documentation")
try:
license_ = license_by_id(local_config.get("license", ""))
except ValueError:
license_ = None
package.license = license_
package.keywords = local_config.get("keywords", [])
package.classifiers = local_config.get("classifiers", [])
if "readme" in local_config:
package.readme = Path(poetry_file.parent) / local_config["readme"]
if "platform" in local_config:
package.platform = local_config["platform"]
if "dependencies" in local_config:
for name, constraint in local_config["dependencies"].items():
if name.lower() == "python":
package.python_versions = constraint
continue
if isinstance(constraint, list):
for _constraint in constraint:
package.add_dependency(name, _constraint)
continue
package.add_dependency(name, constraint)
if "dev-dependencies" in local_config:
for name, constraint in local_config["dev-dependencies"].items():
if isinstance(constraint, list):
for _constraint in constraint:
package.add_dependency(name, _constraint, category="dev")
continue
package.add_dependency(name, constraint, category="dev")
extras = local_config.get("extras", {})
for extra_name, requirements in extras.items():
package.extras[extra_name] = []
# Checking for dependency
for req in requirements:
req = Dependency(req, "*")
for dep in package.requires:
if dep.name == req.name:
dep.in_extras.append(extra_name)
package.extras[extra_name].append(dep)
break
if "build" in local_config:
package.build = local_config["build"]
if "include" in local_config:
package.include = local_config["include"]
if "exclude" in local_config:
package.exclude = local_config["exclude"]
if "packages" in local_config:
package.packages = local_config["packages"]
# Custom urls
if "urls" in local_config:
package.custom_urls = local_config["urls"]
# Moving lock if necessary (pyproject.lock -> poetry.lock)
lock = poetry_file.parent / "poetry.lock"
if not lock.exists():
# Checking for pyproject.lock
old_lock = poetry_file.with_suffix(".lock")
if old_lock.exists():
shutil.move(str(old_lock), str(lock))
locker = Locker(poetry_file.parent / "poetry.lock", local_config)
# Loading global configuration # Loading global configuration
config = self.create_config(io) config = self.create_config(io)
# Loading local configuration # Loading local configuration
local_config_file = TomlFile(poetry_file.parent / "poetry.toml") local_config_file = TomlFile(base_poetry.file.parent / "poetry.toml")
if local_config_file.exists(): if local_config_file.exists():
if io.is_debug(): if io.is_debug():
io.write_line( io.write_line(
...@@ -162,10 +49,16 @@ class Factory: ...@@ -162,10 +49,16 @@ class Factory:
config.merge(local_config_file.read()) config.merge(local_config_file.read())
poetry = Poetry(poetry_file, local_config, package, locker, config) poetry = Poetry(
base_poetry.file.path,
base_poetry.local_config,
base_poetry.package,
locker,
config,
)
# Configuring sources # Configuring sources
for source in local_config.get("source", []): for source in poetry.local_config.get("source", []):
repository = self.create_legacy_repository(source, config) repository = self.create_legacy_repository(source, config)
is_default = source.get("default", False) is_default = source.get("default", False)
is_secondary = source.get("secondary", False) is_secondary = source.get("secondary", False)
...@@ -259,82 +152,3 @@ class Factory: ...@@ -259,82 +152,3 @@ class Factory:
cert=get_cert(auth_config, name), cert=get_cert(auth_config, name),
client_cert=get_client_cert(auth_config, name), client_cert=get_client_cert(auth_config, name),
) )
@classmethod
def validate(
cls, config, strict=False
): # type: (dict, bool) -> Dict[str, List[str]]
"""
Checks the validity of a configuration
"""
result = {"errors": [], "warnings": []}
# Schema validation errors
validation_errors = validate_object(config, "poetry-schema")
result["errors"] += validation_errors
if strict:
# If strict, check the file more thoroughly
# Checking license
license = config.get("license")
if license:
try:
license_by_id(license)
except ValueError:
result["errors"].append("{} is not a valid license".format(license))
if "dependencies" in config:
python_versions = config["dependencies"]["python"]
if python_versions == "*":
result["warnings"].append(
"A wildcard Python dependency is ambiguous. "
"Consider specifying a more explicit one."
)
for name, constraint in config["dependencies"].items():
if not isinstance(constraint, dict):
continue
if "allows-prereleases" in constraint:
result["warnings"].append(
'The "{}" dependency specifies '
'the "allows-prereleases" property, which is deprecated. '
'Use "allow-prereleases" instead.'.format(name)
)
# Checking for scripts with extras
if "scripts" in config:
scripts = config["scripts"]
for name, script in scripts.items():
if not isinstance(script, dict):
continue
extras = script["extras"]
for extra in extras:
if extra not in config["extras"]:
result["errors"].append(
'Script "{}" requires extra "{}" which is not defined.'.format(
name, extra
)
)
return result
@classmethod
def locate(cls, cwd): # type: (Path) -> Path
candidates = [Path(cwd)]
candidates.extend(Path(cwd).parents)
for path in candidates:
poetry_file = path / "pyproject.toml"
if poetry_file.exists():
return poetry_file
else:
raise RuntimeError(
"Poetry could not find a pyproject.toml file in {} or its parents".format(
cwd
)
)
...@@ -4,8 +4,9 @@ from typing import Union ...@@ -4,8 +4,9 @@ from typing import Union
from clikit.api.io import IO from clikit.api.io import IO
from clikit.io import NullIO from clikit.io import NullIO
from poetry.core.packages.package import Package
from poetry.core.semver import parse_constraint
from poetry.packages import Locker from poetry.packages import Locker
from poetry.packages import Package
from poetry.puzzle import Solver from poetry.puzzle import Solver
from poetry.puzzle.operations import Install from poetry.puzzle.operations import Install
from poetry.puzzle.operations import Uninstall from poetry.puzzle.operations import Uninstall
...@@ -14,7 +15,6 @@ from poetry.puzzle.operations.operation import Operation ...@@ -14,7 +15,6 @@ from poetry.puzzle.operations.operation import Operation
from poetry.repositories import Pool from poetry.repositories import Pool
from poetry.repositories import Repository from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
from poetry.semver import parse_constraint
from poetry.utils.extras import get_extra_package_names from poetry.utils.extras import get_extra_package_names
from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import canonicalize_name
......
...@@ -176,7 +176,7 @@ class PipInstaller(BaseInstaller): ...@@ -176,7 +176,7 @@ class PipInstaller(BaseInstaller):
return name return name
def install_directory(self, package): def install_directory(self, package):
from poetry.masonry.builder import SdistBuilder from poetry.core.masonry.builder import SdistBuilder
from poetry.factory import Factory from poetry.factory import Factory
from poetry.utils._compat import decode from poetry.utils._compat import decode
from poetry.utils.env import NullEnv from poetry.utils.env import NullEnv
...@@ -229,8 +229,8 @@ class PipInstaller(BaseInstaller): ...@@ -229,8 +229,8 @@ class PipInstaller(BaseInstaller):
os.remove(setup) os.remove(setup)
def install_git(self, package): def install_git(self, package):
from poetry.packages import Package from poetry.core.packages import Package
from poetry.vcs import Git from poetry.core.vcs import Git
src_dir = self._env.path / "src" / package.name src_dir = self._env.path / "src" / package.name
if src_dir.exists(): if src_dir.exists():
......
...@@ -38,7 +38,7 @@ license = "" ...@@ -38,7 +38,7 @@ license = ""
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
""" """
BUILD_SYSTEM_MIN_VERSION = "0.12" BUILD_SYSTEM_MIN_VERSION = "1.0.0a5"
BUILD_SYSTEM_MAX_VERSION = None BUILD_SYSTEM_MAX_VERSION = None
...@@ -109,8 +109,8 @@ class Layout(object): ...@@ -109,8 +109,8 @@ class Layout(object):
if BUILD_SYSTEM_MAX_VERSION is not None: if BUILD_SYSTEM_MAX_VERSION is not None:
build_system_version += ",<" + BUILD_SYSTEM_MAX_VERSION build_system_version += ",<" + BUILD_SYSTEM_MAX_VERSION
build_system.add("requires", ["poetry" + build_system_version]) build_system.add("requires", ["poetry-core" + build_system_version])
build_system.add("build-backend", "poetry.masonry.api") build_system.add("build-backend", "poetry.core.masonry.api")
content.add("build-system", build_system) content.add("build-system", build_system)
......
"""
This module handles the packaging and publishing
of python projects.
A lot of the code used here has been taken from
`flit <https://github.com/takluyver/flit>`__ and adapted
to work with the poetry codebase, so kudos to them for showing the way.
"""
from .builder import Builder
""" from poetry.core.masonry.api import build_sdist
PEP-517 compliant buildsystem API from poetry.core.masonry.api import build_wheel
""" from poetry.core.masonry.api import get_requires_for_build_sdist
import logging from poetry.core.masonry.api import get_requires_for_build_wheel
import sys from poetry.core.masonry.api import prepare_metadata_for_build_wheel
from clikit.io import NullIO
__all__ = [
from poetry.factory import Factory "build_sdist",
from poetry.utils._compat import Path "build_wheel",
from poetry.utils._compat import unicode "get_requires_for_build_sdist",
from poetry.utils.env import SystemEnv "get_requires_for_build_wheel",
"prepare_metadata_for_build_wheel",
from .builders.sdist import SdistBuilder ]
from .builders.wheel import WheelBuilder
log = logging.getLogger(__name__)
def get_requires_for_build_wheel(config_settings=None):
"""
Returns an additional list of requirements for building, as PEP508 strings,
above and beyond those specified in the pyproject.toml file.
This implementation is optional. At the moment it only returns an empty list, which would be the same as if
not define. So this is just for completeness for future implementation.
"""
return []
# For now, we require all dependencies to build either a wheel or an sdist.
get_requires_for_build_sdist = get_requires_for_build_wheel
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
poetry = Factory().create_poetry(Path("."))
builder = WheelBuilder(poetry, SystemEnv(Path(sys.prefix)), NullIO())
dist_info = Path(metadata_directory, builder.dist_info)
dist_info.mkdir(parents=True, exist_ok=True)
if "scripts" in poetry.local_config or "plugins" in poetry.local_config:
with (dist_info / "entry_points.txt").open("w", encoding="utf-8") as f:
builder._write_entry_points(f)
with (dist_info / "WHEEL").open("w", encoding="utf-8") as f:
builder._write_wheel_file(f)
with (dist_info / "METADATA").open("w", encoding="utf-8") as f:
builder._write_metadata_file(f)
return dist_info.name
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
"""Builds a wheel, places it in wheel_directory"""
poetry = Factory().create_poetry(Path("."))
return unicode(
WheelBuilder.make_in(
poetry, SystemEnv(Path(sys.prefix)), NullIO(), Path(wheel_directory)
)
)
def build_sdist(sdist_directory, config_settings=None):
"""Builds an sdist, places it in sdist_directory"""
poetry = Factory().create_poetry(Path("."))
path = SdistBuilder(poetry, SystemEnv(Path(sys.prefix)), NullIO()).build(
Path(sdist_directory)
)
return unicode(path.name)
from .builders.complete import CompleteBuilder
from .builders.sdist import SdistBuilder
from .builders.wheel import WheelBuilder
class Builder:
_FORMATS = {"sdist": SdistBuilder, "wheel": WheelBuilder, "all": CompleteBuilder}
def __init__(self, poetry, env, io):
self._poetry = poetry
self._env = env
self._io = io
def build(self, fmt):
if fmt not in self._FORMATS:
raise ValueError("Invalid format: {}".format(fmt))
builder = self._FORMATS[fmt](self._poetry, self._env, self._io)
return builder.build()
from .complete import CompleteBuilder
from .editable import EditableBuilder from .editable import EditableBuilder
from .sdist import SdistBuilder
from .wheel import WheelBuilder
# -*- coding: utf-8 -*-
import re
import shutil
import tempfile
from collections import defaultdict
from contextlib import contextmanager
from typing import Set
from typing import Union
from clikit.api.io.flags import VERY_VERBOSE
from poetry.utils._compat import Path
from poetry.utils._compat import glob
from poetry.utils._compat import lru_cache
from poetry.utils._compat import to_str
from poetry.vcs import get_vcs
from ..metadata import Metadata
from ..utils.module import Module
from ..utils.package_include import PackageInclude
AUTHOR_REGEX = re.compile(r"(?u)^(?P<name>[- .,\w\d'’\"()]+) <(?P<email>.+?)>$")
METADATA_BASE = """\
Metadata-Version: 2.1
Name: {name}
Version: {version}
Summary: {summary}
"""
class Builder(object):
AVAILABLE_PYTHONS = {"2", "2.7", "3", "3.4", "3.5", "3.6", "3.7"}
format = None
def __init__(
self, poetry, env, io, ignore_packages_formats=False
): # type: ("Poetry", "Env", "IO", bool) -> None
self._poetry = poetry
self._env = env
self._io = io
self._package = poetry.package
self._path = poetry.file.parent
self._original_path = self._path
packages = []
for p in self._package.packages:
formats = p.get("format", [])
if not isinstance(formats, list):
formats = [formats]
if (
formats
and self.format
and self.format not in formats
and not ignore_packages_formats
):
continue
packages.append(p)
self._module = Module(
self._package.name,
self._path.as_posix(),
packages=packages,
includes=self._package.include,
)
self._meta = Metadata.from_package(self._package)
def build(self):
raise NotImplementedError()
@lru_cache(maxsize=None)
def find_excluded_files(self): # type: () -> Set[str]
# Checking VCS
vcs = get_vcs(self._original_path)
if not vcs:
vcs_ignored_files = set()
else:
vcs_ignored_files = set(vcs.get_ignored_files())
explicitely_excluded = set()
for excluded_glob in self._package.exclude:
for excluded in glob(
Path(self._path, excluded_glob).as_posix(), recursive=True
):
explicitely_excluded.add(
Path(excluded).relative_to(self._path).as_posix()
)
ignored = vcs_ignored_files | explicitely_excluded
result = set()
for file in ignored:
result.add(file)
# The list of excluded files might be big and we will do a lot
# containment check (x in excluded).
# Returning a set make those tests much much faster.
return result
def is_excluded(self, filepath): # type: (Union[str, Path]) -> bool
exclude_path = Path(filepath)
while True:
if exclude_path.as_posix() in self.find_excluded_files():
return True
if len(exclude_path.parts) > 1:
exclude_path = exclude_path.parent
else:
break
return False
def find_files_to_add(self, exclude_build=True): # type: (bool) -> list
"""
Finds all files to add to the tarball
"""
to_add = []
for include in self._module.includes:
for file in include.elements:
if "__pycache__" in str(file):
continue
if file.is_dir():
continue
file = file.relative_to(self._path)
if self.is_excluded(file) and isinstance(include, PackageInclude):
continue
if file.suffix == ".pyc":
continue
if file in to_add:
# Skip duplicates
continue
self._io.write_line(
" - Adding: <comment>{}</comment>".format(str(file)), VERY_VERBOSE
)
to_add.append(file)
# Include project files
self._io.write_line(
" - Adding: <comment>pyproject.toml</comment>", VERY_VERBOSE
)
to_add.append(Path("pyproject.toml"))
# If a license file exists, add it
for license_file in self._path.glob("LICENSE*"):
self._io.write_line(
" - Adding: <comment>{}</comment>".format(
license_file.relative_to(self._path)
),
VERY_VERBOSE,
)
to_add.append(license_file.relative_to(self._path))
# If a README is specified we need to include it
# to avoid errors
if "readme" in self._poetry.local_config:
readme = self._path / self._poetry.local_config["readme"]
if readme.exists():
self._io.write_line(
" - Adding: <comment>{}</comment>".format(
readme.relative_to(self._path)
),
VERY_VERBOSE,
)
to_add.append(readme.relative_to(self._path))
# If a build script is specified and explicitely required
# we add it to the list of files
if self._package.build and not exclude_build:
to_add.append(Path(self._package.build))
return sorted(to_add)
def get_metadata_content(self): # type: () -> bytes
content = METADATA_BASE.format(
name=self._meta.name,
version=self._meta.version,
summary=to_str(self._meta.summary),
)
# Optional fields
if self._meta.home_page:
content += "Home-page: {}\n".format(self._meta.home_page)
if self._meta.license:
content += "License: {}\n".format(self._meta.license)
if self._meta.keywords:
content += "Keywords: {}\n".format(self._meta.keywords)
if self._meta.author:
content += "Author: {}\n".format(to_str(self._meta.author))
if self._meta.author_email:
content += "Author-email: {}\n".format(to_str(self._meta.author_email))
if self._meta.maintainer:
content += "Maintainer: {}\n".format(to_str(self._meta.maintainer))
if self._meta.maintainer_email:
content += "Maintainer-email: {}\n".format(
to_str(self._meta.maintainer_email)
)
if self._meta.requires_python:
content += "Requires-Python: {}\n".format(self._meta.requires_python)
for classifier in self._meta.classifiers:
content += "Classifier: {}\n".format(classifier)
for extra in sorted(self._meta.provides_extra):
content += "Provides-Extra: {}\n".format(extra)
for dep in sorted(self._meta.requires_dist):
content += "Requires-Dist: {}\n".format(dep)
for url in sorted(self._meta.project_urls, key=lambda u: u[0]):
content += "Project-URL: {}\n".format(to_str(url))
if self._meta.description_content_type:
content += "Description-Content-Type: {}\n".format(
self._meta.description_content_type
)
if self._meta.description is not None:
content += "\n" + to_str(self._meta.description) + "\n"
return content
def convert_entry_points(self): # type: () -> dict
result = defaultdict(list)
# Scripts -> Entry points
for name, ep in self._poetry.local_config.get("scripts", {}).items():
extras = ""
if isinstance(ep, dict):
extras = "[{}]".format(", ".join(ep["extras"]))
ep = ep["callable"]
result["console_scripts"].append("{} = {}{}".format(name, ep, extras))
# Plugins -> entry points
plugins = self._poetry.local_config.get("plugins", {})
for groupname, group in plugins.items():
for name, ep in sorted(group.items()):
result[groupname].append("{} = {}".format(name, ep))
for groupname in result:
result[groupname] = sorted(result[groupname])
return dict(result)
@classmethod
def convert_author(cls, author): # type: (...) -> dict
m = AUTHOR_REGEX.match(author)
name = m.group("name")
email = m.group("email")
return {"name": name, "email": email}
@classmethod
@contextmanager
def temporary_directory(cls, *args, **kwargs):
try:
from tempfile import TemporaryDirectory
with TemporaryDirectory(*args, **kwargs) as name:
yield name
except ImportError:
name = tempfile.mkdtemp(*args, **kwargs)
yield name
shutil.rmtree(name)
import os
import tarfile
from contextlib import contextmanager
from poetry.factory import Factory
from poetry.io.null_io import NullIO
from poetry.utils._compat import Path
from poetry.utils.helpers import temporary_directory
from .builder import Builder
from .sdist import SdistBuilder
from .wheel import WheelBuilder
class CompleteBuilder(Builder):
def build(self):
# We start by building the tarball
# We will use it to build the wheel
sdist_builder = SdistBuilder(self._poetry, self._env, self._io)
build_for_all_formats = False
for p in self._package.packages:
formats = p.get("format", [])
if not isinstance(formats, list):
formats = [formats]
if formats and sdist_builder.format not in formats:
build_for_all_formats = True
break
sdist_file = sdist_builder.build()
self._io.write_line("")
dist_dir = self._path / "dist"
if build_for_all_formats:
sdist_builder = SdistBuilder(
self._poetry, self._env, NullIO(), ignore_packages_formats=True
)
with temporary_directory() as tmp_dir:
sdist_file = sdist_builder.build(Path(tmp_dir))
with self.unpacked_tarball(sdist_file) as tmpdir:
WheelBuilder.make_in(
Factory().create_poetry(tmpdir),
self._env,
self._io,
dist_dir,
original=self._poetry,
)
else:
with self.unpacked_tarball(sdist_file) as tmpdir:
WheelBuilder.make_in(
Factory().create_poetry(tmpdir),
self._env,
self._io,
dist_dir,
original=self._poetry,
)
@classmethod
@contextmanager
def unpacked_tarball(cls, path):
tf = tarfile.open(str(path))
with cls.temporary_directory() as tmpdir:
tf.extractall(tmpdir)
files = os.listdir(tmpdir)
assert len(files) == 1, files
yield Path(tmpdir) / files[0]
...@@ -5,19 +5,24 @@ import shutil ...@@ -5,19 +5,24 @@ import shutil
from collections import defaultdict from collections import defaultdict
from poetry.semver.version import Version from poetry.core.masonry.builders.builder import Builder
from poetry.core.masonry.builders.sdist import SdistBuilder
from poetry.core.semver.version import Version
from poetry.utils._compat import decode from poetry.utils._compat import decode
from .builder import Builder
from .sdist import SdistBuilder
class EditableBuilder(Builder): class EditableBuilder(Builder):
def __init__(self, poetry, env, io):
super(EditableBuilder, self).__init__(poetry)
self._env = env
self._io = io
def build(self): def build(self):
return self._setup_build() return self._setup_build()
def _setup_build(self): def _setup_build(self):
builder = SdistBuilder(self._poetry, self._env, self._io) builder = SdistBuilder(self._poetry)
setup = self._path / "setup.py" setup = self._path / "setup.py"
has_setup = setup.exists() has_setup = setup.exists()
......
from poetry.utils.helpers import canonicalize_name
from poetry.utils.helpers import normalize_version
from poetry.version.helpers import format_python_constraint
class Metadata:
metadata_version = "2.1"
# version 1.0
name = None
version = None
platforms = ()
supported_platforms = ()
summary = None
description = None
keywords = None
home_page = None
download_url = None
author = None
author_email = None
license = None
# version 1.1
classifiers = ()
requires = ()
provides = ()
obsoletes = ()
# version 1.2
maintainer = None
maintainer_email = None
requires_python = None
requires_external = ()
requires_dist = []
provides_dist = ()
obsoletes_dist = ()
project_urls = ()
# Version 2.1
description_content_type = None
provides_extra = []
@classmethod
def from_package(cls, package): # type: (...) -> Metadata
meta = cls()
meta.name = canonicalize_name(package.name)
meta.version = normalize_version(package.version.text)
meta.summary = package.description
if package.readme:
with package.readme.open(encoding="utf-8") as f:
meta.description = f.read()
meta.keywords = ",".join(package.keywords)
meta.home_page = package.homepage or package.repository_url
meta.author = package.author_name
meta.author_email = package.author_email
if package.license:
meta.license = package.license.id
meta.classifiers = package.all_classifiers
# Version 1.2
meta.maintainer = package.maintainer_name
meta.maintainer_email = package.maintainer_email
# Requires python
if package.python_versions != "*":
meta.requires_python = format_python_constraint(package.python_constraint)
meta.requires_dist = [d.to_pep_508() for d in package.requires]
# Version 2.1
if package.readme:
if package.readme.suffix == ".rst":
meta.description_content_type = "text/x-rst"
elif package.readme.suffix in [".md", ".markdown"]:
meta.description_content_type = "text/markdown"
else:
meta.description_content_type = "text/plain"
meta.provides_extra = [e for e in package.extras]
if package.urls:
for name, url in package.urls.items():
if name == "Homepage" and meta.home_page == url:
continue
meta.project_urls += ("{}, {}".format(name, url),)
return meta
import re
def normalize_file_permissions(st_mode):
"""
Normalizes the permission bits in the st_mode field from stat to 644/755
Popular VCSs only track whether a file is executable or not. The exact
permissions can vary on systems with different umasks. Normalising
to 644 (non executable) or 755 (executable) makes builds more reproducible.
"""
# Set 644 permissions, leaving higher bits of st_mode unchanged
new_mode = (st_mode | 0o644) & ~0o133
if st_mode & 0o100:
new_mode |= 0o111 # Executable: 644 -> 755
return new_mode
def escape_version(version):
"""
Escaped version in wheel filename. Doesn't exactly follow
the escaping specification in :pep:`427#escaping-and-unicode`
because this conflicts with :pep:`440#local-version-identifiers`.
"""
return re.sub(r"[^\w\d.+]+", "_", version, flags=re.UNICODE)
def escape_name(name):
"""Escaped wheel name as specified in :pep:`427#escaping-and-unicode`."""
return re.sub(r"[^\w\d.]+", "_", name, flags=re.UNICODE)
from typing import List
from typing import Optional
from poetry.utils._compat import Path
class Include(object):
"""
Represents an "include" entry.
It can be a glob string, a single file or a directory.
This class will then detect the type of this include:
- a package
- a module
- a file
- a directory
"""
def __init__(
self, base, include, formats=None
): # type: (Path, str, Optional[List[str]]) -> None
self._base = base
self._include = str(include)
self._formats = formats
self._elements = sorted(list(self._base.glob(str(self._include))))
@property
def base(self): # type: () -> Path
return self._base
@property
def elements(self): # type: () -> List[Path]
return self._elements
@property
def formats(self): # type: () -> Optional[List[str]]
return self._formats
def is_empty(self): # type: () -> bool
return len(self._elements) == 0
def refresh(self): # type: () -> Include
self._elements = sorted(list(self._base.glob(self._include)))
return self
from typing import List
from poetry.utils._compat import Path
from poetry.utils.helpers import module_name
from .include import Include
from .package_include import PackageInclude
class ModuleOrPackageNotFound(ValueError):
pass
class Module:
def __init__(self, name, directory=".", packages=None, includes=None):
self._name = module_name(name)
self._in_src = False
self._is_package = False
self._path = Path(directory)
self._includes = []
packages = packages or []
includes = includes or []
if not packages:
# It must exist either as a .py file or a directory, but not both
pkg_dir = Path(directory, self._name)
py_file = Path(directory, self._name + ".py")
if pkg_dir.is_dir() and py_file.is_file():
raise ValueError("Both {} and {} exist".format(pkg_dir, py_file))
elif pkg_dir.is_dir():
packages = [{"include": str(pkg_dir.relative_to(self._path))}]
elif py_file.is_file():
packages = [{"include": str(py_file.relative_to(self._path))}]
else:
# Searching for a src module
src = Path(directory, "src")
src_pkg_dir = src / self._name
src_py_file = src / (self._name + ".py")
if src_pkg_dir.is_dir() and src_py_file.is_file():
raise ValueError("Both {} and {} exist".format(pkg_dir, py_file))
elif src_pkg_dir.is_dir():
packages = [
{
"include": str(src_pkg_dir.relative_to(src)),
"from": str(src.relative_to(self._path)),
}
]
elif src_py_file.is_file():
packages = [
{
"include": str(src_py_file.relative_to(src)),
"from": str(src.relative_to(self._path)),
}
]
else:
raise ModuleOrPackageNotFound(
"No file/folder found for package {}".format(name)
)
for package in packages:
formats = package.get("format")
if formats and not isinstance(formats, list):
formats = [formats]
self._includes.append(
PackageInclude(
self._path,
package["include"],
formats=formats,
source=package.get("from"),
)
)
for include in includes:
self._includes.append(Include(self._path, include))
@property
def name(self): # type: () -> str
return self._name
@property
def path(self): # type: () -> Path
return self._path
@property
def file(self): # type: () -> Path
if self._is_package:
return self._path / "__init__.py"
else:
return self._path
@property
def includes(self): # type: () -> List
return self._includes
def is_package(self): # type: () -> bool
return self._is_package
def is_in_src(self): # type: () -> bool
return self._in_src
from .include import Include
class PackageInclude(Include):
def __init__(self, base, include, formats=None, source=None):
self._package = None
self._is_package = False
self._is_module = False
self._source = source
if source is not None:
base = base / source
super(PackageInclude, self).__init__(base, include, formats=formats)
self.check_elements()
@property
def package(self): # type: () -> str
return self._package
@property
def source(self): # type: () -> str
return self._source
def is_package(self): # type: () -> bool
return self._is_package
def is_module(self): # type: () -> bool
return self._is_module
def refresh(self): # type: () -> PackageInclude
super(PackageInclude, self).refresh()
return self.check_elements()
def check_elements(self): # type: () -> PackageInclude
root = self._elements[0]
if not self._elements:
raise ValueError(
"{} does not contain any element".format(self._base / self._include)
)
if len(self._elements) > 1:
# Probably glob
self._is_package = True
# Packages no longer need an __init__.py in python3, but there must
# at least be one .py file for it to be considered a package
if not any([element.suffix == ".py" for element in self._elements]):
raise ValueError("{} is not a package.".format(root.name))
self._package = root.parent.name
else:
if root.is_dir():
# If it's a directory, we include everything inside it
self._package = root.name
self._elements = sorted(list(root.glob("**/*")))
if not any([element.suffix == ".py" for element in self._elements]):
raise ValueError("{} is not a package.".format(root.name))
self._is_package = True
else:
self._package = root.stem
self._is_module = True
return self
"""
Generate and work with PEP 425 Compatibility Tags.
Base implementation taken from
https://github.com/pypa/wheel/blob/master/wheel/pep425tags.py
and adapted to work with poetry's env util.
"""
from __future__ import unicode_literals
import distutils.util
import sys
import warnings
def get_abbr_impl(env):
"""Return abbreviated implementation name."""
impl = env.python_implementation
if impl == "PyPy":
return "pp"
elif impl == "Jython":
return "jy"
elif impl == "IronPython":
return "ip"
elif impl == "CPython":
return "cp"
raise LookupError("Unknown Python implementation: " + impl)
def get_impl_ver(env):
"""Return implementation version."""
impl_ver = env.config_var("py_version_nodot")
if not impl_ver or get_abbr_impl(env) == "pp":
impl_ver = "".join(map(str, get_impl_version_info(env)))
return impl_ver
def get_impl_version_info(env):
"""Return sys.version_info-like tuple for use in decrementing the minor
version."""
if get_abbr_impl(env) == "pp":
# as per https://github.com/pypa/pip/issues/2882
return env.version_info[:3]
else:
return env.version_info[:2]
def get_flag(env, var, fallback, expected=True, warn=True):
"""Use a fallback method for determining SOABI flags if the needed config
var is unset or unavailable."""
val = env.config_var(var)
if val is None:
if warn:
warnings.warn(
"Config variable '{0}' is unset, Python ABI tag may "
"be incorrect".format(var),
RuntimeWarning,
2,
)
return fallback()
return val == expected
def get_abi_tag(env):
"""Return the ABI tag based on SOABI (if available) or emulate SOABI
(CPython 2, PyPy)."""
soabi = env.config_var("SOABI")
impl = get_abbr_impl(env)
if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"):
d = ""
m = ""
u = ""
if get_flag(
env,
"Py_DEBUG",
lambda: hasattr(sys, "gettotalrefcount"),
warn=(impl == "cp"),
):
d = "d"
if get_flag(env, "WITH_PYMALLOC", lambda: impl == "cp", warn=(impl == "cp")):
m = "m"
if get_flag(
env,
"Py_UNICODE_SIZE",
lambda: sys.maxunicode == 0x10FFFF,
expected=4,
warn=(impl == "cp" and env.version_info < (3, 3)),
) and env.version_info < (3, 3):
u = "u"
abi = "%s%s%s%s%s" % (impl, get_impl_ver(env), d, m, u)
elif soabi and soabi.startswith("cpython-"):
abi = "cp" + soabi.split("-")[1]
elif soabi:
abi = soabi.replace(".", "_").replace("-", "_")
else:
abi = None
return abi
def get_platform():
"""Return our platform name 'win32', 'linux_x86_64'"""
# XXX remove distutils dependency
result = distutils.util.get_platform().replace(".", "_").replace("-", "_")
if result == "linux_x86_64" and sys.maxsize == 2147483647:
# pip pull request #3497
result = "linux_i686"
return result
def get_supported(env, versions=None, supplied_platform=None):
"""Return a list of supported tags for each version specified in
`versions`.
:param versions: a list of string versions, of the form ["33", "32"],
or None. The first version will be assumed to support our ABI.
"""
supported = []
# Versions must be given with respect to the preference
if versions is None:
versions = []
version_info = get_impl_version_info(env)
major = version_info[:-1]
# Support all previous minor Python versions.
for minor in range(version_info[-1], -1, -1):
versions.append("".join(map(str, major + (minor,))))
impl = get_abbr_impl(env)
abis = []
abi = get_abi_tag(env)
if abi:
abis[0:0] = [abi]
abi3s = set()
import imp
for suffix in imp.get_suffixes():
if suffix[0].startswith(".abi"):
abi3s.add(suffix[0].split(".", 2)[1])
abis.extend(sorted(list(abi3s)))
abis.append("none")
platforms = []
if supplied_platform:
platforms.append(supplied_platform)
platforms.append(get_platform())
# Current version, current API (built specifically for our Python):
for abi in abis:
for arch in platforms:
supported.append(("%s%s" % (impl, versions[0]), abi, arch))
# abi3 modules compatible with older version of Python
for version in versions[1:]:
# abi3 was introduced in Python 3.2
if version in ("31", "30"):
break
for abi in abi3s: # empty set if not Python 3
for arch in platforms:
supported.append(("%s%s" % (impl, version), abi, arch))
# No abi / arch, but requires our implementation:
for i, version in enumerate(versions):
supported.append(("%s%s" % (impl, version), "none", "any"))
if i == 0:
# Tagged specifically as being cross-version compatible
# (with just the major version specified)
supported.append(("%s%s" % (impl, versions[0][0]), "none", "any"))
# Major Python version + platform; e.g. binaries not using the Python API
supported.append(("py%s" % (versions[0][0]), "none", arch))
# No abi / arch, generic Python
for i, version in enumerate(versions):
supported.append(("py%s" % (version,), "none", "any"))
if i == 0:
supported.append(("py%s" % (version[0]), "none", "any"))
return supported
...@@ -2,8 +2,8 @@ from collections import OrderedDict ...@@ -2,8 +2,8 @@ from collections import OrderedDict
from typing import Dict from typing import Dict
from typing import List from typing import List
from poetry.packages import Dependency from poetry.core.packages import Dependency
from poetry.packages import Package from poetry.core.packages import Package
from .assignment import Assignment from .assignment import Assignment
from .incompatibility import Incompatibility from .incompatibility import Incompatibility
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Union from typing import Union
from poetry.packages import Dependency from poetry.core.packages import Dependency
from .set_relation import SetRelation from .set_relation import SetRelation
......
...@@ -6,12 +6,12 @@ from typing import Dict ...@@ -6,12 +6,12 @@ from typing import Dict
from typing import List from typing import List
from typing import Union from typing import Union
from poetry.packages import Dependency from poetry.core.packages import Dependency
from poetry.packages import Package from poetry.core.packages import Package
from poetry.packages import ProjectPackage from poetry.core.packages import ProjectPackage
from poetry.core.semver import Version
from poetry.core.semver import VersionRange
from poetry.puzzle.provider import Provider from poetry.puzzle.provider import Provider
from poetry.semver import Version
from poetry.semver import VersionRange
from .failure import SolveFailure from .failure import SolveFailure
from .incompatibility import Incompatibility from .incompatibility import Incompatibility
......
import os
import re
from poetry.semver import Version
from poetry.utils.patterns import wheel_file_re
from poetry.version.requirements import Requirement
from .dependency import Dependency
from .dependency_package import DependencyPackage from .dependency_package import DependencyPackage
from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency
from .locker import Locker from .locker import Locker
from .package import Package
from .package_collection import PackageCollection from .package_collection import PackageCollection
from .project_package import ProjectPackage
from .url_dependency import URLDependency
from .utils.link import Link
from .utils.utils import convert_markers
from .utils.utils import group_markers
from .utils.utils import is_archive_file
from .utils.utils import is_installable_dir
from .utils.utils import is_url
from .utils.utils import path_to_url
from .utils.utils import strip_extras
from .vcs_dependency import VCSDependency
def dependency_from_pep_508(name):
from poetry.vcs.git import ParsedUrl
# Removing comments
parts = name.split("#", 1)
name = parts[0].strip()
if len(parts) > 1:
rest = parts[1]
if ";" in rest:
name += ";" + rest.split(";", 1)[1]
req = Requirement(name)
if req.marker:
markers = convert_markers(req.marker)
else:
markers = {}
name = req.name
path = os.path.normpath(os.path.abspath(name))
link = None
if is_url(name):
link = Link(name)
elif req.url:
link = Link(req.url)
else:
p, extras = strip_extras(path)
if os.path.isdir(p) and (os.path.sep in name or name.startswith(".")):
if not is_installable_dir(p):
raise ValueError(
"Directory {!r} is not installable. File 'setup.py' "
"not found.".format(name)
)
link = Link(path_to_url(p))
elif is_archive_file(p):
link = Link(path_to_url(p))
# it's a local file, dir, or url
if link:
# Handle relative file URLs
if link.scheme == "file" and re.search(r"\.\./", link.url):
link = Link(path_to_url(os.path.normpath(os.path.abspath(link.path))))
# wheel file
if link.is_wheel:
m = wheel_file_re.match(link.filename)
if not m:
raise ValueError("Invalid wheel name: {}".format(link.filename))
name = m.group("name")
version = m.group("ver")
dep = Dependency(name, version)
else:
name = req.name or link.egg_fragment
if link.scheme.startswith("git+"):
url = ParsedUrl.parse(link.url)
dep = VCSDependency(name, "git", url.url, rev=url.rev)
elif link.scheme == "git":
dep = VCSDependency(name, "git", link.url_without_fragment)
elif link.scheme in ["http", "https"]:
dep = URLDependency(name, link.url_without_fragment)
else:
dep = Dependency(name, "*")
else:
if req.pretty_constraint:
constraint = req.constraint
else:
constraint = "*"
dep = Dependency(name, constraint)
if "extra" in markers:
# If we have extras, the dependency is optional
dep.deactivate()
for or_ in markers["extra"]:
for _, extra in or_:
dep.in_extras.append(extra)
if "python_version" in markers:
ors = []
for or_ in markers["python_version"]:
ands = []
for op, version in or_:
# Expand python version
if op == "==":
version = "~" + version
op = ""
elif op == "!=":
version += ".*"
elif op in ("<=", ">"):
parsed_version = Version.parse(version)
if parsed_version.precision == 1:
if op == "<=":
op = "<"
version = parsed_version.next_major.text
elif op == ">":
op = ">="
version = parsed_version.next_major.text
elif parsed_version.precision == 2:
if op == "<=":
op = "<"
version = parsed_version.next_minor.text
elif op == ">":
op = ">="
version = parsed_version.next_minor.text
elif op in ("in", "not in"):
versions = []
for v in re.split("[ ,]+", version):
split = v.split(".")
if len(split) in [1, 2]:
split.append("*")
op_ = "" if op == "in" else "!="
else:
op_ = "==" if op == "in" else "!="
versions.append(op_ + ".".join(split))
glue = " || " if op == "in" else ", "
if versions:
ands.append(glue.join(versions))
continue
ands.append("{}{}".format(op, version))
ors.append(" ".join(ands))
dep.python_versions = " || ".join(ors)
if req.marker:
dep.marker = req.marker
# Extras
for extra in req.extras:
dep.extras.append(extra)
return dep
import re
from .any_constraint import AnyConstraint
from .base_constraint import BaseConstraint
from .constraint import Constraint
from .union_constraint import UnionConstraint
BASIC_CONSTRAINT = re.compile(r"^(!?==?)?\s*([^\s]+?)\s*$")
def parse_constraint(constraints):
if constraints == "*":
return AnyConstraint()
or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip())
or_groups = []
for constraints in or_constraints:
and_constraints = re.split(
r"(?<!^)(?<![=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)", constraints
)
constraint_objects = []
if len(and_constraints) > 1:
for constraint in and_constraints:
constraint_objects.append(parse_single_constraint(constraint))
else:
constraint_objects.append(parse_single_constraint(and_constraints[0]))
if len(constraint_objects) == 1:
constraint = constraint_objects[0]
else:
constraint = constraint_objects[0]
for next_constraint in constraint_objects[1:]:
constraint = constraint.intersect(next_constraint)
or_groups.append(constraint)
if len(or_groups) == 1:
return or_groups[0]
else:
return UnionConstraint(*or_groups)
def parse_single_constraint(constraint): # type: (str) -> BaseConstraint
# Basic comparator
m = BASIC_CONSTRAINT.match(constraint)
if m:
op = m.group(1)
if op is None:
op = "=="
version = m.group(2).strip()
return Constraint(version, op)
raise ValueError("Could not parse version constraint: {}".format(constraint))
from .base_constraint import BaseConstraint
from .empty_constraint import EmptyConstraint
class AnyConstraint(BaseConstraint):
def allows(self, other):
return True
def allows_all(self, other):
return True
def allows_any(self, other):
return True
def difference(self, other):
if other.is_any():
return EmptyConstraint()
return other
def intersect(self, other):
return other
def union(self, other):
return AnyConstraint()
def is_any(self):
return True
def is_empty(self):
return False
def __str__(self):
return "*"
def __eq__(self, other):
return other.is_any()
class BaseConstraint(object):
def allows_all(self, other):
raise NotImplementedError()
def allows_any(self, other):
raise NotImplementedError()
def difference(self, other):
raise NotImplementedError()
def intersect(self, other):
raise NotImplementedError()
def union(self, other):
raise NotImplementedError()
def is_any(self):
return False
def is_empty(self):
return False
def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, str(self))
def __eq__(self, other):
raise NotImplementedError()
import operator
from .base_constraint import BaseConstraint
from .empty_constraint import EmptyConstraint
class Constraint(BaseConstraint):
OP_EQ = operator.eq
OP_NE = operator.ne
_trans_op_str = {"=": OP_EQ, "==": OP_EQ, "!=": OP_NE}
_trans_op_int = {OP_EQ: "==", OP_NE: "!="}
def __init__(self, version, operator="=="):
if operator == "=":
operator = "=="
self._version = version
self._operator = operator
self._op = self._trans_op_str[operator]
@property
def version(self):
return self._version
@property
def operator(self):
return self._operator
def allows(self, other):
is_equal_op = self._operator == "=="
is_non_equal_op = self._operator == "!="
is_other_equal_op = other.operator == "=="
is_other_non_equal_op = other.operator == "!="
if is_equal_op and is_other_equal_op:
return self._version == other.version
if (
is_equal_op
and is_other_non_equal_op
or is_non_equal_op
and is_other_equal_op
or is_non_equal_op
and is_other_non_equal_op
):
return self._version != other.version
return False
def allows_all(self, other):
if not isinstance(other, Constraint):
return other.is_empty()
return other == self
def allows_any(self, other):
if isinstance(other, Constraint):
is_non_equal_op = self._operator == "!="
is_other_non_equal_op = other.operator == "!="
if is_non_equal_op and is_other_non_equal_op:
return self._version != other.version
return other.allows(self)
def difference(self, other):
if other.allows(self):
return EmptyConstraint()
return self
def intersect(self, other):
from .multi_constraint import MultiConstraint
if isinstance(other, Constraint):
if other == self:
return self
if self.operator == "!=" and other.operator == "==" and self.allows(other):
return other
if other.operator == "!=" and self.operator == "==" and other.allows(self):
return self
if other.operator == "!=" and self.operator == "!=":
return MultiConstraint(self, other)
return EmptyConstraint()
return other.intersect(self)
def union(self, other):
if isinstance(other, Constraint):
from .union_constraint import UnionConstraint
return UnionConstraint(self, other)
return other.union(self)
def is_any(self):
return False
def is_empty(self):
return False
def __eq__(self, other):
if not isinstance(other, Constraint):
return NotImplemented
return (self.version, self.operator) == (other.version, other.operator)
def __hash__(self):
return hash((self._operator, self._version))
def __str__(self):
return "{}{}".format(
self._operator if self._operator != "==" else "", self._version
)
from .base_constraint import BaseConstraint
class EmptyConstraint(BaseConstraint):
pretty_string = None
def matches(self, _):
return True
def is_empty(self):
return True
def allows_all(self, other):
return True
def allows_any(self, other):
return True
def intersect(self, other):
return other
def difference(self, other):
return
def __eq__(self, other):
return other.is_empty()
def __str__(self):
return ""
from .base_constraint import BaseConstraint
from .constraint import Constraint
class MultiConstraint(BaseConstraint):
def __init__(self, *constraints):
if any(c.operator == "==" for c in constraints):
raise ValueError(
"A multi-constraint can only be comprised of negative constraints"
)
self._constraints = constraints
@property
def constraints(self):
return self._constraints
def allows(self, other):
for constraint in self._constraints:
if not constraint.allows(other):
return False
return True
def allows_all(self, other):
if other.is_any():
return False
if other.is_empty():
return True
if isinstance(other, Constraint):
return self.allows(other)
our_constraints = iter(self._constraints)
their_constraints = iter(other.constraints)
our_constraint = next(our_constraints, None)
their_constraint = next(their_constraints, None)
while our_constraint and their_constraint:
if our_constraint.allows_all(their_constraint):
their_constraint = next(their_constraints, None)
else:
our_constraint = next(our_constraints, None)
return their_constraint is None
def allows_any(self, other):
if other.is_any():
return True
if other.is_empty():
return True
if isinstance(other, Constraint):
return self.allows(other)
if isinstance(other, MultiConstraint):
for c1 in self.constraints:
for c2 in other.constraints:
if c1.allows(c2):
return True
return False
def intersect(self, other):
if isinstance(other, Constraint):
constraints = self._constraints
if other not in constraints:
constraints += (other,)
else:
constraints = (other,)
if len(constraints) == 1:
return constraints[0]
return MultiConstraint(*constraints)
def __eq__(self, other):
if not isinstance(other, MultiConstraint):
return False
return sorted(
self._constraints, key=lambda c: (c.operator, c.version)
) == sorted(other.constraints, key=lambda c: (c.operator, c.version))
def __str__(self):
constraints = []
for constraint in self._constraints:
constraints.append(str(constraint))
return "{}".format(", ").join(constraints)
from .base_constraint import BaseConstraint
from .constraint import Constraint
from .empty_constraint import EmptyConstraint
class UnionConstraint(BaseConstraint):
def __init__(self, *constraints):
self._constraints = constraints
@property
def constraints(self):
return self._constraints
def allows(self, other):
for constraint in self._constraints:
if constraint.allows(other):
return True
return False
def allows_any(self, other):
if other.is_empty():
return False
if other.is_any():
return True
if isinstance(other, Constraint):
constraints = [other]
else:
constraints = other.constraints
for our_constraint in self._constraints:
for their_constraint in constraints:
if our_constraint.allows_any(their_constraint):
return True
return False
def allows_all(self, other):
if other.is_any():
return False
if other.is_empty():
return True
if isinstance(other, Constraint):
constraints = [other]
else:
constraints = other.constraints
our_constraints = iter(self._constraints)
their_constraints = iter(constraints)
our_constraint = next(our_constraints, None)
their_constraint = next(their_constraints, None)
while our_constraint and their_constraint:
if our_constraint.allows_all(their_constraint):
their_constraint = next(their_constraints, None)
else:
our_constraint = next(our_constraints, None)
return their_constraint is None
def intersect(self, other):
if other.is_any():
return self
if other.is_empty():
return other
if isinstance(other, Constraint):
if self.allows(other):
return other
return EmptyConstraint()
new_constraints = []
for our_constraint in self._constraints:
for their_constraint in other.constraints:
intersection = our_constraint.intersect(their_constraint)
if not intersection.is_empty() and intersection not in new_constraints:
new_constraints.append(intersection)
if not new_constraints:
return EmptyConstraint()
return UnionConstraint(*new_constraints)
def union(self, other):
if isinstance(other, Constraint):
constraints = self._constraints
if other not in self._constraints:
constraints += (other,)
return UnionConstraint(*constraints)
def __eq__(self, other):
if not isinstance(other, UnionConstraint):
return False
return sorted(
self._constraints, key=lambda c: (c.operator, c.version)
) == sorted(other.constraints, key=lambda c: (c.operator, c.version))
def __str__(self):
constraints = []
for constraint in self._constraints:
constraints.append(str(constraint))
return "{}".format(" || ").join(constraints)
import re
from .constraint import Constraint
class WilcardConstraint(Constraint):
def __init__(self, constraint): # type: (str) -> None
m = re.match(
r"^(!= ?|==)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$", constraint
)
if not m:
raise ValueError("Invalid value for wildcard constraint")
if not m.group(1):
operator = "=="
else:
operator = m.group(1).strip()
super(WilcardConstraint, self).__init__(
operator, ".".join([g if g else "*" for g in m.groups()[1:]])
)
if m.group(4):
position = 2
elif m.group(3):
position = 1
else:
position = 0
from ..version_parser import VersionParser
parser = VersionParser()
groups = m.groups()[1:]
low_version = parser._manipulate_version_string(groups, position)
high_version = parser._manipulate_version_string(groups, position, 1)
if operator == "!=":
if low_version == "0.0.0.0":
self._constraint = Constraint(">=", high_version)
else:
self._constraint = parser.parse_constraints(
"<{} || >={}".format(low_version, high_version)
)
else:
if low_version == "0.0.0.0":
self._constraint = Constraint("<", high_version)
else:
self._constraint = parser.parse_constraints(
">={},<{}".format(low_version, high_version)
)
@property
def supported_operators(self):
return ["!=", "=="]
@property
def constraint(self):
return self._constraint
def matches(self, provider): # type: (Constraint) -> bool
if isinstance(provider, self.__class__):
return self._constraint.matches(provider.constraint)
return provider.matches(self._constraint)
def __str__(self):
op = ""
if self.string_operator == "!=":
op = "!= "
return "{}{}".format(op, self._version)
from pkginfo.distribution import HEADER_ATTRS
from pkginfo.distribution import HEADER_ATTRS_2_0
from poetry.utils._compat import Path
from poetry.utils.toml_file import TomlFile
from .dependency import Dependency
# Patching pkginfo to support Metadata version 2.1 (PEP 566)
HEADER_ATTRS.update(
{"2.1": HEADER_ATTRS_2_0 + (("Provides-Extra", "provides_extra", True),)}
)
class DirectoryDependency(Dependency):
def __init__(
self,
name,
path, # type: Path
category="main", # type: str
optional=False, # type: bool
base=None, # type: Path
develop=True, # type: bool
):
self._path = path
self._base = base
self._full_path = path
self._develop = develop
self._supports_poetry = False
if self._base and not self._path.is_absolute():
self._full_path = self._base / self._path
if not self._full_path.exists():
raise ValueError("Directory {} does not exist".format(self._path))
if self._full_path.is_file():
raise ValueError("{} is a file, expected a directory".format(self._path))
# Checking content to determine actions
setup = self._full_path / "setup.py"
pyproject = TomlFile(self._full_path / "pyproject.toml")
if pyproject.exists():
pyproject_content = pyproject.read()
self._supports_poetry = (
"tool" in pyproject_content and "poetry" in pyproject_content["tool"]
)
if not setup.exists() and not self._supports_poetry:
raise ValueError(
"Directory {} does not seem to be a Python package".format(
self._full_path
)
)
super(DirectoryDependency, self).__init__(
name, "*", category=category, optional=optional, allows_prereleases=True
)
@property
def path(self):
return self._path
@property
def full_path(self):
return self._full_path.resolve()
@property
def base(self):
return self._base
@property
def develop(self):
return self._develop
def supports_poetry(self):
return self._supports_poetry
def is_directory(self):
return True
import hashlib
import io
from pkginfo.distribution import HEADER_ATTRS
from pkginfo.distribution import HEADER_ATTRS_2_0
from poetry.utils._compat import Path
from .dependency import Dependency
# Patching pkginfo to support Metadata version 2.1 (PEP 566)
HEADER_ATTRS.update(
{"2.1": HEADER_ATTRS_2_0 + (("Provides-Extra", "provides_extra", True),)}
)
class FileDependency(Dependency):
def __init__(
self,
name,
path, # type: Path
category="main", # type: str
optional=False, # type: bool
base=None, # type: Path
):
self._path = path
self._base = base
self._full_path = path
if self._base and not self._path.is_absolute():
self._full_path = self._base / self._path
if not self._full_path.exists():
raise ValueError("File {} does not exist".format(self._path))
if self._full_path.is_dir():
raise ValueError("{} is a directory, expected a file".format(self._path))
super(FileDependency, self).__init__(
name, "*", category=category, optional=optional, allows_prereleases=True
)
@property
def path(self):
return self._path
@property
def full_path(self):
return self._full_path.resolve()
def is_file(self):
return True
def hash(self):
h = hashlib.sha256()
with self._full_path.open("rb") as fp:
for content in iter(lambda: fp.read(io.DEFAULT_BUFFER_SIZE), b""):
h.update(content)
return h.hexdigest()
...@@ -10,12 +10,13 @@ from tomlkit import item ...@@ -10,12 +10,13 @@ from tomlkit import item
from tomlkit import table from tomlkit import table
from tomlkit.exceptions import TOMLKitError from tomlkit.exceptions import TOMLKitError
import poetry.packages
import poetry.repositories import poetry.repositories
from poetry.core.packages.package import Dependency
from poetry.core.packages.package import Package
from poetry.core.version.markers import parse_marker
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
from poetry.version.markers import parse_marker
class Locker(object): class Locker(object):
...@@ -83,9 +84,7 @@ class Locker(object): ...@@ -83,9 +84,7 @@ class Locker(object):
return packages return packages
for info in locked_packages: for info in locked_packages:
package = poetry.packages.Package( package = Package(info["name"], info["version"], info["version"])
info["name"], info["version"], info["version"]
)
package.description = info.get("description", "") package.description = info.get("description", "")
package.category = info["category"] package.category = info["category"]
package.optional = info["optional"] package.optional = info["optional"]
...@@ -109,16 +108,14 @@ class Locker(object): ...@@ -109,16 +108,14 @@ class Locker(object):
dep_name = m.group(1) dep_name = m.group(1)
constraint = m.group(2) or "*" constraint = m.group(2) or "*"
package.extras[name].append( package.extras[name].append(Dependency(dep_name, constraint))
poetry.packages.Dependency(dep_name, constraint)
)
if "marker" in info: if "marker" in info:
package.marker = parse_marker(info["marker"]) package.marker = parse_marker(info["marker"])
else: else:
# Compatibility for old locks # Compatibility for old locks
if "requirements" in info: if "requirements" in info:
dep = poetry.packages.Dependency("foo", "0.0.0") dep = Dependency("foo", "0.0.0")
for name, value in info["requirements"].items(): for name, value in info["requirements"].items():
if name == "python": if name == "python":
dep.python_versions = value dep.python_versions = value
...@@ -238,7 +235,7 @@ class Locker(object): ...@@ -238,7 +235,7 @@ class Locker(object):
return locked return locked
def _dump_package(self, package): # type: (poetry.packages.Package) -> dict def _dump_package(self, package): # type: (Package) -> dict
dependencies = {} dependencies = {}
for dependency in sorted(package.requires, key=lambda d: d.name): for dependency in sorted(package.requires, key=lambda d: d.name):
if dependency.is_optional() and not dependency.is_activated(): if dependency.is_optional() and not dependency.is_activated():
......
from poetry.semver import VersionRange
from poetry.semver import parse_constraint
from poetry.version.markers import parse_marker
from .package import Package
from .utils.utils import create_nested_marker
class ProjectPackage(Package):
def __init__(self, name, version, pretty_version=None):
super(ProjectPackage, self).__init__(name, version, pretty_version)
self.build = None
self.packages = []
self.include = []
self.exclude = []
self.custom_urls = {}
if self._python_versions == "*":
self._python_constraint = parse_constraint("~2.7 || >=3.4")
def is_root(self):
return True
def to_dependency(self):
dependency = super(ProjectPackage, self).to_dependency()
dependency.is_root = True
return dependency
@property
def python_versions(self):
return self._python_versions
@python_versions.setter
def python_versions(self, value):
self._python_versions = value
if value == "*" or value == VersionRange():
value = "~2.7 || >=3.4"
self._python_constraint = parse_constraint(value)
self._python_marker = parse_marker(
create_nested_marker("python_version", self._python_constraint)
)
@property
def urls(self):
urls = super(ProjectPackage, self).urls
urls.update(self.custom_urls)
return urls
def clone(self): # type: () -> ProjectPackage
package = super(ProjectPackage, self).clone()
package.build = self.build
package.packages = self.packages[:]
package.include = self.include[:]
package.exclude = self.exclude[:]
return package
from poetry.utils._compat import urlparse
from .dependency import Dependency
class URLDependency(Dependency):
def __init__(
self,
name,
url, # type: str
category="main", # type: str
optional=False, # type: bool
):
self._url = url
parsed = urlparse.urlparse(url)
if not parsed.scheme or not parsed.netloc:
raise ValueError("{} does not seem like a valid url".format(url))
super(URLDependency, self).__init__(
name, "*", category=category, optional=optional, allows_prereleases=True
)
@property
def url(self):
return self._url
@property
def base_pep_508_name(self): # type: () -> str
requirement = self.pretty_name
if self.extras:
requirement += "[{}]".format(",".join(self.extras))
requirement += " @ {}".format(self._url)
return requirement
def is_url(self): # type: () -> bool
return True
import posixpath
import re
from .utils import path_to_url
from .utils import splitext
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
class Link:
def __init__(self, url, comes_from=None, requires_python=None):
"""
Object representing a parsed link from https://pypi.python.org/simple/*
url:
url of the resource pointed to (href of the link)
comes_from:
instance of HTMLPage where the link was found, or string.
requires_python:
String containing the `Requires-Python` metadata field, specified
in PEP 345. This may be specified by a data-requires-python
attribute in the HTML link tag, as described in PEP 503.
"""
# url can be a UNC windows share
if url.startswith("\\\\"):
url = path_to_url(url)
self.url = url
self.comes_from = comes_from
self.requires_python = requires_python if requires_python else None
def __str__(self):
if self.requires_python:
rp = " (requires-python:%s)" % self.requires_python
else:
rp = ""
if self.comes_from:
return "%s (from %s)%s" % (self.url, self.comes_from, rp)
else:
return str(self.url)
def __repr__(self):
return "<Link %s>" % self
def __eq__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url == other.url
def __ne__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url != other.url
def __lt__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url < other.url
def __le__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url <= other.url
def __gt__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url > other.url
def __ge__(self, other):
if not isinstance(other, Link):
return NotImplemented
return self.url >= other.url
def __hash__(self):
return hash(self.url)
@property
def filename(self):
_, netloc, path, _, _ = urlparse.urlsplit(self.url)
name = posixpath.basename(path.rstrip("/")) or netloc
name = urlparse.unquote(name)
assert name, "URL %r produced no filename" % self.url
return name
@property
def scheme(self):
return urlparse.urlsplit(self.url)[0]
@property
def netloc(self):
return urlparse.urlsplit(self.url)[1]
@property
def path(self):
return urlparse.unquote(urlparse.urlsplit(self.url)[2])
def splitext(self):
return splitext(posixpath.basename(self.path.rstrip("/")))
@property
def ext(self):
return self.splitext()[1]
@property
def url_without_fragment(self):
scheme, netloc, path, query, fragment = urlparse.urlsplit(self.url)
return urlparse.urlunsplit((scheme, netloc, path, query, None))
_egg_fragment_re = re.compile(r"[#&]egg=([^&]*)")
@property
def egg_fragment(self):
match = self._egg_fragment_re.search(self.url)
if not match:
return None
return match.group(1)
_subdirectory_fragment_re = re.compile(r"[#&]subdirectory=([^&]*)")
@property
def subdirectory_fragment(self):
match = self._subdirectory_fragment_re.search(self.url)
if not match:
return None
return match.group(1)
_hash_re = re.compile(r"(sha1|sha224|sha384|sha256|sha512|md5)=([a-f0-9]+)")
@property
def hash(self):
match = self._hash_re.search(self.url)
if match:
return match.group(2)
return None
@property
def hash_name(self):
match = self._hash_re.search(self.url)
if match:
return match.group(1)
return None
@property
def show_url(self):
return posixpath.basename(self.url.split("#", 1)[0].split("?", 1)[0])
@property
def is_wheel(self):
return self.ext == ".whl"
@property
def is_artifact(self):
"""
Determines if this points to an actual artifact (e.g. a tarball) or if
it points to an "abstract" thing like a path or a VCS location.
"""
if self.scheme in ["ssh", "git", "hg", "bzr", "sftp", "svn"]:
return False
return True
import os
import posixpath
import re
from poetry.packages.constraints.constraint import Constraint
from poetry.packages.constraints.multi_constraint import MultiConstraint
from poetry.packages.constraints.union_constraint import UnionConstraint
from poetry.semver import EmptyConstraint
from poetry.semver import Version
from poetry.semver import VersionConstraint
from poetry.semver import VersionRange
from poetry.semver import VersionUnion
from poetry.semver import parse_constraint
from poetry.version.markers import BaseMarker
from poetry.version.markers import MarkerUnion
from poetry.version.markers import MultiMarker
from poetry.version.markers import SingleMarker
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
try:
import urllib.request as urllib2
except ImportError:
import urllib2
BZ2_EXTENSIONS = (".tar.bz2", ".tbz")
XZ_EXTENSIONS = (".tar.xz", ".txz", ".tlz", ".tar.lz", ".tar.lzma")
ZIP_EXTENSIONS = (".zip", ".whl")
TAR_EXTENSIONS = (".tar.gz", ".tgz", ".tar")
ARCHIVE_EXTENSIONS = ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS
SUPPORTED_EXTENSIONS = ZIP_EXTENSIONS + TAR_EXTENSIONS
try:
import bz2 # noqa
SUPPORTED_EXTENSIONS += BZ2_EXTENSIONS
except ImportError:
pass
try:
# Only for Python 3.3+
import lzma # noqa
SUPPORTED_EXTENSIONS += XZ_EXTENSIONS
except ImportError:
pass
def path_to_url(path):
"""
Convert a path to a file: URL. The path will be made absolute and have
quoted path parts.
"""
path = os.path.normpath(os.path.abspath(path))
url = urlparse.urljoin("file:", urllib2.pathname2url(path))
return url
def is_url(name):
if ":" not in name:
return False
scheme = name.split(":", 1)[0].lower()
return scheme in [
"http",
"https",
"file",
"ftp",
"ssh",
"git",
"hg",
"bzr",
"sftp",
"svn" "ssh",
]
def strip_extras(path):
m = re.match(r"^(.+)(\[[^\]]+\])$", path)
extras = None
if m:
path_no_extras = m.group(1)
extras = m.group(2)
else:
path_no_extras = path
return path_no_extras, extras
def is_installable_dir(path):
"""Return True if `path` is a directory containing a setup.py file."""
if not os.path.isdir(path):
return False
setup_py = os.path.join(path, "setup.py")
if os.path.isfile(setup_py):
return True
return False
def is_archive_file(name):
"""Return True if `name` is a considered as an archive file."""
ext = splitext(name)[1].lower()
if ext in ARCHIVE_EXTENSIONS:
return True
return False
def splitext(path):
"""Like os.path.splitext, but take off .tar too"""
base, ext = posixpath.splitext(path)
if base.lower().endswith(".tar"):
ext = base[-4:] + ext
base = base[:-4]
return base, ext
def group_markers(markers, or_=False):
groups = [[]]
for marker in markers:
if or_:
groups.append([])
if isinstance(marker, (MultiMarker, MarkerUnion)):
groups[-1].append(
group_markers(marker.markers, isinstance(marker, MarkerUnion))
)
elif isinstance(marker, SingleMarker):
lhs, op, rhs = marker.name, marker.operator, marker.value
groups[-1].append((lhs, op, rhs))
return groups
def convert_markers(marker):
groups = group_markers([marker])
requirements = {}
def _group(_groups, or_=False):
ors = {}
for group in _groups:
if isinstance(group, list):
_group(group, or_=True)
else:
variable, op, value = group
group_name = str(variable)
# python_full_version is equivalent to python_version
# for Poetry so we merge them
if group_name == "python_full_version":
group_name = "python_version"
if group_name not in requirements:
requirements[group_name] = []
if group_name not in ors:
ors[group_name] = or_
if ors[group_name] or not requirements[group_name]:
requirements[group_name].append([])
requirements[group_name][-1].append((str(op), str(value)))
ors[group_name] = False
_group(groups, or_=True)
return requirements
def create_nested_marker(name, constraint):
if constraint.is_any():
return ""
if isinstance(constraint, (MultiConstraint, UnionConstraint)):
parts = []
for c in constraint.constraints:
multi = False
if isinstance(c, (MultiConstraint, UnionConstraint)):
multi = True
parts.append((multi, create_nested_marker(name, c)))
glue = " and "
if isinstance(constraint, UnionConstraint):
parts = ["({})".format(part[1]) if part[0] else part[1] for part in parts]
glue = " or "
else:
parts = [part[1] for part in parts]
marker = glue.join(parts)
elif isinstance(constraint, Constraint):
marker = '{} {} "{}"'.format(name, constraint.operator, constraint.version)
elif isinstance(constraint, VersionUnion):
parts = []
for c in constraint.ranges:
parts.append(create_nested_marker(name, c))
glue = " or "
parts = ["({})".format(part) for part in parts]
marker = glue.join(parts)
elif isinstance(constraint, Version):
marker = '{} == "{}"'.format(name, constraint.text)
else:
if constraint.min is not None:
op = ">="
if not constraint.include_min:
op = ">"
version = constraint.min.text
if constraint.max is not None:
text = '{} {} "{}"'.format(name, op, version)
op = "<="
if not constraint.include_max:
op = "<"
version = constraint.max
text += ' and {} {} "{}"'.format(name, op, version)
return text
elif constraint.max is not None:
op = "<="
if not constraint.include_max:
op = "<"
version = constraint.max
else:
return ""
marker = '{} {} "{}"'.format(name, op, version)
return marker
def get_python_constraint_from_marker(
marker,
): # type: (BaseMarker) -> VersionConstraint
python_marker = marker.only("python_version")
if python_marker.is_any():
return VersionRange()
if python_marker.is_empty():
return EmptyConstraint()
markers = convert_markers(marker)
ors = []
for or_ in markers["python_version"]:
ands = []
for op, version in or_:
# Expand python version
if op == "==":
version = "~" + version
op = ""
elif op == "!=":
version += ".*"
elif op in ("<=", ">"):
parsed_version = Version.parse(version)
if parsed_version.precision == 1:
if op == "<=":
op = "<"
version = parsed_version.next_major.text
elif op == ">":
op = ">="
version = parsed_version.next_major.text
elif parsed_version.precision == 2:
if op == "<=":
op = "<"
version = parsed_version.next_minor.text
elif op == ">":
op = ">="
version = parsed_version.next_minor.text
elif op in ("in", "not in"):
versions = []
for v in re.split("[ ,]+", version):
split = v.split(".")
if len(split) in [1, 2]:
split.append("*")
op_ = "" if op == "in" else "!="
else:
op_ = "==" if op == "in" else "!="
versions.append(op_ + ".".join(split))
glue = " || " if op == "in" else ", "
if versions:
ands.append(glue.join(versions))
continue
ands.append("{}{}".format(op, version))
ors.append(" ".join(ands))
return parse_constraint(" || ".join(ors))
from poetry.vcs import git
from .dependency import Dependency
class VCSDependency(Dependency):
"""
Represents a VCS dependency
"""
def __init__(
self,
name,
vcs,
source,
branch=None,
tag=None,
rev=None,
category="main",
optional=False,
):
self._vcs = vcs
self._source = source
if not any([branch, tag, rev]):
# If nothing has been specified, we assume master
branch = "master"
self._branch = branch
self._tag = tag
self._rev = rev
super(VCSDependency, self).__init__(
name, "*", category=category, optional=optional, allows_prereleases=True
)
@property
def vcs(self):
return self._vcs
@property
def source(self):
return self._source
@property
def branch(self):
return self._branch
@property
def tag(self):
return self._tag
@property
def rev(self):
return self._rev
@property
def reference(self): # type: () -> str
return self._branch or self._tag or self._rev
@property
def pretty_constraint(self): # type: () -> str
if self._branch:
what = "branch"
version = self._branch
elif self._tag:
what = "tag"
version = self._tag
else:
what = "rev"
version = self._rev
return "{} {}".format(what, version)
@property
def base_pep_508_name(self): # type: () -> str
requirement = self.pretty_name
parsed_url = git.ParsedUrl.parse(self._source)
if self.extras:
requirement += "[{}]".format(",".join(self.extras))
if parsed_url.protocol is not None:
requirement += " @ {}+{}@{}".format(self._vcs, self._source, self.reference)
else:
requirement += " @ {}+ssh://{}@{}".format(
self._vcs, parsed_url.format(), self.reference
)
return requirement
def is_vcs(self): # type: () -> bool
return True
def accepts_prereleases(self): # type: () -> bool
return True
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
from poetry.core.packages import ProjectPackage
from poetry.core.poetry import Poetry as BasePoetry
from .__version__ import __version__ from .__version__ import __version__
from .config.config import Config from .config.config import Config
from .packages import Locker from .packages import Locker
from .packages import ProjectPackage
from .repositories.pool import Pool from .repositories.pool import Pool
from .utils._compat import Path from .utils._compat import Path
from .utils.toml_file import TomlFile
class Poetry: class Poetry(BasePoetry):
VERSION = __version__ VERSION = __version__
...@@ -22,26 +23,13 @@ class Poetry: ...@@ -22,26 +23,13 @@ class Poetry:
locker, # type: Locker locker, # type: Locker
config, # type: Config config, # type: Config
): ):
self._file = TomlFile(file) super(Poetry, self).__init__(file, local_config, package)
self._package = package
self._local_config = local_config
self._locker = locker self._locker = locker
self._config = config self._config = config
self._pool = Pool() self._pool = Pool()
@property @property
def file(self):
return self._file
@property
def package(self): # type: () -> ProjectPackage
return self._package
@property
def local_config(self): # type: () -> dict
return self._local_config
@property
def locker(self): # type: () -> Locker def locker(self): # type: () -> Locker
return self._locker return self._locker
......
...@@ -15,14 +15,13 @@ from requests_toolbelt.multipart import MultipartEncoder ...@@ -15,14 +15,13 @@ from requests_toolbelt.multipart import MultipartEncoder
from requests_toolbelt.multipart import MultipartEncoderMonitor from requests_toolbelt.multipart import MultipartEncoderMonitor
from poetry.__version__ import __version__ from poetry.__version__ import __version__
from poetry.core.masonry.metadata import Metadata
from poetry.core.masonry.utils.helpers import escape_name
from poetry.core.masonry.utils.helpers import escape_version
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.helpers import normalize_version from poetry.utils.helpers import normalize_version
from poetry.utils.patterns import wheel_file_re from poetry.utils.patterns import wheel_file_re
from ..metadata import Metadata
from ..utils.helpers import escape_name
from ..utils.helpers import escape_version
_has_blake2 = hasattr(hashlib, "blake2b") _has_blake2 = hasattr(hashlib, "blake2b")
......
class Dependencies:
"""
Proxy to package dependencies to only require them when needed.
"""
def __init__(self, package, provider):
self._package = package
self._provider = provider
self._dependencies = None
@property
def dependencies(self):
if self._dependencies is None:
self._dependencies = self._get_dependencies()
return self._dependencies
def _get_dependencies(self):
self._provider.debug("Getting dependencies for {}".format(self._package), 0)
dependencies = self._provider._dependencies_for(self._package)
if dependencies is None:
dependencies = []
return dependencies
def __len__(self):
return len(self.dependencies)
def __iter__(self):
return self.dependencies.__iter__()
def __add__(self, other):
return self.dependencies + other
__radd__ = __add__
...@@ -14,21 +14,23 @@ import pkginfo ...@@ -14,21 +14,23 @@ import pkginfo
from clikit.ui.components import ProgressIndicator from clikit.ui.components import ProgressIndicator
from poetry.core.packages import Dependency
from poetry.core.packages import DirectoryDependency
from poetry.core.packages import FileDependency
from poetry.core.packages import Package
from poetry.core.packages import URLDependency
from poetry.core.packages import VCSDependency
from poetry.core.packages import dependency_from_pep_508
from poetry.core.packages.utils.utils import get_python_constraint_from_marker
from poetry.core.vcs.git import Git
from poetry.core.version.markers import MarkerUnion
from poetry.factory import Factory from poetry.factory import Factory
from poetry.mixology.incompatibility import Incompatibility from poetry.mixology.incompatibility import Incompatibility
from poetry.mixology.incompatibility_cause import DependencyCause from poetry.mixology.incompatibility_cause import DependencyCause
from poetry.mixology.incompatibility_cause import PythonCause from poetry.mixology.incompatibility_cause import PythonCause
from poetry.mixology.term import Term from poetry.mixology.term import Term
from poetry.packages import Dependency
from poetry.packages import DependencyPackage from poetry.packages import DependencyPackage
from poetry.packages import DirectoryDependency from poetry.packages.package_collection import PackageCollection
from poetry.packages import FileDependency
from poetry.packages import Package
from poetry.packages import PackageCollection
from poetry.packages import URLDependency
from poetry.packages import VCSDependency
from poetry.packages import dependency_from_pep_508
from poetry.packages.utils.utils import get_python_constraint_from_marker
from poetry.repositories import Pool from poetry.repositories import Pool
from poetry.utils._compat import PY35 from poetry.utils._compat import PY35
from poetry.utils._compat import OrderedDict from poetry.utils._compat import OrderedDict
...@@ -43,8 +45,6 @@ from poetry.utils.helpers import temporary_directory ...@@ -43,8 +45,6 @@ from poetry.utils.helpers import temporary_directory
from poetry.utils.inspector import Inspector from poetry.utils.inspector import Inspector
from poetry.utils.setup_reader import SetupReader from poetry.utils.setup_reader import SetupReader
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
from poetry.vcs.git import Git
from poetry.version.markers import MarkerUnion
from .exceptions import CompatibilityError from .exceptions import CompatibilityError
......
...@@ -4,12 +4,12 @@ from typing import Any ...@@ -4,12 +4,12 @@ from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
from poetry.core.packages import Package
from poetry.core.semver import parse_constraint
from poetry.core.version.markers import AnyMarker
from poetry.mixology import resolve_version from poetry.mixology import resolve_version
from poetry.mixology.failure import SolveFailure from poetry.mixology.failure import SolveFailure
from poetry.packages import DependencyPackage from poetry.packages import DependencyPackage
from poetry.packages import Package
from poetry.semver import parse_constraint
from poetry.version.markers import AnyMarker
from .exceptions import CompatibilityError from .exceptions import CompatibilityError
from .exceptions import SolverProblemError from .exceptions import SolverProblemError
...@@ -56,7 +56,7 @@ class Solver: ...@@ -56,7 +56,7 @@ class Solver:
installed = True installed = True
if pkg.source_type == "git" and package.source_type == "git": if pkg.source_type == "git" and package.source_type == "git":
from poetry.vcs.git import Git from poetry.core.vcs.git import Git
# Trying to find the currently installed version # Trying to find the currently installed version
pkg_source_url = Git.normalize_url(pkg.source_url) pkg_source_url = Git.normalize_url(pkg.source_url)
......
from poetry import _CURRENT_VENDOR from poetry.core.packages import Package
from poetry.packages import Package
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import metadata from poetry.utils._compat import metadata
from poetry.utils.env import Env from poetry.utils.env import Env
...@@ -7,6 +6,9 @@ from poetry.utils.env import Env ...@@ -7,6 +6,9 @@ from poetry.utils.env import Env
from .repository import Repository from .repository import Repository
_VENDORS = Path(__file__).parent.parent.joinpath("_vendor")
class InstalledRepository(Repository): class InstalledRepository(Repository):
@classmethod @classmethod
def load(cls, env): # type: (Env) -> InstalledRepository def load(cls, env): # type: (Env) -> InstalledRepository
...@@ -32,7 +34,7 @@ class InstalledRepository(Repository): ...@@ -32,7 +34,7 @@ class InstalledRepository(Repository):
continue continue
try: try:
path.relative_to(_CURRENT_VENDOR) path.relative_to(_VENDORS)
except ValueError: except ValueError:
pass pass
else: else:
...@@ -58,7 +60,7 @@ class InstalledRepository(Repository): ...@@ -58,7 +60,7 @@ class InstalledRepository(Repository):
try: try:
path.relative_to(src_path) path.relative_to(src_path)
from poetry.vcs.git import Git from poetry.core.vcs.git import Git
git = Git() git = Git()
revision = git.rev_parse("HEAD", src_path / package.name).strip() revision = git.rev_parse("HEAD", src_path / package.name).strip()
......
...@@ -13,21 +13,19 @@ from cachecontrol import CacheControl ...@@ -13,21 +13,19 @@ from cachecontrol import CacheControl
from cachecontrol.caches.file_cache import FileCache from cachecontrol.caches.file_cache import FileCache
from cachy import CacheManager from cachy import CacheManager
import poetry.packages from poetry.core.packages import Package
from poetry.core.packages import dependency_from_pep_508
from poetry.core.packages.utils.link import Link
from poetry.core.semver import Version
from poetry.core.semver import VersionConstraint
from poetry.core.semver import VersionRange
from poetry.core.semver import parse_constraint
from poetry.core.version.markers import InvalidMarker
from poetry.locations import REPOSITORY_CACHE_DIR from poetry.locations import REPOSITORY_CACHE_DIR
from poetry.packages import Package
from poetry.packages import dependency_from_pep_508
from poetry.packages.utils.link import Link
from poetry.semver import Version
from poetry.semver import VersionConstraint
from poetry.semver import VersionRange
from poetry.semver import parse_constraint
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import canonicalize_name
from poetry.utils.inspector import Inspector from poetry.utils.inspector import Inspector
from poetry.utils.patterns import wheel_file_re from poetry.utils.patterns import wheel_file_re
from poetry.version.markers import InvalidMarker
from .auth import Auth from .auth import Auth
from .exceptions import PackageNotFound from .exceptions import PackageNotFound
...@@ -283,9 +281,7 @@ class LegacyRepository(PyPiRepository): ...@@ -283,9 +281,7 @@ class LegacyRepository(PyPiRepository):
return packages return packages
def package( def package(self, name, version, extras=None): # type: (...) -> Package
self, name, version, extras=None
): # type: (...) -> poetry.packages.Package
""" """
Retrieve the release information. Retrieve the release information.
...@@ -298,9 +294,7 @@ class LegacyRepository(PyPiRepository): ...@@ -298,9 +294,7 @@ class LegacyRepository(PyPiRepository):
should be much faster. should be much faster.
""" """
try: try:
index = self._packages.index( index = self._packages.index(Package(name, version, version))
poetry.packages.Package(name, version, version)
)
return self._packages[index] return self._packages[index]
except ValueError: except ValueError:
...@@ -309,7 +303,7 @@ class LegacyRepository(PyPiRepository): ...@@ -309,7 +303,7 @@ class LegacyRepository(PyPiRepository):
release_info = self.get_release_info(name, version) release_info = self.get_release_info(name, version)
package = poetry.packages.Package(name, version, version) package = Package(name, version, version)
if release_info["requires_python"]: if release_info["requires_python"]:
package.python_versions = release_info["requires_python"] package.python_versions = release_info["requires_python"]
......
...@@ -15,21 +15,21 @@ from requests import get ...@@ -15,21 +15,21 @@ from requests import get
from requests import session from requests import session
from requests.exceptions import TooManyRedirects from requests.exceptions import TooManyRedirects
from poetry.core.packages import Package
from poetry.core.packages import dependency_from_pep_508
from poetry.core.packages.utils.link import Link
from poetry.core.semver import VersionConstraint
from poetry.core.semver import VersionRange
from poetry.core.semver import parse_constraint
from poetry.core.semver.exceptions import ParseVersionError
from poetry.core.version.markers import InvalidMarker
from poetry.core.version.markers import parse_marker
from poetry.locations import REPOSITORY_CACHE_DIR from poetry.locations import REPOSITORY_CACHE_DIR
from poetry.packages import Package
from poetry.packages import dependency_from_pep_508
from poetry.packages.utils.link import Link
from poetry.semver import VersionConstraint
from poetry.semver import VersionRange
from poetry.semver import parse_constraint
from poetry.semver.exceptions import ParseVersionError
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import to_str from poetry.utils._compat import to_str
from poetry.utils.helpers import temporary_directory from poetry.utils.helpers import temporary_directory
from poetry.utils.inspector import Inspector from poetry.utils.inspector import Inspector
from poetry.utils.patterns import wheel_file_re from poetry.utils.patterns import wheel_file_re
from poetry.version.markers import InvalidMarker
from poetry.version.markers import parse_marker
from .exceptions import PackageNotFound from .exceptions import PackageNotFound
from .remote_repository import RemoteRepository from .remote_repository import RemoteRepository
......
from poetry.semver import VersionConstraint from poetry.core.semver import VersionConstraint
from poetry.semver import VersionRange from poetry.core.semver import VersionRange
from poetry.semver import parse_constraint from poetry.core.semver import parse_constraint
from .base_repository import BaseRepository from .base_repository import BaseRepository
......
import re
from .empty_constraint import EmptyConstraint
from .patterns import BASIC_CONSTRAINT
from .patterns import CARET_CONSTRAINT
from .patterns import TILDE_CONSTRAINT
from .patterns import TILDE_PEP440_CONSTRAINT
from .patterns import X_CONSTRAINT
from .version import Version
from .version_constraint import VersionConstraint
from .version_range import VersionRange
from .version_union import VersionUnion
def parse_constraint(constraints): # type: (str) -> VersionConstraint
if constraints == "*":
return VersionRange()
or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip())
or_groups = []
for constraints in or_constraints:
and_constraints = re.split(
"(?<!^)(?<![=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)", constraints
)
constraint_objects = []
if len(and_constraints) > 1:
for constraint in and_constraints:
constraint_objects.append(parse_single_constraint(constraint))
else:
constraint_objects.append(parse_single_constraint(and_constraints[0]))
if len(constraint_objects) == 1:
constraint = constraint_objects[0]
else:
constraint = constraint_objects[0]
for next_constraint in constraint_objects[1:]:
constraint = constraint.intersect(next_constraint)
or_groups.append(constraint)
if len(or_groups) == 1:
return or_groups[0]
else:
return VersionUnion.of(*or_groups)
def parse_single_constraint(constraint): # type: (str) -> VersionConstraint
m = re.match(r"(?i)^v?[xX*](\.[xX*])*$", constraint)
if m:
return VersionRange()
# Tilde range
m = TILDE_CONSTRAINT.match(constraint)
if m:
version = Version.parse(m.group(1))
high = version.stable.next_minor
if len(m.group(1).split(".")) == 1:
high = version.stable.next_major
return VersionRange(
version, high, include_min=True, always_include_max_prerelease=True
)
# PEP 440 Tilde range (~=)
m = TILDE_PEP440_CONSTRAINT.match(constraint)
if m:
precision = 1
if m.group(3):
precision += 1
if m.group(4):
precision += 1
version = Version.parse(m.group(1))
if precision == 2:
high = version.stable.next_major
else:
high = version.stable.next_minor
return VersionRange(
version, high, include_min=True, always_include_max_prerelease=True
)
# Caret range
m = CARET_CONSTRAINT.match(constraint)
if m:
version = Version.parse(m.group(1))
return VersionRange(
version,
version.next_breaking,
include_min=True,
always_include_max_prerelease=True,
)
# X Range
m = X_CONSTRAINT.match(constraint)
if m:
op = m.group(1)
major = int(m.group(2))
minor = m.group(3)
if minor is not None:
version = Version(major, int(minor), 0)
result = VersionRange(
version,
version.next_minor,
include_min=True,
always_include_max_prerelease=True,
)
else:
if major == 0:
result = VersionRange(max=Version(1, 0, 0))
else:
version = Version(major, 0, 0)
result = VersionRange(
version,
version.next_major,
include_min=True,
always_include_max_prerelease=True,
)
if op == "!=":
result = VersionRange().difference(result)
return result
# Basic comparator
m = BASIC_CONSTRAINT.match(constraint)
if m:
op = m.group(1)
version = m.group(2)
if version == "dev":
version = "0.0-dev"
try:
version = Version.parse(version)
except ValueError:
raise ValueError(
"Could not parse version constraint: {}".format(constraint)
)
if op == "<":
return VersionRange(max=version)
elif op == "<=":
return VersionRange(max=version, include_max=True)
elif op == ">":
return VersionRange(min=version)
elif op == ">=":
return VersionRange(min=version, include_min=True)
elif op == "!=":
return VersionUnion(VersionRange(max=version), VersionRange(min=version))
else:
return version
raise ValueError("Could not parse version constraint: {}".format(constraint))
from .version_constraint import VersionConstraint
class EmptyConstraint(VersionConstraint):
def is_empty(self):
return True
def is_any(self):
return False
def allows(self, version):
return False
def allows_all(self, other):
return other.is_empty()
def allows_any(self, other):
return False
def intersect(self, other):
return self
def union(self, other):
return other
def difference(self, other):
return self
def __str__(self):
return "<empty>"
class ParseVersionError(ValueError):
pass
import re
MODIFIERS = (
"[._-]?"
r"((?!post)(?:beta|b|c|pre|RC|alpha|a|patch|pl|p|dev)(?:(?:[.-]?\d+)*)?)?"
r"([+-]?([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?"
)
_COMPLETE_VERSION = r"v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?{}(?:\+[^\s]+)?".format(
MODIFIERS
)
COMPLETE_VERSION = re.compile("(?i)" + _COMPLETE_VERSION)
CARET_CONSTRAINT = re.compile(r"(?i)^\^({})$".format(_COMPLETE_VERSION))
TILDE_CONSTRAINT = re.compile("(?i)^~(?!=)({})$".format(_COMPLETE_VERSION))
TILDE_PEP440_CONSTRAINT = re.compile("(?i)^~=({})$".format(_COMPLETE_VERSION))
X_CONSTRAINT = re.compile(r"^(!=|==)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$")
BASIC_CONSTRAINT = re.compile(
r"(?i)^(<>|!=|>=?|<=?|==?)?\s*({}|dev)".format(_COMPLETE_VERSION)
)
class VersionConstraint:
def is_empty(self): # type: () -> bool
raise NotImplementedError()
def is_any(self): # type: () -> bool
raise NotImplementedError()
def allows(self, version): # type: ("Version") -> bool
raise NotImplementedError()
def allows_all(self, other): # type: (VersionConstraint) -> bool
raise NotImplementedError()
def allows_any(self, other): # type: (VersionConstraint) -> bool
raise NotImplementedError()
def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint
raise NotImplementedError()
def union(self, other): # type: (VersionConstraint) -> VersionConstraint
raise NotImplementedError()
def difference(self, other): # type: (VersionConstraint) -> VersionConstraint
raise NotImplementedError()
from typing import List
from .empty_constraint import EmptyConstraint
from .version_constraint import VersionConstraint
class VersionUnion(VersionConstraint):
"""
A version constraint representing a union of multiple disjoint version
ranges.
An instance of this will only be created if the version can't be represented
as a non-compound value.
"""
def __init__(self, *ranges):
self._ranges = list(ranges)
@property
def ranges(self):
return self._ranges
@classmethod
def of(cls, *ranges):
from .version_range import VersionRange
flattened = []
for constraint in ranges:
if constraint.is_empty():
continue
if isinstance(constraint, VersionUnion):
flattened += constraint.ranges
continue
flattened.append(constraint)
if not flattened:
return EmptyConstraint()
if any([constraint.is_any() for constraint in flattened]):
return VersionRange()
# Only allow Versions and VersionRanges here so we can more easily reason
# about everything in flattened. _EmptyVersions and VersionUnions are
# filtered out above.
for constraint in flattened:
if isinstance(constraint, VersionRange):
continue
raise ValueError("Unknown VersionConstraint type {}.".format(constraint))
flattened.sort()
merged = []
for constraint in flattened:
# Merge this constraint with the previous one, but only if they touch.
if not merged or (
not merged[-1].allows_any(constraint)
and not merged[-1].is_adjacent_to(constraint)
):
merged.append(constraint)
else:
merged[-1] = merged[-1].union(constraint)
if len(merged) == 1:
return merged[0]
return VersionUnion(*merged)
def is_empty(self):
return False
def is_any(self):
return False
def allows(self, version): # type: ("Version") -> bool
return any([constraint.allows(version) for constraint in self._ranges])
def allows_all(self, other): # type: (VersionConstraint) -> bool
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
our_current_range = next(our_ranges, None)
their_current_range = next(their_ranges, None)
while our_current_range and their_current_range:
if our_current_range.allows_all(their_current_range):
their_current_range = next(their_ranges, None)
else:
our_current_range = next(our_ranges, None)
return their_current_range is None
def allows_any(self, other): # type: (VersionConstraint) -> bool
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
our_current_range = next(our_ranges, None)
their_current_range = next(their_ranges, None)
while our_current_range and their_current_range:
if our_current_range.allows_any(their_current_range):
return True
if their_current_range.allows_higher(our_current_range):
our_current_range = next(our_ranges, None)
else:
their_current_range = next(their_ranges, None)
return False
def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
new_ranges = []
our_current_range = next(our_ranges, None)
their_current_range = next(their_ranges, None)
while our_current_range and their_current_range:
intersection = our_current_range.intersect(their_current_range)
if not intersection.is_empty():
new_ranges.append(intersection)
if their_current_range.allows_higher(our_current_range):
our_current_range = next(our_ranges, None)
else:
their_current_range = next(their_ranges, None)
return VersionUnion.of(*new_ranges)
def union(self, other): # type: (VersionConstraint) -> VersionConstraint
return VersionUnion.of(self, other)
def difference(self, other): # type: (VersionConstraint) -> VersionConstraint
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
new_ranges = []
state = {
"current": next(our_ranges, None),
"their_range": next(their_ranges, None),
}
def their_next_range():
state["their_range"] = next(their_ranges, None)
if state["their_range"]:
return True
new_ranges.append(state["current"])
our_current = next(our_ranges, None)
while our_current:
new_ranges.append(our_current)
our_current = next(our_ranges, None)
return False
def our_next_range(include_current=True):
if include_current:
new_ranges.append(state["current"])
our_current = next(our_ranges, None)
if not our_current:
return False
state["current"] = our_current
return True
while True:
if state["their_range"] is None:
break
if state["their_range"].is_strictly_lower(state["current"]):
if not their_next_range():
break
continue
if state["their_range"].is_strictly_higher(state["current"]):
if not our_next_range():
break
continue
difference = state["current"].difference(state["their_range"])
if isinstance(difference, VersionUnion):
assert len(difference.ranges) == 2
new_ranges.append(difference.ranges[0])
state["current"] = difference.ranges[-1]
if not their_next_range():
break
elif difference.is_empty():
if not our_next_range(False):
break
else:
state["current"] = difference
if state["current"].allows_higher(state["their_range"]):
if not their_next_range():
break
else:
if not our_next_range():
break
if not new_ranges:
return EmptyConstraint()
if len(new_ranges) == 1:
return new_ranges[0]
return VersionUnion.of(*new_ranges)
def _ranges_for(
self, constraint
): # type: (VersionConstraint) -> List["VersionRange"]
from .version_range import VersionRange
if constraint.is_empty():
return []
if isinstance(constraint, VersionUnion):
return constraint.ranges
if isinstance(constraint, VersionRange):
return [constraint]
raise ValueError("Unknown VersionConstraint type {}".format(constraint))
def excludes_single_version(self): # type: () -> bool
from .version import Version
from .version_range import VersionRange
return isinstance(VersionRange().difference(self), Version)
def __eq__(self, other):
if not isinstance(other, VersionUnion):
return False
return self._ranges == other.ranges
def __str__(self):
from .version_range import VersionRange
if self.excludes_single_version():
return "!={}".format(VersionRange().difference(self))
return " || ".join([str(r) for r in self._ranges])
def __repr__(self):
return "<VersionUnion {}>".format(str(self))
import json
import os
from io import open
from .license import License
from .updater import Updater
_licenses = None
def license_by_id(identifier):
if _licenses is None:
load_licenses()
id = identifier.lower()
if id not in _licenses:
raise ValueError("Invalid license id: {}".format(identifier))
return _licenses[id]
def load_licenses():
global _licenses
_licenses = {}
licenses_file = os.path.join(os.path.dirname(__file__), "data", "licenses.json")
with open(licenses_file, encoding="utf-8") as f:
data = json.loads(f.read())
for name, license_info in data.items():
license = License(name, license_info[0], license_info[1], license_info[2])
_licenses[name.lower()] = license
full_name = license_info[0].lower()
if full_name in _licenses:
existing_license = _licenses[full_name]
if not existing_license.is_deprecated:
continue
_licenses[full_name] = license
# Add a Proprietary license for non-standard licenses
_licenses["proprietary"] = License("Proprietary", "Proprietary", False, False)
if __name__ == "__main__":
updater = Updater()
updater.dump()
from collections import namedtuple
class License(namedtuple("License", "id name is_osi_approved is_deprecated")):
CLASSIFIER_SUPPORTED = {
# Not OSI Approved
"Aladdin",
"CC0-1.0",
"CECILL-B",
"CECILL-C",
"NPL-1.0",
"NPL-1.1",
# OSI Approved
"AFPL",
"AFL-1.1",
"AFL-1.2",
"AFL-2.0",
"AFL-2.1",
"AFL-3.0",
"Apache-1.1",
"Apache-2.0",
"APSL-1.1",
"APSL-1.2",
"APSL-2.0",
"Artistic-1.0",
"Artistic-2.0",
"AAL",
"AGPL-3.0",
"AGPL-3.0-only",
"AGPL-3.0-or-later",
"BSL-1.0",
"BSD-2-Clause",
"BSD-3-Clause",
"CDDL-1.0",
"CECILL-2.1",
"CPL-1.0",
"EFL-1.0",
"EFL-2.0",
"EPL-1.0",
"EPL-2.0",
"EUPL-1.1",
"EUPL-1.2",
"GPL-2.0",
"GPL-2.0+",
"GPL-2.0-only",
"GPL-2.0-or-later",
"GPL-3.0",
"GPL-3.0+",
"GPL-3.0-only",
"GPL-3.0-or-later",
"LGPL-2.0",
"LGPL-2.0+",
"LGPL-2.0-only",
"LGPL-2.0-or-later",
"LGPL-3.0",
"LGPL-3.0+",
"LGPL-3.0-only",
"LGPL-3.0-or-later",
"MIT",
"MPL-1.0",
"MPL-1.1",
"MPL-1.2",
"Nokia",
"W3C",
"ZPL-1.0",
"ZPL-2.0",
"ZPL-2.1",
}
CLASSIFIER_NAMES = {
# Not OSI Approved
"AFPL": "Aladdin Free Public License (AFPL)",
"CC0-1.0": "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication",
"CECILL-B": "CeCILL-B Free Software License Agreement (CECILL-B)",
"CECILL-C": "CeCILL-C Free Software License Agreement (CECILL-C)",
"NPL-1.0": "Netscape Public License (NPL)",
"NPL-1.1": "Netscape Public License (NPL)",
# OSI Approved
"AFL-1.1": "Academic Free License (AFL)",
"AFL-1.2": "Academic Free License (AFL)",
"AFL-2.0": "Academic Free License (AFL)",
"AFL-2.1": "Academic Free License (AFL)",
"AFL-3.0": "Academic Free License (AFL)",
"Apache-1.1": "Apache Software License",
"Apache-2.0": "Apache Software License",
"APSL-1.1": "Apple Public Source License",
"APSL-1.2": "Apple Public Source License",
"APSL-2.0": "Apple Public Source License",
"Artistic-1.0": "Artistic License",
"Artistic-2.0": "Artistic License",
"AAL": "Attribution Assurance License",
"AGPL-3.0": "GNU Affero General Public License v3",
"AGPL-3.0-only": "GNU Affero General Public License v3",
"AGPL-3.0-or-later": "GNU Affero General Public License v3 or later (AGPLv3+)",
"BSL-1.0": "Boost Software License 1.0 (BSL-1.0)",
"BSD-2-Clause": "BSD License",
"BSD-3-Clause": "BSD License",
"CDDL-1.0": "Common Development and Distribution License 1.0 (CDDL-1.0)",
"CECILL-2.1": "CEA CNRS Inria Logiciel Libre License, version 2.1 (CeCILL-2.1)",
"CPL-1.0": "Common Public License",
"EPL-1.0": "Eclipse Public License 1.0 (EPL-1.0)",
"EFL-1.0": "Eiffel Forum License",
"EFL-2.0": "Eiffel Forum License",
"EUPL-1.1": "European Union Public Licence 1.1 (EUPL 1.1)",
"EUPL-1.2": "European Union Public Licence 1.2 (EUPL 1.2)",
"GPL-2.0": "GNU General Public License v2 (GPLv2)",
"GPL-2.0-only": "GNU General Public License v2 (GPLv2)",
"GPL-2.0+": "GNU General Public License v2 or later (GPLv2+)",
"GPL-2.0-or-later": "GNU General Public License v2 or later (GPLv2+)",
"GPL-3.0": "GNU General Public License v3 (GPLv3)",
"GPL-3.0-only": "GNU General Public License v3 (GPLv3)",
"GPL-3.0+": "GNU General Public License v3 or later (GPLv3+)",
"GPL-3.0-or-later": "GNU General Public License v3 or later (GPLv3+)",
"LGPL-2.0": "GNU Lesser General Public License v2 (LGPLv2)",
"LGPL-2.0-only": "GNU Lesser General Public License v2 (LGPLv2)",
"LGPL-2.0+": "GNU Lesser General Public License v2 or later (LGPLv2+)",
"LGPL-2.0-or-later": "GNU Lesser General Public License v2 or later (LGPLv2+)",
"LGPL-3.0": "GNU Lesser General Public License v3 (LGPLv3)",
"LGPL-3.0-only": "GNU Lesser General Public License v3 (LGPLv3)",
"LGPL-3.0+": "GNU Lesser General Public License v3 or later (LGPLv3+)",
"LGPL-3.0-or-later": "GNU Lesser General Public License v3 or later (LGPLv3+)",
"MPL-1.0": "Mozilla Public License 1.0 (MPL)",
"MPL-1.1": "Mozilla Public License 1.1 (MPL 1.1)",
"MPL-2.0": "Mozilla Public License 2.0 (MPL 2.0)",
"W3C": "W3C License",
"ZPL-1.1": "Zope Public License",
"ZPL-2.0": "Zope Public License",
"ZPL-2.1": "Zope Public License",
}
@property
def classifier(self):
parts = ["License"]
if self.is_osi_approved:
parts.append("OSI Approved")
name = self.classifier_name
if name is not None:
parts.append(name)
return " :: ".join(parts)
@property
def classifier_name(self):
if self.id not in self.CLASSIFIER_SUPPORTED:
if self.is_osi_approved:
return None
return "Other/Proprietary License"
if self.id in self.CLASSIFIER_NAMES:
return self.CLASSIFIER_NAMES[self.id]
return self.name
import json
import os
from io import open
try:
from urllib.request import urlopen
except ImportError:
from urllib2 import urlopen
class Updater:
BASE_URL = "https://raw.githubusercontent.com/spdx/license-list-data/master/json/"
def __init__(self, base_url=BASE_URL):
self._base_url = base_url
def dump(self, file=None):
if file is None:
file = os.path.join(os.path.dirname(__file__), "data", "licenses.json")
licenses_url = self._base_url + "licenses.json"
with open(file, "w", encoding="utf-8") as f:
f.write(
json.dumps(self.get_licenses(licenses_url), indent=2, sort_keys=True)
)
def get_licenses(self, url):
licenses = {}
with urlopen(url) as r:
data = json.loads(r.read().decode())
for info in data["licenses"]:
licenses[info["licenseId"]] = [
info["name"],
info["isOsiApproved"],
info["isDeprecatedLicenseId"],
]
return licenses
...@@ -20,10 +20,11 @@ import tomlkit ...@@ -20,10 +20,11 @@ import tomlkit
from clikit.api.io import IO from clikit.api.io import IO
from poetry.core.semver import parse_constraint
from poetry.core.semver.version import Version
from poetry.core.version.markers import BaseMarker
from poetry.locations import CACHE_DIR from poetry.locations import CACHE_DIR
from poetry.poetry import Poetry from poetry.poetry import Poetry
from poetry.semver import parse_constraint
from poetry.semver.version import Version
from poetry.utils._compat import CalledProcessError from poetry.utils._compat import CalledProcessError
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import decode from poetry.utils._compat import decode
...@@ -31,7 +32,6 @@ from poetry.utils._compat import encode ...@@ -31,7 +32,6 @@ from poetry.utils._compat import encode
from poetry.utils._compat import list_to_shell_command from poetry.utils._compat import list_to_shell_command
from poetry.utils._compat import subprocess from poetry.utils._compat import subprocess
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
from poetry.version.markers import BaseMarker
GET_ENVIRONMENT_INFO = """\ GET_ENVIRONMENT_INFO = """\
......
...@@ -2,10 +2,10 @@ from typing import Union ...@@ -2,10 +2,10 @@ from typing import Union
from clikit.api.io import IO from clikit.api.io import IO
from poetry.packages.directory_dependency import DirectoryDependency from poetry.core.packages.directory_dependency import DirectoryDependency
from poetry.packages.file_dependency import FileDependency from poetry.core.packages.file_dependency import FileDependency
from poetry.packages.url_dependency import URLDependency from poetry.core.packages.url_dependency import URLDependency
from poetry.packages.vcs_dependency import VCSDependency from poetry.core.packages.vcs_dependency import VCSDependency
from poetry.poetry import Poetry from poetry.poetry import Poetry
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import decode from poetry.utils._compat import decode
......
...@@ -3,7 +3,7 @@ from typing import List ...@@ -3,7 +3,7 @@ from typing import List
from typing import Mapping from typing import Mapping
from typing import Sequence from typing import Sequence
from poetry.packages import Package from poetry.core.packages import Package
from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import canonicalize_name
......
...@@ -9,8 +9,8 @@ from typing import List ...@@ -9,8 +9,8 @@ from typing import List
from typing import Optional from typing import Optional
from poetry.config.config import Config from poetry.config.config import Config
from poetry.core.version import Version
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.version import Version
try: try:
......
import os
import subprocess
from poetry.utils._compat import Path
from poetry.utils._compat import decode
from .git import Git
def get_vcs(directory): # type: (Path) -> Git
working_dir = Path.cwd()
os.chdir(str(directory.resolve()))
try:
git_dir = decode(
subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], stderr=subprocess.STDOUT
)
).strip()
vcs = Git(Path(git_dir))
except (subprocess.CalledProcessError, OSError):
vcs = None
finally:
os.chdir(str(working_dir))
return vcs
# -*- coding: utf-8 -*-
import re
import subprocess
from collections import namedtuple
from poetry.utils._compat import decode
pattern_formats = {
"protocol": r"\w+",
"user": r"[a-zA-Z0-9_.-]+",
"resource": r"[a-zA-Z0-9_.-]+",
"port": r"\d+",
"path": r"[\w~.\-/\\]+",
"name": r"[\w~.\-]+",
"rev": r"[^@#]+",
}
PATTERNS = [
re.compile(
r"^(git\+)?"
r"(?P<protocol>https?|git|ssh|rsync|file)://"
r"(?:(?P<user>{user})@)?"
r"(?P<resource>{resource})?"
r"(:(?P<port>{port}))?"
r"(?P<pathname>[:/\\]({path}[/\\])?"
r"((?P<name>{name}?)(\.git|[/\\])?)?)"
r"([@#](?P<rev>{rev}))?"
r"$".format(
user=pattern_formats["user"],
resource=pattern_formats["resource"],
port=pattern_formats["port"],
path=pattern_formats["path"],
name=pattern_formats["name"],
rev=pattern_formats["rev"],
)
),
re.compile(
r"(git\+)?"
r"((?P<protocol>{protocol})://)"
r"(?:(?P<user>{user})@)?"
r"(?P<resource>{resource}:?)"
r"(:(?P<port>{port}))?"
r"(?P<pathname>({path})"
r"(?P<name>{name})(\.git|/)?)"
r"([@#](?P<rev>{rev}))?"
r"$".format(
protocol=pattern_formats["protocol"],
user=pattern_formats["user"],
resource=pattern_formats["resource"],
port=pattern_formats["port"],
path=pattern_formats["path"],
name=pattern_formats["name"],
rev=pattern_formats["rev"],
)
),
re.compile(
r"^(?:(?P<user>{user})@)?"
r"(?P<resource>{resource})"
r"(:(?P<port>{port}))?"
r"(?P<pathname>([:/]{path}/)"
r"(?P<name>{name})(\.git|/)?)"
r"([@#](?P<rev>{rev}))?"
r"$".format(
user=pattern_formats["user"],
resource=pattern_formats["resource"],
port=pattern_formats["port"],
path=pattern_formats["path"],
name=pattern_formats["name"],
rev=pattern_formats["rev"],
)
),
re.compile(
r"((?P<user>{user})@)?"
r"(?P<resource>{resource})"
r"[:/]{{1,2}}"
r"(?P<pathname>({path})"
r"(?P<name>{name})(\.git|/)?)"
r"([@#](?P<rev>{rev}))?"
r"$".format(
user=pattern_formats["user"],
resource=pattern_formats["resource"],
path=pattern_formats["path"],
name=pattern_formats["name"],
rev=pattern_formats["rev"],
)
),
]
class ParsedUrl:
def __init__(self, protocol, resource, pathname, user, port, name, rev):
self.protocol = protocol
self.resource = resource
self.pathname = pathname
self.user = user
self.port = port
self.name = name
self.rev = rev
@classmethod
def parse(cls, url): # type: () -> ParsedUrl
for pattern in PATTERNS:
m = pattern.match(url)
if m:
groups = m.groupdict()
return ParsedUrl(
groups.get("protocol"),
groups.get("resource"),
groups.get("pathname"),
groups.get("user"),
groups.get("port"),
groups.get("name"),
groups.get("rev"),
)
raise ValueError('Invalid git url "{}"'.format(url))
@property
def url(self): # type: () -> str
return "{}{}{}{}{}".format(
"{}://".format(self.protocol) if self.protocol else "",
"{}@".format(self.user) if self.user else "",
self.resource,
":{}".format(self.port) if self.port else "",
"/" + self.pathname.lstrip(":/"),
)
def format(self):
return "{}".format(self.url, "#{}".format(self.rev) if self.rev else "",)
def __str__(self): # type: () -> str
return self.format()
GitUrl = namedtuple("GitUrl", ["url", "revision"])
class GitConfig:
def __init__(self, requires_git_presence=False):
self._config = {}
try:
config_list = decode(
subprocess.check_output(
["git", "config", "-l"], stderr=subprocess.STDOUT
)
)
m = re.findall("(?ms)^([^=]+)=(.*?)$", config_list)
if m:
for group in m:
self._config[group[0]] = group[1]
except (subprocess.CalledProcessError, OSError):
if requires_git_presence:
raise
def get(self, key, default=None):
return self._config.get(key, default)
def __getitem__(self, item):
return self._config[item]
class Git:
def __init__(self, work_dir=None):
self._config = GitConfig(requires_git_presence=True)
self._work_dir = work_dir
@classmethod
def normalize_url(cls, url): # type: (str) -> GitUrl
parsed = ParsedUrl.parse(url)
formatted = re.sub(r"^git\+", "", url)
if parsed.rev:
formatted = re.sub(r"[#@]{}$".format(parsed.rev), "", formatted)
altered = parsed.format() != formatted
if altered:
if re.match(r"^git\+https?", url) and re.match(
r"^/?:[^0-9]", parsed.pathname
):
normalized = re.sub(r"git\+(.*:[^:]+):(.*)", "\\1/\\2", url)
elif re.match(r"^git\+file", url):
normalized = re.sub(r"git\+", "", url)
else:
normalized = re.sub(r"^(?:git\+)?ssh://", "", url)
else:
normalized = parsed.format()
return GitUrl(re.sub(r"#[^#]*$", "", normalized), parsed.rev)
@property
def config(self): # type: () -> GitConfig
return self._config
def clone(self, repository, dest): # type: (...) -> str
return self.run("clone", repository, str(dest))
def checkout(self, rev, folder=None): # type: (...) -> str
args = []
if folder is None and self._work_dir:
folder = self._work_dir
if folder:
args += [
"--git-dir",
(folder / ".git").as_posix(),
"--work-tree",
folder.as_posix(),
]
args += ["checkout", rev]
return self.run(*args)
def rev_parse(self, rev, folder=None): # type: (...) -> str
args = []
if folder is None and self._work_dir:
folder = self._work_dir
if folder:
args += [
"--git-dir",
(folder / ".git").as_posix(),
"--work-tree",
folder.as_posix(),
]
# We need "^{commit}" to ensure that the commit SHA of the commit the
# tag points to is returned, even in the case of annotated tags.
args += ["rev-parse", rev + "^{commit}"]
return self.run(*args)
def get_ignored_files(self, folder=None): # type: (...) -> list
args = []
if folder is None and self._work_dir:
folder = self._work_dir
if folder:
args += [
"--git-dir",
(folder / ".git").as_posix(),
"--work-tree",
folder.as_posix(),
]
args += ["ls-files", "--others", "-i", "--exclude-standard"]
output = self.run(*args)
return output.strip().split("\n")
def remote_urls(self, folder=None): # type: (...) -> dict
output = self.run(
"config", "--get-regexp", r"remote\..*\.url", folder=folder
).strip()
urls = {}
for url in output.splitlines():
name, url = url.split(" ", 1)
urls[name.strip()] = url.strip()
return urls
def remote_url(self, folder=None): # type: (...) -> str
urls = self.remote_urls(folder=folder)
return urls.get("remote.origin.url", urls[list(urls.keys())[0]])
def run(self, *args, **kwargs): # type: (...) -> str
folder = kwargs.pop("folder", None)
if folder:
args = (
"--git-dir",
(folder / ".git").as_posix(),
"--work-tree",
folder.as_posix(),
) + args
return decode(
subprocess.check_output(["git"] + list(args), stderr=subprocess.STDOUT)
).strip()
import operator
from typing import Union
from .exceptions import InvalidVersion
from .legacy_version import LegacyVersion
from .version import Version
OP_EQ = operator.eq
OP_LT = operator.lt
OP_LE = operator.le
OP_GT = operator.gt
OP_GE = operator.ge
OP_NE = operator.ne
_trans_op = {
"=": OP_EQ,
"==": OP_EQ,
"<": OP_LT,
"<=": OP_LE,
">": OP_GT,
">=": OP_GE,
"!=": OP_NE,
}
def parse(
version, # type: str
strict=False, # type: bool
): # type:(...) -> Union[Version, LegacyVersion]
"""
Parse the given version string and return either a :class:`Version` object
or a LegacyVersion object depending on if the given version is
a valid PEP 440 version or a legacy version.
If strict=True only PEP 440 versions will be accepted.
"""
try:
return Version(version)
except InvalidVersion:
if strict:
raise
return LegacyVersion(version)
class BaseVersion:
def __hash__(self):
return hash(self._key)
def __lt__(self, other):
return self._compare(other, lambda s, o: s < o)
def __le__(self, other):
return self._compare(other, lambda s, o: s <= o)
def __eq__(self, other):
return self._compare(other, lambda s, o: s == o)
def __ge__(self, other):
return self._compare(other, lambda s, o: s >= o)
def __gt__(self, other):
return self._compare(other, lambda s, o: s > o)
def __ne__(self, other):
return self._compare(other, lambda s, o: s != o)
def _compare(self, other, method):
if not isinstance(other, BaseVersion):
return NotImplemented
return method(self._key, other._key)
class InvalidVersion(ValueError):
pass
from poetry.semver import Version
from poetry.semver import VersionUnion
from poetry.semver import parse_constraint
PYTHON_VERSION = [
"2.7.*",
"3.0.*",
"3.1.*",
"3.2.*",
"3.3.*",
"3.4.*",
"3.5.*",
"3.6.*",
"3.7.*",
"3.8.*",
]
def format_python_constraint(constraint):
"""
This helper will help in transforming
disjunctive constraint into proper constraint.
"""
if isinstance(constraint, Version):
if constraint.precision >= 3:
return "=={}".format(str(constraint))
# Transform 3.6 or 3
if constraint.precision == 2:
# 3.6
constraint = parse_constraint(
"~{}.{}".format(constraint.major, constraint.minor)
)
else:
constraint = parse_constraint("^{}.0".format(constraint.major))
if not isinstance(constraint, VersionUnion):
return str(constraint)
formatted = []
accepted = []
for version in PYTHON_VERSION:
version_constraint = parse_constraint(version)
matches = constraint.allows_any(version_constraint)
if not matches:
formatted.append("!=" + version)
else:
accepted.append(version)
# Checking lower bound
low = accepted[0]
formatted.insert(0, ">=" + ".".join(low.split(".")[:2]))
return ", ".join(formatted)
import re
from .base import BaseVersion
class LegacyVersion(BaseVersion):
def __init__(self, version):
self._version = str(version)
self._key = _legacy_cmpkey(self._version)
def __str__(self):
return self._version
def __repr__(self):
return "<LegacyVersion({0})>".format(repr(str(self)))
@property
def public(self):
return self._version
@property
def base_version(self):
return self._version
@property
def local(self):
return None
@property
def is_prerelease(self):
return False
@property
def is_postrelease(self):
return False
_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE)
_legacy_version_replacement_map = {
"pre": "c",
"preview": "c",
"-": "final-",
"rc": "c",
"dev": "@",
}
def _parse_version_parts(s):
for part in _legacy_version_component_re.split(s):
part = _legacy_version_replacement_map.get(part, part)
if not part or part == ".":
continue
if part[:1] in "0123456789":
# pad for numeric comparison
yield part.zfill(8)
else:
yield "*" + part
# ensure that alpha/beta/candidate are before final
yield "*final"
def _legacy_cmpkey(version):
# We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
# greater than or equal to 0. This will effectively put the LegacyVersion,
# which uses the defacto standard originally implemented by setuptools,
# as before all PEP 440 versions.
epoch = -1
# This scheme is taken from pkg_resources.parse_version setuptools prior to
# it's adoption of the packaging library.
parts = []
for part in _parse_version_parts(version.lower()):
if part.startswith("*"):
# remove "-" before a prerelease tag
if part < "*final":
while parts and parts[-1] == "*final-":
parts.pop()
# remove trailing zeros from each series of numeric parts
while parts and parts[-1] == "00000000":
parts.pop()
parts.append(part)
parts = tuple(parts)
return epoch, parts
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment