def handle(self, project: Project, options: argparse.Namespace) -> None: if options.env: venv = next( (venv for key, venv in iter_venvs(project) if key == options.env), None) if not venv: project.core.ui.echo( termui.yellow( f"No virtualenv with key {options.env} is found"), err=True, ) raise SystemExit(1) else: # Use what is saved in .pdm.toml interpreter = project.python_executable if is_venv_python(interpreter): venv = Path(interpreter).parent.parent else: project.core.ui.echo( termui.yellow( f"Can't activate a non-venv Python{interpreter}, " "you can specify one with pdm venv activate <env_name>" )) raise SystemExit(1) project.core.ui.echo(self.get_activate_command(venv))
def _list_config(self, project: Project, options: argparse.Namespace) -> None: ui = project.core.ui ui.echo("Home configuration ({}):".format( project.global_config._config_file)) with ui.indent(" "): for key in sorted(project.global_config): ui.echo( termui.yellow( "# " + project.global_config._config_map[key].description), verbosity=termui.DETAIL, ) ui.echo(f"{termui.cyan(key)} = {project.global_config[key]}") ui.echo() ui.echo("Project configuration ({}):".format( project.project_config._config_file)) with ui.indent(" "): for key in sorted(project.project_config): ui.echo( termui.yellow( "# " + project.project_config._config_map[key].description), verbosity=termui.DETAIL, ) ui.echo(f"{termui.cyan(key)} = {project.project_config[key]}")
def ask_for_import(project: Project) -> None: """Show possible importable files and ask user to decide""" importable_files = list(find_importable_files(project)) if not importable_files: return project.core.ui.echo( termui.cyan("Found following files from other formats that you may import:") ) for i, (key, filepath) in enumerate(importable_files): project.core.ui.echo(f"{i}. {termui.green(filepath.as_posix())} ({key})") project.core.ui.echo( "{}. {}".format( len(importable_files), termui.yellow("don't do anything, I will import later."), ) ) choice = click.prompt( "Please select:", type=click.Choice([str(i) for i in range(len(importable_files) + 1)]), show_default=False, ) if int(choice) == len(importable_files): return key, filepath = importable_files[int(choice)] do_import(project, str(filepath), key)
def handle(self, project: Project, options: argparse.Namespace) -> None: package = options.package req = parse_requirement(package) repository = project.get_repository() # reverse the result so that latest is at first. matches = repository.find_candidates( req, project.environment.python_requires, True ) latest = next(iter(matches), None) if not latest: project.core.ui.echo( termui.yellow(f"No match found for the package {package!r}"), err=True ) return latest_stable = next(filter(filter_stable, matches), None) installed = project.environment.get_working_set().get(package) metadata = latest.get_metadata() assert metadata if metadata._legacy: result = ProjectInfo(dict(metadata._legacy.items()), True) else: result = ProjectInfo(dict(metadata._data), False) if latest_stable: result.latest_stable_version = str(latest_stable.version) if installed: result.installed_version = str(installed.version) project.core.ui.display_columns(list(result.generate_rows()))
def handle(self, project: Project, options: argparse.Namespace) -> None: if not options.force: project.core.ui.echo( termui.red("The following Virtualenvs will be purged:") ) for i, venv in enumerate(get_all_venvs(project)): project.core.ui.echo(f"{i}. {termui.green(venv[0])}") if not options.interactive and ( options.force or click.confirm(termui.yellow("continue? ")) ): self.del_all_venvs(project) if options.interactive: selection = click.prompt( "Please select", type=click.Choice( [str(i) for i in range(len(list(get_all_venvs(project))))] + ["all", "none"] ), default="none", show_choices=False, ) if selection == "all": self.del_all_venvs(project) elif selection != "none": for i, venv in enumerate(get_all_venvs(project)): if i == int(selection): shutil.rmtree(venv[1]) project.core.ui.echo("Purged successfully!")
def __setitem__(self, key: str, value: Any) -> None: if key not in self._config_map and key not in self.deprecated: raise NoConfigError(key) config_key = self.deprecated.get(key, key) config = self._config_map[config_key] if not self.is_global and config.global_only: raise ValueError( f"Config item '{key}' is not allowed to set in project config." ) value = config.coerce(value) env_var = config.env_var if env_var is not None and env_var in os.environ: click.echo( termui.yellow( "WARNING: the config is shadowed by env var '{}', " "the value set won't take effect.".format(env_var) ) ) self._data[config_key] = value self._file_data[config_key] = value if config.replace: self._data.pop(config.replace, None) self._file_data.pop(config.replace, None) self._save_config()
def format_help(self) -> str: formatter = self._get_formatter() if getattr(self, "is_root", False): banner = ( cfonts.render( "PDM", font="slick", gradient=["bright_red", "bright_green"], space=False, ) + "\n" ) formatter._add_item(lambda x: x, [banner]) self._positionals.title = "Commands" self._optionals.title = "Options" # description formatter.add_text(self.description) # usage formatter.add_usage( self.usage, self._actions, self._mutually_exclusive_groups, prefix=termui.yellow("Usage", bold=True) + ": ", ) # positionals, optionals and user-defined groups for action_group in self._action_groups: formatter.start_section( termui.yellow(action_group.title, bold=True) if action_group.title else None ) formatter.add_text(action_group.description) formatter.add_arguments(action_group._group_actions) formatter.end_section() # epilog formatter.add_text(self.epilog) # determine help from format above return formatter.format_help()
def handle(self, project: Project, options: argparse.Namespace) -> None: project.core.ui.echo("Virtualenvs created with this project:") for ident, venv in iter_venvs(project): if ident == options.env: if options.yes or click.confirm( termui.yellow(f"Will remove: {venv}, continue?")): shutil.rmtree(venv) if (project.project_config.get("python.path") and Path(project.project_config["python.path"] ).parent.parent == venv): del project.project_config["python.path"] project.core.ui.echo("Removed successfully!") break else: project.core.ui.echo( termui.yellow( f"No virtualenv with key {options.env} is found"), err=True, ) raise SystemExit(1)
def _show_config(self, config: Config, ui: termui.UI) -> None: for key in sorted(config): config_item = config._config_map[key] deprecated = "" if config_item.replace and config_item.replace in config._data: deprecated = termui.red( f"(deprecating: {config_item.replace})") ui.echo( termui.yellow("# " + config_item.description), verbosity=termui.DETAIL, ) ui.echo(f"{termui.cyan(key)}{deprecated} = {config[key]}")
def _format_usage( self, usage: str, actions: Iterable[Action], groups: Iterable[_ArgumentGroup], prefix: str | None, ) -> str: if prefix is None: prefix = "Usage: " result = super()._format_usage(usage, actions, groups, prefix) if prefix: return result.replace(prefix, termui.yellow(prefix, bold=True)) return result
def migrate_pyproject(project: Project): """Migrate the legacy pyproject format to PEP 621""" if (not project.pyproject_file.exists() or not FORMATS["legacy"].check_fingerprint(project, project.pyproject_file) or "project" in project.pyproject): return project.core.ui.echo( termui.yellow( "Legacy [tool.pdm] metadata detected, migrating to PEP 621..."), err=True, ) do_import(project, project.pyproject_file, "legacy") project.core.ui.echo( termui.green("pyproject.toml") + termui.yellow( " has been migrated to PEP 621 successfully. " "Now you can safely delete the legacy metadata under [tool.pdm] table." ), err=True, )
def migrate_pyproject(project: Project): """Migrate the legacy pyproject format to PEP 621""" if project.pyproject and "project" in project.pyproject: pyproject = project.pyproject settings = {} updated_fields = [] for field in ("includes", "excludes", "build", "package-dir"): if field in pyproject["project"]: updated_fields.append(field) settings[field] = pyproject["project"][field] del pyproject["project"][field] if "dev-dependencies" in pyproject["project"]: if pyproject["project"]["dev-dependencies"]: settings["dev-dependencies"] = { "dev": pyproject["project"]["dev-dependencies"] } del pyproject["project"]["dev-dependencies"] updated_fields.append("dev-dependencies") if updated_fields: if "tool" not in pyproject or "pdm" not in pyproject["tool"]: setdefault(pyproject, "tool", {})["pdm"] = tomlkit.table() pyproject["tool"]["pdm"].update(settings) project.pyproject = pyproject project.write_pyproject() project.core.ui.echo( f"{termui.yellow('[AUTO-MIGRATION]')} These fields are moved from " f"[project] to [tool.pdm] table: {updated_fields}", err=True, ) return if not project.pyproject_file.exists() or not FORMATS["legacy"].check_fingerprint( project, project.pyproject_file ): return project.core.ui.echo( f"{termui.yellow('[AUTO-MIGRATION]')} Legacy pdm 0.x metadata detected, " "migrating to PEP 621...", err=True, ) do_import(project, project.pyproject_file, "legacy") project.core.ui.echo( termui.green("pyproject.toml") + termui.yellow( " has been migrated to PEP 621 successfully. " "Now you can safely delete the legacy metadata under [tool.pdm] table." ), err=True, )
def __delitem__(self, key: str) -> None: self._data.pop(key, None) try: del self._file_data[key] except KeyError: pass else: env_var = self._config_map[key].env_var if env_var is not None and env_var in os.environ: click.echo( termui.yellow( "WARNING: the config is shadowed by env var '{}', " "set value won't take effect.".format(env_var))) self._save_config()
def format_package( graph: DirectedGraph, package: Package, required: str = "", prefix: str = "", visited: Optional[Set[str]] = None, ) -> str: """Format one package. :param graph: the dependency graph :param package: the package instance :param required: the version required by its parent :param prefix: prefix text for children :param visited: the visited package collection """ if visited is None: visited = set() result = [] version = ( termui.red("[ not installed ]") if not package.version else termui.red(package.version) if required and required not in ("Any", "This project") and not SpecifierSet(required).contains(package.version) else termui.yellow(package.version) ) if package.name in visited: version = termui.red("[circular]") required = f"[ required: {required} ]" if required else "[ Not required ]" result.append(f"{termui.green(package.name, bold=True)} {version} {required}\n") if package.name in visited: return "".join(result) visited.add(package.name) children = sorted(graph.iter_children(package), key=lambda p: p.name) for i, child in enumerate(children): is_last = i == len(children) - 1 head = LAST_CHILD if is_last else NON_LAST_CHILD cur_prefix = LAST_PREFIX if is_last else NON_LAST_PREFIX required = str(package.requirements[child.name].specifier or "Any") result.append( prefix + head + format_package( graph, child, required, prefix + cur_prefix, visited.copy() ) ) return "".join(result)
def format_reverse_package( graph: DirectedGraph, package: Package, child: Optional[Package] = None, requires: str = "", prefix: str = "", visited: Optional[Set[str]] = None, ): """Format one package for output reverse dependency graph.""" if visited is None: visited = set() result = [] version = ( termui.red("[ not installed ]") if not package.version else termui.yellow(package.version) ) if package.name in visited: version = termui.red("[circular]") requires = ( f"[ requires: {termui.red(requires)} ]" if requires not in ("Any", "") and child and child.version and not SpecifierSet(requires).contains(child.version) else "" if not requires else f"[ requires: {requires} ]" ) result.append(f"{termui.green(package.name, bold=True)} {version} {requires}\n") if package.name in visited: return "".join(result) visited.add(package.name) parents = sorted(filter(None, graph.iter_parents(package)), key=lambda p: p.name) for i, parent in enumerate(parents): is_last = i == len(parents) - 1 head = LAST_CHILD if is_last else NON_LAST_CHILD cur_prefix = LAST_PREFIX if is_last else NON_LAST_PREFIX requires = str(parent.requirements[package.name].specifier or "Any") result.append( prefix + head + format_reverse_package( graph, parent, package, requires, prefix + cur_prefix, visited.copy() ) ) return "".join(result)
def do_list( project: Project, graph: bool = False, reverse: bool = False, freeze: bool = False, json: bool = False, ) -> None: """Display a list of packages installed in the local packages directory.""" from pdm.cli.utils import build_dependency_graph, format_dependency_graph check_project_file(project) working_set = project.environment.get_working_set() if graph: dep_graph = build_dependency_graph( working_set, project.environment.marker_environment ) project.core.ui.echo( format_dependency_graph(project, dep_graph, reverse=reverse, json=json) ) else: if reverse: raise PdmUsageError("--reverse must be used with --graph") if json: raise PdmUsageError("--json must be used with --graph") if freeze: reqs = sorted( ( Requirement.from_dist(dist) .as_line() .replace( "${PROJECT_ROOT}", project.root.absolute().as_posix().lstrip("/"), ) for dist in sorted( working_set.values(), key=lambda d: d.metadata["Name"] ) ), key=lambda x: x.lower(), ) project.core.ui.echo("\n".join(reqs)) return rows = [ (termui.green(k, bold=True), termui.yellow(v.version), get_dist_location(v)) for k, v in sorted(working_set.items()) ] project.core.ui.display_columns(rows, ["Package", "Version", "Location"])
def __setitem__(self, key: str, value: Any) -> None: if key not in self._config_map: raise NoConfigError(key) if not self.is_global and self._config_map[key].global_only: raise ValueError( f"Config item '{key}' is not allowed to set in project config." ) value = self._config_map[key].coerce(value) env_var = self._config_map[key].env_var if env_var is not None and env_var in os.environ: click.echo( termui.yellow( "WARNING: the config is shadowed by env var '{}', " "the value set won't take effect.".format(env_var))) self._data[key] = value self._file_data[key] = value self._save_config()
def search(self, query: str) -> SearchResult: pypi_simple = self.sources[0]["url"].rstrip("/") results = [] if pypi_simple.endswith("/simple"): search_url = pypi_simple[:-6] + "search" else: search_url = pypi_simple + "/search" with self.environment.get_finder() as finder: session = finder.session resp = session.get(search_url, params={"q": query}) if resp.status_code == 404: self.environment.project.core.ui.echo( termui.yellow( f"{pypi_simple!r} doesn't support '/search' endpoint, fallback " f"to {self.DEFAULT_INDEX_URL!r} now.\n" "This may take longer depending on your network condition." ), err=True, ) resp = session.get(f"{self.DEFAULT_INDEX_URL}/search", params={"q": query}) resp.raise_for_status() content = parse(resp.content, namespaceHTMLElements=False) for result in content.findall(".//*[@class='package-snippet']"): name = result.find("h3/*[@class='package-snippet__name']").text version = result.find( "h3/*[@class='package-snippet__version']").text if not name or not version: continue description = result.find( "p[@class='package-snippet__description']").text if not description: description = "" result = Package(name, version, description) results.append(result) return results
def __delitem__(self, key: str) -> None: config_key = self.deprecated.get(key, key) config = self._config_map[config_key] self._data.pop(config_key, None) self._file_data.pop(config_key, None) if self.is_global and config.should_show(): self._data[config_key] = config.default if config.replace: self._data.pop(config.replace, None) self._file_data.pop(config.replace, None) env_var = config.env_var if env_var is not None and env_var in os.environ: click.echo( termui.yellow( "WARNING: the config is shadowed by env var '{}', " "set value won't take effect.".format(env_var) ) ) self._save_config()
def print_results( ui: termui.UI, hits: SearchResult, working_set: WorkingSet, terminal_width: Optional[int] = None, ) -> None: if not hits: return name_column_width = ( max([len(hit.name) + len(hit.version or "") for hit in hits]) + 4 ) for hit in hits: name = hit.name summary = hit.summary or "" latest = hit.version or "" if terminal_width is not None: target_width = terminal_width - name_column_width - 5 if target_width > 10: # wrap and indent summary to fit terminal summary = textwrap.wrap(summary, target_width) summary = ("\n" + " " * (name_column_width + 2)).join(summary) current_width = len(name) + len(latest) + 4 spaces = " " * (name_column_width - current_width) line = "{name} ({latest}){spaces} - {summary}".format( name=termui.green(name, bold=True), latest=termui.yellow(latest), spaces=spaces, summary=summary, ) try: ui.echo(line) if safe_name(name).lower() in working_set: dist = working_set[safe_name(name).lower()] if dist.version == latest: ui.echo(" INSTALLED: %s (latest)" % dist.version) else: ui.echo(" INSTALLED: %s" % dist.version) ui.echo(" LATEST: %s" % latest) except UnicodeEncodeError: pass
def handle(self, project: Project, options: argparse.Namespace) -> None: package = options.package if package: req = parse_requirement(package) repository = project.get_repository() # reverse the result so that latest is at first. matches = repository.find_candidates( req, project.environment.python_requires, True ) latest = next(iter(matches), None) if not latest: project.core.ui.echo( termui.yellow(f"No match found for the package {package!r}"), err=True, ) return latest_stable = next(filter(filter_stable, matches), None) metadata = latest.metadata else: if not project.meta.name: raise PdmUsageError("This project is not a package") metadata = project.meta package = normalize_name(metadata.name) latest_stable = None assert metadata project_info = ProjectInfo(metadata) if any(getattr(options, key, None) for key in self.metadata_keys): for key in self.metadata_keys: if getattr(options, key, None): project.core.ui.echo(project_info[key]) return installed = project.environment.get_working_set().get(package) if latest_stable: project_info.latest_stable_version = str(latest_stable.version) if installed: project_info.installed_version = str(installed.version) project.core.ui.display_columns(list(project_info.generate_rows()))
def start_section(self, heading: str | None) -> None: return super().start_section( termui.yellow(heading.title() if heading else heading, bold=True))
def format_dist(dist: Distribution) -> str: formatter = "{version}{path}" path = "" if is_dist_editable(dist): path = f" (-e {dist.location})" return formatter.format(version=termui.yellow(dist.version), path=path)