Skip to content

Commit 02416af

Browse files
authored
Merge pull request #1153 from makermelissa-ai-assistant/assistant/uv-missing-pip-guidance
Handle unavailable pip in uv environments
2 parents 969e8d8 + 4448103 commit 02416af

5 files changed

Lines changed: 570 additions & 3 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ jobs:
6565
tests/test_pin_repr.py \
6666
tests/test_rainbowio.py \
6767
tests/test_platform_dependencies.py \
68+
tests/test_uv_dependencies.py \
6869
tests/test_system_gpiod_compat.py \
6970
tests/test_libgpiod_pin.py \
7071
tests/test_libgpiod_legacy_cleanup.py \

‎README.rst‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,32 @@ On supported GNU/Linux systems like the Raspberry Pi, install
8787
source .env/bin/activate
8888
pip3 install Adafruit-Blinka
8989
90+
Environments created with uv
91+
----------------------------
92+
93+
By default, ``uv venv`` does not install pip into the environment. Blinka's
94+
automatic installer requires pip; it does not install pip or run uv for you.
95+
If pip has no runnable entry point, a confirmed installation reports manual
96+
recovery guidance without launching an installer. Missing optional PWM/NeoPixel
97+
packages still do not block unrelated GPIO use.
98+
99+
Use uv to install the required backends into the same environment. For Raspberry
100+
Pi GPIO, for example:
101+
102+
.. code-block:: shell
103+
104+
uv venv .env
105+
uv pip install --python .env/bin/python Adafruit-Blinka 'gpiod>=2.2.2,<3'
106+
107+
Add the PWM or NeoPixel backend if needed, and run your program with
108+
``.env/bin/python``. Manual recovery commands use the running interpreter's path
109+
so an unrelated active environment is not selected. After installing dependencies
110+
while Python is running, restart it before trying again. See uv's
111+
`Python environment documentation
112+
<https://docs.astral.sh/uv/pip/environments/#using-arbitrary-python-environments>`_.
113+
For pip-backed interactive installation, create the environment with
114+
``uv venv --seed .env`` instead.
115+
90116
Services and managed environments
91117
---------------------------------
92118

‎src/adafruit_blinka/platform_dependencies.py‎

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
"""Runtime installation helpers for platform-specific dependencies."""
66

77
import importlib
8+
import importlib.machinery
89
import importlib.metadata
910
import importlib.util
1011
import os
1112
import shlex
13+
import shutil
1214
import subprocess
1315
import sys
1416
import warnings
@@ -149,12 +151,41 @@ def get_unsupported_platform_dependency_message(
149151
return None
150152

151153

154+
def _pip_available():
155+
"""Find this interpreter's pip entry point without importing or executing it."""
156+
try:
157+
spec = importlib.util.find_spec("pip")
158+
if spec is not None and spec.submodule_search_locations is not None:
159+
# Dotted util.find_spec would import pip. Search its package paths
160+
# directly instead, including namespace leftovers from uninstalling.
161+
spec = importlib.machinery.PathFinder.find_spec(
162+
"pip.__main__", spec.submodule_search_locations
163+
)
164+
return (
165+
spec is not None
166+
and spec.loader is not None
167+
and spec.submodule_search_locations is None
168+
)
169+
except (ImportError, AttributeError, KeyError, TypeError, ValueError):
170+
return False
171+
172+
152173
def format_install_command(requirements, executable="pip"):
153-
"""Return a shell-safe command for installing pip requirements."""
174+
"""Return shell-safe manual installation guidance for the target Python."""
154175
if executable == "pip":
155176
command = [executable, "install", *requirements]
156177
else:
157178
command = [executable, "-m", "pip", "install", *requirements]
179+
if executable == sys.executable and not _pip_available():
180+
if shutil.which("uv") is not None:
181+
command = [
182+
"uv",
183+
"pip",
184+
"install",
185+
"--python",
186+
executable,
187+
*requirements,
188+
]
158189
return shlex.join(command)
159190

160191

@@ -268,6 +299,27 @@ def install_missing_platform_dependencies(
268299

269300
def _install_platform_dependencies(detector, python_version, missing, optional):
270301
"""Install confirmed requirements and check same-process usability."""
302+
if not _pip_available():
303+
uv_command = shlex.join(
304+
["uv", "pip", "install", "--python", sys.executable, *missing]
305+
)
306+
message = (
307+
"The current Python environment has no runnable pip entry point, so Blinka cannot "
308+
"install its platform dependencies automatically. Install them with "
309+
"your environment's package manager, or install pip into this "
310+
f"environment and retry. If using uv, run: {uv_command}\n"
311+
"Then restart Python before using Blinka."
312+
)
313+
if set(missing).issubset(optional):
314+
warnings.warn(
315+
message
316+
+ " Blinka startup will continue; optional features may be unavailable.",
317+
RuntimeWarning,
318+
stacklevel=2,
319+
)
320+
return False
321+
raise RuntimeError(message)
322+
271323
# OS-provided modules can be importable without distribution metadata.
272324
# Any loaded module being installed must be restarted, not just known old ones.
273325
loaded = [

‎tests/test_platform_dependencies.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323

2424
@pytest.fixture(autouse=True)
2525
def dependency_check_environment(monkeypatch):
26-
"""Isolate startup-policy tests from a user's configured opt-out."""
26+
"""Isolate startup policy from the host's opt-out and installer tools."""
2727
monkeypatch.delenv("BLINKA_SKIP_DEPENDENCY_CHECK", raising=False)
28+
monkeypatch.setattr(platform_dependencies, "_pip_available", lambda: True)
2829
# System package origin/version handling is tested separately, rather than
2930
# querying the test host's package database from startup-policy tests.
3031
monkeypatch.setattr(_gpiod_compat, "is_supported_system_gpiod", lambda: False)
@@ -475,7 +476,12 @@ def missing_dependencies(*_args):
475476

476477

477478
@pytest.mark.parametrize("answer", ["", "y", "Y", "yes", " YES "])
478-
def test_installer_accepts_confirmation_and_invalidates_cache(installer, answer):
479+
def test_installer_accepts_confirmation_and_invalidates_cache(
480+
monkeypatch, installer, answer
481+
):
482+
monkeypatch.setattr(
483+
platform_dependencies.shutil, "which", lambda _name: "/uv tools/bin/uv"
484+
)
479485
installer.answer = answer
480486
assert installer.run()
481487
assert len(installer.prompts) == 1

0 commit comments

Comments
 (0)