def check_project_file(project: Project) -> None: """Check the existence of the project file and throws an error on failure.""" if not project.tool_settings: raise ProjectError( "The pyproject.toml has not been initialized yet. You can do this " "by running {}.".format(stream.green("'pdm init'")) )
def do_list(project: Project, graph: bool = False, reverse: bool = False) -> None: """Display a list of packages installed in the local packages directory. :param project: the project instance. :param graph: whether to display a graph. :param reverse: wheter to display reverse graph. """ from pdm.cli.utils import ( build_dependency_graph, format_dependency_graph, format_reverse_dependency_graph, ) check_project_file(project) working_set = project.environment.get_working_set() if reverse and not graph: raise PdmUsageError("--reverse must be used with --graph") if graph: with project.environment.activate(): dep_graph = build_dependency_graph(working_set) if reverse: graph = format_reverse_dependency_graph(project, dep_graph) else: graph = format_dependency_graph(project, dep_graph) stream.echo(graph) else: rows = [(stream.green(k, bold=True), format_dist(v)) for k, v in sorted(working_set.items())] stream.display_columns(rows, ["Package", "Version"])
def generate_rows(self) -> Iterator[Tuple[str, str]]: if self.legacy: yield from self._legacy_generate_rows() return yield stream.cyan("Name:"), self._data["name"] yield stream.cyan("Latest version:"), self._data["version"] if self.latest_stable_version: yield (stream.cyan("Latest stable version:"), self.latest_stable_version) if self.installed_version: yield (stream.green("Installed version:"), self.installed_version) yield stream.cyan("Summary:"), self._data.get("summary", "") contacts = (self._data.get("extensions", {}).get("python.details", {}).get("contacts")) if contacts: author_contact = next( iter(c for c in contacts if c["role"] == "author"), {}) yield stream.cyan("Author:"), author_contact.get("name", "") yield stream.cyan("Author email:"), author_contact.get("email", "") yield stream.cyan("License:"), self._data.get("license", "") yield stream.cyan("Homepage:"), self._data.get("extensions", {}).get( "python.details", {}).get("project_urls", {}).get("Home", "") yield stream.cyan("Project URLs:"), self._data.get("project_url", "") yield stream.cyan("Platform:"), self._data.get("platform", "") yield stream.cyan("Keywords:"), ", ".join( self._data.get("keywords", []))
def set_env_in_reg(env_name: str, value: str) -> None: """Manipulate the WinReg, and add value to the environment variable if exists or create new. """ import winreg value = os.path.normcase(value) with winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) as root: with winreg.OpenKey(root, "Environment", 0, winreg.KEY_ALL_ACCESS) as env_key: try: old_value, type_ = winreg.QueryValueEx(env_key, env_name) if value in [ os.path.normcase(item) for item in old_value.split(os.pathsep) ]: return except FileNotFoundError: old_value, type_ = "", winreg.REG_EXPAND_SZ new_value = ";".join(old_value, value) if old_value else value try: winreg.SetValueEx(env_key, env_name, 0, type_, new_value) except PermissionError: stream.echo( stream.red( "Permission denied, please run the terminal as administrator." ), err=True, ) sys.exit(1) stream.echo( stream.green("The environment variable has been saved, " "please restart the session to take effect."))
def do_remove( project: Project, dev: bool = False, section: Optional[str] = None, sync: bool = True, packages: Sequence[str] = (), ): """Remove packages from working set and pyproject.toml :param project: The project instance :param dev: Remove package from dev-dependencies :param section: Remove package from given section :param sync: Whether perform syncing action :param packages: Package names to be removed :return: None """ check_project_file(project) if not packages: raise PdmUsageError("Must specify at least one package to remove.") section = "dev" if dev else section or "default" if section not in list(project.iter_sections()): raise ProjectError(f"No {section} dependencies given in pyproject.toml.") deps = project.get_pyproject_dependencies(section) stream.echo( f"Removing packages from {section} dependencies: " + ", ".join(str(stream.green(name, bold=True)) for name in packages) ) for name in packages: req = parse_requirement(name) matched_indexes = sorted( (i for i, r in enumerate(deps) if req.matches(r, False)), reverse=True ) if not matched_indexes: raise ProjectError( "{} does not exist in {} dependencies.".format( stream.green(name, bold=True), section ) ) for i in matched_indexes: del deps[i] project.write_pyproject() do_lock(project, "reuse") if sync: do_sync(project, sections=(section,), default=False, clean=True)
def handle(self, project: Project, options: argparse.Namespace) -> None: with project.environment.activate(): expanded_command = project.environment.which(options.command) if not expanded_command: raise PdmUsageError( "Command {} is not found on your PATH.".format( stream.green(f"'{options.command}'"))) sys.exit(subprocess.call([expanded_command] + list(options.args)))
def do_use(project: Project, python: str, first: bool = False) -> None: """Use the specified python version and save in project config. The python can be a version string or interpreter path. """ import pythonfinder python = python.strip() if python and not all(c.isdigit() for c in python.split(".")): if Path(python).exists(): python_path = find_python_in_path(python) else: python_path = shutil.which(python) if not python_path: raise NoPythonVersion(f"{python} is not a valid Python.") python_version, is_64bit = get_python_version(python_path, True) else: finder = pythonfinder.Finder() pythons = [] args = [int(v) for v in python.split(".") if v != ""] for i, entry in enumerate(finder.find_all_python_versions(*args)): python_version, is_64bit = get_python_version(entry.path.as_posix(), True) pythons.append((entry.path.as_posix(), python_version, is_64bit)) if not pythons: raise NoPythonVersion(f"Python {python} is not available on the system.") if not first and len(pythons) > 1: for i, (path, python_version, is_64bit) in enumerate(pythons): stream.echo( f"{i}. {stream.green(path)} " f"({get_python_version_string(python_version, is_64bit)})" ) selection = click.prompt( "Please select:", type=click.Choice([str(i) for i in range(len(pythons))]), default="0", show_choices=False, ) else: selection = 0 python_path, python_version, is_64bit = pythons[int(selection)] if not project.python_requires.contains(python_version): raise NoPythonVersion( "The target Python version {} doesn't satisfy " "the Python requirement: {}".format(python_version, project.python_requires) ) stream.echo( "Using Python interpreter: {} ({})".format( stream.green(python_path), get_python_version_string(python_version, is_64bit), ) ) old_path = project.config.get("python.path") new_path = python_path project.project_config["python.path"] = Path(new_path).as_posix() if old_path and Path(old_path) != Path(new_path) and not project.is_global: stream.echo(stream.cyan("Updating executable scripts...")) project.environment.update_shebangs(new_path)
def do_add( project: Project, dev: bool = False, section: Optional[str] = None, sync: bool = True, save: str = "compatible", strategy: str = "reuse", editables: Iterable[str] = (), packages: Iterable[str] = (), ) -> None: """Add packages and install :param project: the project instance :param dev: add to dev dependencies seciton :param section: specify section to be add to :param sync: whether to install added packages :param save: save strategy :param strategy: update strategy :param editables: editable requirements :param packages: normal requirements """ check_project_file(project) if not editables and not packages: raise PdmUsageError("Must specify at least one package or editable package.") section = "dev" if dev else section or "default" tracked_names = set() requirements = {} for r in [parse_requirement(line, True) for line in editables] + [ parse_requirement(line) for line in packages ]: key = r.identify() r.from_section = section tracked_names.add(key) requirements[key] = r stream.echo( f"Adding packages to {section} dependencies: " + ", ".join(stream.green(key or "", bold=True) for key in requirements) ) all_dependencies = project.all_dependencies all_dependencies.setdefault(section, {}).update(requirements) reqs = [r for deps in all_dependencies.values() for r in deps.values()] resolved = do_lock(project, strategy, tracked_names, reqs) # Update dependency specifiers and lockfile hash. save_version_specifiers(requirements, resolved, save) project.add_dependencies(requirements) lockfile = project.lockfile project.write_lockfile(lockfile, False) if sync: do_sync( project, sections=(section,), dev=False, default=False, dry_run=False, clean=False, )
def do_remove( project: Project, dev: bool = False, section: Optional[str] = None, sync: bool = True, packages: Sequence[str] = (), ): """Remove packages from working set and pyproject.toml :param project: The project instance :param dev: Remove package from dev-dependencies :param section: Remove package from given section :param sync: Whether perform syncing action :param packages: Package names to be removed :return: None """ check_project_file(project) if not packages: raise PdmUsageError("Must specify at least one package to remove.") section = "dev" if dev else section or "default" toml_section = f"{section}-dependencies" if section != "default" else "dependencies" if toml_section not in project.tool_settings: raise ProjectError( f"No such section {stream.yellow(toml_section)} in pyproject.toml." ) deps = project.tool_settings[toml_section] stream.echo(f"Removing packages from {section} dependencies: " + ", ".join( str(stream.green(name, bold=True)) for name in packages)) for name in packages: matched_name = next( filter( lambda k: safe_name(k).lower() == safe_name(name).lower(), deps.keys(), ), None, ) if not matched_name: raise ProjectError("{} does not exist in {} dependencies.".format( stream.green(name, bold=True), section)) del deps[matched_name] project.write_pyproject() do_lock(project, "reuse") if sync: do_sync(project, sections=(section, ), default=False, clean=True)
def python_executable(self) -> str: """Get the Python interpreter path.""" config = self.config if self.project_config.get( "python.path") and not os.getenv("PDM_IGNORE_SAVED_PYTHON"): return self.project_config["python.path"] path = None if config["use_venv"]: path = get_venv_python(self.root) if path: stream.echo( f"Virtualenv interpreter {stream.green(path)} is detected.", err=True, verbosity=stream.DETAIL, ) if not path and PYENV_INSTALLED and config.get("python.use_pyenv", True): path = Path(PYENV_ROOT, "shims", "python").as_posix() if not path: path = shutil.which("python") version = None if path: try: version, _ = get_python_version(path, True) except (FileNotFoundError, subprocess.CalledProcessError): version = None if not version or not self.python_requires.contains(version): finder = Finder() for python in finder.find_all_python_versions(): version, _ = get_python_version(python.path.as_posix(), True) if self.python_requires.contains(version): path = python.path.as_posix() break else: version = ".".join(map(str, sys.version_info[:3])) if self.python_requires.contains(version): path = sys.executable if path: if os.path.normcase(path) == os.path.normcase(sys.executable): # Refer to the base interpreter to allow for venvs path = getattr(sys, "_base_executable", sys.executable) stream.echo( "Using Python interpreter: {} ({})".format( stream.green(path), version), err=True, ) if not os.getenv("PDM_IGNORE_SAVED_PYTHON"): self.project_config["python.path"] = Path(path).as_posix() return path raise NoPythonVersion( "No Python that satisfies {} is found on the system.".format( self.python_requires))
def _show_list(self, project: Project) -> None: if not project.scripts: return columns = ["Name", "Type", "Script", "Description"] result = [] for name, script in project.scripts.items(): if name == "_": continue kind, value, options = self._normalize_script(script) result.append( (stream.green(name), kind, value, options.get("help", ""))) stream.display_columns(result, columns)
def _run_command( project: Project, args: Union[List[str], str], shell: bool = False, env: Optional[Dict[str, str]] = None, env_file: Optional[str] = None, ) -> None: if "PYTHONPATH" in os.environ: pythonpath = os.pathsep.join( [PEP582_PATH, os.getenv("PYTHONPATH")]) else: pythonpath = PEP582_PATH project_env = project.environment this_path = project_env.get_paths()["scripts"] python_root = os.path.dirname(project.python_executable) new_path = os.pathsep.join( [python_root, this_path, os.getenv("PATH", "")]) os.environ.update({"PYTHONPATH": pythonpath, "PATH": new_path}) if project_env.packages_path: os.environ.update( {"PEP582_PACKAGES": str(project_env.packages_path)}) if env_file: import dotenv stream.echo(f"Loading .env file: {stream.green(env_file)}", err=True) dotenv.load_dotenv(project.root.joinpath(env_file).as_posix(), override=True) if env: os.environ.update(env) if shell: sys.exit(subprocess.call(os.path.expandvars(args), shell=True)) command, *args = args expanded_command = project_env.which(command) if not expanded_command: raise PdmUsageError("Command {} is not found on your PATH.".format( stream.green(f"'{command}'"))) expanded_command = os.path.expanduser( os.path.expandvars(expanded_command)) expanded_args = [ os.path.expandvars(arg) for arg in [expanded_command] + args ] if os.name == "nt" or "CI" in os.environ: # In order to make sure pytest is playing well, # don't hand over the process under a testing environment. sys.exit(subprocess.call(expanded_args)) else: os.execv(expanded_command, expanded_args)
def progressbar(self, label: str, total: int): bar = progressbar( length=total, fill_char=stream.green(self.BAR_FILLED_CHAR), empty_char=self.BAR_EMPTY_CHAR, show_percent=False, show_pos=True, label=label, bar_template="%(label)s %(bar)s %(info)s", ) if self.parallel: executor = ThreadPoolExecutor() else: executor = DummyExecutor() with executor: yield bar, executor
def do_list(project: Project, graph: bool = False) -> None: """Display a list of packages installed in the local packages directory. :param project: the project instance. :param graph: whether to display a graph. """ from pdm.cli.utils import build_dependency_graph, format_dependency_graph check_project_file(project) working_set = project.environment.get_working_set() if graph: with project.environment.activate(): dep_graph = build_dependency_graph(working_set) stream.echo(format_dependency_graph(dep_graph)) else: rows = [(stream.green(k, bold=True), format_dist(v)) for k, v in sorted(working_set.items())] stream.display_columns(rows, ["Package", "Version"])
def print_results(hits, working_set, terminal_width=None): if not hits: return name_column_width = ( max( [ len(hit["name"]) + len(highest_version(hit.get("versions", ["-"]))) for hit in hits ] ) + 4 ) for hit in hits: name = hit["name"] summary = hit["summary"] or "" latest = highest_version(hit.get("versions", ["-"])) 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=stream.green(name, bold=True), latest=stream.yellow(latest), spaces=spaces, summary=summary, ) try: stream.echo(line) if safe_name(name).lower() in working_set: dist = working_set[safe_name(name).lower()] if dist.version == latest: stream.echo(" INSTALLED: %s (latest)" % dist.version) else: stream.echo(" INSTALLED: %s" % dist.version) stream.echo(" LATEST: %s" % latest) except UnicodeEncodeError: pass
def python_executable(self) -> str: """Get the Python interpreter path.""" config = self.project.config if config.get("python.path"): return config["python.path"] if PYENV_INSTALLED and config.get("python.use_pyenv", True): return os.path.join(PYENV_ROOT, "shims", "python") if "VIRTUAL_ENV" in os.environ: stream.echo( "An activated virtualenv is detected, reuse the interpreter now.", err=True, verbosity=stream.DETAIL, ) return get_venv_python(self.project.root) # First try what `python` refers to. path = shutil.which("python") version = None if path: version, _ = get_python_version(path, True) if not version or not self.python_requires.contains(version): finder = Finder() for python in finder.find_all_python_versions(): version, _ = get_python_version(python.path.as_posix(), True) if self.python_requires.contains(version): path = python.path.as_posix() break else: version = ".".join(map(str, sys.version_info[:3])) if self.python_requires.contains(version): path = sys.executable if path: stream.echo( "Using Python interpreter: {} ({})".format(stream.green(path), version) ) self.project.project_config["python.path"] = Path(path).as_posix() return path raise NoPythonVersion( "No Python that satisfies {} is found on the system.".format( self.python_requires ) )
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 stream.echo( stream.yellow( "Legacy [tool.pdm] metadata detected, migrating to PEP 621..."), err=True, ) do_import(project, project.pyproject_file, "legacy") stream.echo( stream.green("pyproject.toml") + stream.yellow( " has been migrated to PEP 621 successfully. " "Now you can safely delete the legacy metadata under [tool.pdm] table." ), err=True, )
def _legacy_generate_rows(self) -> Iterator[Tuple[str, str]]: yield stream.cyan("Name:"), self._data["Name"] yield stream.cyan("Latest version:"), self._data["Version"] if self.latest_stable_version: yield (stream.cyan("Latest stable version:"), self.latest_stable_version) if self.installed_version: yield (stream.green("Installed version:"), self.installed_version) yield stream.cyan("Summary:"), self._data.get("Summary", "") yield stream.cyan("Author:"), self._data.get("Author", "") yield stream.cyan("Author email:"), self._data.get("Author-email", "") yield stream.cyan("License:"), self._data.get("License", "") yield stream.cyan("Homepage:"), self._data.get("Home-page", "") if self._data.get("Project-URL"): lines = [ ":".join(parts) for parts in self._data.get("Project-URL") ] yield stream.cyan("Project URLs:"), lines[0] for line in lines[1:]: yield "", line yield stream.cyan("Platform:"), ", ".join( self._data.get("Platform", [])) yield stream.cyan("Keywords:"), ", ".join( self._data.get("Keywords", []))
def do_update( project: Project, dev: bool = False, sections: Sequence[str] = (), default: bool = True, strategy: str = "reuse", save: str = "compatible", unconstrained: bool = False, packages: Sequence[str] = (), ) -> None: """Update specified packages or all packages :param project: The project instance :param dev: whether to update dev dependencies :param sections: update speicified sections :param default: update default :param strategy: update strategy (reuse/eager) :param save: save strategy (compatible/exact/wildcard) :param unconstrained: ignore version constraint :param packages: specified packages to update :return: None """ check_project_file(project) if len(packages) > 0 and (len(sections) > 1 or not default): raise PdmUsageError( "packages argument can't be used together with multple -s or --no-default." ) if not packages: if unconstrained: raise PdmUsageError( "--unconstrained must be used with package names given.") # pdm update with no packages given, same as 'lock' + 'sync' do_lock(project) do_sync(project, sections, dev, default, clean=False) return section = sections[0] if sections else ("dev" if dev else "default") all_dependencies = project.all_dependencies dependencies = all_dependencies[section] updated_deps = {} tracked_names = set() for name in packages: matched_name = next( filter( lambda k: safe_name(strip_extras(k)[0]).lower() == safe_name( name).lower(), dependencies.keys(), ), None, ) if not matched_name: raise ProjectError("{} does not exist in {} dependencies.".format( stream.green(name, bold=True), section)) if unconstrained: dependencies[matched_name].specifier = get_specifier("") tracked_names.add(matched_name) updated_deps[matched_name] = dependencies[matched_name] stream.echo("Updating packages: {}.".format(", ".join( stream.green(v, bold=True) for v in tracked_names))) reqs = [r for deps in all_dependencies.values() for r in deps.values()] resolved = do_lock(project, strategy, tracked_names, reqs) do_sync(project, sections=(section, ), default=False, clean=False) if unconstrained: # Need to update version constraints save_version_specifiers(updated_deps, resolved, save) project.add_dependencies(updated_deps) lockfile = project.lockfile project.write_lockfile(lockfile, False)
def __init__(self, requirement, parent): super().__init__("No version available for {}.".format( stream.green(requirement.as_line()))) self.requirement = requirement self.parent = parent