class VersionCommand(Command): name = "version" description = ("Shows the version of the project or bumps it when a valid " "bump rule is provided.") arguments = [ argument( "version", "The version number or the rule to update the version.", optional=True, ) ] options = [option("short", "s", "Output the version number only")] help = """\ The version command shows the current version of the project or bumps the version of the project and writes the new version back to <comment>pyproject.toml</> if a valid bump rule is provided. The new version should ideally be a valid semver string or a valid bump rule: patch, minor, major, prepatch, preminor, premajor, prerelease. """ RESERVED = { "major", "minor", "patch", "premajor", "preminor", "prepatch", "prerelease", } def handle(self) -> None: version = self.argument("version") if version: version = self.increment_version( self.poetry.package.pretty_version, version) if self.option("short"): self.line(version.to_string()) else: self.line( f"Bumping version from <b>{self.poetry.package.pretty_version}</>" f" to <fg=green>{version}</>") content = self.poetry.file.read() poetry_content = content["tool"]["poetry"] poetry_content["version"] = version.text self.poetry.file.write(content) else: if self.option("short"): self.line(self.poetry.package.pretty_version) else: self.line(f"<comment>{self.poetry.package.name}</>" f" <info>{self.poetry.package.pretty_version}</>") def increment_version(self, version: str, rule: str) -> Version: from poetry.core.semver.version import Version try: parsed = Version.parse(version) except ValueError: raise ValueError( "The project's version doesn't seem to follow semver") if rule in {"major", "premajor"}: new = parsed.next_major() if rule == "premajor": new = new.first_prerelease() elif rule in {"minor", "preminor"}: new = parsed.next_minor() if rule == "preminor": new = new.first_prerelease() elif rule in {"patch", "prepatch"}: new = parsed.next_patch() if rule == "prepatch": new = new.first_prerelease() elif rule == "prerelease": if parsed.is_unstable(): new = Version(parsed.epoch, parsed.release, parsed.pre.next()) else: new = parsed.next_patch().first_prerelease() else: new = Version.parse(rule) return new
class SelfUpdateCommand(Command): name = "self update" description = "Updates Poetry to the latest version." arguments = [argument("version", "The version to update to.", optional=True)] options = [option("preview", None, "Install prereleases.")] REPOSITORY_URL = "https://github.com/python-poetry/poetry" BASE_URL = REPOSITORY_URL + "/releases/download" @property def home(self) -> Path: from pathlib import Path return Path(os.environ.get("POETRY_HOME", "~/.poetry")).expanduser() @property def bin(self) -> Path: return self.home / "bin" @property def lib(self) -> Path: return self.home / "lib" @property def lib_backup(self) -> Path: return self.home / "lib-backup" def handle(self) -> None: from poetry.__version__ import __version__ from poetry.core.packages.dependency import Dependency from poetry.core.semver.version import Version from poetry.repositories.pypi_repository import PyPiRepository self._check_recommended_installation() version = self.argument("version") if not version: version = ">=" + __version__ repo = PyPiRepository(fallback=False) packages = repo.find_packages( Dependency("poetry", version, allows_prereleases=self.option("preview")) ) if not packages: self.line("No release found for the specified version") return packages.sort( key=cmp_to_key( lambda x, y: 0 if x.version == y.version else int(x.version < y.version or -1) ) ) release = None for package in packages: if package.is_prerelease(): if self.option("preview"): release = package break continue release = package break if release is None: self.line("No new release found") return if release.version == Version.parse(__version__): self.line("You are using the latest version") return self.update(release) def update(self, release: "Package") -> None: version = release.version self.line("Updating to <info>{}</info>".format(version)) if self.lib_backup.exists(): shutil.rmtree(str(self.lib_backup)) # Backup the current installation if self.lib.exists(): shutil.copytree(str(self.lib), str(self.lib_backup)) shutil.rmtree(str(self.lib)) try: self._update(version) except Exception: if not self.lib_backup.exists(): raise shutil.copytree(str(self.lib_backup), str(self.lib)) shutil.rmtree(str(self.lib_backup)) raise finally: if self.lib_backup.exists(): shutil.rmtree(str(self.lib_backup)) self.make_bin() self.line("") self.line("") self.line( "<info>Poetry</info> (<comment>{}</comment>) is installed now. Great!".format( version ) ) def _update(self, version: "Version") -> None: from poetry.utils.helpers import temporary_directory release_name = self._get_release_name(version) checksum = "{}.sha256sum".format(release_name) base_url = self.BASE_URL try: r = urlopen(base_url + "/{}/{}".format(version, checksum)) except HTTPError as e: if e.code == 404: raise RuntimeError("Could not find {} file".format(checksum)) raise checksum = r.read().decode().strip() # We get the payload from the remote host name = "{}.tar.gz".format(release_name) try: r = urlopen(base_url + "/{}/{}".format(version, name)) except HTTPError as e: if e.code == 404: raise RuntimeError("Could not find {} file".format(name)) raise meta = r.info() size = int(meta["Content-Length"]) current = 0 block_size = 8192 bar = self.progress_bar(max=size) bar.set_format(" - Downloading <info>{}</> <comment>%percent%%</>".format(name)) bar.start() sha = hashlib.sha256() with temporary_directory(prefix="poetry-updater-") as dir_: tar = os.path.join(dir_, name) with open(tar, "wb") as f: while True: buffer = r.read(block_size) if not buffer: break current += len(buffer) f.write(buffer) sha.update(buffer) bar.set_progress(current) bar.finish() # Checking hashes if checksum != sha.hexdigest(): raise RuntimeError( "Hashes for {} do not match: {} != {}".format( name, checksum, sha.hexdigest() ) ) gz = GzipFile(tar, mode="rb") try: with tarfile.TarFile(tar, fileobj=gz, format=tarfile.PAX_FORMAT) as f: f.extractall(str(self.lib)) finally: gz.close() def process(self, *args: Any) -> str: return subprocess.check_output(list(args), stderr=subprocess.STDOUT) def _check_recommended_installation(self) -> None: from pathlib import Path from poetry.console.exceptions import PoetrySimpleConsoleException current = Path(__file__) try: current.relative_to(self.home) except ValueError: raise PoetrySimpleConsoleException( "Poetry was not installed with the recommended installer, " "so it cannot be updated automatically." ) def _get_release_name(self, version: "Version") -> str: platform = sys.platform if platform == "linux2": platform = "linux" return "poetry-{}-{}".format(version, platform) def make_bin(self) -> None: from poetry.utils._compat import WINDOWS self.bin.mkdir(0o755, parents=True, exist_ok=True) python_executable = self._which_python() if WINDOWS: with self.bin.joinpath("poetry.bat").open("w", newline="") as f: f.write( BAT.format( python_executable=python_executable, poetry_bin=str(self.bin / "poetry").replace( os.environ["USERPROFILE"], "%USERPROFILE%" ), ) ) bin_content = BIN if not WINDOWS: bin_content = "#!/usr/bin/env {}\n".format(python_executable) + bin_content self.bin.joinpath("poetry").write_text(bin_content, encoding="utf-8") if not WINDOWS: # Making the file executable st = os.stat(str(self.bin.joinpath("poetry"))) os.chmod(str(self.bin.joinpath("poetry")), st.st_mode | stat.S_IEXEC) def _which_python(self) -> str: """ Decides which python executable we'll embed in the launcher script. """ from poetry.utils._compat import WINDOWS allowed_executables = ["python", "python3"] if WINDOWS: allowed_executables += ["py.exe -3", "py.exe -2"] # \d in regex ensures we can convert to int later version_matcher = re.compile(r"^Python (?P<major>\d+)\.(?P<minor>\d+)\..+$") fallback = None for executable in allowed_executables: try: raw_version = subprocess.check_output( executable + " --version", stderr=subprocess.STDOUT, shell=True ).decode("utf-8") except subprocess.CalledProcessError: continue match = version_matcher.match(raw_version.strip()) if match and tuple(map(int, match.groups())) >= (3, 0): # favor the first py3 executable we can find. return executable if fallback is None: # keep this one as the fallback; it was the first valid executable we found. fallback = executable if fallback is None: # Avoid breaking existing scripts fallback = "python" return fallback
class ShowCommand(EnvCommand): name = "show" description = "Shows information about packages." arguments = [argument("package", "The package to inspect", optional=True)] options = [ option( "without", None, "Do not show the information of the specified groups' dependencies.", flag=False, multiple=True, ), option( "with", None, "Show the information of the specified optional groups' dependencies as well.", flag=False, multiple=True, ), option("default", None, "Only show the information of the default dependencies."), option( "only", None, "Only show the information of dependencies belonging to the specified groups.", flag=False, multiple=True, ), option( "no-dev", None, "Do not list the development dependencies. (<warning>Deprecated</warning>)", ), option("tree", "t", "List the dependencies as a tree."), option("latest", "l", "Show the latest version."), option( "outdated", "o", "Show the latest version but only for packages that are outdated.", ), option( "all", "a", "Show all packages (even those not compatible with current system).", ), ] help = """The show command displays detailed information about a package, or lists all packages available.""" colors = ["cyan", "yellow", "green", "magenta", "blue"] def handle(self) -> Optional[int]: from cleo.io.null_io import NullIO from cleo.terminal import Terminal from poetry.puzzle.solver import Solver from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.pool import Pool from poetry.repositories.repository import Repository from poetry.utils.helpers import get_package_version_display_string package = self.argument("package") if self.option("tree"): self.init_styles(self.io) if self.option("outdated"): self._io.input.set_option("latest", True) excluded_groups = [] included_groups = [] only_groups = [] if self.option("no-dev"): self.line( "<warning>The `<fg=yellow;options=bold>--no-dev</>` option is deprecated, " "use the `<fg=yellow;options=bold>--without dev</>` notation instead.</warning>" ) excluded_groups.append("dev") excluded_groups.extend([ group.strip() for groups in self.option("without") for group in groups.split(",") ]) included_groups.extend([ group.strip() for groups in self.option("with") for group in groups.split(",") ]) only_groups.extend([ group.strip() for groups in self.option("only") for group in groups.split(",") ]) if self.option("default"): only_groups.append("default") locked_repo = self.poetry.locker.locked_repository(True) if only_groups: root = self.poetry.package.with_dependency_groups(only_groups, only=True) else: root = self.poetry.package.with_dependency_groups( included_groups).without_dependency_groups(excluded_groups) # Show tree view if requested if self.option("tree") and not package: requires = root.all_requires packages = locked_repo.packages for pkg in packages: for require in requires: if pkg.name == require.name: self.display_package_tree(self._io, pkg, locked_repo) break return 0 table = self.table(style="compact") locked_packages = locked_repo.packages pool = Pool(ignore_repository_names=True) pool.add_repository(locked_repo) solver = Solver( root, pool=pool, installed=Repository(), locked=locked_repo, io=NullIO(), ) solver.provider.load_deferred(False) with solver.use_environment(self.env): ops = solver.solve().calculate_operations() required_locked_packages = {op.package for op in ops if not op.skipped} if package: pkg = None for locked in locked_packages: if package.lower() == locked.name: pkg = locked break if not pkg: raise ValueError(f"Package {package} not found") if self.option("tree"): self.display_package_tree(self.io, pkg, locked_repo) return 0 required_by = {} for locked in locked_packages: dependencies = { d.name: d.pretty_constraint for d in locked.requires } if pkg.name in dependencies: required_by[locked.pretty_name] = dependencies[pkg.name] rows = [ ["<info>name</>", f" : <c1>{pkg.pretty_name}</>"], ["<info>version</>", f" : <b>{pkg.pretty_version}</b>"], ["<info>description</>", f" : {pkg.description}"], ] table.add_rows(rows) table.render() if pkg.requires: self.line("") self.line("<info>dependencies</info>") for dependency in pkg.requires: self.line( f" - <c1>{dependency.pretty_name}</c1> <b>{dependency.pretty_constraint}</b>" ) if required_by: self.line("") self.line("<info>required by</info>") for parent, requires_version in required_by.items(): self.line( f" - <c1>{parent}</c1> <b>{requires_version}</b>") return 0 show_latest = self.option("latest") show_all = self.option("all") terminal = Terminal() width = terminal.width name_length = version_length = latest_length = 0 latest_packages = {} latest_statuses = {} installed_repo = InstalledRepository.load(self.env) # Computing widths for locked in locked_packages: if locked not in required_locked_packages and not show_all: continue current_length = len(locked.pretty_name) if not self._io.output.is_decorated(): installed_status = self.get_installed_status( locked, installed_repo) if installed_status == "not-installed": current_length += 4 if show_latest: latest = self.find_latest_package(locked, root) if not latest: latest = locked latest_packages[locked.pretty_name] = latest update_status = latest_statuses[ locked.pretty_name] = self.get_update_status( latest, locked) if not self.option( "outdated") or update_status != "up-to-date": name_length = max(name_length, current_length) version_length = max( version_length, len( get_package_version_display_string( locked, root=self.poetry.file.parent)), ) latest_length = max( latest_length, len( get_package_version_display_string( latest, root=self.poetry.file.parent)), ) else: name_length = max(name_length, current_length) version_length = max( version_length, len( get_package_version_display_string( locked, root=self.poetry.file.parent)), ) write_version = name_length + version_length + 3 <= width write_latest = name_length + version_length + latest_length + 3 <= width write_description = name_length + version_length + latest_length + 24 <= width for locked in locked_packages: color = "cyan" name = locked.pretty_name install_marker = "" if locked not in required_locked_packages: if not show_all: continue color = "black;options=bold" else: installed_status = self.get_installed_status( locked, installed_repo) if installed_status == "not-installed": color = "red" if not self._io.output.is_decorated(): # Non installed in non decorated mode install_marker = " (!)" if (show_latest and self.option("outdated") and latest_statuses[locked.pretty_name] == "up-to-date"): continue line = f"<fg={color}>{name:{name_length - len(install_marker)}}{install_marker}</>" if write_version: version = get_package_version_display_string( locked, root=self.poetry.file.parent) line += f" <b>{version:{version_length}}</b>" if show_latest: latest = latest_packages[locked.pretty_name] update_status = latest_statuses[locked.pretty_name] if write_latest: color = "green" if update_status == "semver-safe-update": color = "red" elif update_status == "update-possible": color = "yellow" version = get_package_version_display_string( latest, root=self.poetry.file.parent) line += f" <fg={color}>{version:{latest_length}}</>" if write_description: description = locked.description remaining = width - name_length - version_length - 4 if show_latest: remaining -= latest_length if len(locked.description) > remaining: description = description[:remaining - 3] + "..." line += " " + description self.line(line) return None def display_package_tree(self, io: "IO", package: "Package", installed_repo: "Repository") -> None: io.write(f"<c1>{package.pretty_name}</c1>") description = "" if package.description: description = " " + package.description io.write_line(f" <b>{package.pretty_version}</b>{description}") dependencies = package.requires dependencies = sorted(dependencies, key=lambda x: x.name) tree_bar = "├" total = len(dependencies) for i, dependency in enumerate(dependencies, 1): if i == total: tree_bar = "└" level = 1 color = self.colors[level] info = f"{tree_bar}── <{color}>{dependency.name}</{color}> {dependency.pretty_constraint}" self._write_tree_line(io, info) tree_bar = tree_bar.replace("└", " ") packages_in_tree = [package.name, dependency.name] self._display_tree(io, dependency, installed_repo, packages_in_tree, tree_bar, level + 1) def _display_tree( self, io: "IO", dependency: "Dependency", installed_repo: "Repository", packages_in_tree: List[str], previous_tree_bar: str = "├", level: int = 1, ) -> None: previous_tree_bar = previous_tree_bar.replace("├", "│") dependencies = [] for package in installed_repo.packages: if package.name == dependency.name: dependencies = package.requires break dependencies = sorted(dependencies, key=lambda x: x.name) tree_bar = previous_tree_bar + " ├" total = len(dependencies) for i, dependency in enumerate(dependencies, 1): current_tree = packages_in_tree if i == total: tree_bar = previous_tree_bar + " └" color_ident = level % len(self.colors) color = self.colors[color_ident] circular_warn = "" if dependency.name in current_tree: circular_warn = "(circular dependency aborted here)" info = f"{tree_bar}── <{color}>{dependency.name}</{color}> {dependency.pretty_constraint} {circular_warn}" self._write_tree_line(io, info) tree_bar = tree_bar.replace("└", " ") if dependency.name not in current_tree: current_tree.append(dependency.name) self._display_tree(io, dependency, installed_repo, current_tree, tree_bar, level + 1) def _write_tree_line(self, io: "IO", line: str) -> None: if not io.output.supports_utf8(): line = line.replace("└", "`-") line = line.replace("├", "|-") line = line.replace("──", "-") line = line.replace("│", "|") io.write_line(line) def init_styles(self, io: "IO") -> None: from cleo.formatters.style import Style for color in self.colors: style = Style(color) io.output.formatter.set_style(color, style) io.error_output.formatter.set_style(color, style) def find_latest_package(self, package: "Package", root: "ProjectPackage") -> Union["Package", bool]: from cleo.io.null_io import NullIO from poetry.puzzle.provider import Provider from poetry.version.version_selector import VersionSelector # find the latest version allowed in this pool if package.source_type in ("git", "file", "directory"): requires = root.all_requires for dep in requires: if dep.name == package.name: provider = Provider(root, self.poetry.pool, NullIO()) if dep.is_vcs(): return provider.search_for_vcs(dep)[0] if dep.is_file(): return provider.search_for_file(dep)[0] if dep.is_directory(): return provider.search_for_directory(dep)[0] name = package.name selector = VersionSelector(self.poetry.pool) return selector.find_best_candidate(name, f">={package.pretty_version}") def get_update_status(self, latest: "Package", package: "Package") -> str: from poetry.core.semver.helpers import parse_constraint if latest.full_pretty_version == package.full_pretty_version: return "up-to-date" constraint = parse_constraint("^" + package.pretty_version) if latest.version and constraint.allows(latest.version): # It needs an immediate semver-compliant upgrade return "semver-safe-update" # it needs an upgrade but has potential BC breaks so is not urgent return "update-possible" def get_installed_status(self, locked: "Package", installed_repo: "InstalledRepository") -> str: for package in installed_repo.packages: if locked.name == package.name: return "installed" return "not-installed"
class PluginAddCommand(InitCommand): name = "plugin add" description = "Adds new plugins." arguments = [ argument("plugins", "The names of the plugins to install.", multiple=True), ] options = [ option( "dry-run", None, "Output the operations but do not execute anything (implicitly enables" " --verbose).", ) ] help = """ The <c1>plugin add</c1> command installs Poetry plugins globally. It works similarly to the <c1>add</c1> command: If you do not specify a version constraint, poetry will choose a suitable one based on\ the available package versions. You can specify a package in the following forms: - A single name (<b>requests</b>) - A name and a constraint (<b>requests@^2.23.0</b>) - A git url (<b>git+https://github.com/python-poetry/poetry.git</b>) - A git url with a revision\ (<b>git+https://github.com/python-poetry/poetry.git#develop</b>) - A git SSH url (<b>git+ssh://github.com/python-poetry/poetry.git</b>) - A git SSH url with a revision\ (<b>git+ssh://github.com/python-poetry/poetry.git#develop</b>) - A file path (<b>../my-package/my-package.whl</b>) - A directory (<b>../my-package/</b>) - A url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)\ """ def handle(self) -> int: from pathlib import Path import tomlkit from cleo.io.inputs.string_input import StringInput from cleo.io.io import IO from poetry.core.pyproject.toml import PyProjectTOML from poetry.core.semver.helpers import parse_constraint from poetry.factory import Factory from poetry.packages.project_package import ProjectPackage from poetry.repositories.installed_repository import InstalledRepository from poetry.utils.env import EnvManager plugins = self.argument("plugins") # Plugins should be installed in the system env to be globally available system_env = EnvManager.get_system_env(naive=True) env_dir = Path(os.getenv("POETRY_HOME") or system_env.path) # We check for the plugins existence first. if env_dir.joinpath("pyproject.toml").exists(): pyproject: dict[str, Any] = tomlkit.loads( env_dir.joinpath("pyproject.toml").read_text(encoding="utf-8") ) poetry_content = pyproject["tool"]["poetry"] existing_packages = self.get_existing_packages_from_input( plugins, poetry_content, "dependencies" ) if existing_packages: self.notify_about_existing_packages(existing_packages) plugins = [plugin for plugin in plugins if plugin not in existing_packages] if not plugins: return 0 plugins = self._determine_requirements(plugins) # We retrieve the packages installed in the system environment. # We assume that this environment will be a self contained virtual environment # built by the official installer or by pipx. # If not, it might lead to side effects since other installed packages might not # be required by Poetry but still be taken into account when resolving # dependencies. installed_repository = InstalledRepository.load( system_env, with_dependencies=True ) root_package = None for package in installed_repository.packages: if package.name == "poetry": root_package = ProjectPackage(package.name, package.version) for dependency in package.requires: root_package.add_dependency(dependency) break assert root_package is not None root_package.python_versions = ".".join( str(v) for v in system_env.version_info[:3] ) # We create a `pyproject.toml` file based on all the information # we have about the current environment. if not env_dir.joinpath("pyproject.toml").exists(): Factory.create_pyproject_from_package( root_package, env_dir, ) # We add the plugins to the dependencies section of the previously # created `pyproject.toml` file pyproject_toml = PyProjectTOML(env_dir.joinpath("pyproject.toml")) poetry_content = pyproject_toml.poetry_config poetry_dependency_section = poetry_content["dependencies"] plugin_names = [] for plugin in plugins: if "version" in plugin: # Validate version constraint parse_constraint(plugin["version"]) constraint: dict[str, Any] = tomlkit.inline_table() for name, value in plugin.items(): if name == "name": continue constraint[name] = value if len(constraint) == 1 and "version" in constraint: constraint = constraint["version"] poetry_dependency_section[plugin["name"]] = constraint plugin_names.append(plugin["name"]) pyproject_toml.save() # From this point forward, all the logic will be deferred to # the update command, by using the previously created `pyproject.toml` # file. application = cast(Application, self.application) update_command: UpdateCommand = cast(UpdateCommand, application.find("update")) # We won't go through the event dispatching done by the application # so we need to configure the command manually update_command.set_poetry(Factory().create_poetry(env_dir)) update_command.set_env(system_env) application._configure_installer(update_command, self._io) argv = ["update"] + plugin_names if self.option("dry-run"): argv.append("--dry-run") exit_code: int = update_command.run( IO( StringInput(" ".join(argv)), self._io.output, self._io.error_output, ) ) return exit_code def get_existing_packages_from_input( self, packages: list[str], poetry_content: dict[str, Any], target_section: str ) -> list[str]: existing_packages = [] for name in packages: for key in poetry_content[target_section]: if key.lower() == name.lower(): existing_packages.append(name) return existing_packages def notify_about_existing_packages(self, existing_packages: list[str]) -> None: self.line( "The following plugins are already present in the " "<c2>pyproject.toml</c2> file and will be skipped:\n" ) for name in existing_packages: self.line(f" • <c1>{name}</c1>") self.line( "\nIf you want to update it to the latest compatible version, " "you can use `<c2>poetry plugin update package</c2>`.\n" "If you prefer to upgrade it to the latest available version, " "you can use `<c2>poetry plugin add package@latest</c2>`.\n" )
class DebugResolveCommand(InitCommand): name = "debug resolve" description = "Debugs dependency resolution." arguments = [ argument("package", "The packages to resolve.", optional=True, multiple=True) ] options = [ option( "extras", "E", "Extras to activate for the dependency.", flag=False, multiple=True, ), option("python", None, "Python version(s) to use for resolution.", flag=False), option("tree", None, "Display the dependency tree."), option("install", None, "Show what would be installed for the current system."), ] loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"] def handle(self) -> int: from cleo.io.null_io import NullIO from poetry.core.packages.project_package import ProjectPackage from poetry.factory import Factory from poetry.puzzle import Solver from poetry.repositories.pool import Pool from poetry.repositories.repository import Repository from poetry.utils.env import EnvManager packages = self.argument("package") if not packages: package = self.poetry.package else: # Using current pool for determine_requirements() self._pool = self.poetry.pool package = ProjectPackage(self.poetry.package.name, self.poetry.package.version) # Silencing output verbosity = self.io.output.verbosity self.io.output.set_verbosity(Verbosity.QUIET) requirements = self._determine_requirements(packages) self.io.output.set_verbosity(verbosity) for constraint in requirements: name = constraint.pop("name") extras = [] for extra in self.option("extras"): if " " in extra: extras += [e.strip() for e in extra.split(" ")] else: extras.append(extra) constraint["extras"] = extras package.add_dependency( Factory.create_dependency(name, constraint)) package.python_versions = self.option("python") or ( self.poetry.package.python_versions) pool = self.poetry.pool solver = Solver(package, pool, Repository(), Repository(), self._io) ops = solver.solve().calculate_operations() self.line("") self.line("Resolution results:") self.line("") if self.option("tree"): show_command: "ShowCommand" = self.application.find("show") show_command.init_styles(self.io) packages = [op.package for op in ops] repo = Repository(packages) requires = package.all_requires for pkg in repo.packages: for require in requires: if pkg.name == require.name: show_command.display_package_tree(self.io, pkg, repo) break return 0 table = self.table(style="compact") table.style.set_vertical_border_chars("", " ") rows = [] if self.option("install"): env = EnvManager(self.poetry).get() pool = Pool() locked_repository = Repository() for op in ops: locked_repository.add_package(op.package) pool.add_repository(locked_repository) solver = Solver(package, pool, Repository(), Repository(), NullIO()) with solver.use_environment(env): ops = solver.solve().calculate_operations() for op in ops: if self.option("install") and op.skipped: continue pkg = op.package row = [ f"<c1>{pkg.complete_name}</c1>", f"<b>{pkg.version}</b>", ] if not pkg.marker.is_any(): row[2] = str(pkg.marker) rows.append(row) table.set_rows(rows) table.render() return 0
class RemoveCommand(InstallerCommand): name = "remove" description = "Removes a package from the project dependencies." arguments = [ argument("packages", "The packages to remove.", multiple=True) ] options = [ option("dev", "D", "Remove a package from the development dependencies."), option( "dry-run", None, "Output the operations but do not execute anything " "(implicitly enables --verbose).", ), ] help = """The <info>remove</info> command removes a package from the current list of installed packages <info>poetry remove</info>""" loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"] def handle(self) -> int: packages = self.argument("packages") is_dev = self.option("dev") content = self.poetry.file.read() poetry_content = content["tool"]["poetry"] section = "dependencies" if is_dev: section = "dev-dependencies" # Deleting entries requirements = {} for name in packages: found = False for key in poetry_content[section]: if key.lower() == name.lower(): found = True requirements[key] = poetry_content[section][key] break if not found: raise ValueError("Package {} not found".format(name)) for key in requirements: del poetry_content[section][key] dependencies = (self.poetry.package.requires if section == "dependencies" else self.poetry.package.dev_requires) for i, dependency in enumerate(reversed(dependencies)): if dependency.name == canonicalize_name(key): del dependencies[-i] # Update packages self._installer.use_executor( self.poetry.config.get("experimental.new-installer", False)) self._installer.dry_run(self.option("dry-run")) self._installer.verbose(self._io.is_verbose()) self._installer.update(True) self._installer.whitelist(requirements) status = self._installer.run() if not self.option("dry-run") and status == 0: self.poetry.file.write(content) return status
class NewCommand(Command): name = "new" description = "Creates a new Python project at <path>." arguments = [argument("path", "The path to create the project at.")] options = [ option("name", None, "Set the resulting package name.", flag=False), option("src", None, "Use the src layout for the project."), option( "readme", None, "Specify the readme file format. One of md (default) or rst", flag=False, ), ] def handle(self) -> None: from pathlib import Path from poetry.core.vcs.git import GitConfig from poetry.layouts import layout from poetry.utils.env import SystemEnv if self.option("src"): layout_ = layout("src") else: layout_ = layout("standard") path = Path(self.argument("path")) if not path.is_absolute(): # we do not use resolve here due to compatibility issues # for path.resolve(strict=False) path = Path.cwd().joinpath(path) name = self.option("name") if not name: name = path.name if path.exists() and list(path.glob("*")): # Directory is not empty. Aborting. raise RuntimeError( f"Destination <fg=yellow>{path}</> exists and is not empty") readme_format = self.option("readme") or "md" config = GitConfig() author = None if config.get("user.name"): author = config["user.name"] author_email = config.get("user.email") if author_email: author += f" <{author_email}>" current_env = SystemEnv(Path(sys.executable)) default_python = "^" + ".".join( str(v) for v in current_env.version_info[:2]) layout_ = layout_( name, "0.1.0", author=author, readme_format=readme_format, python=default_python, ) layout_.create(path) path = path.resolve() with suppress(ValueError): path = path.relative_to(Path.cwd()) self.line(f"Created package <info>{layout_._package_name}</> in" f" <fg=blue>{path.as_posix()}</>")
class SelfUpdateCommand(Command): name = "self update" description = "Updates Poetry to the latest version." arguments = [ argument("version", "The version to update to.", optional=True) ] options = [ option("preview", None, "Allow the installation of pre-release versions."), option( "dry-run", None, "Output the operations but do not execute anything " "(implicitly enables --verbose).", ), ] _data_dir = None _bin_dir = None _pool = None @property def data_dir(self) -> Path: if self._data_dir is not None: return self._data_dir from poetry.locations import data_dir self._data_dir = data_dir() return self._data_dir @property def bin_dir(self) -> Path: if self._data_dir is not None: return self._data_dir from poetry.utils._compat import WINDOWS if os.getenv("POETRY_HOME"): return Path(os.getenv("POETRY_HOME"), "bin").expanduser() user_base = site.getuserbase() if WINDOWS: bin_dir = os.path.join(user_base, "Scripts") else: bin_dir = os.path.join(user_base, "bin") self._bin_dir = Path(bin_dir) return self._bin_dir @property def pool(self) -> "Pool": if self._pool is not None: return self._pool from poetry.repositories.pool import Pool from poetry.repositories.pypi_repository import PyPiRepository pool = Pool() pool.add_repository(PyPiRepository()) return pool def handle(self) -> int: from poetry.core.packages.dependency import Dependency from poetry.core.semver.version import Version from poetry.__version__ import __version__ version = self.argument("version") if not version: version = ">=" + __version__ repo = self.pool.repositories[0] packages = repo.find_packages( Dependency("poetry", version, allows_prereleases=self.option("preview"))) if not packages: self.line("No release found for the specified version") return 1 packages.sort(key=cmp_to_key(lambda x, y: 0 if x.version == y.version else int(x.version < y.version or -1))) release = None for package in packages: if package.is_prerelease(): if self.option("preview"): release = package break continue release = package break if release is None: self.line("No new release found") return 1 if release.version == Version.parse(__version__): self.line("You are using the latest version") return 0 self.line(f"Updating <c1>Poetry</c1> to <c2>{release.version}</c2>") self.line("") self.update(release) self.line("") self.line( f"<c1>Poetry</c1> (<c2>{release.version}</c2>) is installed now. Great!" ) return 0 def update(self, release: "Package") -> None: from poetry.utils.env import EnvManager version = release.version env = EnvManager.get_system_env(naive=True) # We can't use is_relative_to() since it's only available in Python 3.9+ try: env.path.relative_to(self.data_dir) except ValueError: # Poetry was not installed using the recommended installer from poetry.console.exceptions import PoetrySimpleConsoleException raise PoetrySimpleConsoleException( "Poetry was not installed with the recommended installer, " "so it cannot be updated automatically.") self._update(version) self._make_bin() def _update(self, version: "Version") -> None: from poetry.core.packages.dependency import Dependency from poetry.core.packages.project_package import ProjectPackage from poetry.config.config import Config from poetry.installation.installer import Installer from poetry.packages.locker import NullLocker from poetry.repositories.installed_repository import InstalledRepository from poetry.utils.env import EnvManager env = EnvManager.get_system_env(naive=True) installed = InstalledRepository.load(env) root = ProjectPackage("poetry-updater", "0.0.0") root.python_versions = ".".join(str(c) for c in env.version_info[:3]) root.add_dependency(Dependency("poetry", version.text)) installer = Installer( self.io, env, root, NullLocker(self.data_dir.joinpath("poetry.lock"), {}), self.pool, Config(), installed=installed, ) installer.update(True) installer.dry_run(self.option("dry-run")) installer.run() def _make_bin(self) -> None: from poetry.utils._compat import WINDOWS self.line("") self.line("Updating the <c1>poetry</c1> script") self.bin_dir.mkdir(parents=True, exist_ok=True) script = "poetry" target_script = "venv/bin/poetry" if WINDOWS: script = "poetry.exe" target_script = "venv/Scripts/poetry.exe" if self.bin_dir.joinpath(script).exists(): self.bin_dir.joinpath(script).unlink() try: self.bin_dir.joinpath(script).symlink_to( self.data_dir.joinpath(target_script)) except OSError: # This can happen if the user # does not have the correct permission on Windows shutil.copy(self.data_dir.joinpath(target_script), self.bin_dir.joinpath(script))
class ConfigCommand(Command): name = "config" description = "Manages configuration settings." arguments = [ argument("key", "Setting key.", optional=True), argument("value", "Setting value.", optional=True, multiple=True), ] options = [ option("list", None, "List configuration settings."), option("unset", None, "Unset configuration setting."), option("local", None, "Set/Get from the project's local configuration."), ] help = """This command allows you to edit the poetry config settings and repositories. To add a repository: <comment>poetry config repositories.foo https://bar.com/simple/</comment> To remove a repository (repo is a short alias for repositories): <comment>poetry config --unset repo.foo</comment>""" LIST_PROHIBITED_SETTINGS = {"http-basic", "pypi-token"} @property def unique_config_values(self) -> Dict[str, Tuple[Any, Any, Any]]: from pathlib import Path from poetry.config.config import boolean_normalizer from poetry.config.config import boolean_validator from poetry.locations import CACHE_DIR unique_config_values = { "cache-dir": ( str, lambda val: str(Path(val)), str(Path(CACHE_DIR) / "virtualenvs"), ), "virtualenvs.create": (boolean_validator, boolean_normalizer, True), "virtualenvs.in-project": (boolean_validator, boolean_normalizer, False), "virtualenvs.options.always-copy": ( boolean_validator, boolean_normalizer, False, ), "virtualenvs.options.system-site-packages": ( boolean_validator, boolean_normalizer, False, ), "virtualenvs.path": ( str, lambda val: str(Path(val)), str(Path(CACHE_DIR) / "virtualenvs"), ), "experimental.new-installer": ( boolean_validator, boolean_normalizer, True, ), "installer.parallel": ( boolean_validator, boolean_normalizer, True, ), } return unique_config_values def handle(self) -> Optional[int]: from pathlib import Path from poetry.config.file_config_source import FileConfigSource from poetry.core.pyproject.exceptions import PyProjectException from poetry.core.toml.file import TOMLFile from poetry.factory import Factory from poetry.locations import CONFIG_DIR config = Factory.create_config(self.io) config_file = TOMLFile(Path(CONFIG_DIR) / "config.toml") try: local_config_file = TOMLFile(self.poetry.file.parent / "poetry.toml") if local_config_file.exists(): config.merge(local_config_file.read()) except (RuntimeError, PyProjectException): local_config_file = TOMLFile(Path.cwd() / "poetry.toml") if self.option("local"): config.set_config_source(FileConfigSource(local_config_file)) if not config_file.exists(): config_file.path.parent.mkdir(parents=True, exist_ok=True) config_file.touch(mode=0o0600) if self.option("list"): self._list_configuration(config.all(), config.raw()) return 0 setting_key = self.argument("key") if not setting_key: return 0 if self.argument("value") and self.option("unset"): raise RuntimeError( "You can not combine a setting value with --unset") # show the value if no value is provided if not self.argument("value") and not self.option("unset"): m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key")) if m: if not m.group(1): value = {} if config.get("repositories") is not None: value = config.get("repositories") else: repo = config.get("repositories.{}".format(m.group(1))) if repo is None: raise ValueError( "There is no {} repository defined".format( m.group(1))) value = repo self.line(str(value)) else: values = self.unique_config_values if setting_key not in values: raise ValueError( "There is no {} setting.".format(setting_key)) value = config.get(setting_key) if not isinstance(value, str): value = json.dumps(value) self.line(value) return 0 values = self.argument("value") unique_config_values = self.unique_config_values if setting_key in unique_config_values: if self.option("unset"): return config.config_source.remove_property(setting_key) return self._handle_single_value( config.config_source, setting_key, unique_config_values[setting_key], values, ) # handle repositories m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key")) if m: if not m.group(1): raise ValueError( "You cannot remove the [repositories] section") if self.option("unset"): repo = config.get("repositories.{}".format(m.group(1))) if repo is None: raise ValueError( "There is no {} repository defined".format(m.group(1))) config.config_source.remove_property("repositories.{}".format( m.group(1))) return 0 if len(values) == 1: url = values[0] config.config_source.add_property( "repositories.{}.url".format(m.group(1)), url) return 0 raise ValueError( "You must pass the url. " "Example: poetry config repositories.foo https://bar.com") # handle auth m = re.match(r"^(http-basic|pypi-token)\.(.+)", self.argument("key")) if m: from poetry.utils.password_manager import PasswordManager password_manager = PasswordManager(config) if self.option("unset"): if m.group(1) == "http-basic": password_manager.delete_http_password(m.group(2)) elif m.group(1) == "pypi-token": password_manager.delete_pypi_token(m.group(2)) return 0 if m.group(1) == "http-basic": if len(values) == 1: username = values[0] # Only username, so we prompt for password password = self.secret("Password:"******"Expected one or two arguments " "(username, password), got {}".format( len(values))) else: username = values[0] password = values[1] password_manager.set_http_password(m.group(2), username, password) elif m.group(1) == "pypi-token": if len(values) != 1: raise ValueError( "Expected only one argument (token), got {}".format( len(values))) token = values[0] password_manager.set_pypi_token(m.group(2), token) return 0 # handle certs m = re.match(r"(?:certificates)\.([^.]+)\.(cert|client-cert)", self.argument("key")) if m: if self.option("unset"): config.auth_config_source.remove_property( "certificates.{}.{}".format(m.group(1), m.group(2))) return 0 if len(values) == 1: config.auth_config_source.add_property( "certificates.{}.{}".format(m.group(1), m.group(2)), values[0]) else: raise ValueError("You must pass exactly 1 value") return 0 raise ValueError("Setting {} does not exist".format( self.argument("key"))) def _handle_single_value( self, source: "ConfigSource", key: str, callbacks: Tuple[Any, Any, Any], values: List[Any], ) -> int: validator, normalizer, _ = callbacks if len(values) > 1: raise RuntimeError("You can only pass one value.") value = values[0] if not validator(value): raise RuntimeError('"{}" is an invalid value for {}'.format( value, key)) source.add_property(key, normalizer(value)) return 0 def _list_configuration(self, config: Dict, raw: Dict, k: str = "") -> None: orig_k = k for key, value in sorted(config.items()): if k + key in self.LIST_PROHIBITED_SETTINGS: continue raw_val = raw.get(key) if isinstance(value, dict): k += "{}.".format(key) self._list_configuration(value, raw_val, k=k) k = orig_k continue elif isinstance(value, list): value = [ json.dumps(val) if isinstance(val, list) else val for val in value ] value = "[{}]".format(", ".join(value)) if k.startswith("repositories."): message = "<c1>{}</c1> = <c2>{}</c2>".format( k + key, json.dumps(raw_val)) elif isinstance(raw_val, str) and raw_val != value: message = "<c1>{}</c1> = <c2>{}</c2> # {}".format( k + key, json.dumps(raw_val), value) else: message = "<c1>{}</c1> = <c2>{}</c2>".format( k + key, json.dumps(value)) self.line(message) def _get_setting( self, contents: Dict, setting: Optional[str] = None, k: Optional[str] = None, default: Optional[Any] = None, ) -> List[Tuple[str, str]]: orig_k = k if setting and setting.split(".")[0] not in contents: value = json.dumps(default) return [((k or "") + setting, value)] else: values = [] for key, value in contents.items(): if setting and key != setting.split(".")[0]: continue if isinstance(value, dict) or key == "repositories" and k is None: if k is None: k = "" k += re.sub(r"^config\.", "", key + ".") if setting and len(setting) > 1: setting = ".".join(setting.split(".")[1:]) values += self._get_setting(value, k=k, setting=setting, default=default) k = orig_k continue if isinstance(value, list): value = [ json.dumps(val) if isinstance(val, list) else val for val in value ] value = "[{}]".format(", ".join(value)) value = json.dumps(value) values.append(((k or "") + key, value)) return values
class NewCommand(Command): name = "new" description = "Creates a new Python project at <path>." arguments = [argument("path", "The path to create the project at.")] options = [ option("name", None, "Set the resulting package name.", flag=False), option("src", None, "Use the src layout for the project."), ] def handle(self) -> None: from pathlib import Path from poetry.core.semver.helpers import parse_constraint from poetry.core.vcs.git import GitConfig from poetry.layouts import layout from poetry.utils.env import SystemEnv if self.option("src"): layout_ = layout("src") else: layout_ = layout("standard") path = Path.cwd() / Path(self.argument("path")) name = self.option("name") if not name: name = path.name if path.exists(): if list(path.glob("*")): # Directory is not empty. Aborting. raise RuntimeError("Destination <fg=yellow>{}</> " "exists and is not empty".format(path)) readme_format = "rst" config = GitConfig() author = None if config.get("user.name"): author = config["user.name"] author_email = config.get("user.email") if author_email: author += " <{}>".format(author_email) current_env = SystemEnv(Path(sys.executable)) default_python = "^{}".format(".".join( str(v) for v in current_env.version_info[:2])) dev_dependencies = {} python_constraint = parse_constraint(default_python) if parse_constraint("<3.5").allows_any(python_constraint): dev_dependencies["pytest"] = "^4.6" if parse_constraint(">=3.5").allows_all(python_constraint): dev_dependencies["pytest"] = "^5.2" layout_ = layout_( name, "0.1.0", author=author, readme_format=readme_format, python=default_python, dev_dependencies=dev_dependencies, ) layout_.create(path) self.line("Created package <info>{}</> in <fg=blue>{}</>".format( module_name(name), path.relative_to(Path.cwd())))
class RemoveCommand(InstallerCommand): name = "remove" description = "Removes a package from the project dependencies." arguments = [ argument("packages", "The packages to remove.", multiple=True) ] options = [ option("group", "G", "The group to remove the dependency from.", flag=False), option("dev", "D", "Remove a package from the development dependencies."), option( "dry-run", None, "Output the operations but do not execute anything " "(implicitly enables --verbose).", ), ] help = """The <info>remove</info> command removes a package from the current list of installed packages <info>poetry remove</info>""" loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"] def handle(self) -> int: packages = self.argument("packages") if self.option("dev"): self.line("<warning>The --dev option is deprecated, " "use the `--group dev` notation instead.</warning>") self.line("") group = "dev" else: group = self.option("group") content = self.poetry.file.read() poetry_content = content["tool"]["poetry"] if group is None: removed = [] group_sections = [] for group_name, group_section in poetry_content.get("group", {}).items(): group_sections.append( (group_name, group_section.get("dependencies", {}))) for group_name, section in [ ("default", poetry_content["dependencies"]) ] + group_sections: removed += self._remove_packages(packages, section, group_name) if group_name != "default": if not section: del poetry_content["group"][group_name] else: poetry_content["group"][group_name][ "dependencies"] = section elif group == "dev" and "dev-dependencies" in poetry_content: # We need to account for the old `dev-dependencies` section removed = self._remove_packages(packages, poetry_content["dev-dependencies"], "dev") if not poetry_content["dev-dependencies"]: del poetry_content["dev-dependencies"] else: removed = self._remove_packages( packages, poetry_content["group"][group].get("dependencies", {}), group) if not poetry_content["group"][group]: del poetry_content["group"][group] if "group" in poetry_content and not poetry_content["group"]: del poetry_content["group"] removed = set(removed) not_found = set(packages).difference(removed) if not_found: raise ValueError( "The following packages were not found: {}".format(", ".join( sorted(not_found)))) # Refresh the locker self.poetry.set_locker( self.poetry.locker.__class__(self.poetry.locker.lock.path, poetry_content)) self._installer.set_locker(self.poetry.locker) # Update packages self._installer.use_executor( self.poetry.config.get("experimental.new-installer", False)) self._installer.dry_run(self.option("dry-run")) self._installer.verbose(self._io.is_verbose()) self._installer.update(True) self._installer.whitelist(removed) status = self._installer.run() if not self.option("dry-run") and status == 0: self.poetry.file.write(content) return status def _remove_packages(self, packages: List[str], section: Dict[str, Any], group_name: str) -> List[str]: removed = [] group = self.poetry.package.dependency_group(group_name) section_keys = list(section.keys()) for package in packages: for existing_package in section_keys: if existing_package.lower() == package.lower(): del section[existing_package] removed.append(package) group.remove_dependency(package) return removed
class AddCommand(InstallerCommand, InitCommand): name = "add" description = "Adds a new dependency to <comment>pyproject.toml</>." arguments = [argument("name", "The packages to add.", multiple=True)] options = [ option( "group", "-G", "The group to add the dependency to.", flag=False, default="default", ), option("dev", "D", "Add as a development dependency."), option("editable", "e", "Add vcs/path dependencies as editable."), option( "extras", "E", "Extras to activate for the dependency.", flag=False, multiple=True, ), option("optional", None, "Add as an optional dependency."), option( "python", None, "Python version for which the dependency must be installed.", flag=False, ), option( "platform", None, "Platforms for which the dependency must be installed.", flag=False, ), option( "source", None, "Name of the source to use to install the package.", flag=False, ), option("allow-prereleases", None, "Accept prereleases."), option( "dry-run", None, "Output the operations but do not execute anything (implicitly enables --verbose).", ), option("lock", None, "Do not perform operations (only update the lockfile)."), ] help = ( "The add command adds required packages to your <comment>pyproject.toml</> and installs them.\n\n" "If you do not specify a version constraint, poetry will choose a suitable one based on the available package versions.\n\n" "You can specify a package in the following forms:\n" " - A single name (<b>requests</b>)\n" " - A name and a constraint (<b>requests@^2.23.0</b>)\n" " - A git url (<b>git+https://github.com/python-poetry/poetry.git</b>)\n" " - A git url with a revision (<b>git+https://github.com/python-poetry/poetry.git#develop</b>)\n" " - A git SSH url (<b>git+ssh://github.com/python-poetry/poetry.git</b>)\n" " - A git SSH url with a revision (<b>git+ssh://github.com/python-poetry/poetry.git#develop</b>)\n" " - A file path (<b>../my-package/my-package.whl</b>)\n" " - A directory (<b>../my-package/</b>)\n" " - A url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)\n" ) loggers = ["poetry.repositories.pypi_repository", "poetry.inspection.info"] def handle(self) -> int: from tomlkit import inline_table from tomlkit import parse as parse_toml from tomlkit import table from poetry.core.semver.helpers import parse_constraint from poetry.factory import Factory packages = self.argument("name") if self.option("dev"): self.line("<warning>The --dev option is deprecated, " "use the `--group dev` notation instead.</warning>") self.line("") group = "dev" else: group = self.option("group") if self.option("extras") and len(packages) > 1: raise ValueError( "You can only specify one package when using the --extras option" ) content = self.poetry.file.read() poetry_content = content["tool"]["poetry"] if group == "default": if "dependencies" not in poetry_content: poetry_content["dependencies"] = table() section = poetry_content["dependencies"] else: if "group" not in poetry_content: group_table = table() group_table._is_super_table = True poetry_content.value._insert_after("dependencies", "group", group_table) groups = poetry_content["group"] if group not in groups: group_table = parse_toml( f"[tool.poetry.group.{group}.dependencies]\n\n" )["tool"]["poetry"]["group"][group] poetry_content["group"][group] = group_table if "dependencies" not in poetry_content["group"][group]: poetry_content["group"][group]["dependencies"] = table() section = poetry_content["group"][group]["dependencies"] existing_packages = self.get_existing_packages_from_input( packages, section) if existing_packages: self.notify_about_existing_packages(existing_packages) packages = [name for name in packages if name not in existing_packages] if not packages: self.line("Nothing to add.") return 0 requirements = self._determine_requirements( packages, allow_prereleases=self.option("allow-prereleases"), source=self.option("source"), ) for _constraint in requirements: if "version" in _constraint: # Validate version constraint parse_constraint(_constraint["version"]) constraint = inline_table() for name, value in _constraint.items(): if name == "name": continue constraint[name] = value if self.option("optional"): constraint["optional"] = True if self.option("allow-prereleases"): constraint["allow-prereleases"] = True if self.option("extras"): extras = [] for extra in self.option("extras"): if " " in extra: extras += [e.strip() for e in extra.split(" ")] else: extras.append(extra) constraint["extras"] = self.option("extras") if self.option("editable"): if "git" in _constraint or "path" in _constraint: constraint["develop"] = True else: self.line_error( "\n" "<error>Failed to add packages. " "Only vcs/path dependencies support editable installs. " f"<c1>{_constraint['name']}</c1> is neither.") self.line_error("\nNo changes were applied.") return 1 if self.option("python"): constraint["python"] = self.option("python") if self.option("platform"): constraint["platform"] = self.option("platform") if self.option("source"): constraint["source"] = self.option("source") if len(constraint) == 1 and "version" in constraint: constraint = constraint["version"] section[_constraint["name"]] = constraint self.poetry.package.add_dependency( Factory.create_dependency( _constraint["name"], constraint, groups=[group], root_dir=self.poetry.file.parent, )) # Refresh the locker self.poetry.set_locker( self.poetry.locker.__class__(self.poetry.locker.lock.path, poetry_content)) self._installer.set_locker(self.poetry.locker) # Cosmetic new line self.line("") self._installer.set_package(self.poetry.package) self._installer.dry_run(self.option("dry-run")) self._installer.verbose(self._io.is_verbose()) self._installer.update(True) if self.option("lock"): self._installer.lock() self._installer.whitelist([r["name"] for r in requirements]) status = self._installer.run() if status == 0 and not self.option("dry-run"): self.poetry.file.write(content) return status def get_existing_packages_from_input(self, packages: List[str], section: Dict) -> List[str]: existing_packages = [] for name in packages: for key in section: if key.lower() == name.lower(): existing_packages.append(name) return existing_packages def notify_about_existing_packages(self, existing_packages: List[str]) -> None: self.line( "The following packages are already present in the pyproject.toml and will be skipped:\n" ) for name in existing_packages: self.line(" • <c1>{name}</c1>".format(name=name)) self.line( "\nIf you want to update it to the latest compatible version, you can use `poetry update package`.\n" "If you prefer to upgrade it to the latest available version, you can use `poetry add package@latest`.\n" )
class ConfigCommand(Command): name = "config" description = "Manages configuration settings." arguments = [ argument("key", "Setting key.", optional=True), argument("value", "Setting value.", optional=True, multiple=True), ] options = [ option("list", None, "List configuration settings."), option("unset", None, "Unset configuration setting."), option("local", None, "Set/Get from the project's local configuration."), ] help = """This command allows you to edit the poetry config settings and repositories. To add a repository: <comment>poetry config repositories.foo https://bar.com/simple/</comment> To remove a repository (repo is a short alias for repositories): <comment>poetry config --unset repo.foo</comment>""" LIST_PROHIBITED_SETTINGS = {"http-basic", "pypi-token"} @property def unique_config_values(self) -> dict[str, tuple[Any, Any, Any]]: unique_config_values = { "cache-dir": ( str, lambda val: str(Path(val)), str(DEFAULT_CACHE_DIR / "virtualenvs"), ), "virtualenvs.create": (boolean_validator, boolean_normalizer, True), "virtualenvs.in-project": (boolean_validator, boolean_normalizer, False), "virtualenvs.options.always-copy": ( boolean_validator, boolean_normalizer, False, ), "virtualenvs.options.system-site-packages": ( boolean_validator, boolean_normalizer, False, ), "virtualenvs.options.no-pip": ( boolean_validator, boolean_normalizer, False, ), "virtualenvs.options.no-setuptools": ( boolean_validator, boolean_normalizer, False, ), "virtualenvs.path": ( str, lambda val: str(Path(val)), str(DEFAULT_CACHE_DIR / "virtualenvs"), ), "virtualenvs.prefer-active-python": ( boolean_validator, boolean_normalizer, False, ), "experimental.new-installer": ( boolean_validator, boolean_normalizer, True, ), "installer.parallel": ( boolean_validator, boolean_normalizer, True, ), "installer.max-workers": ( lambda val: int(val) > 0, int_normalizer, None, ), "virtualenvs.prompt": ( str, lambda val: str(val), "{project_name}-py{python_version}", ), "installer.no-binary": ( PackageFilterPolicy.validator, PackageFilterPolicy.normalize, None, ), } return unique_config_values def handle(self) -> int: from pathlib import Path from poetry.core.pyproject.exceptions import PyProjectException from poetry.core.toml.file import TOMLFile from poetry.config.config import Config from poetry.config.file_config_source import FileConfigSource from poetry.locations import CONFIG_DIR config = Config.create() config_file = TOMLFile(CONFIG_DIR / "config.toml") try: local_config_file = TOMLFile(self.poetry.file.parent / "poetry.toml") if local_config_file.exists(): config.merge(local_config_file.read()) except (RuntimeError, PyProjectException): local_config_file = TOMLFile(Path.cwd() / "poetry.toml") if self.option("local"): config.set_config_source(FileConfigSource(local_config_file)) if not config_file.exists(): config_file.path.parent.mkdir(parents=True, exist_ok=True) config_file.touch(mode=0o0600) if self.option("list"): self._list_configuration(config.all(), config.raw()) return 0 setting_key = self.argument("key") if not setting_key: return 0 if self.argument("value") and self.option("unset"): raise RuntimeError( "You can not combine a setting value with --unset") # show the value if no value is provided if not self.argument("value") and not self.option("unset"): m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key")) value: str | dict[str, Any] if m: if not m.group(1): value = {} if config.get("repositories") is not None: value = config.get("repositories") else: repo = config.get(f"repositories.{m.group(1)}") if repo is None: raise ValueError( f"There is no {m.group(1)} repository defined") value = repo self.line(str(value)) else: if setting_key not in self.unique_config_values: raise ValueError(f"There is no {setting_key} setting.") value = config.get(setting_key) if not isinstance(value, str): value = json.dumps(value) self.line(value) return 0 values: list[str] = self.argument("value") unique_config_values = self.unique_config_values if setting_key in unique_config_values: if self.option("unset"): config.config_source.remove_property(setting_key) return 0 return self._handle_single_value( config.config_source, setting_key, unique_config_values[setting_key], values, ) # handle repositories m = re.match(r"^repos?(?:itories)?(?:\.(.+))?", self.argument("key")) if m: if not m.group(1): raise ValueError( "You cannot remove the [repositories] section") if self.option("unset"): repo = config.get(f"repositories.{m.group(1)}") if repo is None: raise ValueError( f"There is no {m.group(1)} repository defined") config.config_source.remove_property( f"repositories.{m.group(1)}") return 0 if len(values) == 1: url = values[0] config.config_source.add_property( f"repositories.{m.group(1)}.url", url) return 0 raise ValueError( "You must pass the url. " "Example: poetry config repositories.foo https://bar.com") # handle auth m = re.match(r"^(http-basic|pypi-token)\.(.+)", self.argument("key")) if m: from poetry.utils.password_manager import PasswordManager password_manager = PasswordManager(config) if self.option("unset"): if m.group(1) == "http-basic": password_manager.delete_http_password(m.group(2)) elif m.group(1) == "pypi-token": password_manager.delete_pypi_token(m.group(2)) return 0 if m.group(1) == "http-basic": if len(values) == 1: username = values[0] # Only username, so we prompt for password password = self.secret("Password:"******"Expected one or two arguments " f"(username, password), got {len(values)}") else: username = values[0] password = values[1] password_manager.set_http_password(m.group(2), username, password) elif m.group(1) == "pypi-token": if len(values) != 1: raise ValueError( f"Expected only one argument (token), got {len(values)}" ) token = values[0] password_manager.set_pypi_token(m.group(2), token) return 0 # handle certs m = re.match(r"certificates\.([^.]+)\.(cert|client-cert)", self.argument("key")) if m: repository = m.group(1) key = m.group(2) if self.option("unset"): config.auth_config_source.remove_property( f"certificates.{repository}.{key}") return 0 if len(values) == 1: new_value: str | bool = values[0] if key == "cert" and boolean_validator(values[0]): new_value = boolean_normalizer(values[0]) config.auth_config_source.add_property( f"certificates.{repository}.{key}", new_value) else: raise ValueError("You must pass exactly 1 value") return 0 raise ValueError(f"Setting {self.argument('key')} does not exist") def _handle_single_value( self, source: ConfigSource, key: str, callbacks: tuple[Any, Any, Any], values: list[Any], ) -> int: validator, normalizer, _ = callbacks if len(values) > 1: raise RuntimeError("You can only pass one value.") value = values[0] if not validator(value): raise RuntimeError(f'"{value}" is an invalid value for {key}') source.add_property(key, normalizer(value)) return 0 def _list_configuration(self, config: dict[str, Any], raw: dict[str, Any], k: str = "") -> None: orig_k = k for key, value in sorted(config.items()): if k + key in self.LIST_PROHIBITED_SETTINGS: continue raw_val = raw.get(key) if isinstance(value, dict): k += f"{key}." raw_val = cast("dict[str, Any]", raw_val) self._list_configuration(value, raw_val, k=k) k = orig_k continue elif isinstance(value, list): value = ", ".join( json.dumps(val) if isinstance(val, list) else val for val in value) value = f"[{value}]" if k.startswith("repositories."): message = f"<c1>{k + key}</c1> = <c2>{json.dumps(raw_val)}</c2>" elif isinstance(raw_val, str) and raw_val != value: message = ( f"<c1>{k + key}</c1> = <c2>{json.dumps(raw_val)}</c2> # {value}" ) else: message = f"<c1>{k + key}</c1> = <c2>{json.dumps(value)}</c2>" self.line(message)
class VersionCommand(Command): name = "version" description = ( "Shows the version of the project or bumps it when a valid " "bump rule is provided." ) arguments = [ argument( "version", "The version number or the rule to update the version.", optional=True, ) ] options = [option("short", "s", "Output the version number only")] help = """\ The version command shows the current version of the project or bumps the version of the project and writes the new version back to <comment>pyproject.toml</> if a valid bump rule is provided. The new version should ideally be a valid semver string or a valid bump rule: patch, minor, major, prepatch, preminor, premajor, prerelease. """ RESERVED = { "major", "minor", "patch", "premajor", "preminor", "prepatch", "prerelease", } def handle(self): # type: () -> None version = self.argument("version") if version: version = self.increment_version( self.poetry.package.pretty_version, version ) if self.option("short"): self.line("{}".format(version)) else: self.line( "Bumping version from <b>{}</> to <fg=green>{}</>".format( self.poetry.package.pretty_version, version ) ) content = self.poetry.file.read() poetry_content = content["tool"]["poetry"] poetry_content["version"] = version.text self.poetry.file.write(content) else: if self.option("short"): self.line("{}".format(self.poetry.package.pretty_version)) else: self.line( "<comment>{}</> <info>{}</>".format( self.poetry.package.name, self.poetry.package.pretty_version ) ) def increment_version(self, version, rule): # type: (str, str) -> "Version" from poetry.core.semver import Version try: version = Version.parse(version) except ValueError: raise ValueError("The project's version doesn't seem to follow semver") if rule in {"major", "premajor"}: new = version.next_major if rule == "premajor": new = new.first_prerelease elif rule in {"minor", "preminor"}: new = version.next_minor if rule == "preminor": new = new.first_prerelease elif rule in {"patch", "prepatch"}: new = version.next_patch if rule == "prepatch": new = new.first_prerelease elif rule == "prerelease": if version.is_prerelease(): pre = version.prerelease new_prerelease = int(pre[1]) + 1 new = Version.parse( "{}.{}.{}-{}".format( version.major, version.minor, version.patch, ".".join([pre[0], str(new_prerelease)]), ) ) else: new = version.next_patch.first_prerelease else: new = Version.parse(rule) return new