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, ), ]
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()
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()
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
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)
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()
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
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
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
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}</>", ] ) )
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), ]))
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, ) )
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
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
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")
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
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
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
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"), )
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()}</>")
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
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
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
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")
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
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"), )
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
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" )
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