파일 탐색기에는 wheel 파일이 보이는데 uv pip install이 Distribution not found로 끝난다면 전체 경로에 #가 있는지 먼저 확인하세요. 경로를 따옴표로 감싸는 것과 설치 도구가 그 경로를 해석하는 것은 별개의 단계입니다. 여기서는 실제 파일을 읽을 수 있는지 확인한 뒤 같은 wheel을 단순한 경로로 복사해 비교합니다. 패키지 파일명을 임의로 고치거나 기존 프로젝트 환경을 지우는 방법은 사용하지 않습니다.
이 글에서 확인한 범위
2026년 11월 10일 확인한 독자 보고는 Linux의 uv 0.12.22와 소스 배포 파일에 관한 것입니다. 우리 독립 재현은 macOS와 Python 3.11.6에서 이름 규격을 갖춘 작은 wheel을 사용했습니다. uv 0.8.15와 0.13.7에서 일반 디렉터리는 설치됐지만 #가 있는 디렉터리는 실패했습니다. 원 보고와 운영체제·배포 형식이 같다는 뜻은 아닙니다. 특정 수정이 출시됐다는 주장도 하지 않습니다.
독립 대조군은 외부 패키지를 내려받지 않고 --offline --no-deps --no-cache와 격리된 --target을 사용했습니다. 일반 경로에서는 종료 코드 0과 설치 파일의 존재를, # 경로에서는 종료 코드 1과 설치 파일 부재를 확인했습니다. 예제 패키지의 성공이 모든 실제 wheel의 호환성을 보장하지는 않습니다.
먼저 전체 경로와 파일 존재를 확인하세요
다음 명령은 POSIX 셸용입니다. macOS 또는 Linux에서 실행하고 경로를 실제 wheel 위치로 바꾸세요. python3는 사용할 Python 실행 파일이어야 합니다. 파일을 읽거나 진단 정보를 출력하는 단계이므로 프로젝트 의존성을 변경하지 않습니다.
uv --version
python3 --version
python3 - '/absolute/path/issue-#10458/package-1.0-py3-none-any.whl' <<'PY'
from pathlib import Path
import sys
p = Path(sys.argv[1]).expanduser()
print('absolute:', p.absolute())
print('is_file:', p.is_file())
print('hash_in_path:', '#' in str(p))
if p.is_file():
print('bytes:', p.stat().st_size)
PYis_file이 거짓이면 이 글의 경로 해석 문제로 단정하지 마세요. 현재 디렉터리, 대소문자, 마운트, 파일 이동 여부부터 확인해야 합니다. is_file이 참이어도 파일이 정상 wheel이라는 뜻은 아닙니다. 크기가 0이거나 다운로드가 덜 끝난 경우는 별도로 살펴야 합니다. 공유할 오류 로그에는 사용자명·사내 경로·인증 정보가 들어 있지 않은지도 확인하세요.
따옴표만으로 해결된다고 가정하지 마세요
셸의 따옴표는 경로를 하나의 인자로 전달하는 데 필요합니다. 하지만 인자를 받은 설치 도구가 이후 경로를 처리하는 동작까지 바꾸지는 않습니다. 이번 재현은 셸 문자열이 아닌 Python의 인자 목록으로 uv를 실행해 셸의 주석 처리 가능성을 배제했습니다. 그 조건에서도 # 디렉터리의 wheel은 실패했습니다.
따라서 따옴표가 이미 있다면 같은 명령을 반복하기보다 파일 내용은 그대로 두고 경로만 바꾸는 대조 실험이 유용합니다. 오류에 file://가 보인다는 이유만으로 %23 치환을 모든 입력 형태에 적용하지 마세요. 로컬 경로와 URL은 서로 다른 입력이며 이 글은 인코딩 처방을 검증하지 않았습니다.
같은 파일을 단순한 임시 경로에서 비교하세요
먼저 신뢰하는 wheel인지 확인하세요. 다음 Python 프로그램은 지정한 파일을 이름 그대로 임시 디렉터리에 복사하고, 현재 Python을 대상으로 격리 설치를 시도합니다. 기존 가상환경의 패키지를 바꾸지 않습니다. 예제는 macOS·Linux를 대상으로 하며 Windows 셸 명령으로 검증하지 않았습니다.
from pathlib import Path
import hashlib
import shutil
import subprocess
import sys
import tempfile
source = Path(sys.argv[1]).expanduser().resolve(strict=True)
if not source.is_file() or source.suffix != '.whl':
raise SystemExit('Pass an existing, trusted .whl file')
if '#' in source.name:
raise SystemExit('Rebuild a correctly named wheel; do not rename blindly')
with tempfile.TemporaryDirectory(prefix='uv-path-check-') as tmp:
root = Path(tmp)
if '#' in str(root):
raise SystemExit('Choose a temporary parent path without #')
copied = root / source.name
shutil.copy2(source, copied)
digest = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
assert digest(source) == digest(copied)
result = subprocess.run([
'uv', 'pip', 'install', '--offline', '--no-deps', '--no-cache',
'--python', sys.executable, '--target', str(root / 'installed'),
str(copied),
], check=False)
print('isolated installation exit code:', result.returncode)
raise SystemExit(result.returncode)path_check.py로 저장하고 python3 path_check.py '/absolute/path/to/package.whl'로 실행하세요. 성공한 격리 설치도 프로그램 종료 시 정리됩니다. 따라서 이 명령은 경로 비교용이며 실제 프로젝트에 설치하는 명령이 아닙니다.
원 경로에서는 실패하고 복사본은 성공한다면 파일 내용보다 경로가 원인이라는 판단에 근거가 생깁니다. 실제 작업에서는 프로젝트가 관리하는 경로와 절차에 따라 # 없는 위치를 사용하고 원하는 가상환경을 명시해 설치하세요. 임시 대조 설치가 끝났다는 이유로 프로젝트 의존성까지 설치됐다고 생각하면 안 됩니다.
파일명과 메타데이터 오류는 별도로 처리하세요
원 독자 보고에는 소스 배포 파일명을 바꾼 뒤 패키지 이름과 메타데이터가 불일치하는 다른 오류도 있습니다. 이를 첫 번째 경로 오류와 같은 해결책으로 묶지 마세요. 특히 wheel 파일명에는 배포 이름·버전·호환 태그가 들어갑니다. 보기 편하게 바꾸기보다 신뢰하는 원본에서 올바르게 빌드하거나 제공받는 것이 안전합니다.
위 대조 프로그램은 wheel의 파일명에 #가 있으면 중단합니다. 이 글이 검증한 해결 범위는 유효한 wheel의 이름을 유지하면서 상위 경로를 단순하게 만드는 경우입니다. 소스 배포 파일을 재명명해도 된다는 뜻이 아닙니다.
복사한 뒤에도 실패하면 다음 오류를 읽으세요
단순 경로에서도 실패한다면 원래 경로만의 문제로 결론 내리지 않습니다. Python 버전·플랫폼 태그 불일치, 손상된 아카이브, 메타데이터 문제는 다른 원인입니다. 이 실험은 --offline과 --no-deps를 사용하므로 실제 의존성의 다운로드·해결 가능성은 검증하지 않습니다. 설치 성공 후 가져오기 오류가 난다면 의존성이나 실행 환경도 확인해야 합니다.
오류를 보고할 때는 uv·Python 버전, 운영체제, 파일 존재 확인 결과, 원 경로와 단순 경로의 종료 코드를 함께 정리하세요. 비공개 패키지나 전체 경로를 공개할 필요는 없습니다. 아래 링크에서 공식 안내와 이슈의 현재 상태를 확인할 수 있습니다. 이 글의 관측 시점 이후 결과가 달라질 수 있으므로 같은 버전·같은 파일로 비교한 결과를 우선하세요.