Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 49 additions & 20 deletions conan/tools/system/pip_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,30 @@

class PipEnv:

def __init__(self, conanfile, folder=None, name=""):
def __init__(self, conanfile, folder=None, name="", py_version=None):
"""
:param conanfile: The current conanfile "self"
:param folder: Optional folder, by default the "build_folder"
:param name: Optional name for the virtualenv, by default "conan_pipenv"
:param py_version: Optional python version for the virtualenv using UV
"""
self._conanfile = conanfile
# venv info
self.env_name = f"conan_pipenv{f'_{name}' if name else ''}"
self._env_dir = os.path.abspath(os.path.join(folder or conanfile.build_folder,
self.env_name))
bins = "Scripts" if platform.system() == "Windows" else "bin"
self.bin_dir = os.path.join(self._env_dir, bins)
pyexe = "python.exe" if platform.system() == "Windows" else "python"
self._python_exe = os.path.join(self.bin_dir, pyexe)
self._create_venv()
self._base_env_dir = os.path.abspath(os.path.join(folder or conanfile.build_folder))
self._env_dir = os.path.join(self._base_env_dir, self.env_name)
self.bin_dir = os.path.join(self._env_dir,
"Scripts" if platform.system() == "Windows" else "bin")
# init the venv
if py_version:
self._crete_uv_venv(py_version)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What other advantages of using uv could PipEnv enjoy?
Do we want a generic access interface to it like def install(self, packages, pip_args=None):?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The install interface is the same, It only affects how the virtual environment is created

else:
self._create_venv()

@staticmethod
def _get_env_python(env_dir):
_env_bin_dir = os.path.join(env_dir, "Scripts" if platform.system() == "Windows" else "bin")
return os.path.join(_env_bin_dir, "python.exe" if platform.system() == "Windows" else "python")

def generate(self):
"""
Expand All @@ -33,27 +42,46 @@ def generate(self):
env.prepend_path("PATH", self.bin_dir)
env.vars(self._conanfile).save_script(self.env_name)

@staticmethod
def _default_python():
python = "python" if platform.system() == "Windows" else "python3"
default_python = shutil.which(python)
return os.path.realpath(default_python) if default_python else None

def _create_venv(self):
def _default_python_interpreter(self):
python_interpreter = self._conanfile.conf.get("tools.system.pipenv:python_interpreter")
python_interpreter = python_interpreter or self._default_python()
if not python_interpreter:
python = "python" if platform.system() == "Windows" else "python3"
default_python = shutil.which(python)
python_interpreter = os.path.realpath(default_python) if default_python else None
if not python_interpreter:
raise ConanException("PipEnv could not find a Python executable path. Please, install "
"Python system-wide or set the "
"'tools.system.pipenv:python_interpreter' "
"conf to the full path of a Python executable")
return python_interpreter

def _crete_uv_venv(self, py_version):
_python = self._default_python_interpreter()
_uv_env_dir = os.path.join(self._base_env_dir, f"uv_{self.env_name}")
_python_exe = self._get_env_python(_uv_env_dir)
try:
self._conanfile.run(cmd_args_to_string(
[_python, '-m', 'venv', _uv_env_dir])
)
self._conanfile.run(cmd_args_to_string(
[_python_exe, "-m", "pip", "install", "--disable-pip-version-check", "uv"])
)
self._conanfile.run(cmd_args_to_string(
[_python_exe, '-m', 'uv', 'venv', '--seed', '--python', py_version, self._env_dir])
)
self._conanfile.output.info(f"Virtual environment for Python "
f"{py_version} created successfully using UV.")
except Exception as e:
raise ConanException(f"PipEnv could not create a Python {py_version} virtual "
f"environment using UV and '{_python}': {e}")

def _create_venv(self):
_python = self._default_python_interpreter()
try:
self._conanfile.run(cmd_args_to_string([python_interpreter, '-m', 'venv',
self._env_dir]))
self._conanfile.run(cmd_args_to_string([_python, '-m', 'venv', self._env_dir]))
except ConanException as e:
raise ConanException(f"PipEnv could not create a Python virtual "
f"environment using '{python_interpreter}': {e}")
f"environment using '{_python}': {e}")

def install(self, packages, pip_args=None):
"""
Expand All @@ -65,7 +93,8 @@ def install(self, packages, pip_args=None):
Defaults to ``None``.
:return: the return code of the executed pip command.
"""
args = [self._python_exe, "-m", "pip", "install", "--disable-pip-version-check"]
args = [self._get_env_python(self._env_dir),
"-m", "pip", "install", "--disable-pip-version-check"]
if pip_args:
args += list(pip_args)
args += list(packages)
Expand Down
81 changes: 81 additions & 0 deletions test/functional/tools/system/pip_manager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,84 @@ def build(self):

assert "RUN: hello-world" in client.out
assert "Hello Test World!" in client.out


def test_build_pip_manager_using_uv():

pip_package_folder = temp_folder(path_with_spaces=True)
_create_py_hello_world(pip_package_folder)
pip_package_folder = pip_package_folder.replace('\\', '/')

conanfile_pip = textwrap.dedent(f"""
from conan import ConanFile
from conan.tools.system import PipEnv
from conan.tools.layout import basic_layout
import platform
import os


class PipPackage(ConanFile):
name = "pip_hello_test"
version = "0.1"

def layout(self):
basic_layout(self)

def generate(self):
pip_env = PipEnv(self, py_version="3.11.6")
pip_env.install(["{pip_package_folder}"])
pip_env.generate()
self.run(pip_env._get_env_python(pip_env._env_dir) + " --version")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ugly access, need some nicer public API

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only for testing purposes to verify that we are using the defined python version, but it has no real use in any recipe

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is related to the other ticket, if we want to give more visibility over the tools (uv or the Python version)
Once uv is there, and it is possible to install different python versions inside the PipEnv i'd say it is super likely that users might want to run python -m ... or python -c ..., with that specific Python that they explicitly requested.
So this self.run("python --version") is evidencing a very likely need of making that public API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! In that case we can expose it without any problems, of course


def build(self):
self.run("hello-world")
""")

client = TestClient(path_with_spaces=False)
# FIXME: the python shebang inside vitual env packages fails when using path_with_spaces
client.save({"pip/conanfile.py": conanfile_pip})
client.run("build pip/conanfile.py")
assert "Using CPython 3.11.6" in client.out
assert "Creating virtual environment with seed packages" in client.out
assert "Virtual environment for Python 3.11.6 created successfully using UV." in client.out
assert "python --version\nPython 3.11.6" in client.out
assert "RUN: hello-world" in client.out
assert "Hello Test World!" in client.out


def test_fail_build_pip_manager_using_uv():

pip_package_folder = temp_folder(path_with_spaces=True)
_create_py_hello_world(pip_package_folder)
pip_package_folder = pip_package_folder.replace('\\', '/')

conanfile_pip = textwrap.dedent(f"""
from conan import ConanFile
from conan.tools.system import PipEnv
from conan.tools.layout import basic_layout
import platform
import os


class PipPackage(ConanFile):
name = "pip_hello_test"
version = "0.1"

def layout(self):
basic_layout(self)

def generate(self):
pip_env = PipEnv(self, py_version="3.11.86")
pip_env.install(["{pip_package_folder}"])
pip_env.generate()
self.run(pip_env._get_env_python(pip_env._env_dir) + " --version")

def build(self):
self.run("hello-world")
""")

client = TestClient(path_with_spaces=False)
# FIXME: the python shebang inside vitual env packages fails when using path_with_spaces
client.save({"pip/conanfile.py": conanfile_pip})
client.run("build pip/conanfile.py", assert_error=True)
assert "PipEnv could not create a Python 3.11.86 virtual environment using UV" in client.out
Loading