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)
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)
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)
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)
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)