Beispiel #1
0
def write_bytes(filename: str, is_overwritable: bool = False) -> (
    BufferedIOBase):
    '''
    Open and return a filehandle suitable for writing the binary archive file
    with the passed filename.

    This function returns a :class:`file`-like object suitable for use wherever
    the :func:`open` builtin is callable (e.g., in ``with`` statements).

    Parameters
    ----------
    filename : str
        Relative or absolute path of the binary archive file to be written,
        whose **rightmost filetype** (i.e., substring suffixing the last ``.``
        character in this pathname) *must* be that of a supported archive
        format.
    is_overwritable : optional[bool]
        ``True`` if overwriting this file when this file already exists *or*
        ``False`` if raising an exception when this file already exists.
        Defaults to ``False`` for safety.

    Returns
    ----------
    BufferedIOBase
        :class:`file`-like object encapsulating this opened file.

    Raises
    ----------
    BetseArchiveException
        If this filename is *not* suffixed by a supported archive filetype.
    BetsePathException
        If this filename is that of an existing file or directory.
    '''

    # Avoid circular import dependencies.
    from betse.util.path import dirs, paths, pathnames

    # Raise an exception unless this filename has a supported archive filetype.
    die_unless_filetype(filename)

    # If this file is *NOT* overwritable, raise an exception if this path
    # already exists.
    if not is_overwritable:
        paths.die_if_path(filename)

    # Create the parent directory of this file if needed.
    dirs.make_parent_unless_dir(filename)

    # Filetype of this pathname. By the above validation, this filetype is
    # guaranteed to both exist and be a supported archive filetype. Hence, no
    # additional validation is required.
    filetype = pathnames.get_filetype_undotted(filename)

    # Callable writing archives of this filetype.
    writer = _ARCHIVE_FILETYPE_TO_WRITER[filetype]

    # Open and return a filehandle suitable for writing this archive.
    return writer(filename, is_overwritable=is_overwritable)
Beispiel #2
0
def writing_bytes(
    filename: str, is_overwritable: bool = False) -> BufferedIOBase:
    '''
    Open and return a filehandle suitable for writing the binary file with the
    passed filename, transparently compressing this file if the filetype of
    this filename is that of a supported archive format.

    This function returns a file-like object suitable for use wherever the
    :func:`open` builtin is callable (e.g., in ``with`` statements).

    Parameters
    ----------
    filename : str
        Relative or absolute path of the binary file to be written. If this
        filename is suffixed by a supported archive filetype (i.e., if the
        :func:`betse.util.path.archives.is_filetype` function returns ``True``
        for this filename), the returned filehandle automatically writes the
        compressed rather than uncompressed byte contents of this file.
    is_overwritable : optional[bool]
        ``True`` if overwriting this file when this file already exists *or*
        ``False`` if raising an exception when this file already exists.
        Defaults to ``False`` for safety.

    Returns
    ----------
    BufferedIOBase
        File-like object encapsulating this opened file.
    '''

    # Avoid circular import dependencies.
    from betse.util.path import archives, dirs, paths

    # Log this I/O operation.
    logs.log_debug('Writing bytes: %s', filename)

    # If this file is *NOT* overwritable, raise an exception if this path
    # already exists.
    if not is_overwritable:
        paths.die_if_path(filename)

    # Create the parent directory of this file if needed.
    dirs.make_parent_unless_dir(filename)

    # If this file is compressed, open and return a file handle writing
    # compressed bytes to this file.
    if archives.is_filetype(filename):
        return archives.write_bytes(filename, is_overwritable=is_overwritable)
    # Else, this file is uncompressed.
    else:
        # Mode with which to open this file for byte-oriented writing.
        mode = get_mode_write_bytes(is_overwritable)

        # Open and return a file handle writing uncompressed bytes to this file.
        return open(filename, mode=mode)
Beispiel #3
0
def writing_chars(
    filename: str,
    is_overwritable: bool = False,
    encoding: str = 'utf-8',
) -> TextIOWrapper:
    '''
    Open and return a filehandle suitable for writing the plaintext file with
    the passed filename encoded with the passed encoding.

    This function returns a file-like object suitable for use wherever the
    :func:`open` builtin is callable (e.g., in ``with`` statements).

    Parameters
    ----------
    filename : str
        Relative or absolute path of the plaintext file to be written.
    is_overwritable : optional[bool]
        ``True`` if overwriting this file when this file already exists *or*
        ``False`` if raising an exception when this file already exists.
        Defaults to ``False`` for safety.
    encoding : optional[str]
        Name of the encoding to be used. Defaults to UTF-8.

    Returns
    ----------
    TextIOWrapper
        File-like object encapsulating this opened file.
    '''

    # Avoid circular import dependencies.
    from betse.util.path import dirs, paths

    # Log this I/O operation.
    logs.log_debug('Writing chars: %s', filename)

    # If this file is *NOT* overwritable, raise an exception if this path
    # already exists.
    if not is_overwritable:
        paths.die_if_path(filename)

    # Create the parent directory of this file if needed.
    dirs.make_parent_unless_dir(filename)

    # Mode with which to open this file for character-oriented writing.
    mode = get_mode_write_chars(is_overwritable)

    # Open and return a file handle writing uncompressed bytes to this file.
    return open(filename, mode=mode, encoding=encoding)
Beispiel #4
0
def clone_worktree_shallow(
    branch_or_tag_name: str,
    src_dirname: str,
    trg_dirname: str,
) -> None:
    '''
    **Shallowly clone** (i.e., recursively copy without commit history) from
    the passed branch or tag name of the source Git working tree with the
    passed dirname into the target non-existing directory.

    For space efficiency, the target directory will *not* be a Git working tree
    (i.e., will *not* contain a ``.git`` subdirectory). This directory will
    only contain the contents of the source Git working tree isolated at
    either:

    * The ``HEAD`` of the branch with the passed name.
    * The commit referenced by the tag with the passed name.

    Caveats
    ----------
    Due to long-standing issues in Git itself, the passed branch or tag name
    *cannot* be the general-purpose SHA-1 hash of an arbitrary commit. While
    performing a shallow clone of an arbitrary commit from such a hash is
    technically feasible, doing so requires running a pipeline of multiple
    sequential Git subprocesses and hence exceeds the mandate of this function.

    Parameters
    ----------
    branch_or_tag_name : str
        Name of the branch or tag to clone from this source Git working tree.
    src_dirname : str
        Relative or absolute pathname of the source Git working tree to clone
        from.
    trg_dirname : str
        Relative or absolute pathname of the target non-existing directory to
        clone into.

    Raises
    ----------
    BetseGitException
        If this source directory is *not* a Git working tree.
    BetseDirException
        If this target directory already exists.

    See Also
    ----------
    https://stackoverflow.com/questions/26135216/why-isnt-there-a-git-clone-specific-commit-option
        StackOverflow question entitled "Why Isn't There A Git Clone Specific
        Commit Option?", detailing Git's current omission of such
        functionality.
    '''

    # Avoid circular import dependencies.
    from betse.util.path import paths, pathnames
    from betse.util.os.command import cmdrun

    # If this source directory is *NOT* a Git working tree, raise an exception.
    die_unless_worktree(src_dirname)

    # If this target directory already exists, raise an exception.
    paths.die_if_path(trg_dirname)

    # Absolute pathname of the source Git working tree to clone from. The
    # "git clone" command requires "file:///"-prefixed absolute pathnames.
    src_dirname = pathnames.canonicalize(src_dirname)

    # Git-specific URI of this source Git working tree.
    src_dir_uri = 'file://' + src_dirname

    # Tuple of shell words comprising the Git command performing this clone.
    git_command = (
        'git',
        'clone',

        # Branch or tag to be cloned.
        '--branch',
        branch_or_tag_name,

        # Perform a shallow clone.
        '--depth',
        '1',

        # From this source to target directory.
        src_dir_uri,
        trg_dirname,
    )

    # Shallowly clone this source to target directory. Contrary to expectation,
    # "git" redirects non-error or -warning output resembling the following to
    # stderr rather than stdout:
    #
    #    Cloning into '/tmp/pytest-of-leycec/pytest-26/cli_sim_backward_compatibility0/betse_old'...
    #    Note: checking out 'd7d6bf6d61ff2b467f9983bc6395a8ba9d0f234e'.
    #
    #    You are in 'detached HEAD' state. You can look around, make experimental
    #    changes and commit them, and you can discard any commits you make in this
    #    state without impacting any branches by performing another checkout.
    #
    #    If you want to create a new branch to retain commits you create, you may
    #    do so (now or later) by using -b with the checkout command again. Example:
    #
    #      git checkout -b <new-branch-name>
    #
    # This is distasteful. Clearly, this output should *NOT* be logged as either
    # an error or warning. Instead, both stdout and stderr output would ideally
    # be logged as informational messages. Unfortunately, the following call
    # appears to erroneously squelch rather than redirect most stderr output:
    #
    #    cmdrun.log_output_or_die(
    #        command_words=git_command,
    #        stdout_log_level=LogLevel.INFO,
    #        stderr_log_level=LogLevel.ERROR,
    #    )
    #
    # Frankly, we have no idea what is happening here -- and it doesn't
    # particularly matter. The current approach, while non-ideal, suffices.
    cmdrun.run_or_die(command_words=git_command)
Beispiel #5
0
def copy(
    # Mandatory parameters.
    src_filename: str,
    trg_filename: str,

    # Optional parameters.
    is_overwritable: bool = False,
) -> None:
    '''
    Copy the passed source file to the passed target file or directory,
    raising an exception if that target file already exists unless the
    ``is_overwritable`` parameter is explicitly passed as ``True``.

    If the:

    * Source file is a symbolic link, this link (rather than its transitive
      target) will be copied and hence preserved.
    * Target file is a directory, the basename of the source file will be
      appended to this directory -- much like the standard ``cp`` POSIX
      command.

    For safety, the target file is:

    * Copied in a manner maximally preserving *all* existing metadata of the
      source file. This includes owner, group, permissions, times (e.g.,
      access, creation, modification), and extended attributes (if any).
    * *Not* overwritten by default. If this file already exists, an exception
      is raised unless the ``is_overwritable`` parameter is explicitly passed
      as ``True``. For brevity, consider calling the :func:`copy_overwritable`.

    Parameters
    ----------
    src_filename : str
        Absolute or relative path of the source file to be copied from.
    trg_filename : str
        Absolute or relative path of the target file to be copied to.
    is_overwritable : optional[bool]
        If this target file already exists and this boolean is ``True``, this
        file is silently overwritten; else, an exception is raised. Defaults to
        ``False``.

    Raises
    ----------
    BetseFileException
        If either the:

        * Source file does not exist.
        * Target file already exists *and* ``is_overwritable`` is ``False``.

    See Also
    ----------
    :func:`copy_overwritable`
        Higher-level function wrapping this lower-level function by
        unconditionally passing ``is_overwritable`` parameter as ``True``.
    '''

    # Avoid circular import dependencies.
    from betse.util.path import dirs, paths, pathnames

    # Log this copy.
    logs.log_debug('Copying file: %s -> %s', src_filename, trg_filename)

    # If this source file does *NOT* exist, raise an exception.
    die_unless_file(src_filename)

    # If this target file is a directory, append the basename of the passed
    # source file to this directory -- much like the "cp" POSIX command.
    if dirs.is_dir(trg_filename):
        trg_filename = pathnames.join(trg_filename,
                                      pathnames.get_basename(src_filename))

    # If this target file already exists but is *NOT* overwritable, raise an
    # exception.
    if not is_overwritable:
        paths.die_if_path(trg_filename)

    # Create the directory containing this target file *BEFORE* calling the
    # shutil.copy2() function, which assumes this directory to exist.
    dirs.make_parent_unless_dir(trg_filename)

    # Perform this copy in a manner preserving metadata and symbolic links.
    shutil.copy2(src_filename, trg_filename, follow_symlinks=False)