def _configure_io(self, io: IO) -> None: # We need to check if the command being run # is the "run" command. definition = self.definition with suppress(CleoException): io.input.bind(definition) name = io.input.first_argument if name == "run": from poetry.console.io.inputs.run_argv_input import RunArgvInput input = cast(ArgvInput, io.input) run_input = RunArgvInput([self._name or ""] + input._tokens) # For the run command reset the definition # with only the set options (i.e. the options given before the command) for option_name, value in input.options.items(): if value: option = definition.option(option_name) run_input.add_parameter_option("--" + option.name) if option.shortcut: shortcuts = re.split(r"\|-?", option.shortcut.lstrip("-")) shortcuts = [s for s in shortcuts if s] for shortcut in shortcuts: run_input.add_parameter_option("-" + shortcut.lstrip("-")) with suppress(CleoException): run_input.bind(definition) for option_name, value in input.options.items(): if value: run_input.set_option(option_name, value) io.set_input(run_input) super()._configure_io(io)
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}>" f" {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 _write_error(self, io: IO, error: Exception) -> None: """ Outputs an error message. """ message = "<error>{}</error>".format(str(error)) io.write_error_line(message)
def _write_prompt(self, io: IO) -> None: """ Outputs the question prompt. """ message = self._question io.write_error("<question>{}</question> ".format(message))
def _download(self, destination: pathlib.Path, io: cleo_io.IO) -> pathlib.Path: req = requests.get(self.url, stream=True) length = int(req.headers.get("content-length", 0)) progress = progress_bar.ProgressBar(io, max=length) io.write_line(f"Downloading <info>{self.url}</>") if req.status_code < 200 or req.status_code >= 300: raise RuntimeError(f"download failed: {req.status_code}") progress.start(length) try: with open(destination, "wb") as f: for chunk in req.iter_content(chunk_size=4096): if chunk: progress.advance(len(chunk)) f.write(chunk) except BaseException: if destination.exists(): destination.unlink() finally: progress.finish() io.write_line("") try: self.verify(destination) except Exception: destination.unlink() raise return destination
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 _write_prompt(self, io: IO) -> None: message = self._question message = "<question>{} (yes/no)</> [<comment>{}</>] ".format( message, "yes" if self._default else "no" ) io.write_error(message)
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(naive=True) env_dir = Path(os.getenv("POETRY_HOME") or 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, ))
def run(self, io: IO) -> int: self.merge_application_definition() try: io.input.bind(self.definition) except CleoException: if not self._ignore_validation_errors: raise self.initialize(io) if io.is_interactive(): self.interact(io) if io.input.has_argument( "command") and io.input.argument("command") is None: io.input.set_argument("command", self.name) io.input.validate() status_code = self.execute(io) if status_code is None: status_code = 0 return status_code
def download(self, io: cleo_io.IO) -> pathlib.Path: destination_dir = cache.cachedir() / "distfiles" if not destination_dir.exists(): destination_dir.mkdir() destination = destination_dir / self.name if destination.exists(): try: self.verify(destination) except Exception: io.write_line( f"<warning>Cached {self.name} exists, but does pass " f"verification. Downloading anew.") else: return destination return self._download(destination, io)
def _read_from_input(self, io: IO) -> str: """ Read user input. """ ret = io.read_line(4096) if not ret: raise RuntimeError("Aborted") return ret.strip()
def deactivate(self, io: IO) -> None: venv_path = self._poetry.config.get("virtualenvs.path") if venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" else: venv_path = Path(venv_path) name = self._poetry.package.name name = self.generate_env_name(name, str(self._poetry.file.parent)) envs_file = TOMLFile(venv_path / self.ENVS_FILE) if envs_file.exists(): envs = envs_file.read() env = envs.get(name) if env is not None: io.write_line( "Deactivating virtualenv: <comment>{}</comment>".format( venv_path / (name + "-py{}".format(env["minor"])))) del envs[name] envs_file.write(envs)
def configure_sources(cls, poetry: Poetry, sources: list[dict[str, str]], config: Config, io: IO) -> None: for source in sources: repository = cls.create_legacy_repository(source, config) is_default = bool(source.get("default", False)) is_secondary = bool(source.get("secondary", False)) if io.is_debug(): message = f"Adding repository {repository.name} ({repository.url})" if is_default: message += " and setting it as the default one" elif is_secondary: message += " and setting it as secondary" io.write_line(message) poetry.pool.add_repository(repository, is_default, secondary=is_secondary) # Put PyPI last to prefer private repositories # unless we have no default source AND no primary sources # (default = false, secondary = false) if poetry.pool.has_default(): if io.is_debug(): io.write_line("Deactivating the PyPI repository") else: from poetry.repositories.pypi_repository import PyPiRepository default = not poetry.pool.has_primary_repositories() poetry.pool.add_repository(PyPiRepository(), default, not default)
def _write_prompt(self, io: IO) -> None: """ Outputs the question prompt. """ message = self._question default = self._default if default is None: message = "<question>{}</question>: ".format(message) elif self._multi_select: choices = self._choices default = default.split(",") for i, value in enumerate(default): default[i] = choices[int(value.strip())] message = "<question>{}</question> [<comment>{}</comment>]:".format( message, ", ".join(default)) else: choices = self._choices message = "<question>{}</question> [<comment>{}</comment>]:".format( message, choices[int(default)]) if len(self._choices) > 1: width = max( *map(len, [str(k) for k, _ in enumerate(self._choices)])) else: width = 1 messages = [message] for key, value in enumerate(self._choices): messages.append(" [<comment>{:{}}</>] {}".format( key, width, value)) io.write_error_line("\n".join(messages)) message = self._prompt io.write_error(message)
def handle(self) -> int: self.line_error(self.help) application = cast(Application, self.application) command: SelfShowPluginsCommand = cast( SelfShowPluginsCommand, application.find("self show plugins")) exit_code: int = command.run( IO( StringInput(""), self.io.output, self.io.error_output, )) return exit_code
def ask(self, io: IO) -> str: """ Asks the question to the user. """ if not io.is_interactive(): return self.default if not self._validator: return self._do_ask(io) def interviewer(): return self._do_ask(io) return self._validate_attempts(interviewer, io)
def handle(self) -> int: self.line_error(self.deprecation) application = self.get_application() command: 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 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
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
def create_venv( self, io: IO, name: Optional[str] = None, executable: Optional[str] = None, force: bool = False, ) -> Union["SystemEnv", "VirtualEnv"]: if self._env is not None and not force: return self._env cwd = self._poetry.file.parent env = self.get(reload=True) if not env.is_sane(): force = True if env.is_venv() and not force: # Already inside a virtualenv. return env create_venv = self._poetry.config.get("virtualenvs.create") root_venv = self._poetry.config.get("virtualenvs.in-project") venv_path = self._poetry.config.get("virtualenvs.path") if root_venv: venv_path = cwd / ".venv" elif venv_path is None: venv_path = Path(CACHE_DIR) / "virtualenvs" else: venv_path = Path(venv_path) if not name: name = self._poetry.package.name python_patch = ".".join([str(v) for v in sys.version_info[:3]]) python_minor = ".".join([str(v) for v in sys.version_info[:2]]) if executable: python_patch = decode( subprocess.check_output( list_to_shell_command([ executable, "-c", "\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"", ]), shell=True, ).strip()) python_minor = ".".join(python_patch.split(".")[:2]) supported_python = self._poetry.package.python_constraint if not supported_python.allows(Version.parse(python_patch)): # The currently activated or chosen Python version # is not compatible with the Python constraint specified # for the project. # If an executable has been specified, we stop there # and notify the user of the incompatibility. # Otherwise, we try to find a compatible Python version. if executable: raise NoCompatiblePythonVersionFound( self._poetry.package.python_versions, python_patch) io.write_line( "<warning>The currently activated Python version {} " "is not supported by the project ({}).\n" "Trying to find and use a compatible version.</warning> ". format(python_patch, self._poetry.package.python_versions)) for python_to_try in reversed( sorted( self._poetry.package.AVAILABLE_PYTHONS, key=lambda v: (v.startswith("3"), -len(v), v), )): if len(python_to_try) == 1: if not parse_constraint("^{}.0".format( python_to_try)).allows_any(supported_python): continue elif not supported_python.allows_all( parse_constraint(python_to_try + ".*")): continue python = "python" + python_to_try if io.is_debug(): io.write_line("<debug>Trying {}</debug>".format(python)) try: python_patch = decode( subprocess.check_output( list_to_shell_command([ python, "-c", "\"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))\"", ]), stderr=subprocess.STDOUT, shell=True, ).strip()) except CalledProcessError: continue if not python_patch: continue if supported_python.allows(Version.parse(python_patch)): io.write_line("Using <c1>{}</c1> ({})".format( python, python_patch)) executable = python python_minor = ".".join(python_patch.split(".")[:2]) break if not executable: raise NoCompatiblePythonVersionFound( self._poetry.package.python_versions) if root_venv: venv = venv_path else: name = self.generate_env_name(name, str(cwd)) name = "{}-py{}".format(name, python_minor.strip()) venv = venv_path / name if not venv.exists(): if create_venv is False: io.write_line("<fg=black;bg=yellow>" "Skipping virtualenv creation, " "as specified in config file." "</>") return SystemEnv(Path(sys.prefix)) io.write_line("Creating virtualenv <c1>{}</> in {}".format( name, str(venv_path))) self.build_venv( venv, executable=executable, flags=self._poetry.config.get("virtualenvs.options"), ) else: if force: if not env.is_sane(): io.write_line( "<warning>The virtual environment found in {} seems to be broken.</warning>" .format(env.path)) io.write_line("Recreating virtualenv <c1>{}</> in {}".format( name, str(venv))) self.remove_venv(venv) self.build_venv( venv, executable=executable, flags=self._poetry.config.get("virtualenvs.options"), ) elif io.is_very_verbose(): io.write_line( "Virtualenv <c1>{}</> already exists.".format(name)) # venv detection: # stdlib venv may symlink sys.executable, so we can't use realpath. # but others can symlink *to* the venv Python, # so we can't just use sys.executable. # So we just check every item in the symlink tree (generally <= 3) p = os.path.normcase(sys.executable) paths = [p] while os.path.islink(p): p = os.path.normcase( os.path.join(os.path.dirname(p), os.readlink(p))) paths.append(p) p_venv = os.path.normcase(str(venv)) if any(p.startswith(p_venv) for p in paths): # Running properly in the virtualenv, don't need to do anything return SystemEnv(Path(sys.prefix), self.get_base_prefix()) return VirtualEnv(venv)
def _autocomplete(self, io: IO) -> str: """ Autocomplete a question. """ autocomplete = self._autocomplete_values ret = "" i = 0 ofs = -1 matches = [x for x in autocomplete] num_matches = len(matches) stty_mode = subprocess.check_output(["stty", "-g"]).decode().rstrip("\n") # Disable icanon (so we can read each keypress) and echo (we'll do echoing here instead) subprocess.check_output(["stty", "-icanon", "-echo"]) # Add highlighted text style style = Style(options=["reverse"]) io.error_output.formatter.set_style("hl", style) # Read a keypress while True: c = io.read(1) # Backspace character if c == "\177": if num_matches == 0 and i != 0: i -= 1 # Move cursor backwards io.write_error("\033[1D") if i == 0: ofs = -1 matches = [x for x in autocomplete] num_matches = len(matches) else: num_matches = 0 # Pop the last character off the end of our string ret = ret[:i] # Did we read an escape sequence elif c == "\033": c += io.read(2) # A = Up Arrow. B = Down Arrow if c[2] == "A" or c[2] == "B": if c[2] == "A" and ofs == -1: ofs = 0 if num_matches == 0: continue ofs += -1 if c[2] == "A" else 1 ofs = (num_matches + ofs) % num_matches elif ord(c) < 32: if c == "\t" or c == "\n": if num_matches > 0 and ofs != -1: ret = matches[ofs] # Echo out remaining chars for current match io.write_error(ret[i:]) i = len(ret) if c == "\n": io.write_error(c) break num_matches = 0 continue else: io.write_error(c) ret += c i += 1 num_matches = 0 ofs = 0 for value in autocomplete: # If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) if value.startswith(ret) and i != len(value): num_matches += 1 matches[num_matches - 1] = value # Erase characters from cursor to end of line io.write_error("\033[K") if num_matches > 0 and ofs != -1: # Save cursor position io.write_error("\0337") # Write highlighted text io.write_error("<hl>" + matches[ofs][i:] + "</hl>") # Restore cursor position io.write_error("\0338") subprocess.call(["stty", "{}".format(stty_mode)]) return ret
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 = 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 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 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 = PyProjectTOML(env_dir.joinpath("pyproject.toml")) poetry_content = pyproject.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 = 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.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") return update_command.run( IO( StringInput(" ".join(argv)), self._io.output, self._io.error_output, ))
def interact(self, io: IO) -> None: io.write_line("interact called")
def activate(self, poetry: Poetry, io: IO) -> None: io.write_line("Setting readmes") poetry.package.readmes = ("README.md", )
def activate(self, poetry: Poetry, io: IO) -> None: io.write_line("Updating version") poetry.package.set_version("9.9.9")