Beispiel #1
0
class VersionCommand(Command):

    name = "version"
    description = ("Shows the version of the project or bumps it when a valid "
                   "bump rule is provided.")

    arguments = [
        argument(
            "version",
            "The version number or the rule to update the version.",
            optional=True,
        )
    ]
    options = [option("short", "s", "Output the version number only")]

    help = """\
The version command shows the current version of the project or bumps the version of
the project and writes the new version back to <comment>pyproject.toml</> if a valid
bump rule is provided.

The new version should ideally be a valid semver string or a valid bump rule:
patch, minor, major, prepatch, preminor, premajor, prerelease.
"""

    RESERVED = {
        "major",
        "minor",
        "patch",
        "premajor",
        "preminor",
        "prepatch",
        "prerelease",
    }

    def handle(self) -> None:
        version = self.argument("version")

        if version:
            version = self.increment_version(
                self.poetry.package.pretty_version, version)

            if self.option("short"):
                self.line(version.to_string())
            else:
                self.line(
                    f"Bumping version from <b>{self.poetry.package.pretty_version}</>"
                    f" to <fg=green>{version}</>")

            content = self.poetry.file.read()
            poetry_content = content["tool"]["poetry"]
            poetry_content["version"] = version.text

            self.poetry.file.write(content)
        else:
            if self.option("short"):
                self.line(self.poetry.package.pretty_version)
            else:
                self.line(f"<comment>{self.poetry.package.name}</>"
                          f" <info>{self.poetry.package.pretty_version}</>")

    def increment_version(self, version: str, rule: str) -> Version:
        from poetry.core.semver.version import Version

        try:
            parsed = Version.parse(version)
        except ValueError:
            raise ValueError(
                "The project's version doesn't seem to follow semver")

        if rule in {"major", "premajor"}:
            new = parsed.next_major()
            if rule == "premajor":
                new = new.first_prerelease()
        elif rule in {"minor", "preminor"}:
            new = parsed.next_minor()
            if rule == "preminor":
                new = new.first_prerelease()
        elif rule in {"patch", "prepatch"}:
            new = parsed.next_patch()
            if rule == "prepatch":
                new = new.first_prerelease()
        elif rule == "prerelease":
            if parsed.is_unstable():
                new = Version(parsed.epoch, parsed.release, parsed.pre.next())
            else:
                new = parsed.next_patch().first_prerelease()
        else:
            new = Version.parse(rule)

        return new
Beispiel #2
0
class SelfUpdateCommand(Command):

    name = "self update"
    description = "Updates Poetry to the latest version."

    arguments = [argument("version", "The version to update to.", optional=True)]
    options = [option("preview", None, "Install prereleases.")]

    REPOSITORY_URL = "https://github.com/python-poetry/poetry"
    BASE_URL = REPOSITORY_URL + "/releases/download"

    @property
    def home(self) -> Path:
        from pathlib import Path

        return Path(os.environ.get("POETRY_HOME", "~/.poetry")).expanduser()

    @property
    def bin(self) -> Path:
        return self.home / "bin"

    @property
    def lib(self) -> Path:
        return self.home / "lib"

    @property
    def lib_backup(self) -> Path:
        return self.home / "lib-backup"

    def handle(self) -> None:
        from poetry.__version__ import __version__
        from poetry.core.packages.dependency import Dependency
        from poetry.core.semver.version import Version
        from poetry.repositories.pypi_repository import PyPiRepository

        self._check_recommended_installation()

        version = self.argument("version")
        if not version:
            version = ">=" + __version__

        repo = PyPiRepository(fallback=False)
        packages = repo.find_packages(
            Dependency("poetry", version, allows_prereleases=self.option("preview"))
        )
        if not packages:
            self.line("No release found for the specified version")
            return

        packages.sort(
            key=cmp_to_key(
                lambda x, y: 0
                if x.version == y.version
                else int(x.version < y.version or -1)
            )
        )

        release = None
        for package in packages:
            if package.is_prerelease():
                if self.option("preview"):
                    release = package

                    break

                continue

            release = package

            break

        if release is None:
            self.line("No new release found")
            return

        if release.version == Version.parse(__version__):
            self.line("You are using the latest version")
            return

        self.update(release)

    def update(self, release: "Package") -> None:
        version = release.version
        self.line("Updating to <info>{}</info>".format(version))

        if self.lib_backup.exists():
            shutil.rmtree(str(self.lib_backup))

        # Backup the current installation
        if self.lib.exists():
            shutil.copytree(str(self.lib), str(self.lib_backup))
            shutil.rmtree(str(self.lib))

        try:
            self._update(version)
        except Exception:
            if not self.lib_backup.exists():
                raise

            shutil.copytree(str(self.lib_backup), str(self.lib))
            shutil.rmtree(str(self.lib_backup))

            raise
        finally:
            if self.lib_backup.exists():
                shutil.rmtree(str(self.lib_backup))

        self.make_bin()

        self.line("")
        self.line("")
        self.line(
            "<info>Poetry</info> (<comment>{}</comment>) is installed now. Great!".format(
                version
            )
        )

    def _update(self, version: "Version") -> None:
        from poetry.utils.helpers import temporary_directory

        release_name = self._get_release_name(version)

        checksum = "{}.sha256sum".format(release_name)

        base_url = self.BASE_URL

        try:
            r = urlopen(base_url + "/{}/{}".format(version, checksum))
        except HTTPError as e:
            if e.code == 404:
                raise RuntimeError("Could not find {} file".format(checksum))

            raise

        checksum = r.read().decode().strip()

        # We get the payload from the remote host
        name = "{}.tar.gz".format(release_name)
        try:
            r = urlopen(base_url + "/{}/{}".format(version, name))
        except HTTPError as e:
            if e.code == 404:
                raise RuntimeError("Could not find {} file".format(name))

            raise

        meta = r.info()
        size = int(meta["Content-Length"])
        current = 0
        block_size = 8192

        bar = self.progress_bar(max=size)
        bar.set_format(" - Downloading <info>{}</> <comment>%percent%%</>".format(name))
        bar.start()

        sha = hashlib.sha256()
        with temporary_directory(prefix="poetry-updater-") as dir_:
            tar = os.path.join(dir_, name)
            with open(tar, "wb") as f:
                while True:
                    buffer = r.read(block_size)
                    if not buffer:
                        break

                    current += len(buffer)
                    f.write(buffer)
                    sha.update(buffer)

                    bar.set_progress(current)

            bar.finish()

            # Checking hashes
            if checksum != sha.hexdigest():
                raise RuntimeError(
                    "Hashes for {} do not match: {} != {}".format(
                        name, checksum, sha.hexdigest()
                    )
                )

            gz = GzipFile(tar, mode="rb")
            try:
                with tarfile.TarFile(tar, fileobj=gz, format=tarfile.PAX_FORMAT) as f:
                    f.extractall(str(self.lib))
            finally:
                gz.close()

    def process(self, *args: Any) -> str:
        return subprocess.check_output(list(args), stderr=subprocess.STDOUT)

    def _check_recommended_installation(self) -> None:
        from pathlib import Path

        from poetry.console.exceptions import PoetrySimpleConsoleException

        current = Path(__file__)
        try:
            current.relative_to(self.home)
        except ValueError:
            raise PoetrySimpleConsoleException(
                "Poetry was not installed with the recommended installer, "
                "so it cannot be updated automatically."
            )

    def _get_release_name(self, version: "Version") -> str:
        platform = sys.platform
        if platform == "linux2":
            platform = "linux"

        return "poetry-{}-{}".format(version, platform)

    def make_bin(self) -> None:
        from poetry.utils._compat import WINDOWS

        self.bin.mkdir(0o755, parents=True, exist_ok=True)

        python_executable = self._which_python()

        if WINDOWS:
            with self.bin.joinpath("poetry.bat").open("w", newline="") as f:
                f.write(
                    BAT.format(
                        python_executable=python_executable,
                        poetry_bin=str(self.bin / "poetry").replace(
                            os.environ["USERPROFILE"], "%USERPROFILE%"
                        ),
                    )
                )

        bin_content = BIN
        if not WINDOWS:
            bin_content = "#!/usr/bin/env {}\n".format(python_executable) + bin_content

        self.bin.joinpath("poetry").write_text(bin_content, encoding="utf-8")

        if not WINDOWS:
            # Making the file executable
            st = os.stat(str(self.bin.joinpath("poetry")))
            os.chmod(str(self.bin.joinpath("poetry")), st.st_mode | stat.S_IEXEC)

    def _which_python(self) -> str:
        """
        Decides which python executable we'll embed in the launcher script.
        """
        from poetry.utils._compat import WINDOWS

        allowed_executables = ["python", "python3"]
        if WINDOWS:
            allowed_executables += ["py.exe -3", "py.exe -2"]

        # \d in regex ensures we can convert to int later
        version_matcher = re.compile(r"^Python (?P<major>\d+)\.(?P<minor>\d+)\..+$")
        fallback = None
        for executable in allowed_executables:
            try:
                raw_version = subprocess.check_output(
                    executable + " --version", stderr=subprocess.STDOUT, shell=True
                ).decode("utf-8")
            except subprocess.CalledProcessError:
                continue

            match = version_matcher.match(raw_version.strip())
            if match and tuple(map(int, match.groups())) >= (3, 0):
                # favor the first py3 executable we can find.
                return executable

            if fallback is None:
                # keep this one as the fallback; it was the first valid executable we found.
                fallback = executable

        if fallback is None:
            # Avoid breaking existing scripts
            fallback = "python"

        return fallback
Beispiel #3
0
class ShowCommand(EnvCommand):

    name = "show"
    description = "Shows information about packages."

    arguments = [argument("package", "The package to inspect", optional=True)]
    options = [
        option(
            "without",
            None,
            "Do not show the information of the specified groups' dependencies.",
            flag=False,
            multiple=True,
        ),
        option(
            "with",
            None,
            "Show the information of the specified optional groups' dependencies as well.",
            flag=False,
            multiple=True,
        ),
        option("default", None,
               "Only show the information of the default dependencies."),
        option(
            "only",
            None,
            "Only show the information of dependencies belonging to the specified groups.",
            flag=False,
            multiple=True,
        ),
        option(
            "no-dev",
            None,
            "Do not list the development dependencies. (<warning>Deprecated</warning>)",
        ),
        option("tree", "t", "List the dependencies as a tree."),
        option("latest", "l", "Show the latest version."),
        option(
            "outdated",
            "o",
            "Show the latest version but only for packages that are outdated.",
        ),
        option(
            "all",
            "a",
            "Show all packages (even those not compatible with current system).",
        ),
    ]

    help = """The show command displays detailed information about a package, or
lists all packages available."""

    colors = ["cyan", "yellow", "green", "magenta", "blue"]

    def handle(self) -> Optional[int]:
        from cleo.io.null_io import NullIO
        from cleo.terminal import Terminal

        from poetry.puzzle.solver import Solver
        from poetry.repositories.installed_repository import InstalledRepository
        from poetry.repositories.pool import Pool
        from poetry.repositories.repository import Repository
        from poetry.utils.helpers import get_package_version_display_string

        package = self.argument("package")

        if self.option("tree"):
            self.init_styles(self.io)

        if self.option("outdated"):
            self._io.input.set_option("latest", True)

        excluded_groups = []
        included_groups = []
        only_groups = []
        if self.option("no-dev"):
            self.line(
                "<warning>The `<fg=yellow;options=bold>--no-dev</>` option is deprecated, "
                "use the `<fg=yellow;options=bold>--without dev</>` notation instead.</warning>"
            )
            excluded_groups.append("dev")

        excluded_groups.extend([
            group.strip() for groups in self.option("without")
            for group in groups.split(",")
        ])
        included_groups.extend([
            group.strip() for groups in self.option("with")
            for group in groups.split(",")
        ])
        only_groups.extend([
            group.strip() for groups in self.option("only")
            for group in groups.split(",")
        ])

        if self.option("default"):
            only_groups.append("default")

        locked_repo = self.poetry.locker.locked_repository(True)

        if only_groups:
            root = self.poetry.package.with_dependency_groups(only_groups,
                                                              only=True)
        else:
            root = self.poetry.package.with_dependency_groups(
                included_groups).without_dependency_groups(excluded_groups)

        # Show tree view if requested
        if self.option("tree") and not package:
            requires = root.all_requires
            packages = locked_repo.packages
            for pkg in packages:
                for require in requires:
                    if pkg.name == require.name:
                        self.display_package_tree(self._io, pkg, locked_repo)
                        break

            return 0

        table = self.table(style="compact")
        locked_packages = locked_repo.packages
        pool = Pool(ignore_repository_names=True)
        pool.add_repository(locked_repo)
        solver = Solver(
            root,
            pool=pool,
            installed=Repository(),
            locked=locked_repo,
            io=NullIO(),
        )
        solver.provider.load_deferred(False)
        with solver.use_environment(self.env):
            ops = solver.solve().calculate_operations()

        required_locked_packages = {op.package for op in ops if not op.skipped}

        if package:
            pkg = None
            for locked in locked_packages:
                if package.lower() == locked.name:
                    pkg = locked
                    break

            if not pkg:
                raise ValueError(f"Package {package} not found")

            if self.option("tree"):
                self.display_package_tree(self.io, pkg, locked_repo)

                return 0

            required_by = {}
            for locked in locked_packages:
                dependencies = {
                    d.name: d.pretty_constraint
                    for d in locked.requires
                }

                if pkg.name in dependencies:
                    required_by[locked.pretty_name] = dependencies[pkg.name]

            rows = [
                ["<info>name</>", f" : <c1>{pkg.pretty_name}</>"],
                ["<info>version</>", f" : <b>{pkg.pretty_version}</b>"],
                ["<info>description</>", f" : {pkg.description}"],
            ]

            table.add_rows(rows)
            table.render()

            if pkg.requires:
                self.line("")
                self.line("<info>dependencies</info>")
                for dependency in pkg.requires:
                    self.line(
                        f" - <c1>{dependency.pretty_name}</c1> <b>{dependency.pretty_constraint}</b>"
                    )

            if required_by:
                self.line("")
                self.line("<info>required by</info>")
                for parent, requires_version in required_by.items():
                    self.line(
                        f" - <c1>{parent}</c1> <b>{requires_version}</b>")

            return 0

        show_latest = self.option("latest")
        show_all = self.option("all")
        terminal = Terminal()
        width = terminal.width
        name_length = version_length = latest_length = 0
        latest_packages = {}
        latest_statuses = {}
        installed_repo = InstalledRepository.load(self.env)

        # Computing widths
        for locked in locked_packages:
            if locked not in required_locked_packages and not show_all:
                continue

            current_length = len(locked.pretty_name)
            if not self._io.output.is_decorated():
                installed_status = self.get_installed_status(
                    locked, installed_repo)

                if installed_status == "not-installed":
                    current_length += 4

            if show_latest:
                latest = self.find_latest_package(locked, root)
                if not latest:
                    latest = locked

                latest_packages[locked.pretty_name] = latest
                update_status = latest_statuses[
                    locked.pretty_name] = self.get_update_status(
                        latest, locked)

                if not self.option(
                        "outdated") or update_status != "up-to-date":
                    name_length = max(name_length, current_length)
                    version_length = max(
                        version_length,
                        len(
                            get_package_version_display_string(
                                locked, root=self.poetry.file.parent)),
                    )
                    latest_length = max(
                        latest_length,
                        len(
                            get_package_version_display_string(
                                latest, root=self.poetry.file.parent)),
                    )
            else:
                name_length = max(name_length, current_length)
                version_length = max(
                    version_length,
                    len(
                        get_package_version_display_string(
                            locked, root=self.poetry.file.parent)),
                )

        write_version = name_length + version_length + 3 <= width
        write_latest = name_length + version_length + latest_length + 3 <= width
        write_description = name_length + version_length + latest_length + 24 <= width

        for locked in locked_packages:
            color = "cyan"
            name = locked.pretty_name
            install_marker = ""
            if locked not in required_locked_packages:
                if not show_all:
                    continue

                color = "black;options=bold"
            else:
                installed_status = self.get_installed_status(
                    locked, installed_repo)
                if installed_status == "not-installed":
                    color = "red"

                    if not self._io.output.is_decorated():
                        # Non installed in non decorated mode
                        install_marker = " (!)"

            if (show_latest and self.option("outdated")
                    and latest_statuses[locked.pretty_name] == "up-to-date"):
                continue

            line = f"<fg={color}>{name:{name_length - len(install_marker)}}{install_marker}</>"
            if write_version:
                version = get_package_version_display_string(
                    locked, root=self.poetry.file.parent)
                line += f" <b>{version:{version_length}}</b>"
            if show_latest:
                latest = latest_packages[locked.pretty_name]
                update_status = latest_statuses[locked.pretty_name]

                if write_latest:
                    color = "green"
                    if update_status == "semver-safe-update":
                        color = "red"
                    elif update_status == "update-possible":
                        color = "yellow"

                    version = get_package_version_display_string(
                        latest, root=self.poetry.file.parent)
                    line += f" <fg={color}>{version:{latest_length}}</>"

            if write_description:
                description = locked.description
                remaining = width - name_length - version_length - 4
                if show_latest:
                    remaining -= latest_length

                if len(locked.description) > remaining:
                    description = description[:remaining - 3] + "..."

                line += " " + description

            self.line(line)
        return None

    def display_package_tree(self, io: "IO", package: "Package",
                             installed_repo: "Repository") -> None:
        io.write(f"<c1>{package.pretty_name}</c1>")
        description = ""
        if package.description:
            description = " " + package.description

        io.write_line(f" <b>{package.pretty_version}</b>{description}")

        dependencies = package.requires
        dependencies = sorted(dependencies, key=lambda x: x.name)
        tree_bar = "├"
        total = len(dependencies)
        for i, dependency in enumerate(dependencies, 1):
            if i == total:
                tree_bar = "└"

            level = 1
            color = self.colors[level]
            info = f"{tree_bar}── <{color}>{dependency.name}</{color}> {dependency.pretty_constraint}"
            self._write_tree_line(io, info)

            tree_bar = tree_bar.replace("└", " ")
            packages_in_tree = [package.name, dependency.name]

            self._display_tree(io, dependency, installed_repo,
                               packages_in_tree, tree_bar, level + 1)

    def _display_tree(
        self,
        io: "IO",
        dependency: "Dependency",
        installed_repo: "Repository",
        packages_in_tree: List[str],
        previous_tree_bar: str = "├",
        level: int = 1,
    ) -> None:
        previous_tree_bar = previous_tree_bar.replace("├", "│")

        dependencies = []
        for package in installed_repo.packages:
            if package.name == dependency.name:
                dependencies = package.requires

                break

        dependencies = sorted(dependencies, key=lambda x: x.name)
        tree_bar = previous_tree_bar + "   ├"
        total = len(dependencies)
        for i, dependency in enumerate(dependencies, 1):
            current_tree = packages_in_tree
            if i == total:
                tree_bar = previous_tree_bar + "   └"

            color_ident = level % len(self.colors)
            color = self.colors[color_ident]

            circular_warn = ""
            if dependency.name in current_tree:
                circular_warn = "(circular dependency aborted here)"

            info = f"{tree_bar}── <{color}>{dependency.name}</{color}> {dependency.pretty_constraint} {circular_warn}"
            self._write_tree_line(io, info)

            tree_bar = tree_bar.replace("└", " ")

            if dependency.name not in current_tree:
                current_tree.append(dependency.name)

                self._display_tree(io, dependency, installed_repo,
                                   current_tree, tree_bar, level + 1)

    def _write_tree_line(self, io: "IO", line: str) -> None:
        if not io.output.supports_utf8():
            line = line.replace("└", "`-")
            line = line.replace("├", "|-")
            line = line.replace("──", "-")
            line = line.replace("│", "|")

        io.write_line(line)

    def init_styles(self, io: "IO") -> None:
        from cleo.formatters.style import Style

        for color in self.colors:
            style = Style(color)
            io.output.formatter.set_style(color, style)
            io.error_output.formatter.set_style(color, style)

    def find_latest_package(self, package: "Package",
                            root: "ProjectPackage") -> Union["Package", bool]:
        from cleo.io.null_io import NullIO

        from poetry.puzzle.provider import Provider
        from poetry.version.version_selector import VersionSelector

        # find the latest version allowed in this pool
        if package.source_type in ("git", "file", "directory"):
            requires = root.all_requires

            for dep in requires:
                if dep.name == package.name:
                    provider = Provider(root, self.poetry.pool, NullIO())

                    if dep.is_vcs():
                        return provider.search_for_vcs(dep)[0]
                    if dep.is_file():
                        return provider.search_for_file(dep)[0]
                    if dep.is_directory():
                        return provider.search_for_directory(dep)[0]

        name = package.name
        selector = VersionSelector(self.poetry.pool)

        return selector.find_best_candidate(name,
                                            f">={package.pretty_version}")

    def get_update_status(self, latest: "Package", package: "Package") -> str:
        from poetry.core.semver.helpers import parse_constraint

        if latest.full_pretty_version == package.full_pretty_version:
            return "up-to-date"

        constraint = parse_constraint("^" + package.pretty_version)

        if latest.version and constraint.allows(latest.version):
            # It needs an immediate semver-compliant upgrade
            return "semver-safe-update"

        # it needs an upgrade but has potential BC breaks so is not urgent
        return "update-possible"

    def get_installed_status(self, locked: "Package",
                             installed_repo: "InstalledRepository") -> str:
        for package in installed_repo.packages:
            if locked.name == package.name:
                return "installed"

        return "not-installed"
Beispiel #4
0
class PluginAddCommand(InitCommand):

    name = "plugin add"

    description = "Adds new plugins."

    arguments = [
        argument("plugins", "The names of the plugins to install.", multiple=True),
    ]

    options = [
        option(
            "dry-run",
            None,
            "Output the operations but do not execute anything (implicitly enables"
            " --verbose).",
        )
    ]

    help = """
The <c1>plugin add</c1> command installs Poetry plugins globally.

It works similarly to the <c1>add</c1> command:

If you do not specify a version constraint, poetry will choose a suitable one based on\
 the available package versions.

You can specify a package in the following forms:

  - A single name (<b>requests</b>)
  - A name and a constraint (<b>requests@^2.23.0</b>)
  - A git url (<b>git+https://github.com/python-poetry/poetry.git</b>)
  - A git url with a revision\
 (<b>git+https://github.com/python-poetry/poetry.git#develop</b>)
  - A git SSH url (<b>git+ssh://github.com/python-poetry/poetry.git</b>)
  - A git SSH url with a revision\
 (<b>git+ssh://github.com/python-poetry/poetry.git#develop</b>)
  - A file path (<b>../my-package/my-package.whl</b>)
  - A directory (<b>../my-package/</b>)
  - A url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)\
"""

    def handle(self) -> int:
        from pathlib import Path

        import tomlkit

        from cleo.io.inputs.string_input import StringInput
        from cleo.io.io import IO
        from poetry.core.pyproject.toml import PyProjectTOML
        from poetry.core.semver.helpers import parse_constraint

        from poetry.factory import Factory
        from poetry.packages.project_package import ProjectPackage
        from poetry.repositories.installed_repository import InstalledRepository
        from poetry.utils.env import EnvManager

        plugins = self.argument("plugins")

        # Plugins should be installed in the system env to be globally available
        system_env = EnvManager.get_system_env(naive=True)

        env_dir = Path(os.getenv("POETRY_HOME") or system_env.path)

        # We check for the plugins existence first.
        if env_dir.joinpath("pyproject.toml").exists():
            pyproject: dict[str, Any] = tomlkit.loads(
                env_dir.joinpath("pyproject.toml").read_text(encoding="utf-8")
            )
            poetry_content = pyproject["tool"]["poetry"]
            existing_packages = self.get_existing_packages_from_input(
                plugins, poetry_content, "dependencies"
            )

            if existing_packages:
                self.notify_about_existing_packages(existing_packages)

            plugins = [plugin for plugin in plugins if plugin not in existing_packages]

        if not plugins:
            return 0

        plugins = self._determine_requirements(plugins)

        # We retrieve the packages installed in the system environment.
        # We assume that this environment will be a self contained virtual environment
        # built by the official installer or by pipx.
        # If not, it might lead to side effects since other installed packages might not
        # be required by Poetry but still be taken into account when resolving
        # dependencies.
        installed_repository = InstalledRepository.load(
            system_env, with_dependencies=True
        )

        root_package = None
        for package in installed_repository.packages:
            if package.name == "poetry":
                root_package = ProjectPackage(package.name, package.version)
                for dependency in package.requires:
                    root_package.add_dependency(dependency)

                break

        assert root_package is not None

        root_package.python_versions = ".".join(
            str(v) for v in system_env.version_info[:3]
        )
        # We create a `pyproject.toml` file based on all the information
        # we have about the current environment.
        if not env_dir.joinpath("pyproject.toml").exists():
            Factory.create_pyproject_from_package(
                root_package,
                env_dir,
            )

        # We add the plugins to the dependencies section of the previously
        # created `pyproject.toml` file
        pyproject_toml = PyProjectTOML(env_dir.joinpath("pyproject.toml"))
        poetry_content = pyproject_toml.poetry_config
        poetry_dependency_section = poetry_content["dependencies"]
        plugin_names = []
        for plugin in plugins:
            if "version" in plugin:
                # Validate version constraint
                parse_constraint(plugin["version"])

            constraint: dict[str, Any] = tomlkit.inline_table()
            for name, value in plugin.items():
                if name == "name":
                    continue

                constraint[name] = value

            if len(constraint) == 1 and "version" in constraint:
                constraint = constraint["version"]

            poetry_dependency_section[plugin["name"]] = constraint
            plugin_names.append(plugin["name"])

        pyproject_toml.save()

        # From this point forward, all the logic will be deferred to
        # the update command, by using the previously created `pyproject.toml`
        # file.
        application = cast(Application, self.application)
        update_command: UpdateCommand = cast(UpdateCommand, application.find("update"))
        # We won't go through the event dispatching done by the application
        # so we need to configure the command manually
        update_command.set_poetry(Factory().create_poetry(env_dir))
        update_command.set_env(system_env)
        application._configure_installer(update_command, self._io)

        argv = ["update"] + plugin_names
        if self.option("dry-run"):
            argv.append("--dry-run")

        exit_code: int = update_command.run(
            IO(
                StringInput(" ".join(argv)),
                self._io.output,
                self._io.error_output,
            )
        )
        return exit_code

    def get_existing_packages_from_input(
        self, packages: list[str], poetry_content: dict[str, Any], target_section: str
    ) -> list[str]:
        existing_packages = []

        for name in packages:
            for key in poetry_content[target_section]:
                if key.lower() == name.lower():
                    existing_packages.append(name)

        return existing_packages

    def notify_about_existing_packages(self, existing_packages: list[str]) -> None:
        self.line(
            "The following plugins are already present in the "
            "<c2>pyproject.toml</c2> file and will be skipped:\n"
        )
        for name in existing_packages:
            self.line(f"  • <c1>{name}</c1>")

        self.line(
            "\nIf you want to update it to the latest compatible version, "
            "you can use `<c2>poetry plugin update package</c2>`.\n"
            "If you prefer to upgrade it to the latest available version, "
            "you can use `<c2>poetry plugin add package@latest</c2>`.\n"
        )
Beispiel #5
0
class DebugResolveCommand(InitCommand):

    name = "debug resolve"
    description = "Debugs dependency resolution."

    arguments = [
        argument("package",
                 "The packages to resolve.",
                 optional=True,
                 multiple=True)
    ]
    options = [
        option(
            "extras",
            "E",
            "Extras to activate for the dependency.",
            flag=False,
            multiple=True,
        ),
        option("python",
               None,
               "Python version(s) to use for resolution.",
               flag=False),
        option("tree", None, "Display the dependency tree."),
        option("install", None,
               "Show what would be installed for the current system."),
    ]

    loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"]

    def handle(self) -> int:
        from cleo.io.null_io import NullIO
        from poetry.core.packages.project_package import ProjectPackage

        from poetry.factory import Factory
        from poetry.puzzle import Solver
        from poetry.repositories.pool import Pool
        from poetry.repositories.repository import Repository
        from poetry.utils.env import EnvManager

        packages = self.argument("package")

        if not packages:
            package = self.poetry.package
        else:
            # Using current pool for determine_requirements()
            self._pool = self.poetry.pool

            package = ProjectPackage(self.poetry.package.name,
                                     self.poetry.package.version)

            # Silencing output
            verbosity = self.io.output.verbosity
            self.io.output.set_verbosity(Verbosity.QUIET)

            requirements = self._determine_requirements(packages)

            self.io.output.set_verbosity(verbosity)

            for constraint in requirements:
                name = constraint.pop("name")
                extras = []
                for extra in self.option("extras"):
                    if " " in extra:
                        extras += [e.strip() for e in extra.split(" ")]
                    else:
                        extras.append(extra)

                constraint["extras"] = extras

                package.add_dependency(
                    Factory.create_dependency(name, constraint))

        package.python_versions = self.option("python") or (
            self.poetry.package.python_versions)

        pool = self.poetry.pool

        solver = Solver(package, pool, Repository(), Repository(), self._io)

        ops = solver.solve().calculate_operations()

        self.line("")
        self.line("Resolution results:")
        self.line("")

        if self.option("tree"):
            show_command: "ShowCommand" = self.application.find("show")
            show_command.init_styles(self.io)

            packages = [op.package for op in ops]
            repo = Repository(packages)

            requires = package.all_requires
            for pkg in repo.packages:
                for require in requires:
                    if pkg.name == require.name:
                        show_command.display_package_tree(self.io, pkg, repo)
                        break

            return 0

        table = self.table(style="compact")
        table.style.set_vertical_border_chars("", " ")
        rows = []

        if self.option("install"):
            env = EnvManager(self.poetry).get()
            pool = Pool()
            locked_repository = Repository()
            for op in ops:
                locked_repository.add_package(op.package)

            pool.add_repository(locked_repository)

            solver = Solver(package, pool, Repository(), Repository(),
                            NullIO())
            with solver.use_environment(env):
                ops = solver.solve().calculate_operations()

        for op in ops:
            if self.option("install") and op.skipped:
                continue

            pkg = op.package
            row = [
                f"<c1>{pkg.complete_name}</c1>",
                f"<b>{pkg.version}</b>",
            ]

            if not pkg.marker.is_any():
                row[2] = str(pkg.marker)

            rows.append(row)

        table.set_rows(rows)
        table.render()

        return 0
Beispiel #6
0
class RemoveCommand(InstallerCommand):

    name = "remove"
    description = "Removes a package from the project dependencies."

    arguments = [
        argument("packages", "The packages to remove.", multiple=True)
    ]
    options = [
        option("dev", "D",
               "Remove a package from the development dependencies."),
        option(
            "dry-run",
            None,
            "Output the operations but do not execute anything "
            "(implicitly enables --verbose).",
        ),
    ]

    help = """The <info>remove</info> command removes a package from the current
list of installed packages

<info>poetry remove</info>"""

    loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"]

    def handle(self) -> int:
        packages = self.argument("packages")
        is_dev = self.option("dev")

        content = self.poetry.file.read()
        poetry_content = content["tool"]["poetry"]
        section = "dependencies"
        if is_dev:
            section = "dev-dependencies"

        # Deleting entries
        requirements = {}
        for name in packages:
            found = False
            for key in poetry_content[section]:
                if key.lower() == name.lower():
                    found = True
                    requirements[key] = poetry_content[section][key]
                    break

            if not found:
                raise ValueError("Package {} not found".format(name))

        for key in requirements:
            del poetry_content[section][key]

            dependencies = (self.poetry.package.requires
                            if section == "dependencies" else
                            self.poetry.package.dev_requires)

            for i, dependency in enumerate(reversed(dependencies)):
                if dependency.name == canonicalize_name(key):
                    del dependencies[-i]

        # Update packages
        self._installer.use_executor(
            self.poetry.config.get("experimental.new-installer", False))

        self._installer.dry_run(self.option("dry-run"))
        self._installer.verbose(self._io.is_verbose())
        self._installer.update(True)
        self._installer.whitelist(requirements)

        status = self._installer.run()

        if not self.option("dry-run") and status == 0:
            self.poetry.file.write(content)

        return status
Beispiel #7
0
class NewCommand(Command):

    name = "new"
    description = "Creates a new Python project at <path>."

    arguments = [argument("path", "The path to create the project at.")]
    options = [
        option("name", None, "Set the resulting package name.", flag=False),
        option("src", None, "Use the src layout for the project."),
        option(
            "readme",
            None,
            "Specify the readme file format. One of md (default) or rst",
            flag=False,
        ),
    ]

    def handle(self) -> None:
        from pathlib import Path

        from poetry.core.vcs.git import GitConfig

        from poetry.layouts import layout
        from poetry.utils.env import SystemEnv

        if self.option("src"):
            layout_ = layout("src")
        else:
            layout_ = layout("standard")

        path = Path(self.argument("path"))
        if not path.is_absolute():
            # we do not use resolve here due to compatibility issues
            # for path.resolve(strict=False)
            path = Path.cwd().joinpath(path)

        name = self.option("name")
        if not name:
            name = path.name

        if path.exists() and list(path.glob("*")):
            # Directory is not empty. Aborting.
            raise RuntimeError(
                f"Destination <fg=yellow>{path}</> exists and is not empty")

        readme_format = self.option("readme") or "md"

        config = GitConfig()
        author = None
        if config.get("user.name"):
            author = config["user.name"]
            author_email = config.get("user.email")
            if author_email:
                author += f" <{author_email}>"

        current_env = SystemEnv(Path(sys.executable))
        default_python = "^" + ".".join(
            str(v) for v in current_env.version_info[:2])

        layout_ = layout_(
            name,
            "0.1.0",
            author=author,
            readme_format=readme_format,
            python=default_python,
        )
        layout_.create(path)

        path = path.resolve()

        with suppress(ValueError):
            path = path.relative_to(Path.cwd())

        self.line(f"Created package <info>{layout_._package_name}</> in"
                  f" <fg=blue>{path.as_posix()}</>")
Beispiel #8
0
class SelfUpdateCommand(Command):

    name = "self update"
    description = "Updates Poetry to the latest version."

    arguments = [
        argument("version", "The version to update to.", optional=True)
    ]
    options = [
        option("preview", None,
               "Allow the installation of pre-release versions."),
        option(
            "dry-run",
            None,
            "Output the operations but do not execute anything "
            "(implicitly enables --verbose).",
        ),
    ]

    _data_dir = None
    _bin_dir = None
    _pool = None

    @property
    def data_dir(self) -> Path:
        if self._data_dir is not None:
            return self._data_dir

        from poetry.locations import data_dir

        self._data_dir = data_dir()

        return self._data_dir

    @property
    def bin_dir(self) -> Path:
        if self._data_dir is not None:
            return self._data_dir

        from poetry.utils._compat import WINDOWS

        if os.getenv("POETRY_HOME"):
            return Path(os.getenv("POETRY_HOME"), "bin").expanduser()

        user_base = site.getuserbase()

        if WINDOWS:
            bin_dir = os.path.join(user_base, "Scripts")
        else:
            bin_dir = os.path.join(user_base, "bin")

        self._bin_dir = Path(bin_dir)

        return self._bin_dir

    @property
    def pool(self) -> "Pool":
        if self._pool is not None:
            return self._pool

        from poetry.repositories.pool import Pool
        from poetry.repositories.pypi_repository import PyPiRepository

        pool = Pool()
        pool.add_repository(PyPiRepository())

        return pool

    def handle(self) -> int:
        from poetry.core.packages.dependency import Dependency
        from poetry.core.semver.version import Version

        from poetry.__version__ import __version__

        version = self.argument("version")
        if not version:
            version = ">=" + __version__

        repo = self.pool.repositories[0]
        packages = repo.find_packages(
            Dependency("poetry",
                       version,
                       allows_prereleases=self.option("preview")))
        if not packages:
            self.line("No release found for the specified version")
            return 1

        packages.sort(key=cmp_to_key(lambda x, y: 0 if x.version == y.version
                                     else int(x.version < y.version or -1)))

        release = None
        for package in packages:
            if package.is_prerelease():
                if self.option("preview"):
                    release = package

                    break

                continue

            release = package

            break

        if release is None:
            self.line("No new release found")
            return 1

        if release.version == Version.parse(__version__):
            self.line("You are using the latest version")
            return 0

        self.line(f"Updating <c1>Poetry</c1> to <c2>{release.version}</c2>")
        self.line("")

        self.update(release)

        self.line("")
        self.line(
            f"<c1>Poetry</c1> (<c2>{release.version}</c2>) is installed now. Great!"
        )

        return 0

    def update(self, release: "Package") -> None:
        from poetry.utils.env import EnvManager

        version = release.version

        env = EnvManager.get_system_env(naive=True)

        # We can't use is_relative_to() since it's only available in Python 3.9+
        try:
            env.path.relative_to(self.data_dir)
        except ValueError:
            # Poetry was not installed using the recommended installer
            from poetry.console.exceptions import PoetrySimpleConsoleException

            raise PoetrySimpleConsoleException(
                "Poetry was not installed with the recommended installer, "
                "so it cannot be updated automatically.")

        self._update(version)
        self._make_bin()

    def _update(self, version: "Version") -> None:
        from poetry.core.packages.dependency import Dependency
        from poetry.core.packages.project_package import ProjectPackage

        from poetry.config.config import Config
        from poetry.installation.installer import Installer
        from poetry.packages.locker import NullLocker
        from poetry.repositories.installed_repository import InstalledRepository
        from poetry.utils.env import EnvManager

        env = EnvManager.get_system_env(naive=True)
        installed = InstalledRepository.load(env)

        root = ProjectPackage("poetry-updater", "0.0.0")
        root.python_versions = ".".join(str(c) for c in env.version_info[:3])
        root.add_dependency(Dependency("poetry", version.text))

        installer = Installer(
            self.io,
            env,
            root,
            NullLocker(self.data_dir.joinpath("poetry.lock"), {}),
            self.pool,
            Config(),
            installed=installed,
        )
        installer.update(True)
        installer.dry_run(self.option("dry-run"))
        installer.run()

    def _make_bin(self) -> None:
        from poetry.utils._compat import WINDOWS

        self.line("")
        self.line("Updating the <c1>poetry</c1> script")

        self.bin_dir.mkdir(parents=True, exist_ok=True)

        script = "poetry"
        target_script = "venv/bin/poetry"
        if WINDOWS:
            script = "poetry.exe"
            target_script = "venv/Scripts/poetry.exe"

        if self.bin_dir.joinpath(script).exists():
            self.bin_dir.joinpath(script).unlink()

        try:
            self.bin_dir.joinpath(script).symlink_to(
                self.data_dir.joinpath(target_script))
        except OSError:
            # This can happen if the user
            # does not have the correct permission on Windows
            shutil.copy(self.data_dir.joinpath(target_script),
                        self.bin_dir.joinpath(script))
Beispiel #9
0
class ConfigCommand(Command):

    name = "config"
    description = "Manages configuration settings."

    arguments = [
        argument("key", "Setting key.", optional=True),
        argument("value", "Setting value.", optional=True, multiple=True),
    ]

    options = [
        option("list", None, "List configuration settings."),
        option("unset", None, "Unset configuration setting."),
        option("local", None,
               "Set/Get from the project's local configuration."),
    ]

    help = """This command allows you to edit the poetry config settings and repositories.

To add a repository:

    <comment>poetry config repositories.foo https://bar.com/simple/</comment>

To remove a repository (repo is a short alias for repositories):

    <comment>poetry config --unset repo.foo</comment>"""

    LIST_PROHIBITED_SETTINGS = {"http-basic", "pypi-token"}

    @property
    def unique_config_values(self) -> Dict[str, Tuple[Any, Any, Any]]:
        from pathlib import Path

        from poetry.config.config import boolean_normalizer
        from poetry.config.config import boolean_validator
        from poetry.locations import CACHE_DIR

        unique_config_values = {
            "cache-dir": (
                str,
                lambda val: str(Path(val)),
                str(Path(CACHE_DIR) / "virtualenvs"),
            ),
            "virtualenvs.create":
            (boolean_validator, boolean_normalizer, True),
            "virtualenvs.in-project":
            (boolean_validator, boolean_normalizer, False),
            "virtualenvs.options.always-copy": (
                boolean_validator,
                boolean_normalizer,
                False,
            ),
            "virtualenvs.options.system-site-packages": (
                boolean_validator,
                boolean_normalizer,
                False,
            ),
            "virtualenvs.path": (
                str,
                lambda val: str(Path(val)),
                str(Path(CACHE_DIR) / "virtualenvs"),
            ),
            "experimental.new-installer": (
                boolean_validator,
                boolean_normalizer,
                True,
            ),
            "installer.parallel": (
                boolean_validator,
                boolean_normalizer,
                True,
            ),
        }

        return unique_config_values

    def handle(self) -> Optional[int]:
        from pathlib import Path

        from poetry.config.file_config_source import FileConfigSource
        from poetry.core.pyproject.exceptions import PyProjectException
        from poetry.core.toml.file import TOMLFile
        from poetry.factory import Factory
        from poetry.locations import CONFIG_DIR

        config = Factory.create_config(self.io)
        config_file = TOMLFile(Path(CONFIG_DIR) / "config.toml")

        try:
            local_config_file = TOMLFile(self.poetry.file.parent /
                                         "poetry.toml")
            if local_config_file.exists():
                config.merge(local_config_file.read())
        except (RuntimeError, PyProjectException):
            local_config_file = TOMLFile(Path.cwd() / "poetry.toml")

        if self.option("local"):
            config.set_config_source(FileConfigSource(local_config_file))

        if not config_file.exists():
            config_file.path.parent.mkdir(parents=True, exist_ok=True)
            config_file.touch(mode=0o0600)

        if self.option("list"):
            self._list_configuration(config.all(), config.raw())

            return 0

        setting_key = self.argument("key")
        if not setting_key:
            return 0

        if self.argument("value") and self.option("unset"):
            raise RuntimeError(
                "You can not combine a setting value with --unset")

        # show the value if no value is provided
        if not self.argument("value") and not self.option("unset"):
            m = re.match(r"^repos?(?:itories)?(?:\.(.+))?",
                         self.argument("key"))
            if m:
                if not m.group(1):
                    value = {}
                    if config.get("repositories") is not None:
                        value = config.get("repositories")
                else:
                    repo = config.get("repositories.{}".format(m.group(1)))
                    if repo is None:
                        raise ValueError(
                            "There is no {} repository defined".format(
                                m.group(1)))

                    value = repo

                self.line(str(value))
            else:
                values = self.unique_config_values
                if setting_key not in values:
                    raise ValueError(
                        "There is no {} setting.".format(setting_key))

                value = config.get(setting_key)

                if not isinstance(value, str):
                    value = json.dumps(value)

                self.line(value)

            return 0

        values = self.argument("value")

        unique_config_values = self.unique_config_values
        if setting_key in unique_config_values:
            if self.option("unset"):
                return config.config_source.remove_property(setting_key)

            return self._handle_single_value(
                config.config_source,
                setting_key,
                unique_config_values[setting_key],
                values,
            )

        # handle repositories
        m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key"))
        if m:
            if not m.group(1):
                raise ValueError(
                    "You cannot remove the [repositories] section")

            if self.option("unset"):
                repo = config.get("repositories.{}".format(m.group(1)))
                if repo is None:
                    raise ValueError(
                        "There is no {} repository defined".format(m.group(1)))

                config.config_source.remove_property("repositories.{}".format(
                    m.group(1)))

                return 0

            if len(values) == 1:
                url = values[0]

                config.config_source.add_property(
                    "repositories.{}.url".format(m.group(1)), url)

                return 0

            raise ValueError(
                "You must pass the url. "
                "Example: poetry config repositories.foo https://bar.com")

        # handle auth
        m = re.match(r"^(http-basic|pypi-token)\.(.+)", self.argument("key"))
        if m:
            from poetry.utils.password_manager import PasswordManager

            password_manager = PasswordManager(config)
            if self.option("unset"):
                if m.group(1) == "http-basic":
                    password_manager.delete_http_password(m.group(2))
                elif m.group(1) == "pypi-token":
                    password_manager.delete_pypi_token(m.group(2))

                return 0

            if m.group(1) == "http-basic":
                if len(values) == 1:
                    username = values[0]
                    # Only username, so we prompt for password
                    password = self.secret("Password:"******"Expected one or two arguments "
                                     "(username, password), got {}".format(
                                         len(values)))
                else:
                    username = values[0]
                    password = values[1]

                password_manager.set_http_password(m.group(2), username,
                                                   password)
            elif m.group(1) == "pypi-token":
                if len(values) != 1:
                    raise ValueError(
                        "Expected only one argument (token), got {}".format(
                            len(values)))

                token = values[0]

                password_manager.set_pypi_token(m.group(2), token)

            return 0

        # handle certs
        m = re.match(r"(?:certificates)\.([^.]+)\.(cert|client-cert)",
                     self.argument("key"))
        if m:
            if self.option("unset"):
                config.auth_config_source.remove_property(
                    "certificates.{}.{}".format(m.group(1), m.group(2)))

                return 0

            if len(values) == 1:
                config.auth_config_source.add_property(
                    "certificates.{}.{}".format(m.group(1), m.group(2)),
                    values[0])
            else:
                raise ValueError("You must pass exactly 1 value")

            return 0

        raise ValueError("Setting {} does not exist".format(
            self.argument("key")))

    def _handle_single_value(
        self,
        source: "ConfigSource",
        key: str,
        callbacks: Tuple[Any, Any, Any],
        values: List[Any],
    ) -> int:
        validator, normalizer, _ = callbacks

        if len(values) > 1:
            raise RuntimeError("You can only pass one value.")

        value = values[0]
        if not validator(value):
            raise RuntimeError('"{}" is an invalid value for {}'.format(
                value, key))

        source.add_property(key, normalizer(value))

        return 0

    def _list_configuration(self,
                            config: Dict,
                            raw: Dict,
                            k: str = "") -> None:
        orig_k = k
        for key, value in sorted(config.items()):
            if k + key in self.LIST_PROHIBITED_SETTINGS:
                continue

            raw_val = raw.get(key)

            if isinstance(value, dict):
                k += "{}.".format(key)
                self._list_configuration(value, raw_val, k=k)
                k = orig_k

                continue
            elif isinstance(value, list):
                value = [
                    json.dumps(val) if isinstance(val, list) else val
                    for val in value
                ]

                value = "[{}]".format(", ".join(value))

            if k.startswith("repositories."):
                message = "<c1>{}</c1> = <c2>{}</c2>".format(
                    k + key, json.dumps(raw_val))
            elif isinstance(raw_val, str) and raw_val != value:
                message = "<c1>{}</c1> = <c2>{}</c2>  # {}".format(
                    k + key, json.dumps(raw_val), value)
            else:
                message = "<c1>{}</c1> = <c2>{}</c2>".format(
                    k + key, json.dumps(value))

            self.line(message)

    def _get_setting(
        self,
        contents: Dict,
        setting: Optional[str] = None,
        k: Optional[str] = None,
        default: Optional[Any] = None,
    ) -> List[Tuple[str, str]]:
        orig_k = k

        if setting and setting.split(".")[0] not in contents:
            value = json.dumps(default)

            return [((k or "") + setting, value)]
        else:
            values = []
            for key, value in contents.items():
                if setting and key != setting.split(".")[0]:
                    continue

                if isinstance(value,
                              dict) or key == "repositories" and k is None:
                    if k is None:
                        k = ""

                    k += re.sub(r"^config\.", "", key + ".")
                    if setting and len(setting) > 1:
                        setting = ".".join(setting.split(".")[1:])

                    values += self._get_setting(value,
                                                k=k,
                                                setting=setting,
                                                default=default)
                    k = orig_k

                    continue

                if isinstance(value, list):
                    value = [
                        json.dumps(val) if isinstance(val, list) else val
                        for val in value
                    ]

                    value = "[{}]".format(", ".join(value))

                value = json.dumps(value)

                values.append(((k or "") + key, value))

            return values
Beispiel #10
0
class NewCommand(Command):

    name = "new"
    description = "Creates a new Python project at <path>."

    arguments = [argument("path", "The path to create the project at.")]
    options = [
        option("name", None, "Set the resulting package name.", flag=False),
        option("src", None, "Use the src layout for the project."),
    ]

    def handle(self) -> None:
        from pathlib import Path

        from poetry.core.semver.helpers import parse_constraint
        from poetry.core.vcs.git import GitConfig
        from poetry.layouts import layout
        from poetry.utils.env import SystemEnv

        if self.option("src"):
            layout_ = layout("src")
        else:
            layout_ = layout("standard")

        path = Path.cwd() / Path(self.argument("path"))
        name = self.option("name")
        if not name:
            name = path.name

        if path.exists():
            if list(path.glob("*")):
                # Directory is not empty. Aborting.
                raise RuntimeError("Destination <fg=yellow>{}</> "
                                   "exists and is not empty".format(path))

        readme_format = "rst"

        config = GitConfig()
        author = None
        if config.get("user.name"):
            author = config["user.name"]
            author_email = config.get("user.email")
            if author_email:
                author += " <{}>".format(author_email)

        current_env = SystemEnv(Path(sys.executable))
        default_python = "^{}".format(".".join(
            str(v) for v in current_env.version_info[:2]))

        dev_dependencies = {}
        python_constraint = parse_constraint(default_python)
        if parse_constraint("<3.5").allows_any(python_constraint):
            dev_dependencies["pytest"] = "^4.6"
        if parse_constraint(">=3.5").allows_all(python_constraint):
            dev_dependencies["pytest"] = "^5.2"

        layout_ = layout_(
            name,
            "0.1.0",
            author=author,
            readme_format=readme_format,
            python=default_python,
            dev_dependencies=dev_dependencies,
        )
        layout_.create(path)

        self.line("Created package <info>{}</> in <fg=blue>{}</>".format(
            module_name(name), path.relative_to(Path.cwd())))
Beispiel #11
0
class RemoveCommand(InstallerCommand):

    name = "remove"
    description = "Removes a package from the project dependencies."

    arguments = [
        argument("packages", "The packages to remove.", multiple=True)
    ]
    options = [
        option("group",
               "G",
               "The group to remove the dependency from.",
               flag=False),
        option("dev", "D",
               "Remove a package from the development dependencies."),
        option(
            "dry-run",
            None,
            "Output the operations but do not execute anything "
            "(implicitly enables --verbose).",
        ),
    ]

    help = """The <info>remove</info> command removes a package from the current
list of installed packages

<info>poetry remove</info>"""

    loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"]

    def handle(self) -> int:
        packages = self.argument("packages")

        if self.option("dev"):
            self.line("<warning>The --dev option is deprecated, "
                      "use the `--group dev` notation instead.</warning>")
            self.line("")
            group = "dev"
        else:
            group = self.option("group")

        content = self.poetry.file.read()
        poetry_content = content["tool"]["poetry"]

        if group is None:
            removed = []
            group_sections = []
            for group_name, group_section in poetry_content.get("group",
                                                                {}).items():
                group_sections.append(
                    (group_name, group_section.get("dependencies", {})))

            for group_name, section in [
                ("default", poetry_content["dependencies"])
            ] + group_sections:
                removed += self._remove_packages(packages, section, group_name)
                if group_name != "default":
                    if not section:
                        del poetry_content["group"][group_name]
                    else:
                        poetry_content["group"][group_name][
                            "dependencies"] = section
        elif group == "dev" and "dev-dependencies" in poetry_content:
            # We need to account for the old `dev-dependencies` section
            removed = self._remove_packages(packages,
                                            poetry_content["dev-dependencies"],
                                            "dev")

            if not poetry_content["dev-dependencies"]:
                del poetry_content["dev-dependencies"]
        else:
            removed = self._remove_packages(
                packages,
                poetry_content["group"][group].get("dependencies", {}), group)

            if not poetry_content["group"][group]:
                del poetry_content["group"][group]

        if "group" in poetry_content and not poetry_content["group"]:
            del poetry_content["group"]

        removed = set(removed)
        not_found = set(packages).difference(removed)
        if not_found:
            raise ValueError(
                "The following packages were not found: {}".format(", ".join(
                    sorted(not_found))))

        # Refresh the locker
        self.poetry.set_locker(
            self.poetry.locker.__class__(self.poetry.locker.lock.path,
                                         poetry_content))
        self._installer.set_locker(self.poetry.locker)

        # Update packages
        self._installer.use_executor(
            self.poetry.config.get("experimental.new-installer", False))

        self._installer.dry_run(self.option("dry-run"))
        self._installer.verbose(self._io.is_verbose())
        self._installer.update(True)
        self._installer.whitelist(removed)

        status = self._installer.run()

        if not self.option("dry-run") and status == 0:
            self.poetry.file.write(content)

        return status

    def _remove_packages(self, packages: List[str], section: Dict[str, Any],
                         group_name: str) -> List[str]:
        removed = []
        group = self.poetry.package.dependency_group(group_name)
        section_keys = list(section.keys())

        for package in packages:
            for existing_package in section_keys:
                if existing_package.lower() == package.lower():
                    del section[existing_package]
                    removed.append(package)
                    group.remove_dependency(package)

        return removed
Beispiel #12
0
class AddCommand(InstallerCommand, InitCommand):

    name = "add"
    description = "Adds a new dependency to <comment>pyproject.toml</>."

    arguments = [argument("name", "The packages to add.", multiple=True)]
    options = [
        option(
            "group",
            "-G",
            "The group to add the dependency to.",
            flag=False,
            default="default",
        ),
        option("dev", "D", "Add as a development dependency."),
        option("editable", "e", "Add vcs/path dependencies as editable."),
        option(
            "extras",
            "E",
            "Extras to activate for the dependency.",
            flag=False,
            multiple=True,
        ),
        option("optional", None, "Add as an optional dependency."),
        option(
            "python",
            None,
            "Python version for which the dependency must be installed.",
            flag=False,
        ),
        option(
            "platform",
            None,
            "Platforms for which the dependency must be installed.",
            flag=False,
        ),
        option(
            "source",
            None,
            "Name of the source to use to install the package.",
            flag=False,
        ),
        option("allow-prereleases", None, "Accept prereleases."),
        option(
            "dry-run",
            None,
            "Output the operations but do not execute anything (implicitly enables --verbose).",
        ),
        option("lock", None,
               "Do not perform operations (only update the lockfile)."),
    ]
    help = (
        "The add command adds required packages to your <comment>pyproject.toml</> and installs them.\n\n"
        "If you do not specify a version constraint, poetry will choose a suitable one based on the available package versions.\n\n"
        "You can specify a package in the following forms:\n"
        "  - A single name (<b>requests</b>)\n"
        "  - A name and a constraint (<b>requests@^2.23.0</b>)\n"
        "  - A git url (<b>git+https://github.com/python-poetry/poetry.git</b>)\n"
        "  - A git url with a revision (<b>git+https://github.com/python-poetry/poetry.git#develop</b>)\n"
        "  - A git SSH url (<b>git+ssh://github.com/python-poetry/poetry.git</b>)\n"
        "  - A git SSH url with a revision (<b>git+ssh://github.com/python-poetry/poetry.git#develop</b>)\n"
        "  - A file path (<b>../my-package/my-package.whl</b>)\n"
        "  - A directory (<b>../my-package/</b>)\n"
        "  - A url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)\n"
    )

    loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"]

    def handle(self) -> int:
        from tomlkit import inline_table
        from tomlkit import parse as parse_toml
        from tomlkit import table

        from poetry.core.semver.helpers import parse_constraint
        from poetry.factory import Factory

        packages = self.argument("name")
        if self.option("dev"):
            self.line("<warning>The --dev option is deprecated, "
                      "use the `--group dev` notation instead.</warning>")
            self.line("")
            group = "dev"
        else:
            group = self.option("group")

        if self.option("extras") and len(packages) > 1:
            raise ValueError(
                "You can only specify one package when using the --extras option"
            )

        content = self.poetry.file.read()
        poetry_content = content["tool"]["poetry"]

        if group == "default":
            if "dependencies" not in poetry_content:
                poetry_content["dependencies"] = table()

            section = poetry_content["dependencies"]
        else:
            if "group" not in poetry_content:
                group_table = table()
                group_table._is_super_table = True
                poetry_content.value._insert_after("dependencies", "group",
                                                   group_table)

            groups = poetry_content["group"]
            if group not in groups:
                group_table = parse_toml(
                    f"[tool.poetry.group.{group}.dependencies]\n\n"
                )["tool"]["poetry"]["group"][group]
                poetry_content["group"][group] = group_table

            if "dependencies" not in poetry_content["group"][group]:
                poetry_content["group"][group]["dependencies"] = table()

            section = poetry_content["group"][group]["dependencies"]

        existing_packages = self.get_existing_packages_from_input(
            packages, section)

        if existing_packages:
            self.notify_about_existing_packages(existing_packages)

        packages = [name for name in packages if name not in existing_packages]

        if not packages:
            self.line("Nothing to add.")
            return 0

        requirements = self._determine_requirements(
            packages,
            allow_prereleases=self.option("allow-prereleases"),
            source=self.option("source"),
        )

        for _constraint in requirements:
            if "version" in _constraint:
                # Validate version constraint
                parse_constraint(_constraint["version"])

            constraint = inline_table()
            for name, value in _constraint.items():
                if name == "name":
                    continue

                constraint[name] = value

            if self.option("optional"):
                constraint["optional"] = True

            if self.option("allow-prereleases"):
                constraint["allow-prereleases"] = True

            if self.option("extras"):
                extras = []
                for extra in self.option("extras"):
                    if " " in extra:
                        extras += [e.strip() for e in extra.split(" ")]
                    else:
                        extras.append(extra)

                constraint["extras"] = self.option("extras")

            if self.option("editable"):
                if "git" in _constraint or "path" in _constraint:
                    constraint["develop"] = True
                else:
                    self.line_error(
                        "\n"
                        "<error>Failed to add packages. "
                        "Only vcs/path dependencies support editable installs. "
                        f"<c1>{_constraint['name']}</c1> is neither.")
                    self.line_error("\nNo changes were applied.")
                    return 1

            if self.option("python"):
                constraint["python"] = self.option("python")

            if self.option("platform"):
                constraint["platform"] = self.option("platform")

            if self.option("source"):
                constraint["source"] = self.option("source")

            if len(constraint) == 1 and "version" in constraint:
                constraint = constraint["version"]

            section[_constraint["name"]] = constraint
            self.poetry.package.add_dependency(
                Factory.create_dependency(
                    _constraint["name"],
                    constraint,
                    groups=[group],
                    root_dir=self.poetry.file.parent,
                ))

        # Refresh the locker
        self.poetry.set_locker(
            self.poetry.locker.__class__(self.poetry.locker.lock.path,
                                         poetry_content))
        self._installer.set_locker(self.poetry.locker)

        # Cosmetic new line
        self.line("")

        self._installer.set_package(self.poetry.package)
        self._installer.dry_run(self.option("dry-run"))
        self._installer.verbose(self._io.is_verbose())
        self._installer.update(True)
        if self.option("lock"):
            self._installer.lock()

        self._installer.whitelist([r["name"] for r in requirements])

        status = self._installer.run()

        if status == 0 and not self.option("dry-run"):
            self.poetry.file.write(content)

        return status

    def get_existing_packages_from_input(self, packages: List[str],
                                         section: Dict) -> List[str]:
        existing_packages = []

        for name in packages:
            for key in section:
                if key.lower() == name.lower():
                    existing_packages.append(name)

        return existing_packages

    def notify_about_existing_packages(self,
                                       existing_packages: List[str]) -> None:
        self.line(
            "The following packages are already present in the pyproject.toml and will be skipped:\n"
        )
        for name in existing_packages:
            self.line("  • <c1>{name}</c1>".format(name=name))
        self.line(
            "\nIf you want to update it to the latest compatible version, you can use `poetry update package`.\n"
            "If you prefer to upgrade it to the latest available version, you can use `poetry add package@latest`.\n"
        )
Beispiel #13
0
class ConfigCommand(Command):

    name = "config"
    description = "Manages configuration settings."

    arguments = [
        argument("key", "Setting key.", optional=True),
        argument("value", "Setting value.", optional=True, multiple=True),
    ]

    options = [
        option("list", None, "List configuration settings."),
        option("unset", None, "Unset configuration setting."),
        option("local", None,
               "Set/Get from the project's local configuration."),
    ]

    help = """This command allows you to edit the poetry config settings and repositories.

To add a repository:

    <comment>poetry config repositories.foo https://bar.com/simple/</comment>

To remove a repository (repo is a short alias for repositories):

    <comment>poetry config --unset repo.foo</comment>"""

    LIST_PROHIBITED_SETTINGS = {"http-basic", "pypi-token"}

    @property
    def unique_config_values(self) -> dict[str, tuple[Any, Any, Any]]:
        unique_config_values = {
            "cache-dir": (
                str,
                lambda val: str(Path(val)),
                str(DEFAULT_CACHE_DIR / "virtualenvs"),
            ),
            "virtualenvs.create":
            (boolean_validator, boolean_normalizer, True),
            "virtualenvs.in-project":
            (boolean_validator, boolean_normalizer, False),
            "virtualenvs.options.always-copy": (
                boolean_validator,
                boolean_normalizer,
                False,
            ),
            "virtualenvs.options.system-site-packages": (
                boolean_validator,
                boolean_normalizer,
                False,
            ),
            "virtualenvs.options.no-pip": (
                boolean_validator,
                boolean_normalizer,
                False,
            ),
            "virtualenvs.options.no-setuptools": (
                boolean_validator,
                boolean_normalizer,
                False,
            ),
            "virtualenvs.path": (
                str,
                lambda val: str(Path(val)),
                str(DEFAULT_CACHE_DIR / "virtualenvs"),
            ),
            "virtualenvs.prefer-active-python": (
                boolean_validator,
                boolean_normalizer,
                False,
            ),
            "experimental.new-installer": (
                boolean_validator,
                boolean_normalizer,
                True,
            ),
            "installer.parallel": (
                boolean_validator,
                boolean_normalizer,
                True,
            ),
            "installer.max-workers": (
                lambda val: int(val) > 0,
                int_normalizer,
                None,
            ),
            "virtualenvs.prompt": (
                str,
                lambda val: str(val),
                "{project_name}-py{python_version}",
            ),
            "installer.no-binary": (
                PackageFilterPolicy.validator,
                PackageFilterPolicy.normalize,
                None,
            ),
        }

        return unique_config_values

    def handle(self) -> int:
        from pathlib import Path

        from poetry.core.pyproject.exceptions import PyProjectException
        from poetry.core.toml.file import TOMLFile

        from poetry.config.config import Config
        from poetry.config.file_config_source import FileConfigSource
        from poetry.locations import CONFIG_DIR

        config = Config.create()
        config_file = TOMLFile(CONFIG_DIR / "config.toml")

        try:
            local_config_file = TOMLFile(self.poetry.file.parent /
                                         "poetry.toml")
            if local_config_file.exists():
                config.merge(local_config_file.read())
        except (RuntimeError, PyProjectException):
            local_config_file = TOMLFile(Path.cwd() / "poetry.toml")

        if self.option("local"):
            config.set_config_source(FileConfigSource(local_config_file))

        if not config_file.exists():
            config_file.path.parent.mkdir(parents=True, exist_ok=True)
            config_file.touch(mode=0o0600)

        if self.option("list"):
            self._list_configuration(config.all(), config.raw())

            return 0

        setting_key = self.argument("key")
        if not setting_key:
            return 0

        if self.argument("value") and self.option("unset"):
            raise RuntimeError(
                "You can not combine a setting value with --unset")

        # show the value if no value is provided
        if not self.argument("value") and not self.option("unset"):
            m = re.match(r"^repos?(?:itories)?(?:\.(.+))?",
                         self.argument("key"))
            value: str | dict[str, Any]
            if m:
                if not m.group(1):
                    value = {}
                    if config.get("repositories") is not None:
                        value = config.get("repositories")
                else:
                    repo = config.get(f"repositories.{m.group(1)}")
                    if repo is None:
                        raise ValueError(
                            f"There is no {m.group(1)} repository defined")

                    value = repo

                self.line(str(value))
            else:
                if setting_key not in self.unique_config_values:
                    raise ValueError(f"There is no {setting_key} setting.")

                value = config.get(setting_key)

                if not isinstance(value, str):
                    value = json.dumps(value)

                self.line(value)

            return 0

        values: list[str] = self.argument("value")

        unique_config_values = self.unique_config_values
        if setting_key in unique_config_values:
            if self.option("unset"):
                config.config_source.remove_property(setting_key)
                return 0

            return self._handle_single_value(
                config.config_source,
                setting_key,
                unique_config_values[setting_key],
                values,
            )

        # handle repositories
        m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key"))
        if m:
            if not m.group(1):
                raise ValueError(
                    "You cannot remove the [repositories] section")

            if self.option("unset"):
                repo = config.get(f"repositories.{m.group(1)}")
                if repo is None:
                    raise ValueError(
                        f"There is no {m.group(1)} repository defined")

                config.config_source.remove_property(
                    f"repositories.{m.group(1)}")

                return 0

            if len(values) == 1:
                url = values[0]

                config.config_source.add_property(
                    f"repositories.{m.group(1)}.url", url)

                return 0

            raise ValueError(
                "You must pass the url. "
                "Example: poetry config repositories.foo https://bar.com")

        # handle auth
        m = re.match(r"^(http-basic|pypi-token)\.(.+)", self.argument("key"))
        if m:
            from poetry.utils.password_manager import PasswordManager

            password_manager = PasswordManager(config)
            if self.option("unset"):
                if m.group(1) == "http-basic":
                    password_manager.delete_http_password(m.group(2))
                elif m.group(1) == "pypi-token":
                    password_manager.delete_pypi_token(m.group(2))

                return 0

            if m.group(1) == "http-basic":
                if len(values) == 1:
                    username = values[0]
                    # Only username, so we prompt for password
                    password = self.secret("Password:"******"Expected one or two arguments "
                        f"(username, password), got {len(values)}")
                else:
                    username = values[0]
                    password = values[1]

                password_manager.set_http_password(m.group(2), username,
                                                   password)
            elif m.group(1) == "pypi-token":
                if len(values) != 1:
                    raise ValueError(
                        f"Expected only one argument (token), got {len(values)}"
                    )

                token = values[0]

                password_manager.set_pypi_token(m.group(2), token)

            return 0

        # handle certs
        m = re.match(r"certificates\.([^.]+)\.(cert|client-cert)",
                     self.argument("key"))
        if m:
            repository = m.group(1)
            key = m.group(2)

            if self.option("unset"):
                config.auth_config_source.remove_property(
                    f"certificates.{repository}.{key}")

                return 0

            if len(values) == 1:
                new_value: str | bool = values[0]

                if key == "cert" and boolean_validator(values[0]):
                    new_value = boolean_normalizer(values[0])

                config.auth_config_source.add_property(
                    f"certificates.{repository}.{key}", new_value)
            else:
                raise ValueError("You must pass exactly 1 value")

            return 0

        raise ValueError(f"Setting {self.argument('key')} does not exist")

    def _handle_single_value(
        self,
        source: ConfigSource,
        key: str,
        callbacks: tuple[Any, Any, Any],
        values: list[Any],
    ) -> int:
        validator, normalizer, _ = callbacks

        if len(values) > 1:
            raise RuntimeError("You can only pass one value.")

        value = values[0]
        if not validator(value):
            raise RuntimeError(f'"{value}" is an invalid value for {key}')

        source.add_property(key, normalizer(value))

        return 0

    def _list_configuration(self,
                            config: dict[str, Any],
                            raw: dict[str, Any],
                            k: str = "") -> None:
        orig_k = k
        for key, value in sorted(config.items()):
            if k + key in self.LIST_PROHIBITED_SETTINGS:
                continue

            raw_val = raw.get(key)

            if isinstance(value, dict):
                k += f"{key}."
                raw_val = cast("dict[str, Any]", raw_val)
                self._list_configuration(value, raw_val, k=k)
                k = orig_k

                continue
            elif isinstance(value, list):
                value = ", ".join(
                    json.dumps(val) if isinstance(val, list) else val
                    for val in value)
                value = f"[{value}]"

            if k.startswith("repositories."):
                message = f"<c1>{k + key}</c1> = <c2>{json.dumps(raw_val)}</c2>"
            elif isinstance(raw_val, str) and raw_val != value:
                message = (
                    f"<c1>{k + key}</c1> = <c2>{json.dumps(raw_val)}</c2>  # {value}"
                )
            else:
                message = f"<c1>{k + key}</c1> = <c2>{json.dumps(value)}</c2>"

            self.line(message)
Beispiel #14
0
class VersionCommand(Command):

    name = "version"
    description = (
        "Shows the version of the project or bumps it when a valid "
        "bump rule is provided."
    )

    arguments = [
        argument(
            "version",
            "The version number or the rule to update the version.",
            optional=True,
        )
    ]
    options = [option("short", "s", "Output the version number only")]

    help = """\
The version command shows the current version of the project or bumps the version of
the project and writes the new version back to <comment>pyproject.toml</> if a valid
bump rule is provided.

The new version should ideally be a valid semver string or a valid bump rule:
patch, minor, major, prepatch, preminor, premajor, prerelease.
"""

    RESERVED = {
        "major",
        "minor",
        "patch",
        "premajor",
        "preminor",
        "prepatch",
        "prerelease",
    }

    def handle(self):  # type: () -> None
        version = self.argument("version")

        if version:
            version = self.increment_version(
                self.poetry.package.pretty_version, version
            )

            if self.option("short"):
                self.line("{}".format(version))
            else:
                self.line(
                    "Bumping version from <b>{}</> to <fg=green>{}</>".format(
                        self.poetry.package.pretty_version, version
                    )
                )

            content = self.poetry.file.read()
            poetry_content = content["tool"]["poetry"]
            poetry_content["version"] = version.text

            self.poetry.file.write(content)
        else:
            if self.option("short"):
                self.line("{}".format(self.poetry.package.pretty_version))
            else:
                self.line(
                    "<comment>{}</> <info>{}</>".format(
                        self.poetry.package.name, self.poetry.package.pretty_version
                    )
                )

    def increment_version(self, version, rule):  # type: (str, str) -> "Version"
        from poetry.core.semver import Version

        try:
            version = Version.parse(version)
        except ValueError:
            raise ValueError("The project's version doesn't seem to follow semver")

        if rule in {"major", "premajor"}:
            new = version.next_major
            if rule == "premajor":
                new = new.first_prerelease
        elif rule in {"minor", "preminor"}:
            new = version.next_minor
            if rule == "preminor":
                new = new.first_prerelease
        elif rule in {"patch", "prepatch"}:
            new = version.next_patch
            if rule == "prepatch":
                new = new.first_prerelease
        elif rule == "prerelease":
            if version.is_prerelease():
                pre = version.prerelease
                new_prerelease = int(pre[1]) + 1
                new = Version.parse(
                    "{}.{}.{}-{}".format(
                        version.major,
                        version.minor,
                        version.patch,
                        ".".join([pre[0], str(new_prerelease)]),
                    )
                )
            else:
                new = version.next_patch.first_prerelease
        else:
            new = Version.parse(rule)

        return new