Exemple #1
0
 def _group_dependency_options() -> list[Option]:
     return [
         option(
             "without",
             None,
             "The dependency groups to ignore.",
             flag=False,
             multiple=True,
         ),
         option(
             "with",
             None,
             "The optional dependency groups to include.",
             flag=False,
             multiple=True,
         ),
         option(
             "default",
             None,
             "Only include the main dependencies. (<warning>Deprecated</warning>)",
         ),
         option(
             "only",
             None,
             "The only dependency groups to include.",
             flag=False,
             multiple=True,
         ),
     ]
Exemple #2
0
class UpdateCommand(InstallerCommand):

    name = "update"
    description = (
        "Update the dependencies as according to the <comment>pyproject.toml</> file."
    )

    arguments = [
        argument("packages",
                 "The packages to update",
                 optional=True,
                 multiple=True)
    ]
    options = [
        *InstallerCommand._group_dependency_options(),
        option(
            "no-dev",
            None,
            "Do not update the development dependencies."
            " (<warning>Deprecated</warning>)",
        ),
        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)."),
    ]

    loggers = ["poetry.repositories.pypi_repository"]

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

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

        if packages:
            self._installer.whitelist({name: "*" for name in packages})

        self._installer.only_groups(self.activated_groups)
        self._installer.dry_run(self.option("dry-run"))
        self._installer.execute_operations(not self.option("lock"))

        # Force update
        self._installer.update(True)

        return self._installer.run()
Exemple #3
0
class LockCommand(InstallerCommand):

    name = "lock"
    description = "Locks the project dependencies."

    options = [
        option(
            "no-update", None, "Do not update locked versions, only refresh lock file."
        ),
        option(
            "check",
            None,
            "Check that the <comment>poetry.lock</> file corresponds to the current"
            " version of <comment>pyproject.toml</>.",
        ),
    ]

    help = """
The <info>lock</info> command reads the <comment>pyproject.toml</> file from the
current directory, processes it, and locks the dependencies in the\
 <comment>poetry.lock</>
file.

<info>poetry lock</info>
"""

    loggers = ["poetry.repositories.pypi_repository"]

    def handle(self) -> int:
        self._installer.use_executor(
            self.poetry.config.get("experimental.new-installer", False)
        )

        if self.option("check"):
            if self.poetry.locker.is_locked() and self.poetry.locker.is_fresh():
                self.line("poetry.lock is consistent with pyproject.toml.")
                return 0
            self.line_error(
                "<error>"
                "Error: poetry.lock is not consistent with pyproject.toml. "
                "Run `poetry lock [--no-update]` to fix it."
                "</error>"
            )
            return 1

        self._installer.lock(update=not self.option("no-update"))

        return self._installer.run()
Exemple #4
0
class EnvListCommand(Command):
    name = "env list"
    description = "Lists all virtualenvs associated with the current project."

    options = [
        option("full-path", None, "Output the full paths of the virtualenvs.")
    ]

    def handle(self) -> int:
        from poetry.utils.env import EnvManager

        manager = EnvManager(self.poetry)
        current_env = manager.get()

        for venv in manager.list():
            name = venv.path.name
            if self.option("full-path"):
                name = str(venv.path)

            if venv == current_env:
                self.line(f"<info>{name} (Activated)</info>")

                continue

            self.line(name)

        return 0
Exemple #5
0
class BuildCommand(EnvCommand):

    name = "build"
    description = "Builds a package, as a tarball and a wheel by default."

    options = [
        option("format",
               "f",
               "Limit the format to either sdist or wheel.",
               flag=False)
    ]

    loggers = [
        "poetry.core.masonry.builders.builder",
        "poetry.core.masonry.builders.sdist",
        "poetry.core.masonry.builders.wheel",
    ]

    def handle(self) -> None:
        from poetry.core.masonry import Builder

        fmt = "all"
        if self.option("format"):
            fmt = self.option("format")

        package = self.poetry.package
        self.line("Building <c1>{}</c1> (<c2>{}</c2>)".format(
            package.pretty_name, package.version))

        builder = Builder(self.poetry)
        builder.build(fmt, executable=self.env.python)
Exemple #6
0
class LockCommand(InstallerCommand):

    name = "lock"
    description = "Locks the project dependencies."

    options = [
        option("no-update", None,
               "Do not update locked versions, only refresh lock file."),
    ]

    help = """
The <info>lock</info> command reads the <comment>pyproject.toml</> file from the
current directory, processes it, and locks the dependencies in the <comment>poetry.lock</>
file.

<info>poetry lock</info>
"""

    loggers = ["poetry.repositories.pypi_repository"]

    def handle(self) -> int:
        self._installer.use_executor(
            self.poetry.config.get("experimental.new-installer", False))

        self._installer.lock(update=not self.option("no-update"))

        return self._installer.run()
Exemple #7
0
class BuildCommand(EnvCommand):
    name = "build"
    description = "Builds a package, as a tarball and a wheel by default."

    options = [
        option("format",
               "f",
               "Limit the format to either sdist or wheel.",
               flag=False)
    ]

    loggers = [
        "poetry.core.masonry.builders.builder",
        "poetry.core.masonry.builders.sdist",
        "poetry.core.masonry.builders.wheel",
    ]

    def handle(self) -> int:
        from poetry.core.masonry.builder import Builder

        with build_environment(poetry=self.poetry, env=self.env,
                               io=self.io) as env:
            fmt = self.option("format") or "all"
            package = self.poetry.package
            self.line(
                f"Building <c1>{package.pretty_name}</c1> (<c2>{package.version}</c2>)"
            )

            builder = Builder(self.poetry)
            builder.build(fmt, executable=env.python)

        return 0
Exemple #8
0
class SelfShowCommand(SelfCommand, ShowCommand):
    name = "self show"
    options = [
        option("addons", None, "List only add-on packages installed."),
        *[
            o for o in ShowCommand.options
            if o.name in {"tree", "latest", "outdated"}
        ],
    ]
    description = "Show packages from Poetry's runtime environment."
    help = f"""\
The <c1>self show</c1> command behaves similar to the <c1>show</c1> command, but
working within Poetry's runtime environment. This lists all packages installed within
the Poetry install environment.

To show only additional packages that have been added via <c1>self add</c1> and their
dependencies use <c1>self show --addons</c1>.

This is managed in the <comment>{SelfCommand.get_default_system_pyproject_file()}</> \
file.
"""

    @property
    def activated_groups(self) -> set[str]:
        if self.option("addons", False):
            return {SelfCommand.ADDITIONAL_PACKAGE_GROUP}

        groups: set[str] = super(ShowCommand, self).activated_groups
        return groups
Exemple #9
0
class SelfUpdateCommand(SelfCommand):
    name = "self update"
    description = "Updates Poetry to the latest version."

    arguments = [
        argument("version",
                 "The version to update to.",
                 optional=True,
                 default="latest")
    ]
    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).",
        ),
    ]
    help = """\
The <c1>self update</c1> command updates Poetry version in its current runtime \
environment.
"""

    def _system_project_handle(self) -> int:
        self.write("<info>Updating Poetry version ...</info>\n\n")
        application = self.get_application()
        add_command: AddCommand = application.find("add")
        add_command.set_env(self.env)
        application.configure_installer_for_command(add_command, self.io)

        argv = ["add", f"poetry@{self.argument('version')}"]

        if self.option("dry-run"):
            argv.append("--dry-run")

        if self.option("preview"):
            argv.append("--allow-prereleases")

        exit_code: int = add_command.run(
            IO(
                StringInput(" ".join(argv)),
                self.io.output,
                self.io.error_output,
            ))
        return exit_code
Exemple #10
0
class EnvInfoCommand(Command):

    name = "env info"
    description = "Displays information about the current environment."

    options = [option("path", "p", "Only display the environment's path.")]

    def handle(self) -> int | None:
        from poetry.utils.env import EnvManager

        env = EnvManager(self.poetry).get()

        if self.option("path"):
            if not env.is_venv():
                return 1

            self.line(str(env.path))

            return None

        self._display_complete_info(env)
        return None

    def _display_complete_info(self, env: Env) -> None:
        env_python_version = ".".join(str(s) for s in env.version_info[:3])
        self.line("")
        self.line("<b>Virtualenv</b>")
        listing = [
            f"<info>Python</info>:         <comment>{env_python_version}</>",
            f"<info>Implementation</info>: <comment>{env.python_implementation}</>",
            "<info>Path</info>:          "
            f" <comment>{env.path if env.is_venv() else 'NA'}</>",
            "<info>Executable</info>:    "
            f" <comment>{env.python if env.is_venv() else 'NA'}</>",
        ]
        if env.is_venv():
            listing.append(
                "<info>Valid</info>:         "
                f" <{'comment' if env.is_sane() else 'error'}>{env.is_sane()}</>"
            )
        self.line("\n".join(listing))

        self.line("")

        system_env = env.parent_env
        python = ".".join(str(v) for v in system_env.version_info[:3])
        self.line("<b>System</b>")
        self.line(
            "\n".join(
                [
                    f"<info>Platform</info>:   <comment>{env.platform}</>",
                    f"<info>OS</info>:         <comment>{env.os}</>",
                    f"<info>Python</info>:     <comment>{python}</>",
                    f"<info>Path</info>:       <comment>{system_env.path}</>",
                    f"<info>Executable</info>: <comment>{system_env.python}</>",
                ]
            )
        )
Exemple #11
0
class EnvInfoCommand(Command):

    name = "env info"
    description = "Displays information about the current environment."

    options = [option("path", "p", "Only display the environment's path.")]

    def handle(self) -> Optional[int]:
        from poetry.utils.env import EnvManager

        env = EnvManager(self.poetry).get()

        if self.option("path"):
            if not env.is_venv():
                return 1

            self.line(str(env.path))

            return None

        self._display_complete_info(env)
        return None

    def _display_complete_info(self, env: "Env") -> None:
        env_python_version = ".".join(str(s) for s in env.version_info[:3])
        self.line("")
        self.line("<b>Virtualenv</b>")
        listing = [
            "<info>Python</info>:         <comment>{}</>".format(
                env_python_version),
            "<info>Implementation</info>: <comment>{}</>".format(
                env.python_implementation),
            "<info>Path</info>:           <comment>{}</>".format(
                env.path if env.is_venv() else "NA"),
            "<info>Executable</info>:     <comment>{}</>".format(
                env.python if env.is_venv() else "NA"),
        ]
        if env.is_venv():
            listing.append(
                "<info>Valid</info>:          <{tag}>{is_valid}</{tag}>".
                format(tag="comment" if env.is_sane() else "error",
                       is_valid=env.is_sane()))
        self.line("\n".join(listing))

        self.line("")

        system_env = env.parent_env
        self.line("<b>System</b>")
        self.line("\n".join([
            "<info>Platform</info>:   <comment>{}</>".format(env.platform),
            "<info>OS</info>:         <comment>{}</>".format(env.os),
            "<info>Python</info>:     <comment>{}</>".format(".".join(
                str(v) for v in system_env.version_info[:3])),
            "<info>Path</info>:       <comment>{}</>".format(system_env.path),
            "<info>Executable</info>: <comment>{}</>".format(
                system_env.python),
        ]))
Exemple #12
0
class PluginRemoveCommand(Command):

    name = "plugin remove"

    description = "Removes installed 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).",
        )
    ]

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

        from cleo.io.inputs.string_input import StringInput
        from cleo.io.io import IO

        from poetry.factory import Factory
        from poetry.utils.env import EnvManager

        plugins = self.argument("plugins")

        system_env = EnvManager.get_system_env()
        env_dir = Path(
            os.getenv("POETRY_HOME") if os.getenv("POETRY_HOME") else system_env.path
        )

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

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

        return remove_command.run(
            IO(
                StringInput(" ".join(argv)),
                self._io.output,
                self._io.error_output,
            )
        )
Exemple #13
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).",
        )
    ]
    deprecation = (
        "<warning>This command is deprecated. Use <c2>self add</> command instead."
        "</warning>")
    help = f"""
The <c1>plugin add</c1> command installs Poetry plugins globally.

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

{SelfAddCommand.examples}

{deprecation}
"""
    hidden = True

    def handle(self) -> int:
        self.line_error(self.deprecation)

        application = cast(Application, self.application)
        command: SelfAddCommand = cast(SelfAddCommand,
                                       application.find("self add"))
        application.configure_installer_for_command(command, self.io)

        argv: list[str] = ["add", *self.argument("plugins")]

        if self.option("--dry-run"):
            argv.append("--dry-run")

        exit_code: int = command.run(
            IO(
                StringInput(" ".join(argv)),
                self.io.output,
                self.io.error_output,
            ))
        return exit_code
Exemple #14
0
def test_option():
    opt = option("foo", "f", "Foo")

    assert "Foo" == opt.description
    assert not opt.accepts_value()
    assert not opt.requires_value()
    assert not opt.is_list()
    assert opt.default is False

    opt = option("foo", "f", "Foo", flag=False)

    assert "Foo" == opt.description
    assert opt.accepts_value()
    assert opt.requires_value()
    assert not opt.is_list()
    assert opt.default is None

    opt = option("foo", "f", "Foo", flag=False, value_required=False)

    assert "Foo" == opt.description
    assert opt.accepts_value()
    assert not opt.requires_value()
    assert not opt.is_list()

    opt = option("foo", "f", "Foo", flag=False, multiple=True)

    assert "Foo" == opt.description
    assert opt.accepts_value()
    assert opt.requires_value()
    assert opt.is_list()
    assert [] == opt.default

    opt = option("foo", "f", "Foo", flag=False, default="bar")

    assert "Foo" == opt.description
    assert opt.accepts_value()
    assert opt.requires_value()
    assert not opt.is_list()
    assert "bar" == opt.default
Exemple #15
0
class PluginRemoveCommand(Command):

    name = "plugin remove"

    description = "Removes installed 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 = (
        "<warning>This command is deprecated. Use <c2>self remove</> command instead."
        "</warning>")

    hidden = True

    def handle(self) -> int:
        self.line_error(self.help)

        application = cast(Application, self.application)
        command: SelfRemoveCommand = cast(SelfRemoveCommand,
                                          application.find("self remove"))
        application.configure_installer_for_command(command, self.io)

        argv: list[str] = ["remove", *self.argument("plugins")]

        if self.option("--dry-run"):
            argv.append("--dry-run")

        exit_code: int = command.run(
            IO(
                StringInput(" ".join(argv)),
                self.io.output,
                self.io.error_output,
            ))
        return exit_code
class FooCommand(Command):
    """
    Foo command
    """

    name = "foo"

    description = "Foo command"

    arguments = [argument("foo")]

    options = [option("--bar")]

    def handle(self):
        self.line(self.argument("foo"))

        if self.option("bar"):
            self.line("--bar activated")
Exemple #17
0
class EnvRemoveCommand(Command):

    name = "env remove"
    description = "Remove virtual environments associated with the project."

    arguments = [
        argument(
            "python",
            "The python executables associated with, or names of the virtual"
            " environments which are to be removed.",
            optional=True,
            multiple=True,
        )
    ]
    options = [
        option(
            "all",
            description=(
                "Remove all managed virtual environments associated with the project."
            ),
        ),
    ]

    def handle(self) -> int:
        from poetry.utils.env import EnvManager

        pythons = self.argument("python")
        all = self.option("all")
        if not (pythons or all):
            self.line("No virtualenv provided.")

        manager = EnvManager(self.poetry)
        # TODO: refactor env.py to allow removal with one loop
        for python in pythons:
            venv = manager.remove(python)
            self.line(f"Deleted virtualenv: <comment>{venv.path}</comment>")
        if all:
            for venv in manager.list():
                manager.remove_venv(venv.path)
                self.line(f"Deleted virtualenv: <comment>{venv.path}</comment>")

        return 0
Exemple #18
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.solver 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")
                assert isinstance(name, str)
                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=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
Exemple #19
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):  # type: () -> int
        packages = self.argument("packages")
        is_dev = self.option("dev")

        original_content = self.poetry.file.read()
        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]

        # Write the new content back
        self.poetry.file.write(content)

        # Update packages
        self.reset_poetry()

        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)

        try:
            status = self._installer.run()
        except Exception:
            self.poetry.file.write(original_content)

            raise

        if status != 0 or self.option("dry-run"):
            # Revert changes
            if not self.option("dry-run"):
                self.line_error("\n"
                                "Removal failed, reverting pyproject.toml "
                                "to its original content.")

            self.poetry.file.write(original_content)

        return status
Exemple #20
0
class ExportCommand(Command):

    name = "export"
    description = "Exports the lock file to alternative formats."

    options = [
        option(
            "format",
            "f",
            "Format to export to. Currently, only requirements.txt is supported.",
            flag=False,
            default=Exporter.FORMAT_REQUIREMENTS_TXT,
        ),
        option("output", "o", "The name of the output file.", flag=False),
        option("without-hashes", None, "Exclude hashes from the exported file."),
        option(
            "without-urls",
            None,
            "Exclude source repository urls from the exported file.",
        ),
        option("dev", None, "Include development dependencies."),
        option(
            "extras",
            "E",
            "Extra sets of dependencies to include.",
            flag=False,
            multiple=True,
        ),
        option("with-credentials", None, "Include credentials for extra indices."),
    ]

    def handle(self) -> None:
        fmt = self.option("format")

        if fmt not in Exporter.ACCEPTED_FORMATS:
            raise ValueError(f"Invalid export format: {fmt}")

        output = self.option("output")

        locker = self.poetry.locker
        if not locker.is_locked():
            self.line_error("<comment>The lock file does not exist. Locking.</comment>")
            options = []
            if self.io.is_debug():
                options.append("-vvv")
            elif self.io.is_very_verbose():
                options.append("-vv")
            elif self.io.is_verbose():
                options.append("-v")

            self.call("lock", " ".join(options))

        if not locker.is_fresh():
            self.line_error(
                "<warning>"
                "Warning: The lock file is not up to date with "
                "the latest changes in pyproject.toml. "
                "You may be getting outdated dependencies. "
                "Run update to update them."
                "</warning>"
            )

        exporter = Exporter(self.poetry)
        exporter.export(
            fmt,
            self.poetry.file.parent,
            output or self.io,
            with_hashes=not self.option("without-hashes"),
            dev=self.option("dev"),
            extras=self.option("extras"),
            with_credentials=self.option("with-credentials"),
            with_urls=not self.option("without-urls"),
        )
Exemple #21
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_cls = layout("src")
        else:
            layout_cls = 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_cls(
            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()}</>")
Exemple #22
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):  # type: () -> Path
        from pathlib import Path

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

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

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

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

    def handle(self):  # type: () -> None
        from poetry.__version__ import __version__
        from poetry.core.semver 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):  # type: ("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):  # type: ("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):  # type: (*Any) -> str
        return subprocess.check_output(list(args), stderr=subprocess.STDOUT)

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

        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):  # type: ("Version") -> str
        platform = sys.platform
        if platform == "linux2":
            platform = "linux"

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

    def make_bin(self):  # type: () -> 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):  # type: () -> 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
Exemple #23
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.config.config import int_normalizer
        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"),
            ),
            "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,
            ),
        }

        return unique_config_values

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

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

        from poetry.config.file_config_source import FileConfigSource
        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"))
            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 None

            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:
            if self.option("unset"):
                config.auth_config_source.remove_property(
                    f"certificates.{m.group(1)}.{m.group(2)}")

                return 0

            if len(values) == 1:
                config.auth_config_source.add_property(
                    f"certificates.{m.group(1)}.{m.group(2)}", values[0])
            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}."
                self._list_configuration(value, cast(dict, 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)

    def _get_setting(
        self,
        contents: dict,
        setting: str | None = None,
        k: str | None = None,
        default: Any | None = 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(cast(dict, value),
                                                k=k,
                                                setting=setting,
                                                default=default)
                    k = orig_k

                    continue

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

                value = json.dumps(value)

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

            return values
Exemple #24
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_error(
                "<warning>The --dev option is deprecated, "
                "use the `--group dev` notation instead.</warning>")
            group = "dev"
        else:
            group = self.option("group", self.default_group)

        content: dict[str, Any] = self.poetry.file.read()
        poetry_content = content["tool"]["poetry"]

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

            for group_name, section in [
                (MAIN_GROUP, poetry_content["dependencies"])
            ] + group_sections:
                removed += self._remove_packages(packages, section, group_name)
                if group_name != MAIN_GROUP:
                    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 = []
            if "group" in poetry_content:
                if group in poetry_content["group"]:
                    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 = set(removed)
        not_found = set(packages).difference(removed_set)
        if not_found:
            raise ValueError("The following packages were not found: " +
                             ", ".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)

        self._installer.set_package(self.poetry.package)
        self._installer.dry_run(self.option("dry-run", False))
        self._installer.verbose(self.io.is_verbose())
        self._installer.update(True)
        self._installer.whitelist(removed_set)

        status = self._installer.run()

        if not self.option("dry-run") and status == 0:
            assert isinstance(content, TOMLDocument)
            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 canonicalize_name(existing_package) == canonicalize_name(
                        package):
                    del section[existing_package]
                    removed.append(package)
                    group.remove_dependency(package)

        return removed
Exemple #25
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.

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>)
"""

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

    def handle(self) -> int:
        from poetry.core.semver.helpers import parse_constraint
        from tomlkit import inline_table
        from tomlkit import parse as parse_toml
        from tomlkit import table

        from poetry.factory import Factory

        packages = self.argument("name")
        if self.option("dev"):
            self.line_error(
                "<warning>The --dev option is deprecated, "
                "use the `--group dev` notation instead.</warning>")
            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

            with contextlib.suppress(ValueError):
                self.poetry.package.dependency_group(group).remove_dependency(
                    _constraint["name"])

            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([cast(str, 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(f"  • <c1>{name}</c1>")
        self.line(
            "\nIf you want to update it to the latest compatible version, you can use"
            " `poetry update package`.\nIf you prefer to upgrade it to the latest"
            " available version, you can use `poetry add package@latest`.\n")
Exemple #26
0
class SourceAddCommand(Command):

    name = "source add"
    description = "Add source configuration for project."

    arguments = [
        argument(
            "name",
            "Source repository name.",
        ),
        argument("url", "Source repository url."),
    ]

    options = [
        option(
            "default",
            "d",
            "Set this source as the default (disable PyPI). A "
            "default source will also be the fallback source if "
            "you add other sources.",
        ),
        option("secondary", "s", "Set this source as secondary."),
    ]

    @staticmethod
    def source_to_table(source: Source) -> "Table":
        source_table: "Table" = table()
        for key, value in source.to_dict().items():
            source_table.add(key, value)
        source_table.add(nl())
        return source_table

    def handle(self) -> Optional[int]:
        from poetry.factory import Factory
        from poetry.repositories import Pool

        name = self.argument("name")
        url = self.argument("url")
        is_default = self.option("default")
        is_secondary = self.option("secondary")

        if is_default and is_secondary:
            self.line_error(
                "Cannot configure a source as both <c1>default</c1> and <c1>secondary</c1>."
            )
            return 1

        new_source = Source(name=name,
                            url=url,
                            default=is_default,
                            secondary=is_secondary)
        existing_sources = self.poetry.get_sources()

        sources = AoT([])

        for source in existing_sources:
            if source == new_source:
                self.line(
                    f"Source with name <c1>{name}</c1> already exits. Skipping addition."
                )
                return 0
            elif source.default and is_default:
                self.line_error(
                    f"<error>Source with name <c1>{source.name}</c1> is already set to default. "
                    f"Only one default source can be configured at a time.</error>"
                )
                return 1

            if source.name == name:
                self.line(
                    f"Source with name <c1>{name}</c1> already exits. Updating."
                )
                source = new_source
                new_source = None

            sources.append(self.source_to_table(source))

        if new_source is not None:
            self.line(f"Adding source with name <c1>{name}</c1>.")
            sources.append(self.source_to_table(new_source))

        # ensure new source is valid. eg: invalid name etc.
        self.poetry._pool = Pool()
        try:
            Factory.configure_sources(self.poetry, sources, self.poetry.config,
                                      NullIO())
            self.poetry.pool.repository(name)
        except ValueError as e:
            self.line_error(
                f"<error>Failed to validate addition of <c1>{name}</c1>: {e}</error>"
            )
            return 1

        self.poetry.pyproject.poetry_config["source"] = sources
        self.poetry.pyproject.save()

        return 0
Exemple #27
0
class PublishCommand(Command):

    name = "publish"
    description = "Publishes a package to a remote repository."

    options = [
        option("repository",
               "r",
               "The repository to publish the package to.",
               flag=False),
        option("username",
               "u",
               "The username to access the repository.",
               flag=False),
        option("password",
               "p",
               "The password to access the repository.",
               flag=False),
        option("cert",
               None,
               "Certificate authority to access the repository.",
               flag=False),
        option(
            "client-cert",
            None,
            "Client certificate to access the repository.",
            flag=False,
        ),
        option("build", None, "Build the package before publishing."),
        option("dry-run", None,
               "Perform all actions except upload the package."),
    ]

    help = """The publish command builds and uploads the package to a remote repository.

By default, it will upload to PyPI but if you pass the --repository option it will
upload to it instead.

The --repository option should match the name of a configured repository using
the config command.
"""

    loggers = ["poetry.masonry.publishing.publisher"]

    def handle(self) -> Optional[int]:
        from poetry.publishing.publisher import Publisher

        publisher = Publisher(self.poetry, self.io)

        # Building package first, if told
        if self.option("build"):
            if publisher.files:
                if not self.confirm(
                        "There are <info>{}</info> files ready for publishing. "
                        "Build anyway?".format(len(publisher.files))):
                    self.line_error("<error>Aborted!</error>")

                    return 1

            self.call("build")

        files = publisher.files
        if not files:
            self.line_error(
                "<error>No files to publish. "
                "Run poetry build first or use the --build option.</error>")

            return 1

        self.line("")

        cert = Path(self.option("cert")) if self.option("cert") else None
        client_cert = (Path(self.option("client-cert"))
                       if self.option("client-cert") else None)

        publisher.publish(
            self.option("repository"),
            self.option("username"),
            self.option("password"),
            cert,
            client_cert,
            self.option("dry-run"),
        )
Exemple #28
0
class InitCommand(Command):
    name = "init"
    description = (
        "Creates a basic <comment>pyproject.toml</> file in the current directory."
    )

    options = [
        option("name", None, "Name of the package.", flag=False),
        option("description", None, "Description of the package.", flag=False),
        option("author", None, "Author name of the package.", flag=False),
        option("python", None, "Compatible Python versions.", flag=False),
        option(
            "dependency",
            None,
            "Package to require, with an optional version constraint, "
            "e.g. requests:^2.10.0 or requests=2.11.1.",
            flag=False,
            multiple=True,
        ),
        option(
            "dev-dependency",
            None,
            "Package to require for development, with an optional version constraint, "
            "e.g. requests:^2.10.0 or requests=2.11.1.",
            flag=False,
            multiple=True,
        ),
        option("license", "l", "License of the package.", flag=False),
    ]

    help = """\
The <c1>init</c1> command creates a basic <comment>pyproject.toml</> file in the current directory.
"""

    def __init__(self) -> None:
        super(InitCommand, self).__init__()

        self._pool = None

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

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

        pyproject = PyProjectTOML(Path.cwd() / "pyproject.toml")

        if pyproject.file.exists():
            if pyproject.is_poetry_project():
                self.line(
                    "<error>A pyproject.toml file with a poetry section already exists.</error>"
                )
                return 1

            if pyproject.data.get("build-system"):
                self.line(
                    "<error>A pyproject.toml file with a defined build-system already exists.</error>"
                )
                return 1

        vcs_config = GitConfig()

        self.line("")
        self.line(
            "This command will guide you through creating your <info>pyproject.toml</> config."
        )
        self.line("")

        name = self.option("name")
        if not name:
            name = Path.cwd().name.lower()

            question = self.create_question(
                "Package name [<comment>{}</comment>]: ".format(name), default=name
            )
            name = self.ask(question)

        version = "0.1.0"
        question = self.create_question(
            "Version [<comment>{}</comment>]: ".format(version), default=version
        )
        version = self.ask(question)

        description = self.option("description") or ""
        question = self.create_question(
            "Description [<comment>{}</comment>]: ".format(description),
            default=description,
        )
        description = self.ask(question)

        author = self.option("author")
        if not author and vcs_config and vcs_config.get("user.name"):
            author = vcs_config["user.name"]
            author_email = vcs_config.get("user.email")
            if author_email:
                author += " <{}>".format(author_email)

        question = self.create_question(
            "Author [<comment>{}</comment>, n to skip]: ".format(author), default=author
        )
        question.set_validator(lambda v: self._validate_author(v, author))
        author = self.ask(question)

        if not author:
            authors = []
        else:
            authors = [author]

        license = self.option("license") or ""

        question = self.create_question(
            "License [<comment>{}</comment>]: ".format(license), default=license
        )
        question.set_validator(self._validate_license)
        license = self.ask(question)

        python = self.option("python")
        if not python:
            current_env = SystemEnv(Path(sys.executable))
            default_python = "^{}".format(
                ".".join(str(v) for v in current_env.version_info[:2])
            )
            question = self.create_question(
                "Compatible Python versions [<comment>{}</comment>]: ".format(
                    default_python
                ),
                default=default_python,
            )
            python = self.ask(question)

        self.line("")

        requirements = {}
        if self.option("dependency"):
            requirements = self._format_requirements(
                self._determine_requirements(self.option("dependency"))
            )

        question = "Would you like to define your main dependencies interactively?"
        help_message = (
            "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 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"
        )
        help_displayed = False
        if self.confirm(question, True):
            self.line(help_message)
            help_displayed = True
            requirements.update(
                self._format_requirements(self._determine_requirements([]))
            )
            self.line("")

        dev_requirements = {}
        if self.option("dev-dependency"):
            dev_requirements = self._format_requirements(
                self._determine_requirements(self.option("dev-dependency"))
            )

        question = (
            "Would you like to define your development dependencies interactively?"
        )
        if self.confirm(question, True):
            if not help_displayed:
                self.line(help_message)

            dev_requirements.update(
                self._format_requirements(self._determine_requirements([]))
            )
            self.line("")

        layout_ = layout("standard")(
            name,
            version,
            description=description,
            author=authors[0] if authors else None,
            license=license,
            python=python,
            dependencies=requirements,
            dev_dependencies=dev_requirements,
        )

        content = layout_.generate_poetry_content(original=pyproject)
        if self.io.is_interactive():
            self.line("<info>Generated file</info>")
            self.line("")
            self.line(content)
            self.line("")

        if not self.confirm("Do you confirm generation?", True):
            self.line("<error>Command aborted</error>")

            return 1

        with (Path.cwd() / "pyproject.toml").open("w", encoding="utf-8") as f:
            f.write(content)

    def _determine_requirements(
        self,
        requires: List[str],
        allow_prereleases: bool = False,
        source: Optional[str] = None,
    ) -> List[Dict[str, Union[str, List[str]]]]:
        if not requires:
            requires = []

            package = self.ask(
                "Search for package to add (or leave blank to continue):"
            )
            while package is not None:
                constraint = self._parse_requirements([package])[0]
                if (
                    "git" in constraint
                    or "url" in constraint
                    or "path" in constraint
                    or "version" in constraint
                ):
                    self.line("Adding <info>{}</info>".format(package))
                    requires.append(constraint)
                    package = self.ask("\nAdd a package:")
                    continue

                matches = self._get_pool().search(constraint["name"])

                if not matches:
                    self.line("<error>Unable to find package</error>")
                    package = False
                else:
                    choices = []
                    matches_names = [p.name for p in matches]
                    exact_match = constraint["name"] in matches_names
                    if exact_match:
                        choices.append(
                            matches[matches_names.index(constraint["name"])].pretty_name
                        )

                    for found_package in matches:
                        if len(choices) >= 10:
                            break

                        if found_package.name.lower() == constraint["name"].lower():
                            continue

                        choices.append(found_package.pretty_name)

                    self.line(
                        "Found <info>{}</info> packages matching <c1>{}</c1>".format(
                            len(matches), package
                        )
                    )

                    package = self.choice(
                        "\nEnter package # to add, or the complete package name if it is not listed",
                        choices,
                        attempts=3,
                    )

                    # package selected by user, set constraint name to package name
                    if package is not False:
                        constraint["name"] = package

                # no constraint yet, determine the best version automatically
                if package is not False and "version" not in constraint:
                    question = self.create_question(
                        "Enter the version constraint to require "
                        "(or leave blank to use the latest version):"
                    )
                    question.attempts = 3
                    question.validator = lambda x: (x or "").strip() or False

                    package_constraint = self.ask(question)

                    if package_constraint is None:
                        _, package_constraint = self._find_best_version_for_package(
                            package
                        )

                        self.line(
                            "Using version <b>{}</b> for <c1>{}</c1>".format(
                                package_constraint, package
                            )
                        )

                    constraint["version"] = package_constraint

                if package is not False:
                    requires.append(constraint)

                package = self.ask("\nAdd a package:")

            return requires

        requires = self._parse_requirements(requires)
        result = []
        for requirement in requires:
            if "git" in requirement or "url" in requirement or "path" in requirement:
                result.append(requirement)
                continue
            elif "version" not in requirement:
                # determine the best version automatically
                name, version = self._find_best_version_for_package(
                    requirement["name"],
                    allow_prereleases=allow_prereleases,
                    source=source,
                )
                requirement["version"] = version
                requirement["name"] = name

                self.line(
                    "Using version <b>{}</b> for <c1>{}</c1>".format(version, name)
                )
            else:
                # check that the specified version/constraint exists
                # before we proceed
                name, _ = self._find_best_version_for_package(
                    requirement["name"],
                    requirement["version"],
                    allow_prereleases=allow_prereleases,
                    source=source,
                )

                requirement["name"] = name

            result.append(requirement)

        return result

    def _find_best_version_for_package(
        self,
        name: str,
        required_version: Optional[str] = None,
        allow_prereleases: bool = False,
        source: Optional[str] = None,
    ) -> Tuple[str, str]:
        from poetry.version.version_selector import VersionSelector

        selector = VersionSelector(self._get_pool())
        package = selector.find_best_candidate(
            name, required_version, allow_prereleases=allow_prereleases, source=source
        )

        if not package:
            # TODO: find similar
            raise ValueError(
                "Could not find a matching version of package {}".format(name)
            )

        return package.pretty_name, selector.find_recommended_require_version(package)

    def _parse_requirements(self, requirements: List[str]) -> List[Dict[str, str]]:
        from poetry.puzzle.provider import Provider

        result = []

        try:
            cwd = self.poetry.file.parent
        except (PyProjectException, RuntimeError):
            cwd = Path.cwd()

        for requirement in requirements:
            requirement = requirement.strip()
            extras = []
            extras_m = re.search(r"\[([\w\d,-_ ]+)\]$", requirement)
            if extras_m:
                extras = [e.strip() for e in extras_m.group(1).split(",")]
                requirement, _ = requirement.split("[")

            url_parsed = urllib.parse.urlparse(requirement)
            if url_parsed.scheme and url_parsed.netloc:
                # Url
                if url_parsed.scheme in ["git+https", "git+ssh"]:
                    from poetry.core.vcs.git import Git
                    from poetry.core.vcs.git import ParsedUrl

                    parsed = ParsedUrl.parse(requirement)
                    url = Git.normalize_url(requirement)

                    pair = dict([("name", parsed.name), ("git", url.url)])
                    if parsed.rev:
                        pair["rev"] = url.revision

                    if extras:
                        pair["extras"] = extras

                    package = Provider.get_package_from_vcs(
                        "git", url.url, rev=pair.get("rev")
                    )
                    pair["name"] = package.name
                    result.append(pair)

                    continue
                elif url_parsed.scheme in ["http", "https"]:
                    package = Provider.get_package_from_url(requirement)

                    pair = dict([("name", package.name), ("url", package.source_url)])
                    if extras:
                        pair["extras"] = extras

                    result.append(pair)
                    continue
            elif (os.path.sep in requirement or "/" in requirement) and cwd.joinpath(
                requirement
            ).exists():
                path = cwd.joinpath(requirement)
                if path.is_file():
                    package = Provider.get_package_from_file(path.resolve())
                else:
                    package = Provider.get_package_from_directory(path)

                result.append(
                    dict(
                        [
                            ("name", package.name),
                            ("path", path.relative_to(cwd).as_posix()),
                        ]
                        + ([("extras", extras)] if extras else [])
                    )
                )

                continue

            pair = re.sub(
                "^([^@=: ]+)(?:@|==|(?<![<>~!])=|:| )(.*)$", "\\1 \\2", requirement
            )
            pair = pair.strip()

            require = dict()
            if " " in pair:
                name, version = pair.split(" ", 2)
                extras_m = re.search(r"\[([\w\d,-_]+)\]$", name)
                if extras_m:
                    extras = [e.strip() for e in extras_m.group(1).split(",")]
                    name, _ = name.split("[")

                require["name"] = name
                if version != "latest":
                    require["version"] = version
            else:
                m = re.match(
                    r"^([^><=!: ]+)((?:>=|<=|>|<|!=|~=|~|\^).*)$", requirement.strip()
                )
                if m:
                    name, constraint = m.group(1), m.group(2)
                    extras_m = re.search(r"\[([\w\d,-_]+)\]$", name)
                    if extras_m:
                        extras = [e.strip() for e in extras_m.group(1).split(",")]
                        name, _ = name.split("[")

                    require["name"] = name
                    require["version"] = constraint
                else:
                    extras_m = re.search(r"\[([\w\d,-_]+)\]$", pair)
                    if extras_m:
                        extras = [e.strip() for e in extras_m.group(1).split(",")]
                        pair, _ = pair.split("[")

                    require["name"] = pair

            if extras:
                require["extras"] = extras

            result.append(require)

        return result

    def _format_requirements(
        self, requirements: List[Dict[str, str]]
    ) -> Dict[str, Union[str, Dict[str, str]]]:
        requires = {}
        for requirement in requirements:
            name = requirement.pop("name")
            if "version" in requirement and len(requirement) == 1:
                constraint = requirement["version"]
            else:
                constraint = inline_table()
                constraint.trivia.trail = "\n"
                constraint.update(requirement)

            requires[name] = constraint

        return requires

    def _validate_author(self, author: str, default: str) -> Optional[str]:
        from poetry.core.packages.package import AUTHOR_REGEX

        author = author or default

        if author in ["n", "no"]:
            return

        m = AUTHOR_REGEX.match(author)
        if not m:
            raise ValueError(
                "Invalid author string. Must be in the format: "
                "John Smith <*****@*****.**>"
            )

        return author

    def _validate_license(self, license: str) -> str:
        from poetry.core.spdx import license_by_id

        if license:
            license_by_id(license)

        return license

    def _get_pool(self) -> "Pool":
        from poetry.repositories import Pool
        from poetry.repositories.pypi_repository import PyPiRepository

        if isinstance(self, EnvCommand):
            return self.poetry.pool

        if self._pool is None:
            self._pool = Pool()
            self._pool.add_repository(PyPiRepository())

        return self._pool
Exemple #29
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("dev", "D", "Add as a development dependency."),
        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 poetry.core.semver import parse_constraint

        packages = self.argument("name")
        is_dev = self.option("dev")

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

        section = "dependencies"
        if is_dev:
            section = "dev-dependencies"

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

        if section not in poetry_content:
            poetry_content[section] = {}

        existing_packages = self.get_existing_packages_from_input(
            packages, poetry_content, 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("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"]

            poetry_content[section][_constraint["name"]] = constraint

        try:
            # Write new content
            self.poetry.file.write(content)

            # Cosmetic new line
            self.line("")

            # Update packages
            self.reset_poetry()

            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()
        except BaseException:
            # Using BaseException here as some exceptions, eg: KeyboardInterrupt, do not inherit from Exception
            self.poetry.file.write(original_content)
            raise

        if status != 0 or self.option("dry-run"):
            # Revert changes
            if not self.option("dry-run"):
                self.line_error(
                    "\n"
                    "<error>Failed to add packages, reverting the pyproject.toml file "
                    "to its original content.</error>")

            self.poetry.file.write(original_content)

        return status

    def get_existing_packages_from_input(self, packages: List[str],
                                         poetry_content: Dict,
                                         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 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"
        )
Exemple #30
0
class InstallCommand(InstallerCommand):

    name = "install"
    description = "Installs the project dependencies."

    options = [
        option(
            "without",
            None,
            "The dependency groups to ignore for installation.",
            flag=False,
            multiple=True,
        ),
        option(
            "with",
            None,
            "The optional dependency groups to include for installation.",
            flag=False,
            multiple=True,
        ),
        option("default", None, "Only install the default dependencies."),
        option(
            "only",
            None,
            "The only dependency groups to install.",
            flag=False,
            multiple=True,
        ),
        option(
            "no-dev",
            None,
            "Do not install the development dependencies."
            " (<warning>Deprecated</warning>)",
        ),
        option(
            "dev-only",
            None,
            "Only install the development dependencies."
            " (<warning>Deprecated</warning>)",
        ),
        option(
            "sync",
            None,
            "Synchronize the environment with the locked packages and the specified"
            " groups.",
        ),
        option("no-root", None,
               "Do not install the root package (the current project)."),
        option(
            "dry-run",
            None,
            "Output the operations but do not execute anything "
            "(implicitly enables --verbose).",
        ),
        option(
            "remove-untracked",
            None,
            "Removes packages not present in the lock file.",
        ),
        option(
            "extras",
            "E",
            "Extra sets of dependencies to install.",
            flag=False,
            multiple=True,
        ),
    ]

    help = """The <info>install</info> command reads the <comment>poetry.lock</> file from
the current directory, processes it, and downloads and installs all the
libraries and dependencies outlined in that file. If the file does not
exist it will look for <comment>pyproject.toml</> and do the same.

<info>poetry install</info>

By default, the above command will also install the current project. To install only the
dependencies and not including the current project, run the command with the
<info>--no-root</info> option like below:

<info> poetry install --no-root</info>
"""

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

    def handle(self) -> int:
        from poetry.core.masonry.utils.module import ModuleOrPackageNotFound

        from poetry.masonry.builders import EditableBuilder

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

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

        self._installer.extras(extras)

        excluded_groups = []
        included_groups = []
        only_groups = []
        if self.option("no-dev"):
            self.line_error(
                "<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")
        elif self.option("dev-only"):
            self.line_error(
                "<warning>The `<fg=yellow;options=bold>--dev-only</>` option is"
                " deprecated, use the `<fg=yellow;options=bold>--only dev</>` notation"
                " instead.</warning>")
            only_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")

        with_synchronization = self.option("sync")
        if self.option("remove-untracked"):
            self.line_error(
                "<warning>The `<fg=yellow;options=bold>--remove-untracked</>` option is"
                " deprecated, use the `<fg=yellow;options=bold>--sync</>` option"
                " instead.</warning>")

            with_synchronization = True

        self._installer.only_groups(only_groups)
        self._installer.without_groups(excluded_groups)
        self._installer.with_groups(included_groups)
        self._installer.dry_run(self.option("dry-run"))
        self._installer.requires_synchronization(with_synchronization)
        self._installer.verbose(self._io.is_verbose())

        return_code = self._installer.run()

        if return_code != 0:
            return return_code

        if self.option("no-root") or self.option("only"):
            return 0

        try:
            builder = EditableBuilder(self.poetry, self._env, self._io)
        except ModuleOrPackageNotFound:
            # This is likely due to the fact that the project is an application
            # not following the structure expected by Poetry
            # If this is a true error it will be picked up later by build anyway.
            return 0

        log_install = ("<b>Installing</> the current project:"
                       f" <c1>{self.poetry.package.pretty_name}</c1>"
                       f" (<{{tag}}>{self.poetry.package.pretty_version}</>)")
        overwrite = self._io.output.is_decorated() and not self.io.is_debug()
        self.line("")
        self.write(log_install.format(tag="c2"))
        if not overwrite:
            self.line("")

        if self.option("dry-run"):
            self.line("")
            return 0

        builder.build()

        if overwrite:
            self.overwrite(log_install.format(tag="success"))
            self.line("")

        return 0