221 lines
7.5 KiB
Python
221 lines
7.5 KiB
Python
"""A collection for utilities for working with files and paths."""
|
|
import atexit
|
|
import io
|
|
import os
|
|
import sys
|
|
import warnings
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from typing import Any, Optional
|
|
from urllib import parse as urllib_parse
|
|
from urllib import request as urllib_request
|
|
from urllib.parse import quote, urlparse
|
|
|
|
from pipenv.patched.pip._internal.locations import USER_CACHE_DIR
|
|
from pipenv.patched.pip._internal.network.download import PipSession
|
|
from pipenv.utils import err
|
|
|
|
|
|
def is_file_url(url: Any) -> bool:
|
|
"""Returns true if the given url is a file url."""
|
|
if not url:
|
|
return False
|
|
if not isinstance(url, str):
|
|
try:
|
|
url = url.url
|
|
except AttributeError:
|
|
raise ValueError(f"Cannot parse url from unknown type: {url!r}")
|
|
return urllib_parse.urlparse(url.lower()).scheme == "file"
|
|
|
|
|
|
def is_valid_url(url: str) -> bool:
|
|
"""Checks if a given string is an url."""
|
|
pieces = urlparse(url)
|
|
return all([pieces.scheme, pieces.netloc])
|
|
|
|
|
|
def url_to_path(url: str) -> str:
|
|
"""Convert a valid file url to a local filesystem path.
|
|
|
|
Follows logic taken from pip's equivalent function
|
|
"""
|
|
assert is_file_url(url), "Only file: urls can be converted to local paths"
|
|
_, netloc, path, _, _ = urllib_parse.urlsplit(url)
|
|
# Netlocs are UNC paths
|
|
if netloc:
|
|
netloc = "\\\\" + netloc
|
|
|
|
path = urllib_request.url2pathname(netloc + path)
|
|
return urllib_parse.unquote(path)
|
|
|
|
|
|
if os.name == "nt":
|
|
# from click _winconsole.py
|
|
from ctypes import create_unicode_buffer, windll
|
|
|
|
def get_long_path(short_path: str) -> str:
|
|
BUFFER_SIZE = 500
|
|
buffer = create_unicode_buffer(BUFFER_SIZE)
|
|
get_long_path_name = windll.kernel32.GetLongPathNameW
|
|
get_long_path_name(short_path, buffer, BUFFER_SIZE)
|
|
return buffer.value
|
|
|
|
|
|
def normalize_path(path: str) -> str:
|
|
"""Return a case-normalized absolute variable-expanded path."""
|
|
return os.path.expandvars(
|
|
os.path.expanduser(os.path.normcase(os.path.normpath(os.path.abspath(str(path)))))
|
|
)
|
|
|
|
|
|
def normalize_drive(path):
|
|
"""Normalize drive in path so they stay consistent.
|
|
|
|
This currently only affects local drives on Windows, which can be
|
|
identified with either upper or lower cased drive names. The case is
|
|
always converted to uppercase because it seems to be preferred.
|
|
"""
|
|
if os.name != "nt" or not isinstance(path, str):
|
|
return path
|
|
|
|
drive, tail = os.path.splitdrive(path)
|
|
# Only match (lower cased) local drives (e.g. 'c:'), not UNC mounts.
|
|
if drive.islower() and len(drive) == 2 and drive[1] == ":":
|
|
return f"{drive.upper()}{tail}"
|
|
|
|
return path
|
|
|
|
|
|
def path_to_url(path):
|
|
"""Convert the supplied local path to a file uri.
|
|
|
|
:param str path: A string pointing to or representing a local path
|
|
:return: A `file://` uri for the same location
|
|
:rtype: str
|
|
>>> path_to_url("/home/user/code/myrepo/myfile.zip")
|
|
'file:///home/user/code/myrepo/myfile.zip'
|
|
"""
|
|
|
|
if not path:
|
|
return path # type: ignore
|
|
normalized_path = Path(normalize_drive(os.path.abspath(path))).as_posix()
|
|
if os.name == "nt" and normalized_path[1] == ":":
|
|
drive, _, path = normalized_path.partition(":")
|
|
# XXX: This enables us to handle half-surrogates that were never
|
|
# XXX: actually part of a surrogate pair, but were just incidentally
|
|
# XXX: passed in as a piece of a filename
|
|
quoted_path = quote(path, errors="backslashreplace")
|
|
return f"file:///{drive}:{quoted_path}"
|
|
# XXX: This is also here to help deal with incidental dangling surrogates
|
|
# XXX: on linux, by making sure they are preserved during encoding so that
|
|
# XXX: we can urlencode the backslash correctly
|
|
# bytes_path = to_bytes(normalized_path, errors="backslashreplace")
|
|
return "file://{}".format(quote(path, errors="backslashreplace"))
|
|
|
|
|
|
@contextmanager
|
|
def open_file(link, session: Optional[PipSession] = None, stream: bool = False):
|
|
"""Open local or remote file for reading.
|
|
|
|
:param pipenv.patched.pip._internal.index.Link link: A link object from resolving dependencies with
|
|
pip, or else a URL.
|
|
:param Optional[PipSession] session: A :class:`~PipSession` instance
|
|
:param bool stream: Whether to stream the content if remote, default True
|
|
:raises ValueError: If link points to a local directory.
|
|
:return: a context manager to the opened file-like object
|
|
"""
|
|
if not isinstance(link, str):
|
|
try:
|
|
link = link.url_without_fragment
|
|
except AttributeError:
|
|
raise ValueError(f"Cannot parse url from unknown type: {link!r}")
|
|
|
|
if not is_valid_url(link) and os.path.exists(link):
|
|
link = path_to_url(link)
|
|
|
|
if is_file_url(link):
|
|
# Local URL
|
|
local_path = url_to_path(link)
|
|
if os.path.isdir(local_path):
|
|
raise ValueError(f"Cannot open directory for read: {link}")
|
|
else:
|
|
with open(local_path, "rb") as local_file:
|
|
yield local_file
|
|
else:
|
|
# Remote URL
|
|
headers = {"Accept-Encoding": "identity"}
|
|
if not session:
|
|
session = PipSession(cache=USER_CACHE_DIR)
|
|
resp = session.get(link, headers=headers, stream=stream)
|
|
if resp.status_code != 200:
|
|
err.print(f"HTTP error {resp.status_code} while getting {link}")
|
|
yield None
|
|
else:
|
|
# Creating a buffer-like object
|
|
buffer = io.BytesIO(resp.content)
|
|
yield buffer
|
|
|
|
|
|
@contextmanager
|
|
def temp_path():
|
|
# type: () -> Iterator[None]
|
|
"""A context manager which allows the ability to set sys.path temporarily.
|
|
|
|
>>> path_from_virtualenv = load_path("/path/to/venv/bin/python")
|
|
>>> print(sys.path)
|
|
[
|
|
'/home/user/.pyenv/versions/3.7.0/bin',
|
|
'/home/user/.pyenv/versions/3.7.0/lib/python37.zip',
|
|
'/home/user/.pyenv/versions/3.7.0/lib/python3.7',
|
|
'/home/user/.pyenv/versions/3.7.0/lib/python3.7/lib-dynload',
|
|
'/home/user/.pyenv/versions/3.7.0/lib/python3.7/site-packages'
|
|
]
|
|
>>> with temp_path():
|
|
sys.path = path_from_virtualenv
|
|
# Running in the context of the path above
|
|
run(["pip", "install", "stuff"])
|
|
>>> print(sys.path)
|
|
[
|
|
'/home/user/.pyenv/versions/3.7.0/bin',
|
|
'/home/user/.pyenv/versions/3.7.0/lib/python37.zip',
|
|
'/home/user/.pyenv/versions/3.7.0/lib/python3.7',
|
|
'/home/user/.pyenv/versions/3.7.0/lib/python3.7/lib-dynload',
|
|
'/home/user/.pyenv/versions/3.7.0/lib/python3.7/site-packages'
|
|
]
|
|
"""
|
|
path = list(sys.path)
|
|
try:
|
|
yield
|
|
finally:
|
|
sys.path = list(path)
|
|
|
|
|
|
TRACKED_TEMPORARY_DIRECTORIES = []
|
|
|
|
|
|
def create_tracked_tempdir(*args: Any, **kwargs: Any) -> str:
|
|
"""Create a tracked temporary directory.
|
|
|
|
This uses `TemporaryDirectory`, but does not remove the directory
|
|
when the return value goes out of scope, instead registers a handler
|
|
to clean up on program exit. The return value is the path to the
|
|
created directory.
|
|
"""
|
|
tempdir = TemporaryDirectory(*args, **kwargs)
|
|
TRACKED_TEMPORARY_DIRECTORIES.append(tempdir)
|
|
atexit.register(tempdir.cleanup)
|
|
warnings.simplefilter("ignore", ResourceWarning)
|
|
return tempdir.name
|
|
|
|
|
|
def check_for_unc_path(path):
|
|
# type: (Path) -> bool
|
|
"""Checks to see if a pathlib `Path` object is a unc path or not."""
|
|
return bool(
|
|
os.name == "nt"
|
|
and len(path.drive) > 2
|
|
and not path.drive[0].isalpha()
|
|
and path.drive[1] != ":"
|
|
)
|