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 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 do_info( project: Project, python: bool = False, show_project: bool = False, env: bool = False, ) -> None: """Show project information.""" check_project_file(project) python_path = project.python_executable python_version, is_64bit = get_python_version(python_path, True) if not python and not show_project and not env: rows = [ (termui.cyan("PDM version:", bold=True), project.core.version), ( termui.cyan("Python Interpreter:", bold=True), python_path + f" ({get_python_version_string(python_version, is_64bit)})", ), (termui.cyan("Project Root:", bold=True), project.root.as_posix()), ] project.core.ui.display_columns(rows) return if python: project.core.ui.echo(python_path) if show_project: project.core.ui.echo(project.root.as_posix()) if env: project.core.ui.echo( json.dumps(project.environment.marker_environment, indent=2))
def get_finder( self, sources: Optional[List[Source]] = None, ignore_requires_python: bool = False, ) -> Generator[pip_shims.PackageFinder, None, None]: """Return the package finder of given index sources. :param sources: a list of sources the finder should search in. :param ignore_requires_python: whether to ignore the python version constraint. """ if sources is None: sources = self.project.sources for source in sources: source["url"] = expand_env_vars_in_auth(source["url"]) python_version, _ = get_python_version(self.python_executable, digits=2) finder = get_finder( sources, self.project.cache_dir.as_posix(), python_version, ignore_requires_python, ) # Reuse the auth across sessions to avoid prompting repeatly. finder.session.auth = self.auth yield finder finder.session.close()
def handle(self, project: Project, options: argparse.Namespace) -> None: if project.pyproject_file.exists(): stream.echo( "{}".format( stream.cyan("pyproject.toml already exists, update it now.") ) ) else: stream.echo( "{}".format(stream.cyan("Creating a pyproject.toml for PDM...")) ) python = click.prompt( "Please enter the Python interpreter to use", default="", show_default=False ) actions.do_use(project, python) name = click.prompt("Project name", default=project.root.name) version = click.prompt("Project version", default="0.0.0") license = click.prompt("License(SPDX name)", default="MIT") git_user, git_email = get_user_email_from_git() author = click.prompt("Author name", default=git_user) email = click.prompt("Author email", default=git_email) python_version, _ = get_python_version( project.environment.python_executable, True, 2 ) python_requires = click.prompt( "Python requires('*' to allow any)", default=f">={python_version}" ) actions.do_init(project, name, version, license, author, email, python_requires) actions.ask_for_import(project)
def _get_pip_command(self) -> List[str]: """Get a pip command that has pip installed. E.g: ['python', '-m', 'pip'] """ python_version, _ = get_python_version(self.executable) proc = subprocess.run([self.executable, "-Im", "pip", "--version"], capture_output=True) if proc.returncode == 0: # The pip has already been installed with the executable, just use it return [self.executable, "-Im", "pip"] if python_version[0] == 3: # Use the ensurepip to provision one. try: self.subprocess_runner([ self.executable, "-Im", "ensurepip", "--upgrade", "--default-pip" ]) except BuildError: pass else: return [self.executable, "-Im", "pip"] # Otherwise, download a pip wheel from the Internet. pip_wheel = self._env.project.cache_dir / "pip.whl" if not pip_wheel.is_file(): self._download_pip_wheel(pip_wheel) return [self.executable, str(pip_wheel / "pip")]
def handle(self, project: Project, options: argparse.Namespace) -> None: if project.pyproject_file.exists(): project.core.ui.echo("{}".format( termui.cyan("pyproject.toml already exists, update it now."))) else: project.core.ui.echo("{}".format( termui.cyan("Creating a pyproject.toml for PDM..."))) actions.do_use(project) is_library = click.confirm( "Is the project a library that will be upload to PyPI?", ) if is_library: name = click.prompt("Project name", default=project.root.name) version = click.prompt("Project version", default="0.1.0") else: name, version = "", "" license = click.prompt("License(SPDX name)", default="MIT") git_user, git_email = get_user_email_from_git() author = click.prompt("Author name", default=git_user) email = click.prompt("Author email", default=git_email) python_version, _ = get_python_version(project.python_executable, True, 2) python_requires = click.prompt("Python requires('*' to allow any)", default=f">={python_version}") actions.do_init(project, name, version, license, author, email, python_requires) actions.ask_for_import(project)
def get_working_set(self) -> WorkingSet: """Get the working set based on local packages directory.""" paths = self.get_paths() return WorkingSet( [paths["platlib"], paths["purelib"]], python=get_python_version(self.python_executable)[0], )
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 environment(self) -> Environment: if self.is_global: env = GlobalEnvironment(self) # Rewrite global project's python requires to be # compatible with the exact version env.python_requires = PySpecSet( "==" + get_python_version(self.python_executable, True)[0]) return env if self.config["use_venv"] and is_venv_python(self.python_executable): # Only recognize venv created by python -m venv and virtualenv>20 return GlobalEnvironment(self) return Environment(self)
def which(self, command: str) -> str: """Get the full path of the given executable against this environment.""" if not os.path.isabs(command) and command.startswith("python"): python = os.path.splitext(command)[0] version = python[6:] this_version, _ = get_python_version(self.python_executable, True) if not version or this_version.startswith(version): return self.python_executable # Fallback to use shutil.which to find the executable this_path = self.get_paths()["scripts"] python_root = os.path.dirname(self.python_executable) new_path = os.pathsep.join([python_root, this_path, os.getenv("PATH", "")]) return shutil.which(command, path=new_path)
def packages_path(self) -> Path: """The local packages path.""" version, is_64bit = get_python_version(self.python_executable, True, 2) pypackages = (self.project.root / "__pypackages__" / get_python_version_string(version, is_64bit)) if not pypackages.exists() and not is_64bit: compatible_packages = pypackages.parent / get_python_version_string( version, True) if compatible_packages.exists(): pypackages = compatible_packages scripts = "Scripts" if os.name == "nt" else "bin" for subdir in [scripts, "include", "lib"]: pypackages.joinpath(subdir).mkdir(exist_ok=True, parents=True) return pypackages
def environment(self) -> Environment: if self.is_global: env = GlobalEnvironment(self) # Rewrite global project's python requires to be # compatible with the exact version env.python_requires = PySpecSet( "==" + get_python_version(env.python_executable, True)[0]) return env if self.config["use_venv"]: venv_python = get_venv_python(self.root) if venv_python: self.project_config["python.path"] = venv_python return GlobalEnvironment(self) return Environment(self)
def test_init_command(project_no_init, invoke, mocker): mocker.patch( "pdm.cli.commands.init.get_user_email_from_git", return_value=("Testing", "*****@*****.**"), ) do_init = mocker.patch.object(actions, "do_init") result = invoke(["init"], input="\n\n\n\n\n\n", obj=project_no_init) assert result.exit_code == 0 python_version, _ = get_python_version(project_no_init.python_executable, True, 2) do_init.assert_called_with( project_no_init, "", "", "MIT", "Testing", "*****@*****.**", f">={python_version}", )
def environment(self) -> Environment: if self.is_global: env = GlobalEnvironment(self) # Rewrite global project's python requires to be # compatible with the exact version env.python_requires = PySpecSet( "==" + get_python_version(env.python_executable, True)[0]) return env if not self.project_config.get( "python.path") and self.config["use_venv"]: venv_python = get_venv_python(self.root) if venv_python: self.project_config["python.path"] = venv_python if (self.config["use_venv"] and self.project_config.get("python.path") and Path(self.project_config.get("python.path") ).parent.parent.joinpath("pyvenv.cfg").exists()): # Only recognize venv created by python -m venv and virtualenv>20 return GlobalEnvironment(self) return Environment(self)
def build( self, ireq: pip_shims.InstallRequirement, hashes: Optional[Dict[str, str]] = None, allow_all: bool = True, ) -> str: """Build egg_info directory for editable candidates and a wheel for others. :param ireq: the InstallRequirment of the candidate. :param hashes: a dictionary of filename: hash_value to check against downloaded artifacts. :param allow_all: Allow building incompatible wheels. :returns: The full path of the built artifact. """ kwargs = self._make_building_args(ireq) wheel_cache = self.project.make_wheel_cache() with self.get_finder() as finder: with allow_all_wheels(allow_all): # temporarily allow all wheels to get a link. populate_link(finder, ireq, False) if hashes is None: cache_entry = wheel_cache.get_cache_entry( ireq.link, ireq.req.project_name, pip_shims.get_supported( version="".join( map( str, get_python_version(self.python_executable)[0][:2], ) ) ), ) if cache_entry is not None: stream.logger.debug( "Using cached wheel link: %s", cache_entry.link ) ireq.link = cache_entry.link if not ireq.editable and not ireq.req.name: ireq.source_dir = kwargs["build_dir"] else: ireq.ensure_has_source_dir(kwargs["build_dir"]) download_dir = kwargs["download_dir"] only_download = False if ireq.link.is_wheel: download_dir = kwargs["wheel_download_dir"] only_download = True if hashes: ireq.hash_options = convert_hashes(hashes) if not (ireq.editable and ireq.req.is_local_dir): downloader = pip_shims.Downloader(finder.session, "off") if ireq.link.is_vcs: ireq.link = pip_shims.Link(expand_env_vars_in_auth(ireq.link.url)) downloaded = pip_shims.unpack_url( ireq.link, ireq.source_dir, downloader, download_dir, ireq.hashes(False), ) # Preserve the downloaded file so that it won't be cleared. if downloaded and only_download: try: shutil.copy(downloaded.path, download_dir) except shutil.SameFileError: pass if ireq.link.is_wheel: # If the file is a wheel, should be already present under download dir. return (self.project.cache("wheels") / ireq.link.filename).as_posix() else: # Check the built wheel cache again after hashes are resolved. cache_entry = wheel_cache.get_cache_entry( ireq.link, ireq.req.project_name, pip_shims.get_supported( version="".join( map(str, get_python_version(self.python_executable)[0][:2]) ) ), ) if cache_entry is not None: stream.logger.debug("Using cached wheel link: %s", cache_entry.link) return cache_entry.link.file_path # Otherwise, now all source is prepared, build it. with EnvBuilder(ireq.unpacked_source_directory, self) as builder: if ireq.editable: ret = builder.build_egg_info(kwargs["build_dir"]) ireq.metadata_directory = ret else: should_cache = False if ireq.link.is_vcs: vcs = pip_shims.VcsSupport() vcs_backend = vcs.get_backend_for_scheme(ireq.link.scheme) if vcs_backend.is_immutable_rev_checkout( ireq.link.url, ireq.source_dir ): should_cache = True else: base, _ = ireq.link.splitext() if _egg_info_re.search(base) is not None: # Determine whether the string looks like an egg_info. should_cache = True output_dir = ( wheel_cache.get_path_for_link(ireq.link) if should_cache else kwargs["build_dir"] ) if not os.path.exists(output_dir): os.makedirs(output_dir, exist_ok=True) ret = builder.build_wheel(output_dir) return ret
def build( self, ireq: pip_shims.InstallRequirement, hashes: Optional[Dict[str, str]] = None, allow_all: bool = True, ) -> str: """Build egg_info directory for editable candidates and a wheel for others. :param ireq: the InstallRequirment of the candidate. :param hashes: a dictionary of filename: hash_value to check against downloaded artifacts. :param allow_all: Allow building incompatible wheels. :returns: The full path of the built artifact. """ build_dir = self._get_build_dir(ireq) wheel_cache = self.project.make_wheel_cache() supported_tags = pip_shims.get_supported("".join( map(str, get_python_version(self.python_executable, digits=2)[0]))) with self.get_finder(ignore_requires_python=True) as finder: with allow_all_wheels(allow_all): # temporarily allow all wheels to get a link. populate_link(finder, ireq, False) ireq.link = pip_shims.Link( expand_env_vars_in_auth( ireq.link.url.replace( "${PROJECT_ROOT}", self.project.root.as_posix().lstrip("/")))) if hashes is None and not ireq.editable: # If hashes are not given and cache is hit, replace the link with the # cached one. This can speed up by skipping the download and build. cache_entry = wheel_cache.get_cache_entry( ireq.link, ireq.req.project_name, supported_tags, ) if cache_entry is not None: termui.logger.debug("Using cached wheel link: %s", cache_entry.link) ireq.link = cache_entry.link if not ireq.editable and not ireq.req.name: ireq.source_dir = build_dir else: ireq.ensure_has_source_dir(build_dir) if hashes: ireq.hash_options = convert_hashes(hashes) if not (ireq.editable and ireq.req.is_local_dir): downloader = pip_shims.Downloader(finder.session, "off") downloaded = pip_shims.unpack_url( ireq.link, ireq.source_dir, downloader, hashes=ireq.hashes(False), ) if ireq.link.is_wheel: # If the file is a wheel, return the downloaded file directly. return downloaded.path # Check the built wheel cache again after hashes are resolved. if not ireq.editable: cache_entry = wheel_cache.get_cache_entry( ireq.link, ireq.req.project_name, supported_tags, ) if cache_entry is not None: termui.logger.debug("Using cached wheel link: %s", cache_entry.link) return cache_entry.link.file_path # Otherwise, as all source is already prepared, build it. with EnvBuilder(ireq.unpacked_source_directory, self) as builder: if ireq.editable: ret = builder.build_egg_info(build_dir) ireq.metadata_directory = ret return ret should_cache = False if ireq.link.is_vcs: vcs = pip_shims.VcsSupport() vcs_backend = vcs.get_backend_for_scheme(ireq.link.scheme) if vcs_backend.is_immutable_rev_checkout( ireq.link.url, ireq.source_dir): should_cache = True else: base, _ = ireq.link.splitext() if _egg_info_re.search(base) is not None: # Determine whether the string looks like an egg_info. should_cache = True output_dir = (wheel_cache.get_path_for_link(ireq.link) if should_cache else build_dir) if not os.path.exists(output_dir): os.makedirs(output_dir, exist_ok=True) return builder.build_wheel(output_dir)