mirror of https://github.com/pypa/pip
Add File class to represent a file to install
Hiding the file-specific implementation we currently use will let us trade out the implementation for a zip-backed one later. We can also use this interface to represent the other kinds of files that we have to generate as part of wheel installation. We use a Protocol instead of a base class because there's no need for shared behavior right now, and using Protocol is less verbose.
This commit is contained in:
parent
6b26ac911a
commit
aa8dd9cecc
|
@ -54,6 +54,7 @@ else:
|
|||
List,
|
||||
NewType,
|
||||
Optional,
|
||||
Protocol,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
|
@ -69,6 +70,14 @@ else:
|
|||
RecordPath = NewType('RecordPath', text_type)
|
||||
InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]]
|
||||
|
||||
class File(Protocol):
|
||||
src_path = None # type: text_type
|
||||
dest_path = None # type: text_type
|
||||
|
||||
def save(self):
|
||||
# type: () -> None
|
||||
pass
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -388,6 +397,54 @@ def get_console_script_specs(console):
|
|||
return scripts_to_generate
|
||||
|
||||
|
||||
class DiskFile(object):
|
||||
def __init__(self, src_path, dest_path):
|
||||
# type: (text_type, text_type) -> None
|
||||
self.src_path = src_path
|
||||
self.dest_path = dest_path
|
||||
|
||||
def save(self):
|
||||
# type: () -> None
|
||||
# directory creation is lazy and after file filtering
|
||||
# to ensure we don't install empty dirs; empty dirs can't be
|
||||
# uninstalled.
|
||||
parent_dir = os.path.dirname(self.dest_path)
|
||||
ensure_dir(parent_dir)
|
||||
|
||||
# copyfile (called below) truncates the destination if it
|
||||
# exists and then writes the new contents. This is fine in most
|
||||
# cases, but can cause a segfault if pip has loaded a shared
|
||||
# object (e.g. from pyopenssl through its vendored urllib3)
|
||||
# Since the shared object is mmap'd an attempt to call a
|
||||
# symbol in it will then cause a segfault. Unlinking the file
|
||||
# allows writing of new contents while allowing the process to
|
||||
# continue to use the old copy.
|
||||
if os.path.exists(self.dest_path):
|
||||
os.unlink(self.dest_path)
|
||||
|
||||
# We use copyfile (not move, copy, or copy2) to be extra sure
|
||||
# that we are not moving directories over (copyfile fails for
|
||||
# directories) as well as to ensure that we are not copying
|
||||
# over any metadata because we want more control over what
|
||||
# metadata we actually copy over.
|
||||
shutil.copyfile(self.src_path, self.dest_path)
|
||||
|
||||
# Copy over the metadata for the file, currently this only
|
||||
# includes the atime and mtime.
|
||||
st = os.stat(self.src_path)
|
||||
if hasattr(os, "utime"):
|
||||
os.utime(self.dest_path, (st.st_atime, st.st_mtime))
|
||||
|
||||
# If our file is executable, then make our destination file
|
||||
# executable.
|
||||
if os.access(self.src_path, os.X_OK):
|
||||
st = os.stat(self.src_path)
|
||||
permissions = (
|
||||
st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
||||
)
|
||||
os.chmod(self.dest_path, permissions)
|
||||
|
||||
|
||||
class MissingCallableSuffix(Exception):
|
||||
pass
|
||||
|
||||
|
@ -479,49 +536,13 @@ def install_unpacked_wheel(
|
|||
continue
|
||||
srcfile = os.path.join(dir, f)
|
||||
destfile = os.path.join(dest, basedir, f)
|
||||
# directory creation is lazy and after the file filtering above
|
||||
# to ensure we don't install empty dirs; empty dirs can't be
|
||||
# uninstalled.
|
||||
parent_dir = os.path.dirname(destfile)
|
||||
ensure_dir(parent_dir)
|
||||
|
||||
# copyfile (called below) truncates the destination if it
|
||||
# exists and then writes the new contents. This is fine in most
|
||||
# cases, but can cause a segfault if pip has loaded a shared
|
||||
# object (e.g. from pyopenssl through its vendored urllib3)
|
||||
# Since the shared object is mmap'd an attempt to call a
|
||||
# symbol in it will then cause a segfault. Unlinking the file
|
||||
# allows writing of new contents while allowing the process to
|
||||
# continue to use the old copy.
|
||||
if os.path.exists(destfile):
|
||||
os.unlink(destfile)
|
||||
|
||||
# We use copyfile (not move, copy, or copy2) to be extra sure
|
||||
# that we are not moving directories over (copyfile fails for
|
||||
# directories) as well as to ensure that we are not copying
|
||||
# over any metadata because we want more control over what
|
||||
# metadata we actually copy over.
|
||||
shutil.copyfile(srcfile, destfile)
|
||||
|
||||
# Copy over the metadata for the file, currently this only
|
||||
# includes the atime and mtime.
|
||||
st = os.stat(srcfile)
|
||||
if hasattr(os, "utime"):
|
||||
os.utime(destfile, (st.st_atime, st.st_mtime))
|
||||
|
||||
# If our file is executable, then make our destination file
|
||||
# executable.
|
||||
if os.access(srcfile, os.X_OK):
|
||||
st = os.stat(srcfile)
|
||||
permissions = (
|
||||
st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
||||
)
|
||||
os.chmod(destfile, permissions)
|
||||
file = DiskFile(srcfile, destfile)
|
||||
|
||||
file.save()
|
||||
changed = False
|
||||
if fixer:
|
||||
changed = fixer(destfile)
|
||||
record_installed(srcfile, destfile, changed)
|
||||
changed = fixer(file.dest_path)
|
||||
record_installed(file.src_path, file.dest_path, changed)
|
||||
|
||||
clobber(
|
||||
ensure_text(source, encoding=sys.getfilesystemencoding()),
|
||||
|
|
Loading…
Reference in New Issue