diff --git a/git/__init__.py b/git/__init__.py index 5b55e59eb..46d54a960 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -3,8 +3,10 @@ # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ + # flake8: noqa # @PydevCodeAnalysisIgnore + from git.exc import * # @NoMove @IgnorePep8 from typing import List, Optional, Sequence, Tuple, Union, TYPE_CHECKING from git.types import PathLike diff --git a/git/cmd.py b/git/cmd.py index e18bff8c1..f8b6c9e78 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -3,7 +3,9 @@ # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ + from __future__ import annotations + import re import contextlib import io @@ -103,20 +105,21 @@ def handle_process_output( decode_streams: bool = True, kill_after_timeout: Union[None, float] = None, ) -> None: - """Registers for notifications to learn that process output is ready to read, and dispatches lines to - the respective line handlers. + """Register for notifications to learn that process output is ready to read, and + dispatch lines to the respective line handlers. + This function returns once the finalizer returns. - :return: result of finalizer - :param process: subprocess.Popen instance + :return: Result of finalizer + :param process: :class:`subprocess.Popen` instance :param stdout_handler: f(stdout_line_string), or None :param stderr_handler: f(stderr_line_string), or None :param finalizer: f(proc) - wait for proc to finish :param decode_streams: - Assume stdout/stderr streams are binary and decode them before pushing \ + Assume stdout/stderr streams are binary and decode them before pushing their contents to handlers. - Set it to False if `universal_newline == True` (then streams are in text-mode) - or if decoding must happen later (i.e. for Diffs). + Set it to False if ``universal_newlines == True`` (then streams are in + text mode) or if decoding must happen later (i.e. for Diffs). :param kill_after_timeout: float or None, Default = None To specify a timeout in seconds for the git command, after which the process @@ -232,13 +235,11 @@ def dict_to_slots_and__excluded_are_none(self: object, d: Mapping[str, Any], exc # see https://docs.python.org/3/library/subprocess.html#subprocess.Popen.send_signal PROC_CREATIONFLAGS = ( CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP if is_win else 0 # type: ignore[attr-defined] -) # mypy error if not windows +) # mypy error if not Windows. class Git(LazyMixin): - - """ - The Git class manages communication with the Git binary. + """The Git class manages communication with the Git binary. It provides a convenient interface to calling the Git binary, such as in:: @@ -274,44 +275,41 @@ def __setstate__(self, d: Dict[str, Any]) -> None: # CONFIGURATION - git_exec_name = "git" # default that should work on linux and windows + git_exec_name = "git" # Default that should work on Linux and Windows. - # Enables debugging of GitPython's git commands + # Enables debugging of GitPython's git commands. GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) # If True, a shell will be used when executing git commands. # This should only be desirable on Windows, see https://github.com/gitpython-developers/GitPython/pull/126 # and check `git/test_repo.py:TestRepo.test_untracked_files()` TC for an example where it is required. - # Override this value using `Git.USE_SHELL = True` + # Override this value using `Git.USE_SHELL = True`. USE_SHELL = False - # Provide the full path to the git executable. Otherwise it assumes git is in the path + # Provide the full path to the git executable. Otherwise it assumes git is in the path. _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE" _refresh_env_var = "GIT_PYTHON_REFRESH" GIT_PYTHON_GIT_EXECUTABLE = None - # note that the git executable is actually found during the refresh step in - # the top level __init__ + # Note that the git executable is actually found during the refresh step in + # the top level __init__. @classmethod def refresh(cls, path: Union[None, PathLike] = None) -> bool: """This gets called by the refresh function (see the top level __init__).""" - # discern which path to refresh with + # Discern which path to refresh with. if path is not None: new_git = os.path.expanduser(path) new_git = os.path.abspath(new_git) else: new_git = os.environ.get(cls._git_exec_env_var, cls.git_exec_name) - # keep track of the old and new git executable path + # Keep track of the old and new git executable path. old_git = cls.GIT_PYTHON_GIT_EXECUTABLE cls.GIT_PYTHON_GIT_EXECUTABLE = new_git - # test if the new git executable path is valid - - # - a GitCommandNotFound error is spawned by ourselves - # - a PermissionError is spawned if the git executable provided - # cannot be executed for whatever reason - + # Test if the new git executable path is valid. A GitCommandNotFound error is + # spawned by us. A PermissionError is spawned if the git executable cannot be + # executed for whatever reason. has_git = False try: cls().version() @@ -319,7 +317,7 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: except (GitCommandNotFound, PermissionError): pass - # warn or raise exception if test failed + # Warn or raise exception if test failed. if not has_git: err = ( dedent( @@ -334,18 +332,18 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: % cls._git_exec_env_var ) - # revert to whatever the old_git was + # Revert to whatever the old_git was. cls.GIT_PYTHON_GIT_EXECUTABLE = old_git if old_git is None: - # on the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is - # None) we only are quiet, warn, or error depending on the - # GIT_PYTHON_REFRESH value - - # determine what the user wants to happen during the initial - # refresh we expect GIT_PYTHON_REFRESH to either be unset or - # be one of the following values: - # 0|q|quiet|s|silence + # On the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is None) we only + # are quiet, warn, or error depending on the GIT_PYTHON_REFRESH value. + + # Determine what the user wants to happen during the initial refresh we + # expect GIT_PYTHON_REFRESH to either be unset or be one of the + # following values: + # + # 0|q|quiet|s|silence|n|none # 1|w|warn|warning # 2|r|raise|e|error @@ -410,14 +408,13 @@ def refresh(cls, path: Union[None, PathLike] = None) -> bool: ) raise ImportError(err) - # we get here if this was the init refresh and the refresh mode - # was not error, go ahead and set the GIT_PYTHON_GIT_EXECUTABLE - # such that we discern the difference between a first import - # and a second import + # We get here if this was the init refresh and the refresh mode was not + # error. Go ahead and set the GIT_PYTHON_GIT_EXECUTABLE such that we + # discern the difference between a first import and a second import. cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name else: - # after the first refresh (when GIT_PYTHON_GIT_EXECUTABLE - # is no longer None) we raise an exception + # After the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is no longer + # None) we raise an exception. raise GitCommandNotFound("git", err) return has_git @@ -438,18 +435,18 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> str: @classmethod def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike: + """Remove any backslashes from urls to be written in config files. + + Windows might create config files containing paths with backslashes, + but git stops liking them as it will escape the backslashes. Hence we + undo the escaping just to be sure. + """ if is_cygwin is None: is_cygwin = cls.is_cygwin() if is_cygwin: url = cygpath(url) else: - """Remove any backslashes from urls to be written in config files. - - Windows might create config files containing paths with backslashes, - but git stops liking them as it will escape the backslashes. - Hence we undo the escaping just to be sure. - """ url = os.path.expandvars(url) if url.startswith("~"): url = os.path.expanduser(url) @@ -458,12 +455,11 @@ def polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike: @classmethod def check_unsafe_protocols(cls, url: str) -> None: - """ - Check for unsafe protocols. + """Check for unsafe protocols. Apart from the usual protocols (http, git, ssh), - Git allows "remote helpers" that have the form ``::
``, - one of these helpers (``ext::``) can be used to invoke any arbitrary command. + Git allows "remote helpers" that have the form ``::
``. + One of these helpers (``ext::``) can be used to invoke any arbitrary command. See: @@ -479,8 +475,7 @@ def check_unsafe_protocols(cls, url: str) -> None: @classmethod def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None: - """ - Check for unsafe options. + """Check for unsafe options. Some options that are passed to `git ` can be used to execute arbitrary commands, this are blocked by default. @@ -496,17 +491,21 @@ def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> ) class AutoInterrupt(object): - """Kill/Interrupt the stored process instance once this instance goes out of scope. It is - used to prevent processes piling up in case iterators stop reading. - Besides all attributes are wired through to the contained process object. + """Process wrapper that terminates the wrapped process on finalization. + + This kills/interrupts the stored process instance once this instance goes out of + scope. It is used to prevent processes piling up in case iterators stop reading. - The wait method was overridden to perform automatic status code checking - and possibly raise.""" + All attributes are wired through to the contained process object. + + The wait method is overridden to perform automatic status code checking and + possibly raise. + """ __slots__ = ("proc", "args", "status") # If this is non-zero it will override any status code during - # _terminate, used to prevent race conditions in testing + # _terminate, used to prevent race conditions in testing. _status_code_if_terminate: int = 0 def __init__(self, proc: Union[None, subprocess.Popen], args: Any) -> None: @@ -527,7 +526,7 @@ def _terminate(self) -> None: proc.stdout.close() if proc.stderr: proc.stderr.close() - # did the process finish already so we have a return code ? + # Did the process finish already so we have a return code? try: if proc.poll() is not None: self.status = self._status_code_if_terminate or proc.poll() @@ -535,23 +534,23 @@ def _terminate(self) -> None: except OSError as ex: log.info("Ignored error after process had died: %r", ex) - # can be that nothing really exists anymore ... + # It can be that nothing really exists anymore... if os is None or getattr(os, "kill", None) is None: return None - # try to kill it + # Try to kill it. try: proc.terminate() - status = proc.wait() # ensure process goes away + status = proc.wait() # Ensure the process goes away. self.status = self._status_code_if_terminate or status except OSError as ex: log.info("Ignored error after process had died: %r", ex) except AttributeError: - # try windows - # for some reason, providing None for stdout/stderr still prints something. This is why - # we simply use the shell and redirect to nul. Its slower than CreateProcess, question - # is whether we really want to see all these messages. Its annoying no matter what. + # Try Windows. + # For some reason, providing None for stdout/stderr still prints something. This is why + # we simply use the shell and redirect to nul. Slower than CreateProcess. The question + # is whether we really want to see all these messages. It's annoying no matter what. if is_win: call( ("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(proc.pid)), @@ -571,7 +570,8 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int: :param stderr: Previously read value of stderr, in case stderr is already closed. :warn: May deadlock if output or error pipes are used and not handled separately. - :raise GitCommandError: if the return status is not 0""" + :raise GitCommandError: If the return status is not 0. + """ if stderr is None: stderr_b = b"" stderr_b = force_bytes(data=stderr, encoding="utf-8") @@ -579,7 +579,7 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int: if self.proc is not None: status = self.proc.wait() p_stderr = self.proc.stderr - else: # Assume the underlying proc was killed earlier or never existed + else: # Assume the underlying proc was killed earlier or never existed. status = self.status p_stderr = None @@ -605,19 +605,22 @@ def read_all_from_possibly_closed_stream(stream: Union[IO[bytes], None]) -> byte class CatFileContentStream(object): """Object representing a sized read-only stream returning the contents of an object. - It behaves like a stream, but counts the data read and simulates an empty + + This behaves like a stream, but counts the data read and simulates an empty stream once our sized content region is empty. - If not all data is read to the end of the object's lifetime, we read the - rest to assure the underlying stream continues to work.""" + + If not all data are read to the end of the object's lifetime, we read the + rest to assure the underlying stream continues to work. + """ __slots__: Tuple[str, ...] = ("_stream", "_nbr", "_size") def __init__(self, size: int, stream: IO[bytes]) -> None: self._stream = stream self._size = size - self._nbr = 0 # num bytes read + self._nbr = 0 # Number of bytes read. - # special case: if the object is empty, has null bytes, get the + # Special case: If the object is empty, has null bytes, get the # final newline right away. if size == 0: stream.read(1) @@ -628,16 +631,16 @@ def read(self, size: int = -1) -> bytes: if bytes_left == 0: return b"" if size > -1: - # assure we don't try to read past our limit + # Ensure we don't try to read past our limit. size = min(bytes_left, size) else: - # they try to read all, make sure its not more than what remains + # They try to read all, make sure it's not more than what remains. size = bytes_left # END check early depletion data = self._stream.read(size) self._nbr += len(data) - # check for depletion, read our final byte to make the stream usable by others + # Check for depletion, read our final byte to make the stream usable by others. if self._size - self._nbr == 0: self._stream.read(1) # final newline # END finish reading @@ -647,7 +650,7 @@ def readline(self, size: int = -1) -> bytes: if self._nbr == self._size: return b"" - # clamp size to lowest allowed value + # Clamp size to lowest allowed value. bytes_left = self._size - self._nbr if size > -1: size = min(bytes_left, size) @@ -658,7 +661,7 @@ def readline(self, size: int = -1) -> bytes: data = self._stream.readline(size) self._nbr += len(data) - # handle final byte + # Handle final byte. if self._size - self._nbr == 0: self._stream.read(1) # END finish reading @@ -669,7 +672,7 @@ def readlines(self, size: int = -1) -> List[bytes]: if self._nbr == self._size: return [] - # leave all additional logic to our readline method, we just check the size + # Leave all additional logic to our readline method, we just check the size. out = [] nbr = 0 while True: @@ -701,8 +704,8 @@ def __next__(self) -> bytes: def __del__(self) -> None: bytes_left = self._size - self._nbr if bytes_left: - # read and discard - seeking is impossible within a stream - # includes terminating newline + # Read and discard - seeking is impossible within a stream. + # This includes any terminating newline. self._stream.read(bytes_left + 1) # END handle incomplete read @@ -711,9 +714,10 @@ def __init__(self, working_dir: Union[None, PathLike] = None): :param working_dir: Git directory we should work in. If None, we always work in the current - directory as returned by os.getcwd(). + directory as returned by :func:`os.getcwd`. It is meant to be the working tree directory if available, or the - .git directory in case of bare repositories.""" + ``.git`` directory in case of bare repositories. + """ super(Git, self).__init__() self._working_dir = expand_path(working_dir) self._git_options: Union[List[str], Tuple[str, ...]] = () @@ -722,7 +726,7 @@ def __init__(self, working_dir: Union[None, PathLike] = None): # Extra environment variables to pass to git commands self._environment: Dict[str, str] = {} - # cached command slots + # Cached command slots self.cat_file_header: Union[None, TBD] = None self.cat_file_all: Union[None, TBD] = None @@ -730,28 +734,30 @@ def __getattr__(self, name: str) -> Any: """A convenience method as it allows to call the command as if it was an object. - :return: Callable object that will execute call _call_process with your arguments.""" + :return: + Callable object that will execute call :meth:`_call_process` with + your arguments. + """ if name[0] == "_": return LazyMixin.__getattr__(self, name) return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) def set_persistent_git_options(self, **kwargs: Any) -> None: - """Specify command line options to the git executable - for subsequent subcommand calls. + """Specify command line options to the git executable for subsequent + subcommand calls. :param kwargs: - is a dict of keyword arguments. - These arguments are passed as in _call_process - but will be passed to the git command rather than - the subcommand. + A dict of keyword arguments. + These arguments are passed as in :meth:`_call_process`, but will be + passed to the git command rather than the subcommand. """ self._persistent_git_options = self.transform_kwargs(split_single_char_options=True, **kwargs) def _set_cache_(self, attr: str) -> None: if attr == "_version_info": - # We only use the first 4 numbers, as everything else could be strings in fact (on windows) - process_version = self._call_process("version") # should be as default *args and **kwargs used + # We only use the first 4 numbers, as everything else could be strings in fact (on Windows). + process_version = self._call_process("version") # Should be as default *args and **kwargs used. version_numbers = process_version.split(" ")[2] self._version_info = cast( @@ -772,7 +778,9 @@ def version_info(self) -> Tuple[int, int, int, int]: """ :return: tuple(int, int, int, int) tuple with integers representing the major, minor and additional version numbers as parsed from git version. - This value is generated on demand and is cached.""" + + This value is generated on demand and is cached. + """ return self._version_info @overload @@ -839,7 +847,7 @@ def execute( strip_newline_in_stdout: bool = True, **subprocess_kwargs: Any, ) -> Union[str, bytes, Tuple[int, Union[str, bytes], str], AutoInterrupt]: - """Handles executing the command and consumes and returns the returned + R"""Handle executing the command, and consume and return the returned information (stdout). :param command: @@ -848,7 +856,7 @@ def execute( program to execute is the first item in the args sequence or string. :param istream: - Standard input filehandle passed to `subprocess.Popen`. + Standard input filehandle passed to :class:`subprocess.Popen`. :param with_extended_output: Whether to return a (status, stdout, stderr) tuple. @@ -858,17 +866,17 @@ def execute( :param as_process: Whether to return the created process instance directly from which - streams can be read on demand. This will render with_extended_output and - with_exceptions ineffective - the caller will have to deal with the details. - It is important to note that the process will be placed into an AutoInterrupt - wrapper that will interrupt the process once it goes out of scope. If you - use the command in iterators, you should pass the whole process instance - instead of a single stream. + streams can be read on demand. This will render `with_extended_output` + and `with_exceptions` ineffective - the caller will have to deal with + the details. It is important to note that the process will be placed + into an :class:`AutoInterrupt` wrapper that will interrupt the process + once it goes out of scope. If you use the command in iterators, you + should pass the whole process instance instead of a single stream. :param output_stream: If set to a file-like object, data produced by the git command will be output to the given stream directly. - This feature only has any effect if as_process is False. Processes will + This feature only has any effect if `as_process` is False. Processes will always be created with a pipe due to issues with subprocess. This merely is a workaround as data will be copied from the output pipe to the given output stream directly. @@ -881,13 +889,13 @@ def execute( :param kill_after_timeout: Specifies a timeout in seconds for the git command, after which the process - should be killed. This will have no effect if as_process is set to True. It is - set to None by default and will let the process run until the timeout is - explicitly specified. This feature is not supported on Windows. It's also worth - noting that kill_after_timeout uses SIGKILL, which can have negative side - effects on a repository. For example, stale locks in case of ``git gc`` could - render the repository incapable of accepting changes until the lock is manually - removed. + should be killed. This will have no effect if `as_process` is set to True. + It is set to None by default and will let the process run until the timeout + is explicitly specified. This feature is not supported on Windows. It's also + worth noting that `kill_after_timeout` uses SIGKILL, which can have negative + side effects on a repository. For example, stale locks in case of ``git gc`` + could render the repository incapable of accepting changes until the lock is + manually removed. :param with_stdout: If True, default True, we open stdout on the created process. @@ -901,7 +909,7 @@ def execute( It overrides :attr:`USE_SHELL` if it is not `None`. :param env: - A dictionary of environment variables to be passed to `subprocess.Popen`. + A dictionary of environment variables to be passed to :class:`subprocess.Popen`. :param max_chunk_size: Maximum number of bytes in one chunk of data passed to the output_stream in @@ -909,11 +917,11 @@ def execute( the default value is used. :param strip_newline_in_stdout: - Whether to strip the trailing ``\\n`` of the command stdout. + Whether to strip the trailing ``\n`` of the command stdout. :param subprocess_kwargs: - Keyword arguments to be passed to `subprocess.Popen`. Please note that - some of the valid kwargs are already set by this method; the ones you + Keyword arguments to be passed to :class:`subprocess.Popen`. Please note + that some of the valid kwargs are already set by this method; the ones you specify may not be the same ones. :return: @@ -931,8 +939,9 @@ def execute( :note: If you add additional keyword arguments to the signature of this method, - you must update the execute_kwargs tuple housed in this module.""" - # Remove password for the command if present + you must update the execute_kwargs tuple housed in this module. + """ + # Remove password for the command if present. redacted_command = remove_password_if_present(command) if self.GIT_PYTHON_TRACE and (self.GIT_PYTHON_TRACE != "full" or as_process): log.info(" ".join(redacted_command)) @@ -945,12 +954,12 @@ def execute( except FileNotFoundError: cwd = None - # Start the process + # Start the process. inline_env = env env = os.environ.copy() - # Attempt to force all output to plain ascii english, which is what some parsing code - # may expect. - # According to stackoverflow (http://goo.gl/l74GC8), we are setting LANGUAGE as well + # Attempt to force all output to plain ASCII English, which is what some parsing + # code may expect. + # According to https://askubuntu.com/a/311796, we are setting LANGUAGE as well # just to be sure. env["LANGUAGE"] = "C" env["LC_ALL"] = "C" @@ -994,7 +1003,7 @@ def execute( stderr=PIPE, stdout=stdout_sink, shell=shell, - close_fds=is_posix, # unsupported on windows + close_fds=is_posix, # Unsupported on Windows. universal_newlines=universal_newlines, creationflags=PROC_CREATIONFLAGS, **subprocess_kwargs, @@ -1002,7 +1011,7 @@ def execute( except cmd_not_found_exception as err: raise GitCommandNotFound(redacted_command, err) from err else: - # replace with a typeguard for Popen[bytes]? + # Replace with a typeguard for Popen[bytes]? proc.stdout = cast(BinaryIO, proc.stdout) proc.stderr = cast(BinaryIO, proc.stderr) @@ -1024,7 +1033,7 @@ def kill_process(pid: int) -> None: if local_pid.isdigit(): child_pids.append(int(local_pid)) try: - # Windows does not have SIGKILL, so use SIGTERM instead + # Windows does not have SIGKILL, so use SIGTERM instead. sig = getattr(signal, "SIGKILL", signal.SIGTERM) os.kill(pid, sig) for child_pid in child_pids: @@ -1032,7 +1041,7 @@ def kill_process(pid: int) -> None: os.kill(child_pid, sig) except OSError: pass - kill_check.set() # tell the main routine that the process was killed + kill_check.set() # Tell the main routine that the process was killed. except OSError: # It is possible that the process gets completed in the duration after timeout # happens and before we try to kill the process. @@ -1045,7 +1054,7 @@ def kill_process(pid: int) -> None: kill_check = threading.Event() watchdog = threading.Timer(kill_after_timeout, kill_process, args=(proc.pid,)) - # Wait for the process to return + # Wait for the process to return. status = 0 stdout_value: Union[str, bytes] = b"" stderr_value: Union[str, bytes] = b"" @@ -1076,7 +1085,7 @@ def kill_process(pid: int) -> None: stream_copy(proc.stdout, output_stream, max_chunk_size) stdout_value = proc.stdout.read() stderr_value = proc.stderr.read() - # strip trailing "\n" + # Strip trailing "\n". if stderr_value.endswith(newline): # type: ignore stderr_value = stderr_value[:-1] status = proc.wait() @@ -1110,10 +1119,10 @@ def as_text(stdout_value: Union[bytes, str]) -> str: if with_exceptions and status != 0: raise GitCommandError(redacted_command, status, stderr_value, stdout_value) - if isinstance(stdout_value, bytes) and stdout_as_string: # could also be output_stream + if isinstance(stdout_value, bytes) and stdout_as_string: # Could also be output_stream. stdout_value = safe_decode(stdout_value) - # Allow access to the command's status code + # Allow access to the command's status code. if with_extended_output: return (status, stdout_value, safe_decode(stderr_value)) else: @@ -1123,18 +1132,18 @@ def environment(self) -> Dict[str, str]: return self._environment def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]: - """ - Set environment variables for future git invocations. Return all changed - values in a format that can be passed back into this function to revert - the changes: + """Set environment variables for future git invocations. Return all changed + values in a format that can be passed back into this function to revert the + changes. ``Examples``:: old_env = self.update_environment(PWD='/tmp') self.update_environment(**old_env) - :param kwargs: environment variables to use for git processes - :return: dict that maps environment variables to their old values + :param kwargs: Environment variables to use for git processes + + :return: Dict that maps environment variables to their old values """ old_env = {} for key, value in kwargs.items(): @@ -1150,16 +1159,15 @@ def update_environment(self, **kwargs: Any) -> Dict[str, Union[str, None]]: @contextlib.contextmanager def custom_environment(self, **kwargs: Any) -> Iterator[None]: - """ - A context manager around the above ``update_environment`` method to restore the - environment back to its previous state after operation. + """A context manager around the above :meth:`update_environment` method to + restore the environment back to its previous state after operation. ``Examples``:: with self.custom_environment(GIT_SSH='/bin/ssh_wrapper'): repo.remotes.origin.fetch() - :param kwargs: see update_environment + :param kwargs: See :meth:`update_environment` """ old_env = self.update_environment(**kwargs) try: @@ -1184,7 +1192,7 @@ def transform_kwarg(self, name: str, value: Any, split_single_char_options: bool return [] def transform_kwargs(self, split_single_char_options: bool = True, **kwargs: Any) -> List[str]: - """Transforms Python style kwargs into git command line options.""" + """Transform Python style kwargs into git command line options.""" args = [] for k, v in kwargs.items(): if isinstance(v, (list, tuple)): @@ -1206,23 +1214,22 @@ def _unpack_args(cls, arg_list: Sequence[str]) -> List[str]: return outlist def __call__(self, **kwargs: Any) -> "Git": - """Specify command line options to the git executable - for a subcommand call. + """Specify command line options to the git executable for a subcommand call. :param kwargs: - is a dict of keyword arguments. - these arguments are passed as in _call_process - but will be passed to the git command rather than - the subcommand. + A dict of keyword arguments. + These arguments are passed as in :meth:`_call_process`, but will be + passed to the git command rather than the subcommand. ``Examples``:: - git(work_tree='/tmp').difftool()""" + git(work_tree='/tmp').difftool() + """ self._git_options = self.transform_kwargs(split_single_char_options=True, **kwargs) return self @overload def _call_process(self, method: str, *args: None, **kwargs: None) -> str: - ... # if no args given, execute called with all defaults + ... # If no args were given, execute the call with all defaults. @overload def _call_process( @@ -1248,20 +1255,20 @@ def _call_process( the result as a string. :param method: - is the command. Contained "_" characters will be converted to dashes, - such as in 'ls_files' to call 'ls-files'. + The command. Contained ``_`` characters will be converted to dashes, + such as in ``ls_files`` to call ``ls-files``. :param args: - is the list of arguments. If None is included, it will be pruned. + The list of arguments. If None is included, it will be pruned. This allows your commands to call git more conveniently as None is realized as non-existent. :param kwargs: - It contains key-values for the following: - - the :meth:`execute()` kwds, as listed in :var:`execute_kwargs`; - - "command options" to be converted by :meth:`transform_kwargs()`; - - the `'insert_kwargs_after'` key which its value must match one of ``*args`` - and any cmd-options will be appended after the matched arg. + Contains key-values for the following: + - The :meth:`execute()` kwds, as listed in :var:`execute_kwargs`. + - "Command options" to be converted by :meth:`transform_kwargs`. + - The `'insert_kwargs_after'` key which its value must match one of ``*args``. + It also contains any command options, to be appended after the matched arg. Examples:: @@ -1271,17 +1278,18 @@ def _call_process( git rev-list max-count 10 --header master - :return: Same as ``execute`` - if no args given used execute default (esp. as_process = False, stdout_as_string = True) - and return str""" - # Handle optional arguments prior to calling transform_kwargs - # otherwise these'll end up in args, which is bad. + :return: Same as :meth:`execute`. + If no args are given, used :meth:`execute`'s default (especially + ``as_process = False``, ``stdout_as_string = True``) and return str. + """ + # Handle optional arguments prior to calling transform_kwargs. + # Otherwise these'll end up in args, which is bad. exec_kwargs = {k: v for k, v in kwargs.items() if k in execute_kwargs} opts_kwargs = {k: v for k, v in kwargs.items() if k not in execute_kwargs} insert_after_this_arg = opts_kwargs.pop("insert_kwargs_after", None) - # Prepare the argument list + # Prepare the argument list. opt_args = self.transform_kwargs(**opts_kwargs) ext_args = self._unpack_args([a for a in args if a is not None]) @@ -1302,11 +1310,10 @@ def _call_process( call = [self.GIT_PYTHON_GIT_EXECUTABLE] - # add persistent git options + # Add persistent git options. call.extend(self._persistent_git_options) - # add the git options, then reset to empty - # to avoid side_effects + # Add the git options, then reset to empty to avoid side effects. call.extend(self._git_options) self._git_options = () @@ -1322,7 +1329,7 @@ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]: :return: (hex_sha, type_string, size_as_int) - :raise ValueError: if the header contains indication for an error due to + :raise ValueError: If the header contains indication for an error due to incorrect input sha""" tokens = header_line.split() if len(tokens) != 3: @@ -1338,12 +1345,12 @@ def _parse_object_header(self, header_line: str) -> Tuple[str, str, int]: return (tokens[0], tokens[1], int(tokens[2])) def _prepare_ref(self, ref: AnyStr) -> bytes: - # required for command to separate refs on stdin, as bytes + # Required for command to separate refs on stdin, as bytes. if isinstance(ref, bytes): - # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text + # Assume 40 bytes hexsha - bin-to-ascii for some reason returns bytes, not text. refstr: str = ref.decode("ascii") elif not isinstance(ref, str): - refstr = str(ref) # could be ref-object + refstr = str(ref) # Could be ref-object. else: refstr = ref @@ -1379,7 +1386,8 @@ def get_object_header(self, ref: str) -> Tuple[str, str, int]: :note: The method will only suffer from the costs of command invocation once and reuses the command in subsequent calls. - :return: (hexsha, type_string, size_as_int)""" + :return: (hexsha, type_string, size_as_int) + """ cmd = self._get_persistent_cmd("cat_file_header", "cat_file", batch_check=True) return self.__get_object_header(cmd, ref) @@ -1387,7 +1395,8 @@ def get_object_data(self, ref: str) -> Tuple[str, str, int, bytes]: """As get_object_header, but returns object data as well. :return: (hexsha, type_string, size_as_int, data_string) - :note: not threadsafe""" + :note: Not threadsafe. + """ hexsha, typename, size, stream = self.stream_object_data(ref) data = stream.read(size) del stream @@ -1397,7 +1406,8 @@ def stream_object_data(self, ref: str) -> Tuple[str, str, int, "Git.CatFileConte """As get_object_header, but returns the data as a stream. :return: (hexsha, type_string, size_as_int, stream) - :note: This method is not threadsafe, you need one independent Command instance per thread to be safe!""" + :note: This method is not threadsafe, you need one independent Command instance per thread to be safe! + """ cmd = self._get_persistent_cmd("cat_file_all", "cat_file", batch=True) hexsha, typename, size = self.__get_object_header(cmd, ref) cmd_stdout = cmd.stdout if cmd.stdout is not None else io.BytesIO() @@ -1408,7 +1418,8 @@ def clear_cache(self) -> "Git": Currently persistent commands will be interrupted. - :return: self""" + :return: self + """ for cmd in (self.cat_file_all, self.cat_file_header): if cmd: cmd.__del__() diff --git a/git/compat.py b/git/compat.py index 624f26116..93e06d2ec 100644 --- a/git/compat.py +++ b/git/compat.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- -# config.py +# compat.py # Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ -"""utilities to help provide compatibility with python 3""" + +"""Utilities to help provide compatibility with Python 3.""" + # flake8: noqa import locale @@ -50,7 +52,7 @@ def safe_decode(s: AnyStr) -> str: def safe_decode(s: Union[AnyStr, None]) -> Optional[str]: - """Safely decodes a binary string to unicode""" + """Safely decode a binary string to Unicode.""" if isinstance(s, str): return s elif isinstance(s, bytes): @@ -72,7 +74,7 @@ def safe_encode(s: AnyStr) -> bytes: def safe_encode(s: Optional[AnyStr]) -> Optional[bytes]: - """Safely encodes a binary string to unicode""" + """Safely encode a binary string to Unicode.""" if isinstance(s, str): return s.encode(defenc) elif isinstance(s, bytes): @@ -94,7 +96,7 @@ def win_encode(s: AnyStr) -> bytes: def win_encode(s: Optional[AnyStr]) -> Optional[bytes]: - """Encode unicodes for process arguments on Windows.""" + """Encode Unicode strings for process arguments on Windows.""" if isinstance(s, str): return s.encode(locale.getpreferredencoding(False)) elif isinstance(s, bytes): diff --git a/git/config.py b/git/config.py index 76b149179..31b8a665d 100644 --- a/git/config.py +++ b/git/config.py @@ -3,8 +3,9 @@ # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ + """Module containing module parser implementation able to properly read and write -configuration files""" +configuration files.""" import sys import abc @@ -85,12 +86,12 @@ class MetaParserBuilder(abc.ABCMeta): # noqa: B024 - """Utility class wrapping base-class methods into decorators that assure read-only properties""" + """Utility class wrapping base-class methods into decorators that assure read-only properties.""" def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParserBuilder": + """Equip all base-class methods with a needs_values decorator, and all non-const + methods with a set_dirty_and_flush_changes decorator in addition to that. """ - Equip all base-class methods with a needs_values decorator, and all non-const methods - with a set_dirty_and_flush_changes decorator in addition to that.""" kmm = "_mutating_methods_" if kmm in clsdict: mutating_methods = clsdict[kmm] @@ -114,7 +115,7 @@ def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParse def needs_values(func: Callable[..., _T]) -> Callable[..., _T]: - """Returns method assuring we read values (on demand) before we try to access them""" + """Return a method for ensuring we read values (on demand) before we try to access them.""" @wraps(func) def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: @@ -126,9 +127,10 @@ def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _ def set_dirty_and_flush_changes(non_const_func: Callable[..., _T]) -> Callable[..., _T]: - """Return method that checks whether given non constant function may be called. - If so, the instance will be set dirty. - Additionally, we flush the changes right to disk""" + """Return a method that checks whether given non constant function may be called. + + If so, the instance will be set dirty. Additionally, we flush the changes right to disk. + """ def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: rval = non_const_func(self, *args, **kwargs) @@ -142,14 +144,13 @@ def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: class SectionConstraint(Generic[T_ConfigParser]): - """Constrains a ConfigParser to only option commands which are constrained to always use the section we have been initialized with. It supports all ConfigParser methods that operate on an option. - :note: - If used as a context manager, will release the wrapped ConfigParser.""" + :note: If used as a context manager, will release the wrapped ConfigParser. + """ __slots__ = ("_config", "_section_name") _valid_attrs_ = ( @@ -183,16 +184,17 @@ def __getattr__(self, attr: str) -> Any: def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any: """Call the configuration at the given method which must take a section name - as first argument""" + as first argument.""" return getattr(self._config, method)(self._section_name, *args, **kwargs) @property def config(self) -> T_ConfigParser: - """return: Configparser instance we constrain""" + """return: ConfigParser instance we constrain""" return self._config def release(self) -> None: - """Equivalent to GitConfigParser.release(), which is called on our underlying parser instance""" + """Equivalent to GitConfigParser.release(), which is called on our underlying + parser instance.""" return self._config.release() def __enter__(self) -> "SectionConstraint[T_ConfigParser]": @@ -248,8 +250,8 @@ def items_all(self) -> List[Tuple[str, List[_T]]]: def get_config_path(config_level: Lit_config_levels) -> str: - # we do not support an absolute path of the gitconfig on windows , - # use the global config instead + # We do not support an absolute path of the gitconfig on Windows. + # Use the global config instead. if is_win and config_level == "system": config_level = "global" @@ -271,7 +273,6 @@ def get_config_path(config_level: Lit_config_levels) -> str: class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): - """Implements specifics required to read git style configuration files. This variation behaves much like the git.config command such that the configuration @@ -286,7 +287,10 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): :note: The config is case-sensitive even when queried, hence section and option names must match perfectly. - If used as a context manager, will release the locked file.""" + + :note: + If used as a context manager, this will release the locked file. + """ # { Configuration # The lock type determines the type of lock to use in new configuration readers. @@ -317,29 +321,34 @@ def __init__( repo: Union["Repo", None] = None, ) -> None: """Initialize a configuration reader to read the given file_or_files and to - possibly allow changes to it by setting read_only False + possibly allow changes to it by setting read_only False. :param file_or_files: - A single file path or file objects or multiple of these + A file path or file object, or a sequence of possibly more than one of them. :param read_only: - If True, the ConfigParser may only read the data , but not change it. - If False, only a single file path or file object may be given. We will write back the changes - when they happen, or when the ConfigParser is released. This will not happen if other - configuration files have been included - :param merge_includes: if True, we will read files mentioned in [include] sections and merge their - contents into ours. This makes it impossible to write back an individual configuration file. - Thus, if you want to modify a single configuration file, turn this off to leave the original - dataset unaltered when reading it. - :param repo: Reference to repository to use if [includeIf] sections are found in configuration files. - + If True, the ConfigParser may only read the data, but not change it. + If False, only a single file path or file object may be given. We will write + back the changes when they happen, or when the ConfigParser is released. + This will not happen if other configuration files have been included. + + :param merge_includes: + If True, we will read files mentioned in ``[include]`` sections and merge + their contents into ours. This makes it impossible to write back an + individual configuration file. Thus, if you want to modify a single + configuration file, turn this off to leave the original dataset unaltered + when reading it. + + :param repo: + Reference to repository to use if ``[includeIf]`` sections are found in + configuration files. """ cp.RawConfigParser.__init__(self, dict_type=_OMD) self._dict: Callable[..., _OMD] # type: ignore # mypy/typeshed bug? self._defaults: _OMD self._sections: _OMD # type: ignore # mypy/typeshed bug? - # Used in python 3, needs to stay in sync with sections for underlying implementation to work + # Used in Python 3. Needs to stay in sync with sections for underlying implementation to work. if not hasattr(self, "_proxies"): self._proxies = self._dict() @@ -377,7 +386,7 @@ def _acquire_lock(self) -> None: file_or_files = self._file_or_files.name # END get filename from handle/stream - # initialize lock base - we want to write + # Initialize lock base - we want to write. self._lock = self.t_lock(file_or_files) # END lock check @@ -386,7 +395,7 @@ def _acquire_lock(self) -> None: def __del__(self) -> None: """Write pending changes if required and release locks""" - # NOTE: only consistent in PY2 + # NOTE: Only consistent in Python 2. self.release() def __enter__(self) -> "GitConfigParser": @@ -398,10 +407,11 @@ def __exit__(self, *args: Any) -> None: def release(self) -> None: """Flush changes and release the configuration write lock. This instance must not be used anymore afterwards. + In Python 3, it's required to explicitly release locks and flush changes, as __del__ is not called deterministically anymore.""" - # checking for the lock here makes sure we do not raise during write() - # in case an invalid parser was created who could not get a lock + # Checking for the lock here makes sure we do not raise during write() + # in case an invalid parser was created who could not get a lock. if self.read_only or (self._lock and not self._lock._has_lock()): return @@ -410,33 +420,31 @@ def release(self) -> None: except IOError: log.error("Exception during destruction of GitConfigParser", exc_info=True) except ReferenceError: - # This happens in PY3 ... and usually means that some state cannot be - # written as the sections dict cannot be iterated - # Usually when shutting down the interpreter, don't know how to fix this + # This happens in Python 3... and usually means that some state cannot be + # written as the sections dict cannot be iterated. This usually happens when + # the interpreter is shutting down. Can it be fixed? pass finally: if self._lock is not None: self._lock._release_lock() def optionxform(self, optionstr: str) -> str: - """Do not transform options in any way when writing""" + """Do not transform options in any way when writing.""" return optionstr def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None: - """A direct copy of the py2.4 version of the super class's _read method - to assure it uses ordered dicts. Had to change one line to make it work. - - Future versions have this fixed, but in fact its quite embarrassing for the - guys not to have done it right in the first place ! - - Removed big comments to make it more compact. + """Originally a direct copy of the Python 2.4 version of RawConfigParser._read, + to ensure it uses ordered dicts. - Made sure it ignores initial whitespace as git uses tabs""" - cursect = None # None, or a dictionary + The ordering bug was fixed in Python 2.4, and dict itself keeps ordering since + Python 3.7. This has some other changes, especially that it ignores initial + whitespace, since git uses tabs. (Big comments are removed to be more compact.) + """ + cursect = None # None, or a dictionary. optname = None lineno = 0 is_multi_line = False - e = None # None, or an exception + e = None # None, or an exception. def string_decode(v: str) -> str: if v[-1] == "\\": @@ -449,19 +457,19 @@ def string_decode(v: str) -> str: # end while True: - # we assume to read binary ! + # We assume to read binary! line = fp.readline().decode(defenc) if not line: break lineno = lineno + 1 - # comment or blank line? + # Comment or blank line? if line.strip() == "" or self.re_comment.match(line): continue if line.split(None, 1)[0].lower() == "rem" and line[0] in "rR": - # no leading whitespace + # No leading whitespace. continue - # is it a section header? + # Is it a section header? mo = self.SECTCRE.match(line.strip()) if not is_multi_line and mo: sectname: str = mo.group("header").strip() @@ -473,16 +481,16 @@ def string_decode(v: str) -> str: cursect = self._dict((("__name__", sectname),)) self._sections[sectname] = cursect self._proxies[sectname] = None - # So sections can't start with a continuation line + # So sections can't start with a continuation line. optname = None - # no section header in the file? + # No section header in the file? elif cursect is None: raise cp.MissingSectionHeaderError(fpname, lineno, line) - # an option line? + # An option line? elif not is_multi_line: mo = self.OPTCRE.match(line) if mo: - # We might just have handled the last line, which could contain a quotation we want to remove + # We might just have handled the last line, which could contain a quotation we want to remove. optname, vi, optval = mo.group("option", "vi", "value") if vi in ("=", ":") and ";" in optval and not optval.strip().startswith('"'): pos = optval.find(";") @@ -497,10 +505,10 @@ def string_decode(v: str) -> str: is_multi_line = True optval = string_decode(optval[1:]) # end handle multi-line - # preserves multiple values for duplicate optnames + # Preserves multiple values for duplicate optnames. cursect.add(optname, optval) else: - # check if it's an option with no value - it's just ignored by git + # Check if it's an option with no value - it's just ignored by git. if not self.OPTVALUEONLY.match(line): if not e: e = cp.ParsingError(fpname) @@ -517,7 +525,7 @@ def string_decode(v: str) -> str: # END parse section or option # END while reading - # if any parsing errors occurred, raise an exception + # If any parsing errors occurred, raise an exception. if e: raise e @@ -525,8 +533,9 @@ def _has_includes(self) -> Union[bool, int]: return self._merge_includes and len(self._included_paths()) def _included_paths(self) -> List[Tuple[str, str]]: - """Return List all paths that must be included to configuration - as Tuples of (option, value). + """List all paths that must be included to configuration. + + :return: The list of paths, where each path is a tuple of ``(option, value)``. """ paths = [] @@ -573,21 +582,24 @@ def _included_paths(self) -> List[Tuple[str, str]]: return paths def read(self) -> None: # type: ignore[override] - """Reads the data stored in the files we have been initialized with. It will - ignore files that cannot be read, possibly leaving an empty configuration + """Read the data stored in the files we have been initialized with. + + This will ignore files that cannot be read, possibly leaving an empty + configuration. :return: Nothing - :raise IOError: if a file cannot be handled""" + :raise IOError: If a file cannot be handled + """ if self._is_initialized: return None self._is_initialized = True files_to_read: List[Union[PathLike, IO]] = [""] if isinstance(self._file_or_files, (str, os.PathLike)): - # for str or Path, as str is a type of Sequence + # For str or Path, as str is a type of Sequence. files_to_read = [self._file_or_files] elif not isinstance(self._file_or_files, (tuple, list, Sequence)): - # could merge with above isinstance once runtime type known + # Could merge with above isinstance once runtime type known. files_to_read = [self._file_or_files] else: # for lists or tuples files_to_read = list(self._file_or_files) @@ -600,11 +612,11 @@ def read(self) -> None: # type: ignore[override] file_ok = False if hasattr(file_path, "seek"): - # must be a file objectfile-object - file_path = cast(IO[bytes], file_path) # replace with assert to narrow type, once sure + # Must be a file objectfile-object. + file_path = cast(IO[bytes], file_path) # TODO: Replace with assert to narrow type, once sure. self._read(file_path, file_path.name) else: - # assume a path if it is not a file-object + # Assume a path if it is not a file-object. file_path = cast(PathLike, file_path) try: with open(file_path, "rb") as fp: @@ -613,8 +625,8 @@ def read(self) -> None: # type: ignore[override] except IOError: continue - # Read includes and append those that we didn't handle yet - # We expect all paths to be normalized and absolute (and will assure that is the case) + # Read includes and append those that we didn't handle yet. + # We expect all paths to be normalized and absolute (and will assure that is the case). if self._has_includes(): for _, include_path in self._included_paths(): if include_path.startswith("~"): @@ -631,27 +643,27 @@ def read(self) -> None: # type: ignore[override] if include_path in seen or not os.access(include_path, os.R_OK): continue seen.add(include_path) - # insert included file to the top to be considered first + # Insert included file to the top to be considered first. files_to_read.insert(0, include_path) num_read_include_files += 1 # each include path in configuration file # end handle includes # END for each file object to read - # If there was no file included, we can safely write back (potentially) the configuration file - # without altering it's meaning + # If there was no file included, we can safely write back (potentially) the + # configuration file without altering its meaning. if num_read_include_files == 0: self._merge_includes = False # end def _write(self, fp: IO) -> None: """Write an .ini-format representation of the configuration state in - git compatible format""" + git compatible format.""" def write_section(name: str, section_dict: _OMD) -> None: fp.write(("[%s]\n" % name).encode(defenc)) - values: Sequence[str] # runtime only gets str in tests, but should be whatever _OMD stores + values: Sequence[str] # Runtime only gets str in tests, but should be whatever _OMD stores. v: str for key, values in section_dict.items_all(): if key == "__name__": @@ -692,9 +704,9 @@ def items_all(self, section_name: str) -> List[Tuple[str, List[str]]]: @needs_values def write(self) -> None: - """Write changes to our file, if there are changes at all + """Write changes to our file, if there are changes at all. - :raise IOError: if this is a read-only writer instance or if we could not obtain + :raise IOError: If this is a read-only writer instance or if we could not obtain a file lock""" self._assure_writable("write") if not self._dirty: @@ -717,9 +729,9 @@ def write(self) -> None: fp = self._file_or_files - # we have a physical file on disk, so get a lock - is_file_lock = isinstance(fp, (str, os.PathLike, IOBase)) # can't use Pathlike until 3.5 dropped - if is_file_lock and self._lock is not None: # else raise Error? + # We have a physical file on disk, so get a lock. + is_file_lock = isinstance(fp, (str, os.PathLike, IOBase)) # TODO: Use PathLike (having dropped 3.5). + if is_file_lock and self._lock is not None: # Else raise error? self._lock._obtain_lock() if not hasattr(fp, "seek"): @@ -729,7 +741,7 @@ def write(self) -> None: else: fp = cast("BytesIO", fp) fp.seek(0) - # make sure we do not overwrite into an existing file + # Make sure we do not overwrite into an existing file. if hasattr(fp, "truncate"): fp.truncate() self._write(fp) @@ -747,13 +759,13 @@ def read_only(self) -> bool: """:return: True if this instance may change the configuration file""" return self._read_only + # FIXME: Figure out if default or return type can really include bool. def get_value( self, section: str, option: str, default: Union[int, float, str, bool, None] = None, ) -> Union[int, float, str, bool]: - # can default or return type include bool? """Get an option's value. If multiple values are specified for this option in the section, the @@ -762,10 +774,12 @@ def get_value( :param default: If not None, the given default value will be returned in case the option did not exist + :return: a properly typed value, either int, float or string :raise TypeError: in case the value could not be understood - Otherwise the exceptions known to the ConfigParser will be raised.""" + Otherwise the exceptions known to the ConfigParser will be raised. + """ try: valuestr = self.get(section, option) except Exception: @@ -789,10 +803,12 @@ def get_values( :param default: If not None, a list containing the given default value will be returned in case the option did not exist + :return: a list of properly typed values, either int, float or string :raise TypeError: in case the value could not be understood - Otherwise the exceptions known to the ConfigParser will be raised.""" + Otherwise the exceptions known to the ConfigParser will be raised. + """ try: self.sections() lst = self._sections[section].getall(option) @@ -816,7 +832,7 @@ def _string_to_value(self, valuestr: str) -> Union[int, float, str, bool]: continue # END for each numeric type - # try boolean values as git uses them + # Try boolean values as git uses them. vl = valuestr.lower() if vl == "false": return False @@ -839,16 +855,17 @@ def _value_to_string(self, value: Union[str, bytes, int, float, bool]) -> str: @needs_values @set_dirty_and_flush_changes def set_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser": - """Sets the given option in section to the given value. - It will create the section if required, and will not throw as opposed to the default - ConfigParser 'set' method. + """Set the given option in section to the given value. + + This will create the section if required, and will not throw as opposed to the + default ConfigParser 'set' method. :param section: Name of the section in which the option resides or should reside :param option: Name of the options whose value to set - - :param value: Value to set the option to. It must be a string or convertible - to a string - :return: this instance""" + :param value: Value to set the option to. It must be a string or convertible to + a string. + :return: This instance + """ if not self.has_section(section): self.add_section(section) self.set(section, option, self._value_to_string(value)) @@ -857,27 +874,29 @@ def set_value(self, section: str, option: str, value: Union[str, bytes, int, flo @needs_values @set_dirty_and_flush_changes def add_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser": - """Adds a value for the given option in section. - It will create the section if required, and will not throw as opposed to the default + """Add a value for the given option in section. + + This will create the section if required, and will not throw as opposed to the default ConfigParser 'set' method. The value becomes the new value of the option as returned by 'get_value', and appends to the list of values returned by 'get_values`'. :param section: Name of the section in which the option resides or should reside :param option: Name of the option - :param value: Value to add to option. It must be a string or convertible to a string - :return: this instance""" + :return: This instance + """ if not self.has_section(section): self.add_section(section) self._sections[section].add(option, self._value_to_string(value)) return self def rename_section(self, section: str, new_name: str) -> "GitConfigParser": - """rename the given section to new_name - :raise ValueError: if section doesn't exit - :raise ValueError: if a section with new_name does already exist - :return: this instance + """Rename the given section to new_name. + + :raise ValueError: If ``section`` doesn't exist + :raise ValueError: If a section with ``new_name`` does already exist + :return: This instance """ if not self.has_section(section): raise ValueError("Source section '%s' doesn't exist" % section) @@ -890,6 +909,6 @@ def rename_section(self, section: str, new_name: str) -> "GitConfigParser": new_section.setall(k, vs) # end for each value to copy - # This call writes back the changes, which is why we don't have the respective decorator + # This call writes back the changes, which is why we don't have the respective decorator. self.remove_section(section) return self diff --git a/git/db.py b/git/db.py index b1a0d108a..1aacd0c84 100644 --- a/git/db.py +++ b/git/db.py @@ -1,4 +1,5 @@ -"""Module with our own gitdb implementation - it uses the git command""" +"""Module with our own gitdb implementation - it uses the git command.""" + from git.util import bin_to_hex, hex_to_bin from gitdb.base import OInfo, OStream from gitdb.db import GitDB @@ -22,17 +23,17 @@ class GitCmdObjectDB(LooseObjectDB): - """A database representing the default git object store, which includes loose - objects, pack files and an alternates file + objects, pack files and an alternates file. It will create objects only in the loose object database. - :note: for now, we use the git command to do all the lookup, just until he - have packs and the other implementations + + :note: For now, we use the git command to do all the lookup, just until we + have packs and the other implementations. """ def __init__(self, root_path: PathLike, git: "Git") -> None: - """Initialize this instance with the root and a git command""" + """Initialize this instance with the root and a git command.""" super(GitCmdObjectDB, self).__init__(root_path) self._git = git @@ -48,11 +49,15 @@ def stream(self, binsha: bytes) -> OStream: # { Interface def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes: - """:return: Full binary 20 byte sha from the given partial hexsha + """ + :return: Full binary 20 byte sha from the given partial hexsha + :raise AmbiguousObjectName: :raise BadObject: - :note: currently we only raise BadObject as git does not communicate - AmbiguousObjects separately""" + + :note: Currently we only raise :class:`BadObject` as git does not communicate + AmbiguousObjects separately. + """ try: hexsha, _typename, _size = self._git.get_object_header(partial_hexsha) return hex_to_bin(hexsha) diff --git a/git/diff.py b/git/diff.py index 8ee2527d4..1fde4d676 100644 --- a/git/diff.py +++ b/git/diff.py @@ -50,7 +50,7 @@ __all__ = ("Diffable", "DiffIndex", "Diff", "NULL_TREE") -# Special object to compare against the empty tree in diffs +# Special object to compare against the empty tree in diffs. NULL_TREE = object() _octal_byte_re = re.compile(rb"\\([0-9]{3})") @@ -80,27 +80,28 @@ def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]: class Diffable(object): - - """Common interface for all object that can be diffed against another object of compatible type. + """Common interface for all objects that can be diffed against another object of + compatible type. :note: - Subclasses require a repo member as it is the case for Object instances, for practical - reasons we do not derive from Object.""" + Subclasses require a repo member as it is the case for Object instances, for + practical reasons we do not derive from Object. + """ __slots__ = () - # standin indicating you want to diff against the index class Index(object): - pass + """Stand-in indicating you want to diff against the index.""" def _process_diff_args( self, args: List[Union[str, "Diffable", Type["Diffable.Index"], object]] ) -> List[Union[str, "Diffable", Type["Diffable.Index"], object]]: """ :return: - possibly altered version of the given args list. - Method is called right before git command execution. - Subclasses can use it to alter the behaviour of the superclass""" + Possibly altered version of the given args list. + This method is called right before git command execution. + Subclasses can use it to alter the behaviour of the superclass. + """ return args def diff( @@ -110,41 +111,47 @@ def diff( create_patch: bool = False, **kwargs: Any, ) -> "DiffIndex": - """Creates diffs between two items being trees, trees and index or an - index and the working tree. It will detect renames automatically. + """Create diffs between two items being trees, trees and index or an + index and the working tree. Detects renames automatically. :param other: - Is the item to compare us with. - If None, we will be compared to the working tree. - If Treeish, it will be compared against the respective tree - If Index ( type ), it will be compared against the index. - If git.NULL_TREE, it will compare against the empty tree. - It defaults to Index to assure the method will not by-default fail - on bare repositories. + This the item to compare us with. + + * If None, we will be compared to the working tree. + * If :class:`Treeish `, it will be compared against + the respective tree. + * If :class:`Index `, it will be compared against the index. + * If :attr:`git.NULL_TREE`, it will compare against the empty tree. + * It defaults to :class:`Index ` to assure the method will + not by-default fail on bare repositories. :param paths: - is a list of paths or a single path to limit the diff to. - It will only include at least one of the given path or paths. + This a list of paths or a single path to limit the diff to. It will only + include at least one of the given path or paths. :param create_patch: - If True, the returned Diff contains a detailed patch that if applied - makes the self to other. Patches are somewhat costly as blobs have to be read - and diffed. + If True, the returned :class:`Diff` contains a detailed patch that if + applied makes the self to other. Patches are somewhat costly as blobs have + to be read and diffed. :param kwargs: - Additional arguments passed to git-diff, such as - R=True to swap both sides of the diff. + Additional arguments passed to git-diff, such as ``R=True`` to swap both + sides of the diff. :return: git.DiffIndex :note: - On a bare repository, 'other' needs to be provided as Index or as - as Tree/Commit, or a git command error will occur""" + On a bare repository, 'other' needs to be provided + as :class:`Index `, + or as :class:`Tree ` + or :class:`Commit `, or a git command error will + occur. + """ args: List[Union[PathLike, Diffable, Type["Diffable.Index"], object]] = [] - args.append("--abbrev=40") # we need full shas - args.append("--full-index") # get full index paths, not only filenames + args.append("--abbrev=40") # We need full shas. + args.append("--full-index") # Get full index paths, not only filenames. - # remove default '-M' arg (check for renames) if user is overriding it + # Remove default '-M' arg (check for renames) if user is overriding it. if not any(x in kwargs for x in ("find_renames", "no_renames", "M")): args.append("-M") @@ -154,8 +161,8 @@ def diff( args.append("--raw") args.append("-z") - # in any way, assure we don't see colored output, - # fixes https://github.com/gitpython-developers/GitPython/issues/172 + # Ensure we never see colored output. + # Fixes: https://github.com/gitpython-developers/GitPython/issues/172 args.append("--no-color") if paths is not None and not isinstance(paths, (tuple, list)): @@ -168,17 +175,17 @@ def diff( if other is self.Index: args.insert(0, "--cached") elif other is NULL_TREE: - args.insert(0, "-r") # recursive diff-tree + args.insert(0, "-r") # Recursive diff-tree. args.insert(0, "--root") diff_cmd = self.repo.git.diff_tree elif other is not None: - args.insert(0, "-r") # recursive diff-tree + args.insert(0, "-r") # Recursive diff-tree. args.insert(0, other) diff_cmd = self.repo.git.diff_tree args.insert(0, self) - # paths is list here or None + # paths is list here, or None. if paths: args.append("--") args.extend(paths) @@ -198,13 +205,13 @@ def diff( class DiffIndex(List[T_Diff]): + """An Index for diffs, allowing a list of Diffs to be queried by the diff + properties. - """Implements an Index for diffs, allowing a list of Diffs to be queried by - the diff properties. - - The class improves the diff handling convenience""" + The class improves the diff handling convenience. + """ - # change type invariant identifying possible ways a blob can have changed + # Change type invariant identifying possible ways a blob can have changed: # A = Added # D = Deleted # R = Renamed @@ -215,10 +222,10 @@ class DiffIndex(List[T_Diff]): def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]: """ :return: - iterator yielding Diff instances that match the given change_type + iterator yielding :class:`Diff` instances that match the given `change_type` :param change_type: - Member of DiffIndex.change_type, namely: + Member of :attr:`DiffIndex.change_type`, namely: * 'A' for added paths * 'D' for deleted paths @@ -246,11 +253,10 @@ def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]: class Diff(object): - """A Diff contains diff information between two Trees. It contains two sides a and b of the diff, members are prefixed with - "a" and "b" respectively to inidcate that. + "a" and "b" respectively to indicate that. Diffs keep information about the changed blob objects, the file mode, renames, deletions and new files. @@ -273,11 +279,13 @@ class Diff(object): When comparing to working trees, the working tree blob will have a null hexsha as a corresponding object does not yet exist. The mode will be null as well. - But the path will be available though. - If it is listed in a diff the working tree version of the file must - be different to the version in the index or tree, and hence has been modified.""" + The path will be available, though. + + If it is listed in a diff, the working tree version of the file must + differ from the version in the index or tree, and hence has been modified. + """ - # precompiled regex + # Precompiled regex. re_header = re.compile( rb""" ^diff[ ]--git @@ -299,7 +307,8 @@ class Diff(object): """, re.VERBOSE | re.MULTILINE, ) - # can be used for comparisons + + # These can be used for comparisons. NULL_HEX_SHA = "0" * 40 NULL_BIN_SHA = b"\0" * 20 @@ -346,8 +355,8 @@ def __init__( self.a_mode = mode_str_to_int(a_mode) if a_mode else None self.b_mode = mode_str_to_int(b_mode) if b_mode else None - # Determine whether this diff references a submodule, if it does then - # we need to overwrite "repo" to the corresponding submodule's repo instead + # Determine whether this diff references a submodule. If it does then + # we need to overwrite "repo" to the corresponding submodule's repo instead. if repo and a_rawpath: for submodule in repo.submodules: if submodule.path == a_rawpath.decode(defenc, "replace"): @@ -371,7 +380,7 @@ def __init__( self.deleted_file: bool = deleted_file self.copied_file: bool = copied_file - # be clear and use None instead of empty strings + # Be clear and use None instead of empty strings. assert raw_rename_from is None or isinstance(raw_rename_from, bytes) assert raw_rename_to is None or isinstance(raw_rename_to, bytes) self.raw_rename_from = raw_rename_from or None @@ -395,15 +404,15 @@ def __hash__(self) -> int: return hash(tuple(getattr(self, n) for n in self.__slots__)) def __str__(self) -> str: - h: str = "%s" + h = "%s" if self.a_blob: h %= self.a_blob.path elif self.b_blob: h %= self.b_blob.path - msg: str = "" - line = None # temp line - line_length = 0 # line length + msg = "" + line = None + line_length = 0 for b, n in zip((self.a_blob, self.b_blob), ("lhs", "rhs")): if b: line = "\n%s: %o | %s" % (n, b.mode, b.hexsha) @@ -414,7 +423,7 @@ def __str__(self) -> str: msg += line # END for each blob - # add headline + # Add headline. h += "\n" + "=" * line_length if self.deleted_file: @@ -437,11 +446,7 @@ def __str__(self) -> str: msg += "\n---" # END diff info - # Python2 silliness: have to assure we convert our likely to be unicode object to a string with the - # right encoding. Otherwise it tries to convert it using ascii, which may fail ungracefully - res = h + msg - # end - return res + return h + msg @property def a_path(self) -> Optional[str]: @@ -461,8 +466,11 @@ def rename_to(self) -> Optional[str]: @property def renamed(self) -> bool: - """:returns: True if the blob of our diff has been renamed - :note: This property is deprecated, please use ``renamed_file`` instead. + """ + :returns: True if the blob of our diff has been renamed + + :note: This property is deprecated. + Please use the :attr:`renamed_file` property instead. """ return self.renamed_file @@ -493,17 +501,17 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn :return: git.DiffIndex """ - ## FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise. + # FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise. text_list: List[bytes] = [] handle_process_output(proc, text_list.append, None, finalize_process, decode_streams=False) - # for now, we have to bake the stream + # For now, we have to bake the stream. text = b"".join(text_list) index: "DiffIndex" = DiffIndex() previous_header: Union[Match[bytes], None] = None header: Union[Match[bytes], None] = None - a_path, b_path = None, None # for mypy - a_mode, b_mode = None, None # for mypy + a_path, b_path = None, None # For mypy. + a_mode, b_mode = None, None # For mypy. for _header in cls.re_header.finditer(text): ( a_path_fallback, @@ -532,13 +540,13 @@ def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoIn b_path = cls._pick_best_path(b_path, rename_to, b_path_fallback) # Our only means to find the actual text is to see what has not been matched by our regex, - # and then retro-actively assign it to our index + # and then retro-actively assign it to our index. if previous_header is not None: index[-1].diff = text[previous_header.end() : _header.start()] # end assign actual diff - # Make sure the mode is set if the path is set. Otherwise the resulting blob is invalid - # We just use the one mode we should have parsed + # Make sure the mode is set if the path is set. Otherwise the resulting blob is invalid. + # We just use the one mode we should have parsed. a_mode = old_mode or deleted_file_mode or (a_path and (b_mode or new_mode or new_file_mode)) b_mode = b_mode or new_mode or new_file_mode or (b_path and a_mode) index.append( @@ -579,7 +587,7 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> Non for line in lines.split("\x00:"): if not line: - # The line data is empty, skip + # The line data is empty, skip. continue meta, _, path = line.partition("\x00") path = path.rstrip("\x00") @@ -603,7 +611,7 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> Non rename_to = None # NOTE: We cannot conclude from the existence of a blob to change type - # as diffs with the working do not have blobs yet + # as diffs with the working do not have blobs yet. if change_type == "D": b_blob_id = None # Optional[str] deleted_file = True @@ -621,7 +629,7 @@ def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex) -> Non b_path = b_path_str.encode(defenc) rename_from, rename_to = a_path, b_path elif change_type == "T": - # Nothing to do + # Nothing to do. pass # END add/remove handling diff --git a/git/exc.py b/git/exc.py index 32c371d0b..bfb023fa5 100644 --- a/git/exc.py +++ b/git/exc.py @@ -3,7 +3,8 @@ # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ -""" Module containing all exceptions thrown throughout the git package """ + +"""Module containing all exceptions thrown throughout the git package.""" __all__ = [ # Defined in gitdb.exc: @@ -57,7 +58,7 @@ class GitError(Exception): - """Base class for all package exceptions""" + """Base class for all package exceptions.""" class InvalidGitRepositoryError(GitError): @@ -65,7 +66,7 @@ class InvalidGitRepositoryError(GitError): class WorkTreeRepositoryUnsupported(InvalidGitRepositoryError): - """Thrown to indicate we can't handle work tree repositories""" + """Thrown to indicate we can't handle work tree repositories.""" class NoSuchPathError(GitError, OSError): @@ -133,7 +134,7 @@ def __str__(self) -> str: class GitCommandNotFound(CommandError): """Thrown if we cannot find the `git` executable in the PATH or at the path given by - the GIT_PYTHON_GIT_EXECUTABLE environment variable""" + the GIT_PYTHON_GIT_EXECUTABLE environment variable.""" def __init__(self, command: Union[List[str], Tuple[str], str], cause: Union[str, Exception]) -> None: super(GitCommandNotFound, self).__init__(command, cause) @@ -157,15 +158,15 @@ class CheckoutError(GitError): """Thrown if a file could not be checked out from the index as it contained changes. - The .failed_files attribute contains a list of relative paths that failed - to be checked out as they contained changes that did not exist in the index. + The :attr:`failed_files` attribute contains a list of relative paths that failed to + be checked out as they contained changes that did not exist in the index. - The .failed_reasons attribute contains a string informing about the actual + The :attr:`failed_reasons` attribute contains a string informing about the actual cause of the issue. - The .valid_files attribute contains a list of relative paths to files that - were checked out successfully and hence match the version stored in the - index""" + The :attr:`valid_files` attribute contains a list of relative paths to files that + were checked out successfully and hence match the version stored in the index. + """ def __init__( self, @@ -184,18 +185,20 @@ def __str__(self) -> str: class CacheError(GitError): - - """Base for all errors related to the git index, which is called cache internally""" + """Base for all errors related to the git index, which is called cache + internally.""" class UnmergedEntriesError(CacheError): """Thrown if an operation cannot proceed as there are still unmerged - entries in the cache""" + entries in the cache.""" class HookExecutionError(CommandError): - """Thrown if a hook exits with a non-zero exit code. It provides access to the exit code and the string returned - via standard output""" + """Thrown if a hook exits with a non-zero exit code. + + This provides access to the exit code and the string returned via standard output. + """ def __init__( self, @@ -209,7 +212,8 @@ def __init__( class RepositoryDirtyError(GitError): - """Thrown whenever an operation on a repository fails as it has uncommitted changes that would be overwritten""" + """Thrown whenever an operation on a repository fails as it has uncommitted changes + that would be overwritten.""" def __init__(self, repo: "Repo", message: str) -> None: self.repo = repo diff --git a/git/index/__init__.py b/git/index/__init__.py index 96b721f07..f9a534ee7 100644 --- a/git/index/__init__.py +++ b/git/index/__init__.py @@ -1,4 +1,6 @@ -"""Initialize the index package""" +"""Initialize the index package.""" + # flake8: noqa + from .base import * from .typ import * diff --git a/git/index/base.py b/git/index/base.py index 5da904b4e..4f1b6c469 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -95,12 +95,11 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable): - """ - Implements an Index that can be manipulated using a native implementation in - order to save git command function calls wherever possible. + An Index that can be manipulated using a native implementation in order to save git + command function calls wherever possible. - It provides custom merging facilities allowing to merge without actually changing + This provides custom merging facilities allowing to merge without actually changing your index or your working tree. This way you can perform own test-merges based on the index only without having to deal with the working copy. This is useful in case of partial working trees. @@ -115,18 +114,23 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable): index.entries[index.entry_key(index_entry_instance)] = index_entry_instance Make sure you use index.write() once you are done manipulating the index directly - before operating on it using the git command""" + before operating on it using the git command. + """ __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path") - _VERSION = 2 # latest version we support - S_IFGITLINK = S_IFGITLINK # a submodule + + _VERSION = 2 # Latest version we support. + + S_IFGITLINK = S_IFGITLINK # A submodule. def __init__(self, repo: "Repo", file_path: Union[PathLike, None] = None) -> None: """Initialize this Index instance, optionally from the given ``file_path``. + If no file_path is given, we will be created from the current index file. If a stream is not given, the stream will be initialized from the current - repository's index on demand.""" + repository's index on demand. + """ self.repo = repo self.version = self._VERSION self._extension_data = b"" @@ -137,7 +141,7 @@ def _set_cache_(self, attr: str) -> None: try: fd = os.open(self._file_path, os.O_RDONLY) except OSError: - # in new repositories, there may be no index, which means we are empty + # In new repositories, there may be no index, which means we are empty. self.entries: Dict[Tuple[PathLike, StageType], IndexEntry] = {} return None # END exception handling @@ -163,18 +167,19 @@ def path(self) -> PathLike: return self._file_path def _delete_entries_cache(self) -> None: - """Safely clear the entries cache so it can be recreated""" + """Safely clear the entries cache so it can be recreated.""" try: del self.entries except AttributeError: - # fails in python 2.6.5 with this exception + # It failed in Python 2.6.5 with AttributeError. + # FIXME: Look into whether we can just remove this except clause now. pass # END exception handling # { Serializable Interface def _deserialize(self, stream: IO) -> "IndexFile": - """Initialize this instance with index values read from the given stream""" + """Initialize this instance with index values read from the given stream.""" self.version, self.entries, self._extension_data, _conten_sha = read_cache(stream) return self @@ -197,7 +202,7 @@ def write( file_path: Union[None, PathLike] = None, ignore_extension_data: bool = False, ) -> None: - """Write the current state to our file path or to the given one + """Write the current state to our file path or to the given one. :param file_path: If None, we will write to our stored file path from which we have @@ -214,12 +219,11 @@ def write( If this data is present in the written index, git-write-tree will instead write the stored/cached tree. Alternatively, use IndexFile.write_tree() to handle this case - automatically - - :return: self # does it? or returns None?""" - # make sure we have our entries read before getting a write lock - # else it would be done when streaming. This can happen - # if one doesn't change the index, but writes it right away + automatically. + """ + # Make sure we have our entries read before getting a write lock. + # Otherwise it would be done when streaming. This can happen if one + # doesn't change the index, but writes it right away. self.entries lfd = LockedFD(file_path or self._file_path) stream = lfd.open(write=True, stream=True) @@ -232,7 +236,7 @@ def write( lfd.commit() - # make sure we represent what we have written + # Make sure we represent what we have written. if file_path is not None: self._file_path = file_path @@ -242,26 +246,26 @@ def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexF """Merge the given rhs treeish into the current index, possibly taking a common base treeish into account. - As opposed to the :func:`IndexFile.from_tree` method, this allows you to use an already - existing tree as the left side of the merge + As opposed to the :func:`IndexFile.from_tree` method, this allows you to use an + already existing tree as the left side of the merge. :param rhs: - treeish reference pointing to the 'other' side of the merge. + Treeish reference pointing to the 'other' side of the merge. :param base: - optional treeish reference pointing to the common base of 'rhs' and - this index which equals lhs + Optional treeish reference pointing to the common base of 'rhs' and this + index which equals lhs. :return: - self ( containing the merge and possibly unmerged entries in case of - conflicts ) + self (containing the merge and possibly unmerged entries in case of + conflicts) :raise GitCommandError: - If there is a merge conflict. The error will - be raised at the first conflicting path. If you want to have proper - merge resolution to be done by yourself, you have to commit the changed - index ( or make a valid tree from it ) and retry with a three-way - index.from_tree call.""" + If there is a merge conflict. The error will be raised at the first + conflicting path. If you want to have proper merge resolution to be done by + yourself, you have to commit the changed index (or make a valid tree from + it) and retry with a three-way index.from_tree call. + """ # -i : ignore working tree status # --aggressive : handle more merge cases # -m : do an actual merge @@ -276,22 +280,24 @@ def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexF @classmethod def new(cls, repo: "Repo", *tree_sha: Union[str, Tree]) -> "IndexFile": """Merge the given treeish revisions into a new index which is returned. - This method behaves like git-read-tree --aggressive when doing the merge. + + This method behaves like ``git-read-tree --aggressive`` when doing the merge. :param repo: The repository treeish are located in. :param tree_sha: - 20 byte or 40 byte tree sha or tree objects + 20 byte or 40 byte tree sha or tree objects. :return: New IndexFile instance. Its path will be undefined. If you intend to write such a merged Index, supply an alternate file_path - to its 'write' method.""" + to its 'write' method. + """ tree_sha_bytes: List[bytes] = [to_bin_sha(str(t)) for t in tree_sha] base_entries = aggressive_tree_merge(repo.odb, tree_sha_bytes) inst = cls(repo) - # convert to entries dict + # Convert to entries dict. entries: Dict[Tuple[PathLike, int], IndexEntry] = dict( zip( ((e.path, e.stage) for e in base_entries), @@ -305,7 +311,7 @@ def new(cls, repo: "Repo", *tree_sha: Union[str, Tree]) -> "IndexFile": @classmethod def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile": """Merge the given treeish revisions into a new index which is returned. - The original index will remain unaltered + The original index will remain unaltered. :param repo: The repository treeish are located in. @@ -319,10 +325,10 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile one. It behaves like a fast-forward. If 3 Trees are given, a 3-way merge will be performed with the first tree being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current' tree, - tree 3 is the 'other' one + tree 3 is the 'other' one. :param kwargs: - Additional arguments passed to git-read-tree + Additional arguments passed to git-read-tree. :return: New IndexFile instance. It will point to a temporary index location which @@ -336,46 +342,49 @@ def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile As the underlying git-read-tree command takes into account the current index, it will be temporarily moved out of the way to assure there are no unsuspected - interferences.""" + interferences. + """ if len(treeish) == 0 or len(treeish) > 3: raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish)) arg_list: List[Union[Treeish, str]] = [] - # ignore that working tree and index possibly are out of date + # Ignore that the working tree and index possibly are out of date. if len(treeish) > 1: - # drop unmerged entries when reading our index and merging + # Drop unmerged entries when reading our index and merging. arg_list.append("--reset") - # handle non-trivial cases the way a real merge does + # Handle non-trivial cases the way a real merge does. arg_list.append("--aggressive") # END merge handling # tmp file created in git home directory to be sure renaming - # works - /tmp/ dirs could be on another device + # works - /tmp/ dirs could be on another device. with ExitStack() as stack: tmp_index = stack.enter_context(tempfile.NamedTemporaryFile(dir=repo.git_dir)) arg_list.append("--index-output=%s" % tmp_index.name) arg_list.extend(treeish) - # move current index out of the way - otherwise the merge may fail - # as it considers existing entries. moving it essentially clears the index. + # Move current index out of the way - otherwise the merge may fail + # as it considers existing entries. Moving it essentially clears the index. # Unfortunately there is no 'soft' way to do it. - # The TemporaryFileSwap assure the original file get put back + # The TemporaryFileSwap assure the original file get put back. stack.enter_context(TemporaryFileSwap(join_path_native(repo.git_dir, "index"))) repo.git.read_tree(*arg_list, **kwargs) index = cls(repo, tmp_index.name) - index.entries # force it to read the file as we will delete the temp-file + index.entries # Force it to read the file as we will delete the temp-file. return index # END index merge handling # UTILITIES @unbare_repo def _iter_expand_paths(self: "IndexFile", paths: Sequence[PathLike]) -> Iterator[PathLike]: - """Expand the directories in list of paths to the corresponding paths accordingly, + """Expand the directories in list of paths to the corresponding paths accordingly. - Note: git will add items multiple times even if a glob overlapped - with manually specified paths or if paths where specified multiple - times - we respect that and do not prune""" + :note: + git will add items multiple times even if a glob overlapped + with manually specified paths or if paths where specified multiple + times - we respect that and do not prune. + """ def raise_exc(e: Exception) -> NoReturn: raise e @@ -389,9 +398,9 @@ def raise_exc(e: Exception) -> NoReturn: # END make absolute path try: - st = os.lstat(abs_path) # handles non-symlinks as well + st = os.lstat(abs_path) # Handles non-symlinks as well. except OSError: - # the lstat call may fail as the path may contain globs as well + # The lstat call may fail as the path may contain globs as well. pass else: if S_ISLNK(st.st_mode): @@ -399,7 +408,7 @@ def raise_exc(e: Exception) -> NoReturn: continue # end check symlink - # if the path is not already pointing to an existing file, resolve globs if possible + # If the path is not already pointing to an existing file, resolve globs if possible. if not os.path.exists(abs_path) and ("?" in abs_path or "*" in abs_path or "[" in abs_path): resolved_paths = glob.glob(abs_path) # not abs_path in resolved_paths: @@ -416,12 +425,12 @@ def raise_exc(e: Exception) -> NoReturn: try: for root, _dirs, files in os.walk(abs_path, onerror=raise_exc): for rela_file in files: - # add relative paths only + # Add relative paths only. yield osp.join(root.replace(rs, ""), rela_file) # END for each file in subdir # END for each subdirectory except OSError: - # was a file or something that could not be iterated + # It was a file or something that could not be iterated. yield abs_path.replace(rs, "") # END path exception handling # END for each path @@ -438,17 +447,20 @@ def _write_path_to_stdin( """Write path to proc.stdin and make sure it processes the item, including progress. :return: stdout string + :param read_from_stdout: if True, proc.stdout will be read after the item - was sent to stdin. In that case, it will return None + was sent to stdin. In that case, it will return None. + :note: There is a bug in git-update-index that prevents it from sending reports just in time. This is why we have a version that tries to read stdout and one which doesn't. In fact, the stdout is not - important as the piped-in files are processed anyway and just in time + important as the piped-in files are processed anyway and just in time. + :note: Newlines are essential here, gits behaviour is somewhat inconsistent on this depending on the version, hence we try our best to deal with newlines carefully. Usually the last newline will not be sent, instead - we will close stdin to break the pipe.""" - + we will close stdin to break the pipe. + """ fprogress(filepath, False, item) rval: Union[None, str] = None @@ -456,7 +468,7 @@ def _write_path_to_stdin( try: proc.stdin.write(("%s\n" % filepath).encode(defenc)) except IOError as e: - # pipe broke, usually because some error happened + # Pipe broke, usually because some error happened. raise fmakeexc() from e # END write exception handling proc.stdin.flush() @@ -475,7 +487,8 @@ def iter_blobs( :param predicate: Function(t) returning True if tuple(stage, Blob) should be yielded by the iterator. A default filter, the BlobFilter, allows you to yield blobs - only if they match a given list of paths.""" + only if they match a given list of paths. + """ for entry in self.entries.values(): blob = entry.to_blob(self.repo) blob.size = entry.size @@ -489,8 +502,7 @@ def unmerged_blobs(self) -> Dict[PathLike, List[Tuple[StageType, Blob]]]: :return: Dict(path : list( tuple( stage, Blob, ...))), being a dictionary associating a path in the index with a list containing - sorted stage/blob pairs - + sorted stage/blob pairs. :note: Blobs that have been removed in one side simply do not exist in the @@ -512,19 +524,21 @@ def entry_key(cls, *entry: Union[BaseIndexEntry, PathLike, StageType]) -> Tuple[ return entry_key(*entry) def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile": - """Resolve the blobs given in blob iterator. This will effectively remove the - index entries of the respective path at all non-null stages and add the given - blob as new stage null blob. + """Resolve the blobs given in blob iterator. + + This will effectively remove the index entries of the respective path at all + non-null stages and add the given blob as new stage null blob. For each path there may only be one blob, otherwise a ValueError will be raised claiming the path is already at stage 0. :raise ValueError: if one of the blobs already existed at stage 0 + :return: self :note: You will have to write the index manually once you are done, i.e. - index.resolve_blobs(blobs).write() + ``index.resolve_blobs(blobs).write()``. """ for blob in iter_blobs: stage_null_key = (blob.path, 0) @@ -532,7 +546,7 @@ def resolve_blobs(self, iter_blobs: Iterator[Blob]) -> "IndexFile": raise ValueError("Path %r already exists at stage 0" % str(blob.path)) # END assert blob is not stage 0 already - # delete all possible stages + # Delete all possible stages. for stage in (1, 2, 3): try: del self.entries[(blob.path, stage)] @@ -550,34 +564,40 @@ def update(self) -> "IndexFile": """Reread the contents of our index file, discarding all cached information we might have. - :note: This is a possibly dangerious operations as it will discard your changes - to index.entries - :return: self""" + :note: This is a possibly dangerous operations as it will discard your changes + to index.entries. + + :return: self + """ self._delete_entries_cache() - # allows to lazily reread on demand + # Allows to lazily reread on demand. return self def write_tree(self) -> Tree: - """Writes this index to a corresponding Tree object into the repository's + """Write this index to a corresponding Tree object into the repository's object database and return it. - :return: Tree object representing this index + :return: Tree object representing this index. + :note: The tree will be written even if one or more objects the tree refers to does not yet exist in the object database. This could happen if you added Entries to the index directly. + :raise ValueError: if there are no entries in the cache - :raise UnmergedEntriesError:""" - # we obtain no lock as we just flush our contents to disk as tree - # If we are a new index, the entries access will load our data accordingly + + :raise UnmergedEntriesError: + """ + # We obtain no lock as we just flush our contents to disk as tree. + # If we are a new index, the entries access will load our data accordingly. mdb = MemoryDB() entries = self._entries_sorted() binsha, tree_items = write_tree_from_cache(entries, mdb, slice(0, len(entries))) - # copy changed trees only + # Copy changed trees only. mdb.stream_copy(mdb.sha_iter(), self.repo.odb) - # note: additional deserialization could be saved if write_tree_from_cache - # would return sorted tree entries + # Note: Additional deserialization could be saved if write_tree_from_cache + # would return sorted tree entries. root_tree = Tree(self.repo, binsha, path="") root_tree._cache = tree_items return root_tree @@ -630,9 +650,10 @@ def _preprocess_add_items( def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry: """Store file at filepath in the database and return the base index entry Needs the git_working_dir decorator active ! This must be assured in the calling code""" - st = os.lstat(filepath) # handles non-symlinks as well + st = os.lstat(filepath) # Handles non-symlinks as well. if S_ISLNK(st.st_mode): - # in PY3, readlink is string, but we need bytes. In PY2, it's just OS encoded bytes, we assume UTF-8 + # In PY3, readlink is a string, but we need bytes. + # In PY2, it was just OS encoded bytes, we assumed UTF-8. open_stream: Callable[[], BinaryIO] = lambda: BytesIO(force_bytes(os.readlink(filepath), encoding=defenc)) else: open_stream = lambda: open(filepath, "rb") @@ -708,51 +729,54 @@ def add( relative or absolute. - path string - strings denote a relative or absolute path into the repository pointing to - an existing file, i.e. CHANGES, lib/myfile.ext, '/home/gitrepo/lib/myfile.ext'. + Strings denote a relative or absolute path into the repository pointing + to an existing file, e.g., CHANGES, lib/myfile.ext, + '/home/gitrepo/lib/myfile.ext'. - Absolute paths must start with working tree directory of this index's repository - to be considered valid. For example, if it was initialized with a non-normalized path, like - `/root/repo/../repo`, absolute paths to be added must start with `/root/repo/../repo`. + Absolute paths must start with working tree directory of this index's + repository to be considered valid. For example, if it was initialized + with a non-normalized path, like ``/root/repo/../repo``, absolute paths + to be added must start with ``/root/repo/../repo``. Paths provided like this must exist. When added, they will be written into the object database. - PathStrings may contain globs, such as 'lib/__init__*' or can be directories - like 'lib', the latter ones will add all the files within the directory and - subdirectories. + PathStrings may contain globs, such as ``lib/__init__*``. Or they can be + directories like ``lib``, which will add all the files within the + directory and subdirectories. This equals a straight git-add. - They are added at stage 0 + They are added at stage 0. - Blob or Submodule object Blobs are added as they are assuming a valid mode is set. - The file they refer to may or may not exist in the file system, but - must be a path relative to our repository. - - If their sha is null ( 40*0 ), their path must exist in the file system - relative to the git repository as an object will be created from - the data at the path. - The handling now very much equals the way string paths are processed, except that - the mode you have set will be kept. This allows you to create symlinks - by settings the mode respectively and writing the target of the symlink - directly into the file. This equals a default Linux-Symlink which - is not dereferenced automatically, except that it can be created on - filesystems not supporting it as well. + + The file they refer to may or may not exist in the file system, but must + be a path relative to our repository. + + If their sha is null (40*0), their path must exist in the file system + relative to the git repository as an object will be created from the + data at the path. + + The handling now very much equals the way string paths are processed, + except that the mode you have set will be kept. This allows you to + create symlinks by settings the mode respectively and writing the target + of the symlink directly into the file. This equals a default + Linux-Symlink which is not dereferenced automatically, except that it + can be created on filesystems not supporting it as well. Please note that globs or directories are not allowed in Blob objects. - They are added at stage 0 + They are added at stage 0. - BaseIndexEntry or type - Handling equals the one of Blob objects, but the stage may be - explicitly set. Please note that Index Entries require binary sha's. + Handling equals the one of Blob objects, but the stage may be explicitly + set. Please note that Index Entries require binary sha's. :param force: **CURRENTLY INEFFECTIVE** - If True, otherwise ignored or excluded files will be - added anyway. + If True, otherwise ignored or excluded files will be added anyway. As opposed to the git-add command, we enable this flag by default as the API user usually wants the item to be added even though they might be excluded. @@ -766,26 +790,31 @@ def add( in the index already as the index is currently being processed. :param path_rewriter: - Function with signature (string) func(BaseIndexEntry) function returning a path - for each passed entry which is the path to be actually recorded for the - object created from entry.path. This allows you to write an index which - is not identical to the layout of the actual files on your hard-disk. - If not None and ``items`` contain plain paths, these paths will be - converted to Entries beforehand and passed to the path_rewriter. - Please note that entry.path is relative to the git repository. + Function with signature (string) func(BaseIndexEntry) function returning a + path for each passed entry which is the path to be actually recorded for the + object created from entry.path. This allows you to write an index which is + not identical to the layout of the actual files on your hard-disk. If not + None and ``items`` contain plain paths, these paths will be converted to + Entries beforehand and passed to the path_rewriter. Please note that + entry.path is relative to the git repository. :param write: If True, the index will be written once it was altered. Otherwise the changes only exist in memory and are not available to git commands. :param write_extension_data: - If True, extension data will be written back to the index. This can lead to issues in case - it is containing the 'TREE' extension, which will cause the `git commit` command to write an - old tree, instead of a new one representing the now changed index. - This doesn't matter if you use `IndexFile.commit()`, which ignores the `TREE` extension altogether. - You should set it to True if you intend to use `IndexFile.commit()` exclusively while maintaining - support for third-party extensions. Besides that, you can usually safely ignore the built-in - extensions when using GitPython on repositories that are not handled manually at all. + If True, extension data will be written back to the index. This can lead to + issues in case it is containing the 'TREE' extension, which will cause the + `git commit` command to write an old tree, instead of a new one representing + the now changed index. + + This doesn't matter if you use :meth:`IndexFile.commit`, which ignores the + `TREE` extension altogether. You should set it to True if you intend to use + :meth:`IndexFile.commit` exclusively while maintaining support for + third-party extensions. Besides that, you can usually safely ignore the + built-in extensions when using GitPython on repositories that are not + handled manually at all. + All current built-in extensions are listed here: http://opensource.apple.com/source/Git/Git-26/src/git-htmldocs/technical/index-format.txt @@ -793,18 +822,17 @@ def add( List(BaseIndexEntries) representing the entries just actually added. :raise OSError: - if a supplied Path did not exist. Please note that BaseIndexEntry + If a supplied Path did not exist. Please note that BaseIndexEntry Objects that do not have a null sha will be added even if their paths do not exist. """ - # sort the entries into strings and Entries, Blobs are converted to entries - # automatically - # paths can be git-added, for everything else we use git-update-index + # Sort the entries into strings and Entries. Blobs are converted to entries automatically. + # Paths can be git-added. For everything else we use git-update-index. paths, entries = self._preprocess_add_items(items) entries_added: List[BaseIndexEntry] = [] # This code needs a working tree, therefore we try not to run it unless required. # That way, we are OK on a bare repository as well. - # If there are no paths, the rewriter has nothing to do either + # If there are no paths, the rewriter has nothing to do either. if paths: entries_added.extend(self._entries_for_paths(paths, path_rewriter, fprogress, entries)) @@ -818,7 +846,7 @@ def add( # END null mode should be remove # HANDLE ENTRY OBJECT CREATION - # create objects if required, otherwise go with the existing shas + # Create objects if required, otherwise go with the existing shas. null_entries_indices = [i for i, e in enumerate(entries) if e.binsha == Object.NULL_BIN_SHA] if null_entries_indices: @@ -828,7 +856,7 @@ def handle_null_entries(self: "IndexFile") -> None: null_entry = entries[ei] new_entry = self._store_path(null_entry.path, fprogress) - # update null entry + # Update null entry. entries[ei] = BaseIndexEntry( ( null_entry.mode, @@ -844,15 +872,14 @@ def handle_null_entries(self: "IndexFile") -> None: # END null_entry handling # REWRITE PATHS - # If we have to rewrite the entries, do so now, after we have generated - # all object sha's + # If we have to rewrite the entries, do so now, after we have generated all object sha's. if path_rewriter: for i, e in enumerate(entries): entries[i] = BaseIndexEntry((e.mode, e.binsha, e.stage, path_rewriter(e))) # END for each entry # END handle path rewriting - # just go through the remaining entries and provide progress info + # Just go through the remaining entries and provide progress info. for i, entry in enumerate(entries): progress_sent = i in null_entries_indices if not progress_sent: @@ -864,7 +891,7 @@ def handle_null_entries(self: "IndexFile") -> None: # END if there are base entries # FINALIZE - # add the new entries to this instance + # Add the new entries to this instance. for entry in entries_added: self.entries[(entry.path, 0)] = IndexEntry.from_base(entry) @@ -879,9 +906,9 @@ def _items_to_rela_paths( items: Union[PathLike, Sequence[Union[PathLike, BaseIndexEntry, Blob, Submodule]]], ) -> List[PathLike]: """Returns a list of repo-relative paths from the given items which - may be absolute or relative paths, entries or blobs""" + may be absolute or relative paths, entries or blobs.""" paths = [] - # if string put in list + # If string, put in list. if isinstance(items, (str, os.PathLike)): items = [items] @@ -937,17 +964,18 @@ def remove( List(path_string, ...) list of repository relative paths that have been removed effectively. This is interesting to know in case you have provided a directory or - globs. Paths are relative to the repository.""" + globs. Paths are relative to the repository. + """ args = [] if not working_tree: args.append("--cached") args.append("--") - # preprocess paths + # Preprocess paths. paths = self._items_to_rela_paths(items) removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines() - # process output to gain proper paths + # Process output to gain proper paths. # rm 'path' return [p[4:-1] for p in removed_paths] @@ -960,14 +988,14 @@ def move( **kwargs: Any, ) -> List[Tuple[str, str]]: """Rename/move the items, whereas the last item is considered the destination of - the move operation. If the destination is a file, the first item ( of two ) + the move operation. If the destination is a file, the first item (of two) must be a file as well. If the destination is a directory, it may be preceded by one or more directories or files. The working tree will be affected in non-bare repositories. :parma items: - Multiple types of items are supported, please see the 'remove' method + Multiple types of items are supported, please see the :meth:`remove` method for reference. :param skip_errors: If True, errors such as ones resulting from missing source files will @@ -981,7 +1009,8 @@ def move( actual destination. Relative to the repository root. :raise ValueError: If only one item was given - :raise GitCommandError: If git could not handle your request""" + :raise GitCommandError: If git could not handle your request + """ args = [] if skip_errors: args.append("-k") @@ -993,13 +1022,13 @@ def move( was_dry_run = kwargs.pop("dry_run", kwargs.pop("n", None)) kwargs["dry_run"] = True - # first execute rename in dryrun so the command tells us what it actually does - # ( for later output ) + # First execute rename in dryrun so the command tells us what it actually does. + # (for later output) out = [] mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines() - # parse result - first 0:n/2 lines are 'checking ', the remaining ones - # are the 'renaming' ones which we parse + # Parse result - first 0:n/2 lines are 'checking ', the remaining ones + # are the 'renaming' ones which we parse. for ln in range(int(len(mvlines) / 2), len(mvlines)): tokens = mvlines[ln].split(" to ") assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln] @@ -1009,12 +1038,12 @@ def move( out.append((tokens[0][9:], tokens[1])) # END for each line to parse - # either prepare for the real run, or output the dry-run result + # Either prepare for the real run, or output the dry-run result. if was_dry_run: return out # END handle dryrun - # now apply the actual operation + # Now apply the actual operation. kwargs.pop("dry_run") self.repo.git.mv(args, paths, **kwargs) @@ -1031,14 +1060,18 @@ def commit( commit_date: Union[datetime.datetime, str, None] = None, skip_hooks: bool = False, ) -> Commit: - """Commit the current default index file, creating a commit object. - For more information on the arguments, see Commit.create_from_tree(). - - :note: If you have manually altered the .entries member of this instance, - don't forget to write() your changes to disk beforehand. - Passing skip_hooks=True is the equivalent of using `-n` - or `--no-verify` on the command line. - :return: Commit object representing the new commit""" + """Commit the current default index file, creating a Commit object. + + For more information on the arguments, see + :meth:`Commit.create_from_tree `. + + :note: If you have manually altered the :attr:`entries` member of this instance, + don't forget to :meth:`write` your changes to disk beforehand. + Passing ``skip_hooks=True`` is the equivalent of using ``-n`` + or ``--no-verify`` on the command line. + + :return: :class:`Commit` object representing the new commit + """ if not skip_hooks: run_commit_hook("pre-commit", self) @@ -1099,11 +1132,11 @@ def checkout( fprogress: Callable = lambda *args: None, **kwargs: Any, ) -> Union[None, Iterator[PathLike], Sequence[PathLike]]: - """Checkout the given paths or all files from the version known to the index into - the working tree. + """Check out the given paths or all files from the version known to the index + into the working tree. - :note: Be sure you have written pending changes using the ``write`` method - in case you have altered the enties dictionary directly + :note: Be sure you have written pending changes using the :meth:`write` method + in case you have altered the entries dictionary directly. :param paths: If None, all paths in the index will be checked out. Otherwise an iterable @@ -1112,20 +1145,20 @@ def checkout( :param force: If True, existing files will be overwritten even if they contain local modifications. - If False, these will trigger a CheckoutError. + If False, these will trigger a :class:`CheckoutError`. :param fprogress: see :func:`IndexFile.add` for signature and explanation. The provided progress information will contain None as path and item if no explicit paths are given. Otherwise progress information will be send - prior and after a file has been checked out + prior and after a file has been checked out. :param kwargs: - Additional arguments to be passed to git-checkout-index + Additional arguments to be passed to git-checkout-index. :return: iterable yielding paths to files which have been checked out and are - guaranteed to match the version stored in the index + guaranteed to match the version stored in the index. :raise exc.CheckoutError: If at least one file failed to be checked out. This is a summary, @@ -1133,7 +1166,7 @@ def checkout( If one of files or directories do not exist in the index ( as opposed to the original git command who ignores them ). Raise GitCommandError if error lines could not be parsed - this truly is - an exceptional state + an exceptional state. .. note:: The checkout is limited to checking out the files in the index. Files which are not in the index anymore and exist in @@ -1307,9 +1340,9 @@ def reset( :param kwargs: Additional keyword arguments passed to git-reset - .. note:: IndexFile.reset, as opposed to HEAD.reset, will not delete anyfiles + .. note:: IndexFile.reset, as opposed to HEAD.reset, will not delete any files in order to maintain a consistent working tree. Instead, it will just - checkout the files according to their state in the index. + check out the files according to their state in the index. If you want git-reset like behaviour, use *HEAD.reset* instead. :return: self""" @@ -1355,40 +1388,40 @@ def diff( create_patch: bool = False, **kwargs: Any, ) -> git_diff.DiffIndex: - """Diff this index against the working copy or a Tree or Commit object + """Diff this index against the working copy or a Tree or Commit object. - For a documentation of the parameters and return values, see, - Diffable.diff + For a documentation of the parameters and return values, see + :meth:`Diffable.diff `. :note: Will only work with indices that represent the default git index as they have not been initialized with a stream. """ - # only run if we are the default repository index + # Only run if we are the default repository index. if self._file_path != self._index_path(): raise AssertionError("Cannot call %r on indices that do not represent the default git index" % self.diff()) - # index against index is always empty + # Index against index is always empty. if other is self.Index: return git_diff.DiffIndex() - # index against anything but None is a reverse diff with the respective + # Index against anything but None is a reverse diff with the respective # item. Handle existing -R flags properly. Transform strings to the object - # so that we can call diff on it + # so that we can call diff on it. if isinstance(other, str): other = self.repo.rev_parse(other) # END object conversion if isinstance(other, Object): # for Tree or Commit - # invert the existing R flag + # Invert the existing R flag. cur_val = kwargs.get("R", False) kwargs["R"] = not cur_val return other.diff(self.Index, paths, create_patch, **kwargs) # END diff against other item handling - # if other is not None here, something is wrong + # If other is not None here, something is wrong. if other is not None: raise ValueError("other must be None, Diffable.Index, a Tree or Commit, was %r" % other) - # diff against working copy - can be handled by superclass natively + # Diff against working copy - can be handled by superclass natively. return super(IndexFile, self).diff(other, paths, create_patch, **kwargs) diff --git a/git/index/fun.py b/git/index/fun.py index b50f1f465..ace0866f4 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -1,6 +1,5 @@ -# Contains standalone functions to accompany the index implementation and make it -# more versatile -# NOTE: Autodoc hates it if this is a docstring +# Standalone functions to accompany the index implementation and make it more versatile. +# NOTE: Autodoc hates it if this is a docstring. from io import BytesIO from pathlib import Path @@ -56,7 +55,7 @@ # ------------------------------------------------------------------------------------ -S_IFGITLINK = S_IFLNK | S_IFDIR # a submodule +S_IFGITLINK = S_IFLNK | S_IFDIR # A submodule. CE_NAMEMASK_INV = ~CE_NAMEMASK __all__ = ( @@ -81,12 +80,13 @@ def _has_file_extension(path: str) -> str: def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: - """Run the commit hook of the given name. Silently ignores hooks that do not exist. + """Run the commit hook of the given name. Silently ignore hooks that do not exist. :param name: name of hook, like 'pre-commit' :param index: IndexFile instance - :param args: arguments passed to hook file - :raises HookExecutionError:""" + :param args: Arguments passed to hook file + :raises HookExecutionError: + """ hp = hook_path(name, index.repo.git_dir) if not os.access(hp, os.X_OK): return None @@ -128,7 +128,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None: def stat_mode_to_index_mode(mode: int) -> int: """Convert the given mode from a stat call to the corresponding index mode - and return it""" + and return it.""" if S_ISLNK(mode): # symlinks return S_IFLNK if S_ISDIR(mode) or S_IFMT(mode) == S_IFGITLINK: # submodules @@ -142,9 +142,10 @@ def write_cache( extension_data: Union[None, bytes] = None, ShaStreamCls: Type[IndexFileSHA1Writer] = IndexFileSHA1Writer, ) -> None: - """Write the cache represented by entries to a stream + """Write the cache represented by entries to a stream. :param entries: **sorted** list of entries + :param stream: stream to wrap into the AdapterStreamCls - it is used for final output. @@ -152,28 +153,29 @@ def write_cache( while writing to it, before the data is passed on to the wrapped stream :param extension_data: any kind of data to write as a trailer, it must begin - a 4 byte identifier, followed by its size ( 4 bytes )""" - # wrap the stream into a compatible writer + a 4 byte identifier, followed by its size (4 bytes). + """ + # Wrap the stream into a compatible writer. stream_sha = ShaStreamCls(stream) tell = stream_sha.tell write = stream_sha.write - # header + # Header version = 2 write(b"DIRC") write(pack(">LL", version, len(entries))) - # body + # Body for entry in entries: beginoffset = tell() write(entry.ctime_bytes) # ctime write(entry.mtime_bytes) # mtime path_str = str(entry.path) path: bytes = force_bytes(path_str, encoding=defenc) - plen = len(path) & CE_NAMEMASK # path length + plen = len(path) & CE_NAMEMASK # Path length assert plen == len(path), "Path %s too long to fit into index" % entry.path - flags = plen | (entry.flags & CE_NAMEMASK_INV) # clear possible previous values + flags = plen | (entry.flags & CE_NAMEMASK_INV) # Clear possible previous values. write( pack( ">LLLLLL20sH", @@ -192,11 +194,11 @@ def write_cache( write(b"\0" * ((beginoffset + real_size) - tell())) # END for each entry - # write previously cached extensions data + # Write previously cached extensions data. if extension_data is not None: stream_sha.write(extension_data) - # write the sha over the content + # Write the sha over the content. stream_sha.write_sha() @@ -208,14 +210,16 @@ def read_header(stream: IO[bytes]) -> Tuple[int, int]: unpacked = cast(Tuple[int, int], unpack(">LL", stream.read(4 * 2))) version, num_entries = unpacked - # TODO: handle version 3: extended data, see read-cache.c + # TODO: Handle version 3: extended data, see read-cache.c. assert version in (1, 2) return version, num_entries def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, int]: """:return: Key suitable to be used for the index.entries dictionary - :param entry: One instance of type BaseIndexEntry or the path and the stage""" + + :param entry: One instance of type BaseIndexEntry or the path and the stage + """ # def is_entry_key_tup(entry_key: Tuple) -> TypeGuard[Tuple[PathLike, int]]: # return isinstance(entry_key, tuple) and len(entry_key) == 2 @@ -234,14 +238,15 @@ def entry_key(*entry: Union[BaseIndexEntry, PathLike, int]) -> Tuple[PathLike, i def read_cache( stream: IO[bytes], ) -> Tuple[int, Dict[Tuple[PathLike, int], "IndexEntry"], bytes, bytes]: - """Read a cache file from the given stream + """Read a cache file from the given stream. :return: tuple(version, entries_dict, extension_data, content_sha) - * version is the integer version number - * entries dict is a dictionary which maps IndexEntry instances to a path at a stage - * extension_data is '' or 4 bytes of type + 4 bytes of size + size bytes - * content_sha is a 20 byte sha on all cache file contents""" + * version is the integer version number. + * entries dict is a dictionary which maps IndexEntry instances to a path at a stage. + * extension_data is '' or 4 bytes of type + 4 bytes of size + size bytes. + * content_sha is a 20 byte sha on all cache file contents. + """ version, num_entries = read_header(stream) count = 0 entries: Dict[Tuple[PathLike, int], "IndexEntry"] = {} @@ -259,17 +264,17 @@ def read_cache( real_size = (tell() - beginoffset + 8) & ~7 read((beginoffset + real_size) - tell()) entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size)) - # entry_key would be the method to use, but we safe the effort + # entry_key would be the method to use, but we save the effort. entries[(path, entry.stage)] = entry count += 1 # END for each entry - # the footer contains extension data and a sha on the content so far - # Keep the extension footer,and verify we have a sha in the end + # The footer contains extension data and a sha on the content so far. + # Keep the extension footer,and verify we have a sha in the end. # Extension data format is: - # 4 bytes ID - # 4 bytes length of chunk - # repeated 0 - N times + # 4 bytes ID + # 4 bytes length of chunk + # Repeated 0 - N times extension_data = stream.read(~0) assert ( len(extension_data) > 19 @@ -277,7 +282,7 @@ def read_cache( content_sha = extension_data[-20:] - # truncate the sha in the end as we will dynamically create it anyway + # Truncate the sha in the end as we will dynamically create it anyway. extension_data = extension_data[:-20] return (version, entries, extension_data, content_sha) @@ -287,14 +292,15 @@ def write_tree_from_cache( entries: List[IndexEntry], odb: "GitCmdObjectDB", sl: slice, si: int = 0 ) -> Tuple[bytes, List["TreeCacheTup"]]: """Create a tree from the given sorted list of entries and put the respective - trees into the given object database + trees into the given object database. - :param entries: **sorted** list of IndexEntries - :param odb: object database to store the trees in - :param si: start index at which we should start creating subtrees - :param sl: slice indicating the range we should process on the entries list + :param entries: **Sorted** list of IndexEntries + :param odb: Object database to store the trees in + :param si: Start index at which we should start creating subtrees + :param sl: Slice indicating the range we should process on the entries list :return: tuple(binsha, list(tree_entry, ...)) a tuple of a sha and a list of - tree entries being a tuple of hexsha, mode, name""" + tree entries being a tuple of hexsha, mode, name + """ tree_items: List["TreeCacheTup"] = [] ci = sl.start @@ -307,10 +313,10 @@ def write_tree_from_cache( ci += 1 rbound = entry.path.find("/", si) if rbound == -1: - # its not a tree + # It's not a tree. tree_items.append((entry.binsha, entry.mode, entry.path[si:])) else: - # find common base range + # Find common base range. base = entry.path[si:rbound] xi = ci while xi < end: @@ -322,19 +328,19 @@ def write_tree_from_cache( xi += 1 # END find common base - # enter recursion - # ci - 1 as we want to count our current item as well + # Enter recursion. + # ci - 1 as we want to count our current item as well. sha, _tree_entry_list = write_tree_from_cache(entries, odb, slice(ci - 1, xi), rbound + 1) tree_items.append((sha, S_IFDIR, base)) - # skip ahead + # Skip ahead. ci = xi # END handle bounds # END for each entry - # finally create the tree + # Finally create the tree. sio = BytesIO() - tree_to_stream(tree_items, sio.write) # writes to stream as bytes, but doesn't change tree_items + tree_to_stream(tree_items, sio.write) # Writes to stream as bytes, but doesn't change tree_items. sio.seek(0) istream = odb.store(IStream(str_tree_type, len(sio.getvalue()), sio)) @@ -347,16 +353,18 @@ def _tree_entry_to_baseindexentry(tree_entry: "TreeCacheTup", stage: int) -> Bas def aggressive_tree_merge(odb: "GitCmdObjectDB", tree_shas: Sequence[bytes]) -> List[BaseIndexEntry]: """ - :return: list of BaseIndexEntries representing the aggressive merge of the given + :return: List of BaseIndexEntries representing the aggressive merge of the given trees. All valid entries are on stage 0, whereas the conflicting ones are left on stage 1, 2 or 3, whereas stage 1 corresponds to the common ancestor tree, 2 to our tree and 3 to 'their' tree. - :param tree_shas: 1, 2 or 3 trees as identified by their binary 20 byte shas - If 1 or two, the entries will effectively correspond to the last given tree - If 3 are given, a 3 way merge is performed""" + + :param tree_shas: 1, 2 or 3 trees as identified by their binary 20 byte shas. + If 1 or two, the entries will effectively correspond to the last given tree. + If 3 are given, a 3 way merge is performed. + """ out: List[BaseIndexEntry] = [] - # one and two way is the same for us, as we don't have to handle an existing + # One and two way is the same for us, as we don't have to handle an existing # index, instrea if len(tree_shas) in (1, 2): for entry in traverse_tree_recursive(odb, tree_shas[-1], ""): @@ -368,72 +376,72 @@ def aggressive_tree_merge(odb: "GitCmdObjectDB", tree_shas: Sequence[bytes]) -> if len(tree_shas) > 3: raise ValueError("Cannot handle %i trees at once" % len(tree_shas)) - # three trees + # Three trees. for base, ours, theirs in traverse_trees_recursive(odb, tree_shas, ""): if base is not None: - # base version exists + # Base version exists. if ours is not None: - # ours exists + # Ours exists. if theirs is not None: - # it exists in all branches, if it was changed in both - # its a conflict, otherwise we take the changed version - # This should be the most common branch, so it comes first + # It exists in all branches. Ff it was changed in both + # its a conflict. Otherwise, we take the changed version. + # This should be the most common branch, so it comes first. if (base[0] != ours[0] and base[0] != theirs[0] and ours[0] != theirs[0]) or ( base[1] != ours[1] and base[1] != theirs[1] and ours[1] != theirs[1] ): - # changed by both + # Changed by both. out.append(_tree_entry_to_baseindexentry(base, 1)) out.append(_tree_entry_to_baseindexentry(ours, 2)) out.append(_tree_entry_to_baseindexentry(theirs, 3)) elif base[0] != ours[0] or base[1] != ours[1]: - # only we changed it + # Only we changed it. out.append(_tree_entry_to_baseindexentry(ours, 0)) else: - # either nobody changed it, or they did. In either - # case, use theirs + # Either nobody changed it, or they did. In either + # case, use theirs. out.append(_tree_entry_to_baseindexentry(theirs, 0)) # END handle modification else: if ours[0] != base[0] or ours[1] != base[1]: - # they deleted it, we changed it, conflict + # They deleted it, we changed it, conflict. out.append(_tree_entry_to_baseindexentry(base, 1)) out.append(_tree_entry_to_baseindexentry(ours, 2)) # else: - # we didn't change it, ignore + # # We didn't change it, ignore. # pass # END handle our change # END handle theirs else: if theirs is None: - # deleted in both, its fine - its out + # Deleted in both, its fine - it's out. pass else: if theirs[0] != base[0] or theirs[1] != base[1]: - # deleted in ours, changed theirs, conflict + # Deleted in ours, changed theirs, conflict. out.append(_tree_entry_to_baseindexentry(base, 1)) out.append(_tree_entry_to_baseindexentry(theirs, 3)) # END theirs changed # else: - # theirs didn't change + # # Theirs didn't change. # pass # END handle theirs # END handle ours else: - # all three can't be None + # All three can't be None. if ours is None: - # added in their branch + # Added in their branch. assert theirs is not None out.append(_tree_entry_to_baseindexentry(theirs, 0)) elif theirs is None: - # added in our branch + # Added in our branch. out.append(_tree_entry_to_baseindexentry(ours, 0)) else: - # both have it, except for the base, see whether it changed + # Both have it, except for the base, see whether it changed. if ours[0] != theirs[0] or ours[1] != theirs[1]: out.append(_tree_entry_to_baseindexentry(ours, 2)) out.append(_tree_entry_to_baseindexentry(theirs, 3)) else: - # it was added the same in both + # It was added the same in both. out.append(_tree_entry_to_baseindexentry(ours, 0)) # END handle two items # END handle heads diff --git a/git/index/typ.py b/git/index/typ.py index b2c6c371b..046df6e83 100644 --- a/git/index/typ.py +++ b/git/index/typ.py @@ -1,4 +1,4 @@ -"""Module with additional types used by the index""" +"""Module with additional types used by the index.""" from binascii import b2a_hex from pathlib import Path @@ -33,7 +33,6 @@ class BlobFilter(object): - """ Predicate to be used by iter_blobs allowing to filter only return blobs which match the given list of directories or files. @@ -46,7 +45,7 @@ class BlobFilter(object): def __init__(self, paths: Sequence[PathLike]) -> None: """ :param paths: - tuple or list of paths which are either pointing to directories or + Tuple or list of paths which are either pointing to directories or to files relative to the current repository """ self.paths = paths @@ -84,8 +83,7 @@ class BaseIndexEntryHelper(NamedTuple): class BaseIndexEntry(BaseIndexEntryHelper): - - """Small Brother of an index entry which can be created to describe changes + """Small brother of an index entry which can be created to describe changes done to the index in which case plenty of additional information is not required. As the first 4 data members match exactly to the IndexEntry type, methods @@ -138,25 +136,26 @@ def to_blob(self, repo: "Repo") -> Blob: class IndexEntry(BaseIndexEntry): - """Allows convenient access to IndexEntry data without completely unpacking it. - Attributes usully accessed often are cached in the tuple whereas others are + Attributes usually accessed often are cached in the tuple whereas others are unpacked on demand. - See the properties for a mapping between names and tuple indices.""" + See the properties for a mapping between names and tuple indices. + """ @property def ctime(self) -> Tuple[int, int]: """ :return: Tuple(int_time_seconds_since_epoch, int_nano_seconds) of the - file's creation time""" + file's creation time + """ return cast(Tuple[int, int], unpack(">LL", self.ctime_bytes)) @property def mtime(self) -> Tuple[int, int]: - """See ctime property, but returns modification time""" + """See ctime property, but returns modification time.""" return cast(Tuple[int, int], unpack(">LL", self.mtime_bytes)) @classmethod @@ -164,9 +163,10 @@ def from_base(cls, base: "BaseIndexEntry") -> "IndexEntry": """ :return: Minimal entry as created from the given BaseIndexEntry instance. - Missing values will be set to null-like values + Missing values will be set to null-like values. - :param base: Instance of type BaseIndexEntry""" + :param base: Instance of type :class:`BaseIndexEntry` + """ time = pack(">LL", 0, 0) return IndexEntry((base.mode, base.binsha, base.flags, base.path, time, time, 0, 0, 0, 0, 0)) diff --git a/git/index/util.py b/git/index/util.py index 6cf838f3b..f52b61b4a 100644 --- a/git/index/util.py +++ b/git/index/util.py @@ -1,4 +1,5 @@ -"""Module containing index utilities""" +"""Module containing index utilities.""" + from functools import wraps import os import struct @@ -33,7 +34,6 @@ class TemporaryFileSwap(object): - """Utility class moving a file to a temporary location within the same directory and moving it back on to where on object deletion.""" @@ -42,7 +42,7 @@ class TemporaryFileSwap(object): def __init__(self, file_path: PathLike) -> None: self.file_path = file_path self.tmp_file_path = str(self.file_path) + tempfile.mktemp("", "", "") - # it may be that the source does not exist + # It may be that the source does not exist. try: os.rename(self.file_path, self.tmp_file_path) except OSError: @@ -90,9 +90,11 @@ def post_clear_cache_if_not_raised(self: "IndexFile", *args: Any, **kwargs: Any) def default_index(func: Callable[..., _T]) -> Callable[..., _T]: - """Decorator assuring the wrapped method may only run if we are the default - repository index. This is as we rely on git commands that operate - on that index only.""" + """Decorator ensuring the wrapped method may only run if we are the default + repository index. + + This is as we rely on git commands that operate on that index only. + """ @wraps(func) def check_default_index(self: "IndexFile", *args: Any, **kwargs: Any) -> _T: @@ -109,7 +111,7 @@ def check_default_index(self: "IndexFile", *args: Any, **kwargs: Any) -> _T: def git_working_dir(func: Callable[..., _T]) -> Callable[..., _T]: """Decorator which changes the current working dir to the one of the git - repository in order to assure relative paths are handled correctly""" + repository in order to ensure relative paths are handled correctly.""" @wraps(func) def set_git_working_dir(self: "IndexFile", *args: Any, **kwargs: Any) -> _T: diff --git a/git/objects/__init__.py b/git/objects/__init__.py index 5910ac58a..2a4a114c7 100644 --- a/git/objects/__init__.py +++ b/git/objects/__init__.py @@ -1,7 +1,7 @@ -""" -Import all submodules main classes into the package space -""" +"""Import all submodules' main classes into the package space.""" + # flake8: noqa + import inspect from .base import * @@ -14,11 +14,10 @@ from .tree import * # Fix import dependency - add IndexObject to the util module, so that it can be -# imported by the submodule.base +# imported by the submodule.base. smutil.IndexObject = IndexObject # type: ignore[attr-defined] smutil.Object = Object # type: ignore[attr-defined] del smutil -# must come after submodule was made available - +# Must come after submodule was made available. __all__ = [name for name, obj in locals().items() if not (name.startswith("_") or inspect.ismodule(obj))] diff --git a/git/objects/base.py b/git/objects/base.py index 0dab5ccdb..0d88aa185 100644 --- a/git/objects/base.py +++ b/git/objects/base.py @@ -38,8 +38,7 @@ class Object(LazyMixin): - - """Implements an Object which may be Blobs, Trees, Commits and Tags""" + """An Object which may be Blobs, Trees, Commits and Tags.""" NULL_HEX_SHA = "0" * 40 NULL_BIN_SHA = b"\0" * 20 @@ -50,7 +49,9 @@ class Object(LazyMixin): dbtyp.str_commit_type, dbtyp.str_tag_type, ) + __slots__ = ("repo", "binsha", "size") + type: Union[Lit_commit_ish, None] = None def __init__(self, repo: "Repo", binsha: bytes): @@ -59,7 +60,8 @@ def __init__(self, repo: "Repo", binsha: bytes): :param repo: repository this object is located in - :param binsha: 20 byte SHA1""" + :param binsha: 20 byte SHA1 + """ super(Object, self).__init__() self.repo = repo self.binsha = binsha @@ -71,14 +73,15 @@ def __init__(self, repo: "Repo", binsha: bytes): @classmethod def new(cls, repo: "Repo", id: Union[str, "Reference"]) -> Commit_ish: """ - :return: New Object instance of a type appropriate to the object type behind - id. The id of the newly created object will be a binsha even though - the input id may have been a Reference or Rev-Spec + :return: New :class:`Object`` instance of a type appropriate to the object type + behind `id`. The id of the newly created object will be a binsha even though + the input id may have been a Reference or Rev-Spec. :param id: reference, rev-spec, or hexsha - :note: This cannot be a __new__ method as it would always call __init__ - with the input id which is not necessarily a binsha.""" + :note: This cannot be a ``__new__`` method as it would always call + :meth:`__init__` with the input id which is not necessarily a binsha. + """ return repo.rev_parse(str(id)) @classmethod @@ -86,9 +89,11 @@ def new_from_sha(cls, repo: "Repo", sha1: bytes) -> Commit_ish: """ :return: new object instance of a type appropriate to represent the given binary sha1 - :param sha1: 20 byte binary sha1""" + + :param sha1: 20 byte binary sha1 + """ if sha1 == cls.NULL_BIN_SHA: - # the NULL binsha is always the root commit + # The NULL binsha is always the root commit. return get_object_type_by_name(b"commit")(repo, sha1) # END handle special case oinfo = repo.odb.info(sha1) @@ -97,7 +102,7 @@ def new_from_sha(cls, repo: "Repo", sha1: bytes) -> Commit_ish: return inst def _set_cache_(self, attr: str) -> None: - """Retrieve object information""" + """Retrieve object information.""" if attr == "size": oinfo = self.repo.odb.info(self.binsha) self.size = oinfo.size # type: int @@ -137,28 +142,31 @@ def hexsha(self) -> str: @property def data_stream(self) -> "OStream": - """:return: File Object compatible stream to the uncompressed raw data of the object - :note: returned streams must be read in order""" + """ + :return: File Object compatible stream to the uncompressed raw data of the object + + :note: Returned streams must be read in order. + """ return self.repo.odb.stream(self.binsha) def stream_data(self, ostream: "OStream") -> "Object": - """Writes our data directly to the given output stream + """Write our data directly to the given output stream. :param ostream: File object compatible stream object. - :return: self""" + :return: self + """ istream = self.repo.odb.stream(self.binsha) stream_copy(istream, ostream) return self class IndexObject(Object): - - """Base for all objects that can be part of the index file , namely Tree, Blob and - SubModule objects""" + """Base for all objects that can be part of the index file, namely Tree, Blob and + SubModule objects.""" __slots__ = ("path", "mode") - # for compatibility with iterable lists + # For compatibility with iterable lists. _id_attribute_ = "path" def __init__( @@ -168,19 +176,20 @@ def __init__( mode: Union[None, int] = None, path: Union[None, PathLike] = None, ) -> None: - """Initialize a newly instanced IndexObject + """Initialize a newly instanced IndexObject. - :param repo: is the Repo we are located in - :param binsha: 20 byte sha1 + :param repo: The :class:`Repo ` we are located in. + :param binsha: 20 byte sha1. :param mode: - is the stat compatible file mode as int, use the stat module - to evaluate the information + The stat compatible file mode as int, use the :mod:`stat` module to evaluate + the information. :param path: - is the path to the file in the file system, relative to the git repository root, i.e. - file.ext or folder/other.ext + The path to the file in the file system, relative to the git repository + root, like ``file.ext`` or ``folder/other.ext``. :note: - Path may not be set of the index object has been created directly as it cannot - be retrieved without knowing the parent tree.""" + Path may not be set if the index object has been created directly, as it + cannot be retrieved without knowing the parent tree. + """ super(IndexObject, self).__init__(repo, binsha) if mode is not None: self.mode = mode @@ -191,7 +200,8 @@ def __hash__(self) -> int: """ :return: Hash of our path as index items are uniquely identifiable by path, not - by their data !""" + by their data! + """ return hash(self.path) def _set_cache_(self, attr: str) -> None: @@ -214,10 +224,11 @@ def name(self) -> str: def abspath(self) -> PathLike: R""" :return: - Absolute path to this index object in the file system ( as opposed to the - .path field which is a path relative to the git repository ). + Absolute path to this index object in the file system (as opposed to the + :attr:`path` field which is a path relative to the git repository). - The returned path will be native to the system and contains '\' on windows.""" + The returned path will be native to the system and contains '\' on Windows. + """ if self.repo.working_tree_dir is not None: return join_path_native(self.repo.working_tree_dir, self.path) else: diff --git a/git/objects/blob.py b/git/objects/blob.py index 96ce486f5..f0d3181c2 100644 --- a/git/objects/blob.py +++ b/git/objects/blob.py @@ -3,6 +3,7 @@ # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ + from mimetypes import guess_type from . import base @@ -12,13 +13,12 @@ class Blob(base.IndexObject): - - """A Blob encapsulates a git blob object""" + """A Blob encapsulates a git blob object.""" DEFAULT_MIME_TYPE = "text/plain" type: Literal["blob"] = "blob" - # valid blob modes + # Valid blob modes executable_mode = 0o100755 file_mode = 0o100644 link_mode = 0o120000 @@ -29,7 +29,9 @@ class Blob(base.IndexObject): def mime_type(self) -> str: """ :return: String describing the mime type of this file (based on the filename) - :note: Defaults to 'text/plain' in case the actual file type is unknown.""" + + :note: Defaults to 'text/plain' in case the actual file type is unknown. + """ guesses = None if self.path: guesses = guess_type(str(self.path)) diff --git a/git/objects/commit.py b/git/objects/commit.py index fd65fa1e4..29123a44f 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -3,6 +3,7 @@ # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ + import datetime import re from subprocess import Popen, PIPE @@ -66,7 +67,7 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable): value on demand only if it involves calling the git binary.""" # ENVIRONMENT VARIABLES - # read when creating new commits + # Read when creating new commits. env_author_date = "GIT_AUTHOR_DATE" env_committer_date = "GIT_COMMITTER_DATE" @@ -113,36 +114,38 @@ def __init__( be implicitly set on first query. :param binsha: 20 byte sha1 - :param parents: tuple( Commit, ... ) - is a tuple of commit ids or actual Commits + :param parents: tuple(Commit, ...) + A tuple of commit ids or actual Commits :param tree: Tree object :param author: Actor - is the author Actor object + The author Actor object :param authored_date: int_seconds_since_epoch - is the authored DateTime - use time.gmtime() to convert it into a + The authored DateTime - use time.gmtime() to convert it into a different format :param author_tz_offset: int_seconds_west_of_utc - is the timezone that the authored_date is in + The timezone that the authored_date is in :param committer: Actor - is the committer string + The committer string :param committed_date: int_seconds_since_epoch - is the committed DateTime - use time.gmtime() to convert it into a + The committed DateTime - use time.gmtime() to convert it into a different format :param committer_tz_offset: int_seconds_west_of_utc - is the timezone that the committed_date is in + The timezone that the committed_date is in :param message: string - is the commit message + The commit message :param encoding: string - encoding of the message, defaults to UTF-8 + Encoding of the message, defaults to UTF-8 :param parents: List or tuple of Commit objects which are our parent(s) in the commit dependency graph + :return: git.Commit :note: Timezone information is in the same format and in the same sign as what time.altzone returns. The sign is inverted compared to git's - UTC timezone.""" + UTC timezone. + """ super(Commit, self).__init__(repo, binsha) self.binsha = binsha if tree is not None: @@ -211,7 +214,7 @@ def replace(self, **kwargs: Any) -> "Commit": def _set_cache_(self, attr: str) -> None: if attr in Commit.__slots__: - # read the data in a chunk, its faster - then provide a file wrapper + # Read the data in a chunk, its faster - then provide a file wrapper. _binsha, _typename, self.size, stream = self.repo.odb.stream(self.binsha) self._deserialize(BytesIO(stream.read())) else: @@ -235,17 +238,19 @@ def summary(self) -> Union[str, bytes]: return self.message.split(b"\n", 1)[0] def count(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any) -> int: - """Count the number of commits reachable from this commit + """Count the number of commits reachable from this commit. :param paths: - is an optional path or a list of paths restricting the return value - to commits actually containing the paths + An optional path or a list of paths restricting the return value + to commits actually containing the paths. :param kwargs: Additional options to be passed to git-rev-list. They must not alter - the output style of the command, or parsing will yield incorrect results - :return: int defining the number of reachable commits""" - # yes, it makes a difference whether empty paths are given or not in our case + the output style of the command, or parsing will yield incorrect results. + + :return: An int defining the number of reachable commits + """ + # Yes, it makes a difference whether empty paths are given or not in our case # as the empty paths version will ignore merge commits for some reason. if paths: return len(self.repo.git.rev_list(self.hexsha, "--", paths, **kwargs).splitlines()) @@ -256,7 +261,8 @@ def name_rev(self) -> str: """ :return: String describing the commits hex sha based on the closest Reference. - Mostly useful for UI purposes""" + Mostly useful for UI purposes + """ return self.repo.git.name_rev(self) @classmethod @@ -269,23 +275,29 @@ def iter_items( ) -> Iterator["Commit"]: """Find all commits matching the given criteria. - :param repo: is the Repo - :param rev: revision specifier, see git-rev-parse for viable options + :param repo: The Repo + + :param rev: Revision specifier, see git-rev-parse for viable options. + :param paths: - is an optional path or list of paths, if set only Commits that include the path - or paths will be considered + An optional path or list of paths, if set only Commits that include the path + or paths will be considered. + :param kwargs: - optional keyword arguments to git rev-list where - ``max_count`` is the maximum number of commits to fetch - ``skip`` is the number of commits to skip - ``since`` all commits since i.e. '1970-01-01' - :return: iterator yielding Commit items""" + Optional keyword arguments to ``git rev-list`` where: + + * ``max_count`` is the maximum number of commits to fetch + * ``skip`` is the number of commits to skip + * ``since`` all commits since e.g. '1970-01-01' + + :return: Iterator yielding :class:`Commit` items. + """ if "pretty" in kwargs: raise ValueError("--pretty cannot be used as parsing expects single sha's only") # END handle pretty - # use -- in any case, to prevent possibility of ambiguous arguments - # see https://github.com/gitpython-developers/GitPython/issues/264 + # Use -- in all cases, to prevent possibility of ambiguous arguments. + # See https://github.com/gitpython-developers/GitPython/issues/264. args_list: List[PathLike] = ["--"] @@ -309,7 +321,8 @@ def iter_parents(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs Optional path or list of paths limiting the Commits to those that contain at least one of the paths :param kwargs: All arguments allowed by git-rev-list - :return: Iterator yielding Commit objects which are parents of self""" + :return: Iterator yielding Commit objects which are parents of self + """ # skip ourselves skip = kwargs.get("skip", 1) if skip == 0: # skip ourselves @@ -323,7 +336,8 @@ def stats(self) -> Stats: """Create a git stat from changes between this commit and its first parent or from all changes done if this is the very first commit. - :return: git.Stats""" + :return: git.Stats + """ if not self.parents: text = self.repo.git.diff_tree(self.hexsha, "--", numstat=True, no_renames=True, root=True) text2 = "" @@ -339,17 +353,18 @@ def stats(self) -> Stats: def trailers(self) -> Dict[str, str]: """Get the trailers of the message as a dictionary - :note: This property is deprecated, please use either ``Commit.trailers_list`` or ``Commit.trailers_dict``. + :note: This property is deprecated, please use either ``Commit.trailers_list`` + or ``Commit.trailers_dict``. :return: - Dictionary containing whitespace stripped trailer information. - Only contains the latest instance of each trailer key. + Dictionary containing whitespace stripped trailer information. Only contains + the latest instance of each trailer key. """ return {k: v[0] for k, v in self.trailers_dict.items()} @property def trailers_list(self) -> List[Tuple[str, str]]: - """Get the trailers of the message as a list + """Get the trailers of the message as a list. Git messages can contain trailer information that are similar to RFC 822 e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers). @@ -399,7 +414,7 @@ def trailers_list(self) -> List[Tuple[str, str]]: @property def trailers_dict(self) -> Dict[str, List[str]]: - """Get the trailers of the message as a dictionary + """Get the trailers of the message as a dictionary. Git messages can contain trailer information that are similar to RFC 822 e-mail headers (see: https://git-scm.com/docs/git-interpret-trailers). @@ -440,12 +455,14 @@ def trailers_dict(self) -> Dict[str, List[str]]: @classmethod def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, IO]) -> Iterator["Commit"]: - """Parse out commit information into a list of Commit objects + """Parse out commit information into a list of Commit objects. + We expect one-line per commit, and parse the actual commit information directly - from our lighting fast object database + from our lighting fast object database. :param proc: git-rev-list process instance - one sha per line - :return: iterator returning Commit objects""" + :return: iterator supplying :class:`Commit` objects + """ # def is_proc(inp) -> TypeGuard[Popen]: # return hasattr(proc_or_stream, 'wait') and not hasattr(proc_or_stream, 'readline') @@ -468,15 +485,16 @@ def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, break hexsha = line.strip() if len(hexsha) > 40: - # split additional information, as returned by bisect for instance + # Split additional information, as returned by bisect for instance. hexsha, _ = line.split(None, 1) # END handle extra info assert len(hexsha) == 40, "Invalid line: %s" % hexsha yield cls(repo, hex_to_bin(hexsha)) # END for each line in stream + # TODO: Review this - it seems process handling got a bit out of control - # due to many developers trying to fix the open file handles issue + # due to many developers trying to fix the open file handles issue. if hasattr(proc_or_stream, "wait"): proc_or_stream = cast(Popen, proc_or_stream) finalize_process(proc_or_stream) @@ -497,38 +515,38 @@ def create_from_tree( """Commit the given tree, creating a commit object. :param repo: Repo object the commit should be part of - :param tree: Tree object or hex or bin sha - the tree of the new commit - :param message: Commit message. It may be an empty string if no message is provided. - It will be converted to a string , in any case. + :param tree: Tree object or hex or bin sha. The tree of the new commit. + :param message: Commit message. It may be an empty string if no message is + provided. It will be converted to a string, in any case. :param parent_commits: - Optional Commit objects to use as parents for the new commit. + Optional :class:`Commit` objects to use as parents for the new commit. If empty list, the commit will have no parents at all and become a root commit. - If None , the current head commit will be the parent of the - new commit object + If None, the current head commit will be the parent of the + new commit object. :param head: If True, the HEAD will be advanced to the new commit automatically. - Else the HEAD will remain pointing on the previous commit. This could + Otherwise the HEAD will remain pointing on the previous commit. This could lead to undesired results when diffing files. :param author: The name of the author, optional. If unset, the repository configuration is used to obtain this value. :param committer: The name of the committer, optional. If unset, the repository configuration is used to obtain this value. - :param author_date: The timestamp for the author field - :param commit_date: The timestamp for the committer field + :param author_date: The timestamp for the author field. + :param commit_date: The timestamp for the committer field. - :return: Commit object representing the new commit + :return: Commit object representing the new commit. :note: Additional information about the committer and Author are taken from the environment or from the git configuration, see git-commit-tree for - more information""" + more information. + """ if parent_commits is None: try: parent_commits = [repo.head.commit] except ValueError: - # empty repositories have no head commit + # Empty repositories have no head commit. parent_commits = [] # END handle parent commits else: @@ -538,11 +556,10 @@ def create_from_tree( # end check parent commit types # END if parent commits are unset - # retrieve all additional information, create a commit object, and - # serialize it + # Retrieve all additional information, create a commit object, and serialize it. # Generally: - # * Environment variables override configuration values - # * Sensible defaults are set according to the git documentation + # * Environment variables override configuration values. + # * Sensible defaults are set according to the git documentation. # COMMITTER AND AUTHOR INFO cr = repo.config_reader() @@ -574,14 +591,14 @@ def create_from_tree( committer_time, committer_offset = unix_time, offset # END set committer time - # assume utf8 encoding + # Assume UTF-8 encoding. enc_section, enc_option = cls.conf_encoding.split(".") conf_encoding = cr.get_value(enc_section, enc_option, cls.default_encoding) if not isinstance(conf_encoding, str): raise TypeError("conf_encoding could not be coerced to str") - # if the tree is no object, make sure we create one - otherwise - # the created commit object is invalid + # If the tree is no object, make sure we create one - otherwise + # the created commit object is invalid. if isinstance(tree, str): tree = repo.tree(tree) # END tree conversion @@ -605,15 +622,15 @@ def create_from_tree( new_commit.binsha = cls._calculate_sha_(repo, new_commit) if head: - # need late import here, importing git at the very beginning throws - # as well ... + # Need late import here, importing git at the very beginning throws + # as well... import git.refs try: repo.head.set_commit(new_commit, logmsg=message) except ValueError: - # head is not yet set to the ref our HEAD points to - # Happens on first commit + # head is not yet set to the ref our HEAD points to. + # Happens on first commit. master = git.refs.Head.create( repo, repo.head.ref, @@ -651,7 +668,7 @@ def _serialize(self, stream: BytesIO) -> "Commit": ).encode(self.encoding) ) - # encode committer + # Encode committer. aname = c.name write( ( @@ -679,7 +696,7 @@ def _serialize(self, stream: BytesIO) -> "Commit": write(b"\n") - # write plain bytes, be sure its encoded according to our encoding + # Write plain bytes, be sure its encoded according to our encoding. if isinstance(self.message, str): write(self.message.encode(self.encoding)) else: @@ -703,11 +720,11 @@ def _deserialize(self, stream: BytesIO) -> "Commit": # END for each parent line self.parents = tuple(self.parents) - # we don't know actual author encoding before we have parsed it, so keep the lines around + # We don't know actual author encoding before we have parsed it, so keep the lines around. author_line = next_line committer_line = readline() - # we might run into one or more mergetag blocks, skip those for now + # We might run into one or more mergetag blocks, skip those for now. next_line = readline() while next_line.startswith(b"mergetag "): next_line = readline() @@ -715,12 +732,11 @@ def _deserialize(self, stream: BytesIO) -> "Commit": next_line = readline() # end skip mergetags - # now we can have the encoding line, or an empty line followed by the optional - # message. + # Now we can have the encoding line, or an empty line followed by the optional message. self.encoding = self.default_encoding self.gpgsig = "" - # read headers + # Read headers. enc = next_line buf = enc.strip() while buf: @@ -743,8 +759,8 @@ def _deserialize(self, stream: BytesIO) -> "Commit": if is_next_header: continue buf = readline().strip() - # decode the authors name + # Decode the author's name. try: ( self.author, @@ -774,8 +790,8 @@ def _deserialize(self, stream: BytesIO) -> "Commit": ) # END handle author's encoding - # a stream from our data simply gives us the plain message - # The end of our message stream is marked with a newline that we strip + # A stream from our data simply gives us the plain message. + # The end of our message stream is marked with a newline that we strip. self.message = stream.read() try: self.message = self.message.decode(self.encoding, "replace") @@ -796,6 +812,7 @@ def _deserialize(self, stream: BytesIO) -> "Commit": def co_authors(self) -> List[Actor]: """ Search the commit message for any co-authors of this commit. + Details on co-authors: https://github.blog/2018-01-29-commit-together-with-co-authors/ :return: List of co-authors for this commit (as Actor objects). diff --git a/git/objects/fun.py b/git/objects/fun.py index 043eec721..7756154be 100644 --- a/git/objects/fun.py +++ b/git/objects/fun.py @@ -1,4 +1,5 @@ -"""Module with functions which are supposed to be as fast as possible""" +"""Module with functions which are supposed to be as fast as possible.""" + from stat import S_ISDIR @@ -36,12 +37,13 @@ def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer"], Union[int, None]]) -> None: - """Write the give list of entries into a stream using its write method + """Write the given list of entries into a stream using its write method. :param entries: **sorted** list of tuples with (binsha, mode, name) - :param write: write method which takes a data string""" + :param write: write method which takes a data string + """ ord_zero = ord("0") - bit_mask = 7 # 3 bits set + bit_mask = 7 # 3 bits set. for binsha, mode, name in entries: mode_str = b"" @@ -49,16 +51,16 @@ def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer mode_str = bytes([((mode >> (i * 3)) & bit_mask) + ord_zero]) + mode_str # END for each 8 octal value - # git slices away the first octal if its zero + # git slices away the first octal if it's zero. if mode_str[0] == ord_zero: mode_str = mode_str[1:] # END save a byte - # here it comes: if the name is actually unicode, the replacement below + # Here it comes: If the name is actually unicode, the replacement below # will not work as the binsha is not part of the ascii unicode encoding - - # hence we must convert to an utf8 string for it to work properly. + # hence we must convert to an UTF-8 string for it to work properly. # According to my tests, this is exactly what git does, that is it just - # takes the input literally, which appears to be utf8 on linux. + # takes the input literally, which appears to be UTF-8 on linux. if isinstance(name, str): name_bytes = name.encode(defenc) else: @@ -80,32 +82,32 @@ def tree_entries_from_data(data: bytes) -> List[EntryTup]: while i < len_data: mode = 0 - # read mode - # Some git versions truncate the leading 0, some don't - # The type will be extracted from the mode later + # Read Mode + # Some git versions truncate the leading 0, some don't. + # The type will be extracted from the mode later. while data[i] != space_ord: - # move existing mode integer up one level being 3 bits - # and add the actual ordinal value of the character + # Move existing mode integer up one level being 3 bits + # and add the actual ordinal value of the character. mode = (mode << 3) + (data[i] - ord_zero) i += 1 # END while reading mode - # byte is space now, skip it + # Byte is space now, skip it. i += 1 - # parse name, it is NULL separated + # Parse name, it is NULL separated. ns = i while data[i] != 0: i += 1 # END while not reached NULL - # default encoding for strings in git is utf8 - # Only use the respective unicode object if the byte stream was encoded + # Default encoding for strings in git is UTF-8. + # Only use the respective unicode object if the byte stream was encoded. name_bytes = data[ns:i] name = safe_decode(name_bytes) - # byte is NULL, get next 20 + # Byte is NULL, get next 20. i += 1 sha = data[i : i + 20] i = i + 20 @@ -115,10 +117,11 @@ def tree_entries_from_data(data: bytes) -> List[EntryTup]: def _find_by_name(tree_data: MutableSequence[EntryTupOrNone], name: str, is_dir: bool, start_at: int) -> EntryTupOrNone: - """return data entry matching the given name and tree mode - or None. - Before the item is returned, the respective data item is set - None in the tree_data list to mark it done""" + """Return data entry matching the given name and tree mode or None. + + Before the item is returned, the respective data item is set None in the + tree_data list to mark it done. + """ try: item = tree_data[start_at] @@ -148,7 +151,7 @@ def _to_full_path(item: EntryTup, path_prefix: str) -> EntryTup: def _to_full_path(item: EntryTupOrNone, path_prefix: str) -> EntryTupOrNone: - """Rebuild entry with given path prefix""" + """Rebuild entry with given path prefix.""" if not item: return item return (item[0], item[1], path_prefix + item[2]) @@ -160,17 +163,23 @@ def traverse_trees_recursive( """ :return: list of list with entries according to the given binary tree-shas. The result is encoded in a list - of n tuple|None per blob/commit, (n == len(tree_shas)), where + of n tuple|None per blob/commit, (n == len(tree_shas)), where: + * [0] == 20 byte sha * [1] == mode as int * [2] == path relative to working tree root + The entry tuple is None if the respective blob/commit did not exist in the given tree. + :param tree_shas: iterable of shas pointing to trees. All trees must - be on the same level. A tree-sha may be None in which case None + be on the same level. A tree-sha may be None in which case None. + :param path_prefix: a prefix to be added to the returned paths on this level, - set it '' for the first iteration - :note: The ordering of the returned items will be partially lost""" + set it '' for the first iteration. + + :note: The ordering of the returned items will be partially lost. + """ trees_data: List[List[EntryTupOrNone]] = [] nt = len(tree_shas) @@ -178,7 +187,7 @@ def traverse_trees_recursive( if tree_sha is None: data: List[EntryTupOrNone] = [] else: - # make new list for typing as list invariant + # Make new list for typing as list invariant. data = list(tree_entries_from_data(odb.stream(tree_sha).read())) # END handle muted trees trees_data.append(data) @@ -186,9 +195,9 @@ def traverse_trees_recursive( out: List[Tuple[EntryTupOrNone, ...]] = [] - # find all matching entries and recursively process them together if the match + # Find all matching entries and recursively process them together if the match # is a tree. If the match is a non-tree item, put it into the result. - # Processed items will be set None + # Processed items will be set None. for ti, tree_data in enumerate(trees_data): for ii, item in enumerate(tree_data): if not item: @@ -198,17 +207,17 @@ def traverse_trees_recursive( entries = [None for _ in range(nt)] entries[ti] = item _sha, mode, name = item - is_dir = S_ISDIR(mode) # type mode bits + is_dir = S_ISDIR(mode) # Type mode bits - # find this item in all other tree data items - # wrap around, but stop one before our current index, hence - # ti+nt, not ti+1+nt + # Find this item in all other tree data items. + # Wrap around, but stop one before our current index, hence + # ti+nt, not ti+1+nt. for tio in range(ti + 1, ti + nt): tio = tio % nt entries[tio] = _find_by_name(trees_data[tio], name, is_dir, ii) # END for each other item data - # if we are a directory, enter recursion + # If we are a directory, enter recursion. if is_dir: out.extend( traverse_trees_recursive( @@ -221,11 +230,11 @@ def traverse_trees_recursive( out.append(tuple(_to_full_path(e, path_prefix) for e in entries)) # END handle recursion - # finally mark it done + # Finally mark it done. tree_data[ii] = None # END for each item - # we are done with one tree, set all its data empty + # We are done with one tree, set all its data empty. del tree_data[:] # END for each tree_data chunk return out @@ -233,16 +242,20 @@ def traverse_trees_recursive( def traverse_tree_recursive(odb: "GitCmdObjectDB", tree_sha: bytes, path_prefix: str) -> List[EntryTup]: """ - :return: list of entries of the tree pointed to by the binary tree_sha. An entry - has the following format: + :return: list of entries of the tree pointed to by the binary tree_sha. + + An entry has the following format: + * [0] 20 byte sha * [1] mode as int * [2] path relative to the repository - :param path_prefix: prefix to prepend to the front of all returned paths""" + + :param path_prefix: Prefix to prepend to the front of all returned paths. + """ entries = [] data = tree_entries_from_data(odb.stream(tree_sha).read()) - # unpacking/packing is faster than accessing individual items + # Unpacking/packing is faster than accessing individual items. for sha, mode, name in data: if S_ISDIR(mode): entries.extend(traverse_tree_recursive(odb, sha, path_prefix + name + "/")) diff --git a/git/objects/submodule/__init__.py b/git/objects/submodule/__init__.py index 82df59b0d..8edc13be4 100644 --- a/git/objects/submodule/__init__.py +++ b/git/objects/submodule/__init__.py @@ -1,2 +1,2 @@ -# NOTE: Cannot import anything here as the top-level _init_ has to handle -# our dependencies +# NOTE: Cannot import anything here as the top-level __init__ has to handle +# our dependencies. diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index 61c300652..24ea5569c 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -57,9 +57,8 @@ class UpdateProgress(RemoteProgress): - """Class providing detailed progress information to the caller who should - derive from it and implement the ``update(...)`` message""" + derive from it and implement the ``update(...)`` message.""" CLONE, FETCH, UPDWKTREE = [1 << x for x in range(RemoteProgress._num_op_codes, RemoteProgress._num_op_codes + 3)] _num_op_codes: int = RemoteProgress._num_op_codes + 3 @@ -78,25 +77,27 @@ class UpdateProgress(RemoteProgress): # mechanism which cause plenty of trouble of the only reason for packages and # modules is refactoring - subpackages shouldn't depend on parent packages class Submodule(IndexObject, TraversableIterableObj): - """Implements access to a git submodule. They are special in that their sha represents a commit in the submodule's repository which is to be checked out at the path of this instance. + The submodule type does not have a string type associated with it, as it exists solely as a marker in the tree and index. - All methods work in bare and non-bare repositories.""" + All methods work in bare and non-bare repositories. + """ _id_attribute_ = "name" k_modules_file = ".gitmodules" k_head_option = "branch" k_head_default = "master" - k_default_mode = stat.S_IFDIR | stat.S_IFLNK # submodules are directories with link-status + k_default_mode = stat.S_IFDIR | stat.S_IFLNK # Submodules are directories with link-status. - # this is a bogus type for base class compatibility + # This is a bogus type for base class compatibility. type: Literal["submodule"] = "submodule" # type: ignore __slots__ = ("_parent_commit", "_url", "_branch_path", "_name", "__weakref__") + _cache_attrs = ("path", "_url", "_branch_path") def __init__( @@ -110,14 +111,17 @@ def __init__( url: Union[str, None] = None, branch_path: Union[PathLike, None] = None, ) -> None: - """Initialize this instance with its attributes. We only document the ones - that differ from ``IndexObject`` + """Initialize this instance with its attributes. + + We only document the parameters that differ + from :class:`IndexObject `. :param repo: Our parent repository :param binsha: binary sha referring to a commit in the remote repository, see url parameter :param parent_commit: see set_parent_commit() :param url: The url to the remote repository which is the submodule - :param branch_path: full (relative) path to ref to checkout when cloning the remote repository""" + :param branch_path: full (relative) path to ref to checkout when cloning the remote repository + """ super(Submodule, self).__init__(repo, binsha, mode, path) self.size = 0 self._parent_commit = parent_commit @@ -165,18 +169,18 @@ def _need_gitfile_submodules(cls, git: Git) -> bool: return git.version_info[:3] >= (1, 7, 5) def __eq__(self, other: Any) -> bool: - """Compare with another submodule""" - # we may only compare by name as this should be the ID they are hashed with - # Otherwise this type wouldn't be hashable + """Compare with another submodule.""" + # We may only compare by name as this should be the ID they are hashed with. + # Otherwise this type wouldn't be hashable. # return self.path == other.path and self.url == other.url and super(Submodule, self).__eq__(other) return self._name == other._name def __ne__(self, other: object) -> bool: - """Compare with another submodule for inequality""" + """Compare with another submodule for inequality.""" return not (self == other) def __hash__(self) -> int: - """Hash this instance using its logical id, not the sha""" + """Hash this instance using its logical id, not the sha.""" return hash(self._name) def __str__(self) -> str: @@ -195,16 +199,19 @@ def __repr__(self) -> str: def _config_parser( cls, repo: "Repo", parent_commit: Union[Commit_ish, None], read_only: bool ) -> SubmoduleConfigParser: - """:return: Config Parser constrained to our submodule in read or write mode - :raise IOError: If the .gitmodules file cannot be found, either locally or in the repository - at the given parent commit. Otherwise the exception would be delayed until the first - access of the config parser""" + """ + :return: Config Parser constrained to our submodule in read or write mode + + :raise IOError: If the .gitmodules file cannot be found, either locally or in + the repository at the given parent commit. Otherwise the exception would be + delayed until the first access of the config parser. + """ parent_matches_head = True if parent_commit is not None: try: parent_matches_head = repo.head.commit == parent_commit except ValueError: - # We are most likely in an empty repository, so the HEAD doesn't point to a valid ref + # We are most likely in an empty repository, so the HEAD doesn't point to a valid ref. pass # end handle parent_commit fp_module: Union[str, BytesIO] @@ -228,7 +235,7 @@ def _config_parser( return SubmoduleConfigParser(fp_module, read_only=read_only) def _clear_cache(self) -> None: - # clear the possibly changed values + """Clear the possibly changed values.""" for name in self._cache_attrs: try: delattr(self, name) @@ -275,14 +282,16 @@ def _clone_repo( allow_unsafe_protocols: bool = False, **kwargs: Any, ) -> "Repo": - """:return: Repo instance of newly cloned repository - :param repo: our parent repository - :param url: url to clone from - :param path: repository - relative path to the submodule checkout location - :param name: canonical of the submodule + """ + :return: Repo instance of newly cloned repository + :param repo: Our parent repository + :param url: URL to clone from + :param path: Repository - relative path to the submodule checkout location + :param name: Canonical name of the submodule :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack - :param kwargs: additional arguments given to git.clone""" + :param kwargs: Additional arguments given to git.clone + """ module_abspath = cls._module_abspath(repo, path, name) module_checkout_path = module_abspath if cls._need_gitfile_submodules(repo.git): @@ -331,14 +340,16 @@ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike: @classmethod def _write_git_file_and_module_config(cls, working_tree_dir: PathLike, module_abspath: PathLike) -> None: - """Writes a .git file containing a(preferably) relative path to the actual git module repository. + """Write a .git file containing a(preferably) relative path to the actual git module repository. + It is an error if the module_abspath cannot be made into a relative path, relative to the working_tree_dir - :note: will overwrite existing files ! + + :note: This will overwrite existing files! :note: as we rewrite both the git file as well as the module configuration, we might fail on the configuration and will not roll back changes done to the git file. This should be a non - issue, but may easily be fixed - if it becomes one - :param working_tree_dir: directory to write the .git file into - :param module_abspath: absolute path to the bare repository + if it becomes one. + :param working_tree_dir: Directory to write the .git file into + :param module_abspath: Absolute path to the bare repository """ git_file = osp.join(working_tree_dir, ".git") rela_path = osp.relpath(module_abspath, start=working_tree_dir) @@ -377,15 +388,15 @@ def add( If the submodule already exists, no matter if the configuration differs from the one provided, the existing submodule will be returned. - :param repo: Repository instance which should receive the submodule - :param name: The name/identifier for the submodule - :param path: repository-relative or absolute path at which the submodule - should be located + :param repo: Repository instance which should receive the submodule. + :param name: The name/identifier for the submodule. + :param path: Repository-relative or absolute path at which the submodule + should be located. It will be created as required during the repository initialization. - :param url: git-clone compatible URL, see git-clone reference for more information + :param url: git-clone compatible URL, see git-clone reference for more information. If None, the repository is assumed to exist, and the url of the first remote is taken instead. This is useful if you want to make an existing - repository a submodule of anotherone. + repository a submodule of another one. :param branch: name of branch at which the submodule should (later) be checked out. The given branch must exist in the remote repository, and will be checked out locally as a tracking branch. @@ -393,24 +404,25 @@ def add( when the checked out branch will be the one the remote HEAD pointed to. The result you get in these situation is somewhat fuzzy, and it is recommended to specify at least 'master' here. - Examples are 'master' or 'feature/new' - :param no_checkout: if True, and if the repository has to be cloned manually, - no checkout will be performed + Examples are 'master' or 'feature/new'. + :param no_checkout: If True, and if the repository has to be cloned manually, + no checkout will be performed. :param depth: Create a shallow clone with a history truncated to the specified number of commits. :param env: Optional dictionary containing the desired environment variables. - Note: Provided variables will be used to update the execution - environment for `git`. If some variable is not specified in `env` - and is defined in `os.environ`, value from `os.environ` will be used. - If you want to unset some variable, consider providing empty string - as its value. - :param clone_multi_options: A list of Clone options. Please see ``git.repo.base.Repo.clone`` - for details. - :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext + Note: Provided variables will be used to update the execution environment + for ``git``. If some variable is not specified in `env` and is defined in + attr:`os.environ`, the value from attr:`os.environ` will be used. If you + want to unset some variable, consider providing an empty string as its + value. + :param clone_multi_options: A list of Clone options. Please see + :meth:`Repo.clone ` for details. + :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext. :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack - :return: The newly created submodule instance - :note: works atomically, such that no change will be done if the repository - update fails for instance""" + :return: The newly created submodule instance. + :note: Works atomically, such that no change will be done if the repository + update fails for instance. + """ if repo.bare: raise InvalidGitRepositoryError("Cannot add submodules to bare repositories") @@ -418,11 +430,11 @@ def add( path = cls._to_relative_path(repo, path) - # assure we never put backslashes into the url, as some operating systems - # like it ... + # Ensure we never put backslashes into the URL, as some operating systems + # like it... if url is not None: url = to_native_path_linux(url) - # END assure url correctness + # END ensure URL correctness # INSTANTIATE INTERMEDIATE SM sm = cls( @@ -434,13 +446,13 @@ def add( url="invalid-temporary", ) if sm.exists(): - # reretrieve submodule from tree + # Reretrieve submodule from tree. try: sm = repo.head.commit.tree[str(path)] sm._name = name return sm except KeyError: - # could only be in index + # Could only be in index. index = repo.index entry = index.entries[index.entry_key(path, 0)] sm.binsha = entry.binsha @@ -448,7 +460,7 @@ def add( # END handle exceptions # END handle existing - # fake-repo - we only need the functionality on the branch instance + # fake-repo - we only need the functionality on the branch instance. br = git.Head(repo, git.Head.to_full_path(str(branch) or cls.k_head_default)) has_module = sm.module_exists() branch_is_default = branch is None @@ -501,11 +513,11 @@ def add( ) # END verify url - ## See #525 for ensuring git urls in config-files valid under Windows. + ## See #525 for ensuring git URLs in config-files are valid under Windows. url = Git.polish_url(url) # It's important to add the URL to the parent config, to let `git submodule` know. - # otherwise there is a '-' character in front of the submodule listing + # Otherwise there is a '-' character in front of the submodule listing: # a38efa84daef914e4de58d1905a500d8d14aaf45 mymodule (v0.9.0-1-ga38efa8) # -a38efa84daef914e4de58d1905a500d8d14aaf45 submodules/intermediate/one writer: Union[GitConfigParser, SectionConstraint] @@ -513,7 +525,7 @@ def add( with sm.repo.config_writer() as writer: writer.set_value(sm_section(name), "url", url) - # update configuration and index + # Update configuration and index. index = sm.repo.index with sm.config_writer(index=index, write=False) as writer: writer.set_value("url", url) @@ -525,7 +537,7 @@ def add( writer.set_value(cls.k_head_option, br.path) sm._branch_path = br.path - # we deliberately assume that our head matches our index ! + # We deliberately assume that our head matches our index! if mrepo: sm.binsha = mrepo.head.commit.binsha index.add([sm], write=True) @@ -549,40 +561,54 @@ def update( """Update the repository of this submodule to point to the checkout we point at with the binsha of this instance. - :param recursive: if True, we will operate recursively and update child- - modules as well. - :param init: if True, the module repository will be cloned into place if necessary - :param to_latest_revision: if True, the submodule's sha will be ignored during checkout. - Instead, the remote will be fetched, and the local tracking branch updated. - This only works if we have a local tracking branch, which is the case - if the remote repository had a master branch, or of the 'branch' option - was specified for this submodule and the branch existed remotely - :param progress: UpdateProgress instance or None if no progress should be shown - :param dry_run: if True, the operation will only be simulated, but not performed. - All performed operations are read - only + :param recursive: + If True, we will operate recursively and update child modules as well. + :param init: + If True, the module repository will be cloned into place if necessary. + :param to_latest_revision: + If True, the submodule's sha will be ignored during checkout. Instead, the + remote will be fetched, and the local tracking branch updated. This only + works if we have a local tracking branch, which is the case if the remote + repository had a master branch, or of the 'branch' option was specified for + this submodule and the branch existed remotely. + :param progress: + UpdateProgress instance or None if no progress should be shown. + :param dry_run: + If True, the operation will only be simulated, but not performed. + All performed operations are read-only. :param force: - If True, we may reset heads even if the repository in question is dirty. Additinoally we will be allowed - to set a tracking branch which is ahead of its remote branch back into the past or the location of the - remote branch. This will essentially 'forget' commits. - If False, local tracking branches that are in the future of their respective remote branches will simply - not be moved. - :param keep_going: if True, we will ignore but log all errors, and keep going recursively. - Unless dry_run is set as well, keep_going could cause subsequent / inherited errors you wouldn't see - otherwise. - In conjunction with dry_run, it can be useful to anticipate all errors when updating submodules + If True, we may reset heads even if the repository in question is dirty. + Additionally we will be allowed to set a tracking branch which is ahead of + its remote branch back into the past or the location of the remote branch. + This will essentially 'forget' commits. + If False, local tracking branches that are in the future of their respective + remote branches will simply not be moved. + :param keep_going: + If True, we will ignore but log all errors, and keep going recursively. + Unless dry_run is set as well, keep_going could cause subsequent / inherited + errors you wouldn't see otherwise. + In conjunction with dry_run, it can be useful to anticipate all errors when + updating submodules. :param env: Optional dictionary containing the desired environment variables. - Note: Provided variables will be used to update the execution - environment for `git`. If some variable is not specified in `env` - and is defined in `os.environ`, value from `os.environ` will be used. - If you want to unset some variable, consider providing empty string - as its value. - :param clone_multi_options: list of Clone options. Please see ``git.repo.base.Repo.clone`` - for details. Only take effect with `init` option. - :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext - :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack - :note: does nothing in bare repositories - :note: method is definitely not atomic if recurisve is True - :return: self""" + Note: Provided variables will be used to update the execution environment + for ``git``. If some variable is not specified in `env` and is defined in + attr:`os.environ`, value from attr:`os.environ` will be used. + If you want to unset some variable, consider providing the empty string as + its value. + :param clone_multi_options: + List of Clone options. + Please see :meth:`Repo.clone ` for details. + They only take effect with the `init` option. + :param allow_unsafe_protocols: + Allow unsafe protocols to be used, like ext. + :param allow_unsafe_options: + Allow unsafe options to be used, like --upload-pack. + + :note: Does nothing in bare repositories. + :note: This method is definitely not atomic if `recursive` is True. + + :return: self + """ if self.repo.bare: return self # END pass in bare mode @@ -595,14 +621,14 @@ def update( prefix = "DRY-RUN: " # END handle prefix - # to keep things plausible in dry-run mode + # To keep things plausible in dry-run mode. if dry_run: mrepo = None # END init mrepo try: - # ASSURE REPO IS PRESENT AND UPTODATE - ##################################### + # ENSURE REPO IS PRESENT AND UP-TO-DATE + ####################################### try: mrepo = self.module() rmts = mrepo.remotes @@ -640,7 +666,7 @@ def update( return self # END early abort if init is not allowed - # there is no git-repository yet - but delete empty paths + # There is no git-repository yet - but delete empty paths. checkout_module_abspath = self.abspath if not dry_run and osp.isdir(checkout_module_abspath): try: @@ -652,8 +678,8 @@ def update( # END handle OSError # END handle directory removal - # don't check it out at first - nonetheless it will create a local - # branch according to the remote-HEAD if possible + # Don't check it out at first - nonetheless it will create a local + # branch according to the remote-HEAD if possible. progress.update( BEGIN | CLONE, 0, @@ -682,19 +708,19 @@ def update( ) if not dry_run: - # see whether we have a valid branch to checkout + # See whether we have a valid branch to check out. try: mrepo = cast("Repo", mrepo) - # find a remote which has our branch - we try to be flexible + # Find a remote which has our branch - we try to be flexible. remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name) local_branch = mkhead(mrepo, self.branch_path) - # have a valid branch, but no checkout - make sure we can figure - # that out by marking the commit with a null_sha + # Have a valid branch, but no checkout - make sure we can figure + # that out by marking the commit with a null_sha. local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA)) # END initial checkout + branch creation - # make sure HEAD is not detached + # Make sure HEAD is not detached. mrepo.head.set_reference( local_branch, logmsg="submodule: attaching head to %s" % local_branch, @@ -704,21 +730,21 @@ def update( log.warning("Failed to checkout tracking branch %s", self.branch_path) # END handle tracking branch - # NOTE: Have to write the repo config file as well, otherwise - # the default implementation will be offended and not update the repository - # Maybe this is a good way to assure it doesn't get into our way, but - # we want to stay backwards compatible too ... . Its so redundant ! + # NOTE: Have to write the repo config file as well, otherwise the + # default implementation will be offended and not update the repository. + # Maybe this is a good way to ensure it doesn't get into our way, but + # we want to stay backwards compatible too... It's so redundant! with self.repo.config_writer() as writer: writer.set_value(sm_section(self.name), "url", self.url) # END handle dry_run # END handle initialization - # DETERMINE SHAS TO CHECKOUT - ############################ + # DETERMINE SHAS TO CHECK OUT + ############################# binsha = self.binsha hexsha = self.hexsha if mrepo is not None: - # mrepo is only set if we are not in dry-run mode or if the module existed + # mrepo is only set if we are not in dry-run mode or if the module existed. is_detached = mrepo.head.is_detached # END handle dry_run @@ -742,13 +768,13 @@ def update( # END handle detached head # END handle to_latest_revision option - # update the working tree - # handles dry_run + # Update the working tree. + # Handles dry_run. if mrepo is not None and mrepo.head.commit.binsha != binsha: - # We must assure that our destination sha (the one to point to) is in the future of our current head. - # Otherwise, we will reset changes that might have been done on the submodule, but were not yet pushed + # We must ensure that our destination sha (the one to point to) is in the future of our current head. + # Otherwise, we will reset changes that might have been done on the submodule, but were not yet pushed. # We also handle the case that history has been rewritten, leaving no merge-base. In that case - # we behave conservatively, protecting possible changes the user had done + # we behave conservatively, protecting possible changes the user had done. may_reset = True if mrepo.head.commit.binsha != self.NULL_BIN_SHA: base_commit = mrepo.merge_base(mrepo.head.commit, hexsha) @@ -785,7 +811,7 @@ def update( if not dry_run and may_reset: if is_detached: - # NOTE: for now we force, the user is no supposed to change detached + # NOTE: For now we force. The user is not supposed to change detached # submodules anyway. Maybe at some point this becomes an option, to # properly handle user modifications - see below for future options # regarding rebase and merge. @@ -793,7 +819,7 @@ def update( else: mrepo.head.reset(hexsha, index=True, working_tree=True) # END handle checkout - # if we may reset/checkout + # If we may reset/checkout. progress.update( END | UPDWKTREE, 0, @@ -810,7 +836,7 @@ def update( # HANDLE RECURSION ################## if recursive: - # in dry_run mode, the module might not exist + # In dry_run mode, the module might not exist. if mrepo is not None: for submodule in self.iter_items(self.module()): submodule.update( @@ -834,19 +860,19 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = the repository at our current path, changing the configuration, as well as adjusting our index entry accordingly. - :param module_path: the path to which to move our module in the parent repostory's working tree, - given as repository - relative or absolute path. Intermediate directories will be created - accordingly. If the path already exists, it must be empty. - Trailing(back)slashes are removed automatically - :param configuration: if True, the configuration will be adjusted to let + :param module_path: The path to which to move our module in the parent + repository's working tree, given as repository - relative or absolute path. + Intermediate directories will be created accordingly. If the path already + exists, it must be empty. Trailing (back)slashes are removed automatically. + :param configuration: If True, the configuration will be adjusted to let the submodule point to the given path. - :param module: if True, the repository managed by this submodule - will be moved as well. If False, we don't move the submodule's checkout, which may leave - the parent repository in an inconsistent state. + :param module: If True, the repository managed by this submodule + will be moved as well. If False, we don't move the submodule's checkout, + which may leave the parent repository in an inconsistent state. :return: self - :raise ValueError: if the module path existed and was not empty, or was a file + :raise ValueError: If the module path existed and was not empty, or was a file. :note: Currently the method is not atomic, and it could leave the repository - in an inconsistent state if a sub - step fails for some reason + in an inconsistent state if a sub-step fails for some reason. """ if module + configuration < 1: raise ValueError("You must specify to move at least the module or the configuration of the submodule") @@ -871,7 +897,7 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = raise ValueError("Index entry for target path did already exist") # END handle index key already there - # remove existing destination + # Remove existing destination. if module: if osp.exists(module_checkout_abspath): if len(os.listdir(module_checkout_abspath)): @@ -884,13 +910,13 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = os.rmdir(module_checkout_abspath) # END handle link else: - # recreate parent directories - # NOTE: renames() does that now + # Recreate parent directories. + # NOTE: renames() does that now. pass # END handle existence # END handle module - # move the module into place if possible + # Move the module into place if possible. cur_path = self.abspath renamed_module = False if module and osp.exists(cur_path): @@ -903,8 +929,8 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = # end handle git file rewrite # END move physical module - # rename the index entry - have to manipulate the index directly as - # git-mv cannot be used on submodules ... yeah + # Rename the index entry - we have to manipulate the index directly as + # git-mv cannot be used on submodules... yeah. previous_sm_path = self.path try: if configuration: @@ -918,8 +944,8 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = raise InvalidGitRepositoryError("Submodule's entry at %r did not exist" % (self.path)) from e # END handle submodule doesn't exist - # update configuration - with self.config_writer(index=index) as writer: # auto-write + # Update configuration. + with self.config_writer(index=index) as writer: # Auto-write. writer.set_value("path", module_checkout_path) self.path = module_checkout_path # END handle configuration flag @@ -930,7 +956,7 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool = raise # END handle undo rename - # Auto-rename submodule if it's name was 'default', that is, the checkout directory + # Auto-rename submodule if it's name was 'default', that is, the checkout directory. if previous_sm_path == self.name: self.rename(module_checkout_path) # end @@ -946,7 +972,7 @@ def remove( dry_run: bool = False, ) -> "Submodule": """Remove this submodule from the repository. This will remove our entry - from the .gitmodules file and the entry in the .git / config file. + from the .gitmodules file and the entry in the .git/config file. :param module: If True, the checked out module we point to will be deleted as well.If that module is currently on a commit outside any branch in the @@ -959,22 +985,23 @@ def remove( :param force: Enforces the deletion of the module even though it contains modifications. This basically enforces a brute-force file system based deletion. - :param configuration: if True, the submodule is deleted from the configuration, - otherwise it isn't. Although this should be enabled most of the times, + :param configuration: If True, the submodule is deleted from the configuration, + otherwise it isn't. Although this should be enabled most of the time, this flag enables you to safely delete the repository of your submodule. - :param dry_run: if True, we will not actually do anything, but throw the errors - we would usually throw + :param dry_run: If True, we will not actually do anything, but throw the errors + we would usually throw. :return: self - :note: doesn't work in bare repositories - :note: doesn't work atomically, as failure to remove any part of the submodule will leave - an inconsistent state - :raise InvalidGitRepositoryError: thrown if the repository cannot be deleted - :raise OSError: if directories or files could not be removed""" + :note: Doesn't work in bare repositories. + :note: Doesn't work atomically, as failure to remove any part of the submodule + will leave an inconsistent state. + :raise InvalidGitRepositoryError: Thrown if the repository cannot be deleted. + :raise OSError: If directories or files could not be removed. + """ if not (module or configuration): raise ValueError("Need to specify to delete at least the module, or the configuration") # END handle parameters - # Recursively remove children of this submodule + # Recursively remove children of this submodule. nc = 0 for csm in self.children(): nc += 1 @@ -982,8 +1009,8 @@ def remove( del csm # end if configuration and not dry_run and nc > 0: - # Assure we don't leave the parent repository in a dirty state, and commit our changes - # It's important for recursive, unforced, deletions to work as expected + # Ensure we don't leave the parent repository in a dirty state, and commit our changes. + # It's important for recursive, unforced, deletions to work as expected. self.module().index.commit("Removed at least one of child-modules of '%s'" % self.name) # end handle recursion @@ -993,9 +1020,9 @@ def remove( mod = self.module() git_dir = mod.git_dir if force: - # take the fast lane and just delete everything in our module path + # Take the fast lane and just delete everything in our module path. # TODO: If we run into permission problems, we have a highly inconsistent - # state. Delete the .git folders last, start with the submodules first + # state. Delete the .git folders last, start with the submodules first. mp = self.abspath method: Union[None, Callable[[PathLike], None]] = None if osp.islink(mp): @@ -1010,7 +1037,7 @@ def remove( method(mp) # END apply deletion method else: - # verify we may delete our module + # Verify we may delete our module. if mod.is_dirty(index=True, working_tree=True, untracked_files=True): raise InvalidGitRepositoryError( "Cannot delete module at %s with any modifications, unless force is specified" @@ -1018,25 +1045,27 @@ def remove( ) # END check for dirt - # figure out whether we have new commits compared to the remotes - # NOTE: If the user pulled all the time, the remote heads might - # not have been updated, so commits coming from the remote look - # as if they come from us. But we stay strictly read-only and - # don't fetch beforehand. + # Figure out whether we have new commits compared to the remotes. + # NOTE: If the user pulled all the time, the remote heads might not have + # been updated, so commits coming from the remote look as if they come + # from us. But we stay strictly read-only and don't fetch beforehand. for remote in mod.remotes: num_branches_with_new_commits = 0 rrefs = remote.refs for rref in rrefs: num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0 # END for each remote ref - # not a single remote branch contained all our commits + # Not a single remote branch contained all our commits. if len(rrefs) and num_branches_with_new_commits == len(rrefs): raise InvalidGitRepositoryError( "Cannot delete module at %s as there are new commits" % mod.working_tree_dir ) # END handle new commits - # have to manually delete references as python's scoping is - # not existing, they could keep handles open ( on windows this is a problem ) + # We have to manually delete some references to allow resources to + # be cleaned up immediately when we are done with them, because + # Python's scoping is no more granular than the whole function (loop + # bodies are not scopes). When the objects stay alive longer, they + # can keep handles open. On Windows, this is a problem. if len(rrefs): del rref # skipcq: PYL-W0631 # END handle remotes @@ -1044,11 +1073,11 @@ def remove( del remote # END for each remote - # finally delete our own submodule + # Finally delete our own submodule. if not dry_run: self._clear_cache() wtd = mod.working_tree_dir - del mod # release file-handles (windows) + del mod # Release file-handles (Windows). import gc gc.collect() @@ -1062,14 +1091,14 @@ def remove( # end handle separate bare repository # END handle module deletion - # void our data not to delay invalid access + # Void our data so as not to delay invalid access. if not dry_run: self._clear_cache() # DELETE CONFIGURATION ###################### if configuration and not dry_run: - # first the index-entry + # First the index-entry. parent_index = self.repo.index try: del parent_index.entries[parent_index.entry_key(self.path, 0)] @@ -1078,8 +1107,8 @@ def remove( # END delete entry parent_index.write() - # now git config - need the config intact, otherwise we can't query - # information anymore + # Now git config - we need the config intact, otherwise we can't query + # information anymore. with self.repo.config_writer() as gcp_writer: gcp_writer.remove_section(sm_section(self.name)) @@ -1095,15 +1124,16 @@ def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True) contain the .gitmodules blob. :param commit: - Commit-ish reference pointing at the root_tree, or None to always point to the - most recent commit + Commit-ish reference pointing at the root_tree, or None to always point to + the most recent commit :param check: - if True, relatively expensive checks will be performed to verify + If True, relatively expensive checks will be performed to verify validity of the submodule. - :raise ValueError: if the commit's tree didn't contain the .gitmodules blob. + :raise ValueError: If the commit's tree didn't contain the .gitmodules blob. :raise ValueError: - if the parent commit didn't store this submodule under the current path - :return: self""" + If the parent commit didn't store this submodule under the current path. + :return: self + """ if commit is None: self._parent_commit = None return self @@ -1125,9 +1155,9 @@ def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True) # END handle submodule did not exist # END handle checking mode - # update our sha, it could have changed - # If check is False, we might see a parent-commit that doesn't even contain the submodule anymore. - # in that case, mark our sha as being NULL + # Update our sha, it could have changed. + # If check is False, we might see a parent-commit that doesn't even contain the + # submodule anymore. in that case, mark our sha as being NULL. try: self.binsha = pctree[str(self.path)].binsha except KeyError: @@ -1141,19 +1171,23 @@ def set_parent_commit(self, commit: Union[Commit_ish, None], check: bool = True) def config_writer( self, index: Union["IndexFile", None] = None, write: bool = True ) -> SectionConstraint["SubmoduleConfigParser"]: - """:return: a config writer instance allowing you to read and write the data + """ + :return: A config writer instance allowing you to read and write the data belonging to this submodule into the .gitmodules file. - :param index: if not None, an IndexFile instance which should be written. - defaults to the index of the Submodule's parent repository. - :param write: if True, the index will be written each time a configuration + :param index: If not None, an IndexFile instance which should be written. + Defaults to the index of the Submodule's parent repository. + :param write: If True, the index will be written each time a configuration value changes. - :note: the parameters allow for a more efficient writing of the index, + + :note: The parameters allow for a more efficient writing of the index, as you can pass in a modified index on your own, prevent automatic writing, - and write yourself once the whole operation is complete - :raise ValueError: if trying to get a writer on a parent_commit which does not - match the current head commit - :raise IOError: If the .gitmodules file/blob could not be read""" + and write yourself once the whole operation is complete. + + :raise ValueError: If trying to get a writer on a parent_commit which does not + match the current head commit. + :raise IOError: If the .gitmodules file/blob could not be read + """ writer = self._config_parser_constrained(read_only=False) if index is not None: writer.config._index = index @@ -1162,17 +1196,19 @@ def config_writer( @unbare_repo def rename(self, new_name: str) -> "Submodule": - """Rename this submodule - :note: This method takes care of renaming the submodule in various places, such as + """Rename this submodule. + + :note: + This method takes care of renaming the submodule in various places, such as: * $parent_git_dir / config * $working_tree_dir / .gitmodules * (git >= v1.8.0: move submodule repository to new name) - As .gitmodules will be changed, you would need to make a commit afterwards. The changed .gitmodules file - will already be added to the index + As .gitmodules will be changed, you would need to make a commit afterwards. The + changed .gitmodules file will already be added to the index. - :return: this submodule instance + :return: This submodule instance """ if self.name == new_name: return self @@ -1195,7 +1231,7 @@ def rename(self, new_name: str) -> "Submodule": if mod.has_separate_working_tree(): destination_module_abspath = self._module_abspath(self.repo, self.path, new_name) source_dir = mod.git_dir - # Let's be sure the submodule name is not so obviously tied to a directory + # Let's be sure the submodule name is not so obviously tied to a directory. if str(destination_module_abspath).startswith(str(mod.git_dir)): tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4())) os.renames(source_dir, tmp_dir) @@ -1214,9 +1250,12 @@ def rename(self, new_name: str) -> "Submodule": @unbare_repo def module(self) -> "Repo": - """:return: Repo instance initialized from the repository at our submodule path - :raise InvalidGitRepositoryError: if a repository was not available. This could - also mean that it was not yet initialized""" + """ + :return: Repo instance initialized from the repository at our submodule path + + :raise InvalidGitRepositoryError: If a repository was not available. This could + also mean that it was not yet initialized. + """ module_checkout_abspath = self.abspath try: repo = git.Repo(module_checkout_abspath) @@ -1230,7 +1269,7 @@ def module(self) -> "Repo": # END handle exceptions def module_exists(self) -> bool: - """:return: True if our module exists and is a valid git repository. See module() method""" + """:return: True if our module exists and is a valid git repository. See module() method.""" try: self.module() return True @@ -1241,10 +1280,11 @@ def module_exists(self) -> bool: def exists(self) -> bool: """ :return: True if the submodule exists, False otherwise. Please note that - a submodule may exist ( in the .gitmodules file) even though its module - doesn't exist on disk""" - # keep attributes for later, and restore them if we have no valid data - # this way we do not actually alter the state of the object + a submodule may exist (in the .gitmodules file) even though its module + doesn't exist on disk. + """ + # Keep attributes for later, and restore them if we have no valid data. + # This way we do not actually alter the state of the object. loc = locals() for attr in self._cache_attrs: try: @@ -1252,7 +1292,7 @@ def exists(self) -> bool: loc[attr] = getattr(self, attr) # END if we have the attribute cache except (cp.NoSectionError, ValueError): - # on PY3, this can happen apparently ... don't know why this doesn't happen on PY2 + # On PY3, this can happen apparently... don't know why this doesn't happen on PY2. pass # END for each attr self._clear_cache() @@ -1274,22 +1314,26 @@ def exists(self) -> bool: @property def branch(self) -> "Head": - """:return: The branch instance that we are to checkout - :raise InvalidGitRepositoryError: if our module is not yet checked out""" + """ + :return: The branch instance that we are to checkout + + :raise InvalidGitRepositoryError: If our module is not yet checked out + """ return mkhead(self.module(), self._branch_path) @property def branch_path(self) -> PathLike: """ - :return: full(relative) path as string to the branch we would checkout - from the remote and track""" + :return: Full (relative) path as string to the branch we would checkout + from the remote and track + """ return self._branch_path @property def branch_name(self) -> str: - """:return: the name of the branch, which is the shortest possible branch name""" - # use an instance method, for this we create a temporary Head instance - # which uses a repository that is available at least ( it makes no difference ) + """:return: The name of the branch, which is the shortest possible branch name""" + # Use an instance method, for this we create a temporary Head instance + # which uses a repository that is available at least (it makes no difference). return git.Head(self.repo, self._branch_path).name @property @@ -1299,37 +1343,46 @@ def url(self) -> str: @property def parent_commit(self) -> "Commit_ish": - """:return: Commit instance with the tree containing the .gitmodules file - :note: will always point to the current head's commit if it was not set explicitly""" + """ + :return: Commit instance with the tree containing the .gitmodules file + + :note: Will always point to the current head's commit if it was not set explicitly. + """ if self._parent_commit is None: return self.repo.commit() return self._parent_commit @property def name(self) -> str: - """:return: The name of this submodule. It is used to identify it within the + """ + :return: The name of this submodule. It is used to identify it within the .gitmodules file. - :note: by default, the name is the path at which to find the submodule, but - in git - python it should be a unique identifier similar to the identifiers - used for remotes, which allows to change the path of the submodule - easily + + :note: By default, this is the name is the path at which to find the submodule, + but in GitPython it should be a unique identifier similar to the identifiers + used for remotes, which allows to change the path of the submodule easily. """ return self._name def config_reader(self) -> SectionConstraint[SubmoduleConfigParser]: """ - :return: ConfigReader instance which allows you to qurey the configuration values - of this submodule, as provided by the .gitmodules file - :note: The config reader will actually read the data directly from the repository - and thus does not need nor care about your working tree. - :note: Should be cached by the caller and only kept as long as needed - :raise IOError: If the .gitmodules file/blob could not be read""" + :return: ConfigReader instance which allows you to query the configuration + values of this submodule, as provided by the .gitmodules file. + + :note: The config reader will actually read the data directly from the + repository and thus does not need nor care about your working tree. + + :note: Should be cached by the caller and only kept as long as needed. + + :raise IOError: If the .gitmodules file/blob could not be read. + """ return self._config_parser_constrained(read_only=True) def children(self) -> IterableList["Submodule"]: """ :return: IterableList(Submodule, ...) an iterable list of submodules instances - which are children of this submodule or 0 if the submodule is not checked out""" + which are children of this submodule or 0 if the submodule is not checked out. + """ return self._get_intermediate_items(self) # } END query interface @@ -1344,9 +1397,9 @@ def iter_items( *Args: Any, **kwargs: Any, ) -> Iterator["Submodule"]: - """:return: iterator yielding Submodule instances available in the given repository""" + """:return: Iterator yielding Submodule instances available in the given repository""" try: - pc = repo.commit(parent_commit) # parent commit instance + pc = repo.commit(parent_commit) # Parent commit instance parser = cls._config_parser(repo, pc, read_only=True) except (IOError, BadName): return iter([]) @@ -1361,13 +1414,13 @@ def iter_items( b = str(parser.get(sms, cls.k_head_option)) # END handle optional information - # get the binsha + # Get the binsha. index = repo.index try: - rt = pc.tree # root tree + rt = pc.tree # Root tree sm = rt[p] except KeyError: - # try the index, maybe it was just added + # Try the index, maybe it was just added. try: entry = index.entries[index.entry_key(p, 0)] sm = Submodule(repo, entry.binsha, entry.mode, entry.path) @@ -1378,15 +1431,15 @@ def iter_items( # END handle keyerror # END handle critical error - # Make sure we are looking at a submodule object + # Make sure we are looking at a submodule object. if type(sm) is not git.objects.submodule.base.Submodule: continue - # fill in remaining info - saves time as it doesn't have to be parsed again + # Fill in remaining info - saves time as it doesn't have to be parsed again. sm._name = n if pc != repo.commit(): sm._parent_commit = pc - # end set only if not most recent ! + # end set only if not most recent! sm._branch_path = git.Head.to_full_path(b) sm._url = u diff --git a/git/objects/submodule/root.py b/git/objects/submodule/root.py index d338441ef..b3c06ce9a 100644 --- a/git/objects/submodule/root.py +++ b/git/objects/submodule/root.py @@ -24,7 +24,7 @@ class RootUpdateProgress(UpdateProgress): - """Utility class which adds more opcodes to the UpdateProgress""" + """Utility class which adds more opcodes to the UpdateProgress.""" REMOVE, PATHCHANGE, BRANCHCHANGE, URLCHANGE = [ 1 << x for x in range(UpdateProgress._num_op_codes, UpdateProgress._num_op_codes + 4) @@ -67,7 +67,7 @@ def __init__(self, repo: "Repo"): ) def _clear_cache(self) -> None: - """May not do anything""" + """May not do anything.""" pass # { Interface @@ -85,37 +85,48 @@ def update( keep_going: bool = False, ) -> "RootModule": """Update the submodules of this repository to the current HEAD commit. + This method behaves smartly by determining changes of the path of a submodules repository, next to changes to the to-be-checked-out commit or the branch to be checked out. This works if the submodules ID does not change. - Additionally it will detect addition and removal of submodules, which will be handled - gracefully. + Additionally it will detect addition and removal of submodules, which will be + handled gracefully. - :param previous_commit: If set to a commit-ish, the commit we should use - as the previous commit the HEAD pointed to before it was set to the commit it points to now. - If None, it defaults to HEAD@{1} otherwise + :param previous_commit: If set to a commit-ish, the commit we should use as the + previous commit the HEAD pointed to before it was set to the commit it + points to now. + If None, it defaults to ``HEAD@{1}`` otherwise :param recursive: if True, the children of submodules will be updated as well - using the same technique - :param force_remove: If submodules have been deleted, they will be forcibly removed. - Otherwise the update may fail if a submodule's repository cannot be deleted as - changes have been made to it (see Submodule.update() for more information) - :param init: If we encounter a new module which would need to be initialized, then do it. - :param to_latest_revision: If True, instead of checking out the revision pointed to - by this submodule's sha, the checked out tracking branch will be merged with the - latest remote branch fetched from the repository's origin. - Unless force_reset is specified, a local tracking branch will never be reset into its past, therefore - the remote branch must be in the future for this to have an effect. - :param force_reset: if True, submodules may checkout or reset their branch even if the repository has - pending changes that would be overwritten, or if the local tracking branch is in the future of the - remote tracking branch and would be reset into its past. - :param progress: RootUpdateProgress instance or None if no progress should be sent - :param dry_run: if True, operations will not actually be performed. Progress messages - will change accordingly to indicate the WOULD DO state of the operation. - :param keep_going: if True, we will ignore but log all errors, and keep going recursively. - Unless dry_run is set as well, keep_going could cause subsequent/inherited errors you wouldn't see - otherwise. - In conjunction with dry_run, it can be useful to anticipate all errors when updating submodules - :return: self""" + using the same technique. + :param force_remove: If submodules have been deleted, they will be forcibly + removed. Otherwise the update may fail if a submodule's repository cannot be + deleted as changes have been made to it. + (See :meth:`Submodule.update ` + for more information.) + :param init: If we encounter a new module which would need to be initialized, + then do it. + :param to_latest_revision: If True, instead of checking out the revision pointed + to by this submodule's sha, the checked out tracking branch will be merged + with the latest remote branch fetched from the repository's origin. + Unless `force_reset` is specified, a local tracking branch will never be + reset into its past, therefore the remote branch must be in the future for + this to have an effect. + :param force_reset: If True, submodules may checkout or reset their branch even + if the repository has pending changes that would be overwritten, or if the + local tracking branch is in the future of the remote tracking branch and + would be reset into its past. + :param progress: :class:`RootUpdateProgress` instance or None if no progress + should be sent. + :param dry_run: If True, operations will not actually be performed. Progress + messages will change accordingly to indicate the WOULD DO state of the + operation. + :param keep_going: If True, we will ignore but log all errors, and keep going + recursively. Unless `dry_run` is set as well, `keep_going` could cause + subsequent/inherited errors you wouldn't see otherwise. + In conjunction with `dry_run`, this can be useful to anticipate all errors + when updating submodules. + :return: self + """ if self.repo.bare: raise InvalidGitRepositoryError("Cannot update submodules in bare repositories") # END handle bare @@ -141,11 +152,11 @@ def update( raise IndexError # END handle initial commit except IndexError: - # in new repositories, there is no previous commit + # In new repositories, there is no previous commit. previous_commit = cur_commit # END exception handling else: - previous_commit = repo.commit(previous_commit) # obtain commit object + previous_commit = repo.commit(previous_commit) # Obtain commit object. # END handle previous commit psms: "IterableList[Submodule]" = self.list_items(repo, parent_commit=previous_commit) @@ -164,8 +175,8 @@ def update( op |= BEGIN # END handle begin - # fake it into thinking its at the current commit to allow deletion - # of previous module. Trigger the cache to be updated before that + # Fake it into thinking its at the current commit to allow deletion + # of previous module. Trigger the cache to be updated before that. progress.update( op, i, @@ -188,7 +199,7 @@ def update( # HANDLE PATH RENAMES ##################### - # url changes + branch changes + # URL changes + branch changes. csms = spsms & ssms len_csms = len(csms) for i, csm in enumerate(csms): @@ -204,7 +215,7 @@ def update( len_csms, prefix + "Moving repository of submodule %r from %s to %s" % (sm.name, psm.abspath, sm.abspath), ) - # move the module to the new path + # Move the module to the new path. if not dry_run: psm.move(sm.path, module=True, configuration=False) # END handle dry_run @@ -220,14 +231,14 @@ def update( # HANDLE URL CHANGE ################### if sm.url != psm.url: - # Add the new remote, remove the old one + # Add the new remote, remove the old one. # This way, if the url just changes, the commits will not - # have to be re-retrieved + # have to be re-retrieved. nn = "__new_origin__" smm = sm.module() rmts = smm.remotes - # don't do anything if we already have the url we search in place + # Don't do anything if we already have the url we search in place. if len([r for r in rmts if r.url == sm.url]) == 0: progress.update( BEGIN | URLCHANGE, @@ -250,7 +261,7 @@ def update( ) # END head is not detached - # now delete the changed one + # Now delete the changed one. rmt_for_deletion = None for remote in rmts: if remote.url == psm.url: @@ -259,17 +270,17 @@ def update( # END if urls match # END for each remote - # if we didn't find a matching remote, but have exactly one, - # we can safely use this one + # If we didn't find a matching remote, but have exactly one, + # we can safely use this one. if rmt_for_deletion is None: if len(rmts) == 1: rmt_for_deletion = rmts[0] else: - # if we have not found any remote with the original url + # If we have not found any remote with the original URL # we may not have a name. This is a special case, - # and its okay to fail here - # Alternatively we could just generate a unique name and leave all - # existing ones in place + # and its okay to fail here. + # Alternatively we could just generate a unique name and + # leave all existing ones in place. raise InvalidGitRepositoryError( "Couldn't find original remote-repo at url %r" % psm.url ) @@ -280,15 +291,15 @@ def update( smm.delete_remote(rmt_for_deletion) # NOTE: Currently we leave tags from the deleted remotes # as well as separate tracking branches in the possibly totally - # changed repository ( someone could have changed the url to - # another project ). At some point, one might want to clean + # changed repository (someone could have changed the url to + # another project). At some point, one might want to clean # it up, but the danger is high to remove stuff the user - # has added explicitly + # has added explicitly. - # rename the new remote back to what it was + # Rename the new remote back to what it was. smr.rename(orig_name) - # early on, we verified that the our current tracking branch + # Early on, we verified that the our current tracking branch # exists in the remote. Now we have to assure that the # sha we point to is still contained in the new remote # tracking branch. @@ -303,10 +314,10 @@ def update( # END for each commit if not found: - # adjust our internal binsha to use the one of the remote - # this way, it will be checked out in the next step + # Adjust our internal binsha to use the one of the remote + # this way, it will be checked out in the next step. # This will change the submodule relative to us, so - # the user will be able to commit the change easily + # the user will be able to commit the change easily. log.warning( "Current sha %s was not contained in the tracking\ branch at the new remote, setting it the the remote's tracking branch", @@ -315,7 +326,7 @@ def update( sm.binsha = rref.commit.binsha # END reset binsha - # NOTE: All checkout is performed by the base implementation of update + # NOTE: All checkout is performed by the base implementation of update. # END handle dry_run progress.update( END | URLCHANGE, @@ -329,8 +340,7 @@ def update( # HANDLE PATH CHANGES ##################### if sm.branch_path != psm.branch_path: - # finally, create a new tracking branch which tracks the - # new remote branch + # Finally, create a new tracking branch which tracks the new remote branch. progress.update( BEGIN | BRANCHCHANGE, i, @@ -342,7 +352,8 @@ def update( if not dry_run: smm = sm.module() smmr = smm.remotes - # As the branch might not exist yet, we will have to fetch all remotes to be sure ... . + # As the branch might not exist yet, we will have to fetch all remotes + # to be sure... for remote in smmr: remote.fetch(progress=progress) # end for each remote @@ -354,15 +365,16 @@ def update( logmsg="branch: Created from HEAD", ) except OSError: - # ... or reuse the existing one + # ...or reuse the existing one. tbr = git.Head(smm, sm.branch_path) # END assure tracking branch exists tbr.set_tracking_branch(find_first_remote_branch(smmr, sm.branch_name)) # NOTE: All head-resetting is done in the base implementation of update - # but we will have to checkout the new branch here. As it still points to the currently - # checkout out commit, we don't do any harm. - # As we don't want to update working-tree or index, changing the ref is all there is to do + # but we will have to checkout the new branch here. As it still points + # to the currently checked out commit, we don't do any harm. + # As we don't want to update working-tree or index, changing the ref is + # all there is to do. smm.head.reference = tbr # END handle dry_run @@ -384,7 +396,7 @@ def update( # FINALLY UPDATE ALL ACTUAL SUBMODULES ###################################### for sm in sms: - # update the submodule using the default method + # Update the submodule using the default method. sm.update( recursive=False, init=init, @@ -395,12 +407,12 @@ def update( keep_going=keep_going, ) - # update recursively depth first - question is which inconsistent + # Update recursively depth first - question is which inconsistent # state will be better in case it fails somewhere. Defective branch # or defective depth. The RootSubmodule type will never process itself, - # which was done in the previous expression + # which was done in the previous expression. if recursive: - # the module would exist by now if we are not in dry_run mode + # The module would exist by now if we are not in dry_run mode. if sm.module_exists(): type(self)(sm.module()).update( recursive=True, @@ -419,7 +431,7 @@ def update( return self def module(self) -> "Repo": - """:return: the actual repository containing the submodules""" + """:return: The actual repository containing the submodules""" return self.repo # } END interface diff --git a/git/objects/submodule/util.py b/git/objects/submodule/util.py index 56ce1489a..e13528a8f 100644 --- a/git/objects/submodule/util.py +++ b/git/objects/submodule/util.py @@ -32,12 +32,12 @@ def sm_section(name: str) -> str: - """:return: section title used in .gitmodules configuration file""" + """:return: Section title used in .gitmodules configuration file""" return f'submodule "{name}"' def sm_name(section: str) -> str: - """:return: name of the submodule as parsed from the section name""" + """:return: Name of the submodule as parsed from the section name""" section = section.strip() return section[11:-1] @@ -48,7 +48,7 @@ def mkhead(repo: "Repo", path: PathLike) -> "Head": def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> "RemoteReference": - """Find the remote branch matching the name of the given branch or raise InvalidGitRepositoryError""" + """Find the remote branch matching the name of the given branch or raise InvalidGitRepositoryError.""" for remote in remotes: try: return remote.refs[branch_name] @@ -66,14 +66,13 @@ def find_first_remote_branch(remotes: Sequence["Remote"], branch_name: str) -> " class SubmoduleConfigParser(GitConfigParser): + """Catches calls to _write, and updates the .gitmodules blob in the index + with the new data, if we have written into a stream. - """ - Catches calls to _write, and updates the .gitmodules blob in the index - with the new data, if we have written into a stream. Otherwise it will - add the local file to the index to make it correspond with the working tree. - Additionally, the cache must be cleared + Otherwise it would add the local file to the index to make it correspond + with the working tree. Additionally, the cache must be cleared. - Please note that no mutating method will work in bare mode + Please note that no mutating method will work in bare mode. """ def __init__(self, *args: Any, **kwargs: Any) -> None: @@ -85,13 +84,13 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # { Interface def set_submodule(self, submodule: "Submodule") -> None: """Set this instance's submodule. It must be called before - the first write operation begins""" + the first write operation begins.""" self._smref = weakref.ref(submodule) def flush_to_index(self) -> None: - """Flush changes in our configuration file to the index""" + """Flush changes in our configuration file to the index.""" assert self._smref is not None - # should always have a file here + # Should always have a file here. assert not isinstance(self._file_or_files, BytesIO) sm = self._smref() diff --git a/git/objects/tag.py b/git/objects/tag.py index 56fd05d1a..55f7e19da 100644 --- a/git/objects/tag.py +++ b/git/objects/tag.py @@ -3,7 +3,9 @@ # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ -""" Module containing all object based types. """ + +"""Module containing all Object-based types.""" + from . import base from .util import get_object_type_by_name, parse_actor_and_date from ..util import hex_to_bin @@ -24,10 +26,10 @@ class TagObject(base.Object): - - """Non-Lightweight tag carrying additional information about an object we are pointing to.""" + """Non-lightweight tag carrying additional information about an object we are pointing to.""" type: Literal["tag"] = "tag" + __slots__ = ( "object", "tag", @@ -48,18 +50,20 @@ def __init__( tagger_tz_offset: Union[int, None] = None, message: Union[str, None] = None, ) -> None: # @ReservedAssignment - """Initialize a tag object with additional data + """Initialize a tag object with additional data. - :param repo: repository this object is located in + :param repo: Repository this object is located in :param binsha: 20 byte SHA1 :param object: Object instance of object we are pointing to - :param tag: name of this tag + :param tag: Name of this tag :param tagger: Actor identifying the tagger :param tagged_date: int_seconds_since_epoch - is the DateTime of the tag creation - use time.gmtime to convert - it into a different format - :param tagged_tz_offset: int_seconds_west_of_utc is the timezone that the - authored_date is in, in a format similar to time.altzone""" + The :class:`DateTime` of the tag creation. + Use :func:`time.gmtime` to convert it into a different format. + :param tagged_tz_offset: int_seconds_west_of_utc + The timezone that the authored_date is in, in a format similar + to :attr:`time.altzone`. + """ super(TagObject, self).__init__(repo, binsha) if object is not None: self.object: Union["Commit", "Blob", "Tree", "TagObject"] = object @@ -75,7 +79,7 @@ def __init__( self.message = message def _set_cache_(self, attr: str) -> None: - """Cache all our attributes at once""" + """Cache all our attributes at once.""" if attr in TagObject.__slots__: ostream = self.repo.odb.stream(self.binsha) lines: List[str] = ostream.read().decode(defenc, "replace").splitlines() @@ -95,9 +99,9 @@ def _set_cache_(self, attr: str) -> None: self.tagger_tz_offset, ) = parse_actor_and_date(tagger_info) - # line 4 empty - it could mark the beginning of the next header - # in case there really is no message, it would not exist. Otherwise - # a newline separates header from message + # Line 4 empty - it could mark the beginning of the next header. + # In case there really is no message, it would not exist. + # Otherwise a newline separates header from message. if len(lines) > 5: self.message = "\n".join(lines[5:]) else: diff --git a/git/objects/tree.py b/git/objects/tree.py index 4f490af54..fec98d6e8 100644 --- a/git/objects/tree.py +++ b/git/objects/tree.py @@ -103,11 +103,11 @@ def merge_sort(a: List[TreeCacheTup], cmp: Callable[[TreeCacheTup, TreeCacheTup] class TreeModifier(object): - """A utility class providing methods to alter the underlying cache in a list-like fashion. Once all adjustments are complete, the _cache, which really is a reference to - the cache of a tree, will be sorted. Assuring it will be in a serializable state""" + the cache of a tree, will be sorted. This ensures it will be in a serializable state. + """ __slots__ = "_cache" @@ -126,10 +126,12 @@ def _index_by_name(self, name: str) -> int: # { Interface def set_done(self) -> "TreeModifier": """Call this method once you are done modifying the tree information. - It may be called several times, but be aware that each call will cause - a sort operation - :return self:""" + This may be called several times, but be aware that each call will cause + a sort operation. + + :return self: + """ merge_sort(self._cache, git_cmp) return self @@ -137,16 +139,21 @@ def set_done(self) -> "TreeModifier": # { Mutators def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeModifier": - """Add the given item to the tree. If an item with the given name already - exists, nothing will be done, but a ValueError will be raised if the - sha and mode of the existing item do not match the one you add, unless - force is True + """Add the given item to the tree. + + If an item with the given name already exists, nothing will be done, but a + ValueError will be raised if the sha and mode of the existing item do not match + the one you add, unless force is True :param sha: The 20 or 40 byte sha of the item to add + :param mode: int representing the stat compatible mode of the item - :param force: If True, an item with your name and information will overwrite - any existing item with the same name, no matter which information it has - :return: self""" + + :param force: If True, an item with your name and information will overwrite any + existing item with the same name, no matter which information it has + + :return: self + """ if "/" in name: raise ValueError("Name must not contain '/' characters") if (mode >> 12) not in Tree._map_id_to_type: @@ -173,18 +180,20 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeMod return self def add_unchecked(self, binsha: bytes, mode: int, name: str) -> None: - """Add the given item to the tree, its correctness is assumed, which + """Add the given item to the tree. Its correctness is assumed, which puts the caller into responsibility to assure the input is correct. - For more information on the parameters, see ``add`` - :param binsha: 20 byte binary sha""" + For more information on the parameters, see :meth:`add`. + + :param binsha: 20 byte binary sha + """ assert isinstance(binsha, bytes) and isinstance(mode, int) and isinstance(name, str) tree_cache = (binsha, mode, name) self._cache.append(tree_cache) def __delitem__(self, name: str) -> None: - """Deletes an item with the given name if it exists""" + """Delete an item with the given name if it exists.""" index = self._index_by_name(name) if index > -1: del self._cache[index] @@ -193,7 +202,6 @@ def __delitem__(self, name: str) -> None: class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable): - """Tree objects represent an ordered list of Blobs and other Trees. ``Tree as a list``:: @@ -208,8 +216,8 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable): type: Literal["tree"] = "tree" __slots__ = "_cache" - # actual integer ids for comparison - commit_id = 0o16 # equals stat.S_IFDIR | stat.S_IFLNK - a directory link + # Actual integer IDs for comparison. + commit_id = 0o16 # Equals stat.S_IFDIR | stat.S_IFLNK - a directory link. blob_id = 0o10 symlink_id = 0o12 tree_id = 0o04 @@ -218,7 +226,7 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable): commit_id: Submodule, blob_id: Blob, symlink_id: Blob - # tree id added once Tree is defined + # Tree ID added once Tree is defined. } def __init__( @@ -241,7 +249,7 @@ def _get_intermediate_items( def _set_cache_(self, attr: str) -> None: if attr == "_cache": - # Set the data when we need it + # Set the data when we need it. ostream = self.repo.odb.stream(self.binsha) self._cache: List[TreeCacheTup] = tree_entries_from_data(ostream.read()) else: @@ -250,7 +258,8 @@ def _set_cache_(self, attr: str) -> None: def _iter_convert_to_object(self, iterable: Iterable[TreeCacheTup]) -> Iterator[IndexObjUnion]: """Iterable yields tuples of (binsha, mode, name), which will be converted - to the respective object representation""" + to the respective object representation. + """ for binsha, mode, name in iterable: path = join_path(self.path, name) try: @@ -260,10 +269,11 @@ def _iter_convert_to_object(self, iterable: Iterable[TreeCacheTup]) -> Iterator[ # END for each item def join(self, file: str) -> IndexObjUnion: - """Find the named object in this tree's contents + """Find the named object in this tree's contents. :return: ``git.Blob`` or ``git.Tree`` or ``git.Submodule`` - :raise KeyError: if given file or tree does not exist in tree""" + :raise KeyError: if given file or tree does not exist in tree + """ msg = "Blob or Tree named %r not found" if "/" in file: tree = self @@ -274,7 +284,7 @@ def join(self, file: str) -> IndexObjUnion: if item.type == "tree": tree = item else: - # safety assertion - blobs are at the end of the path + # Safety assertion - blobs are at the end of the path. if i != len(tokens) - 1: raise KeyError(msg % file) return item @@ -294,7 +304,10 @@ def join(self, file: str) -> IndexObjUnion: # END handle long paths def __truediv__(self, file: str) -> IndexObjUnion: - """For PY3 only""" + """The ``/`` operator is another syntax for joining. + + See :meth:`join` for details. + """ return self.join(file) @property @@ -313,7 +326,8 @@ def cache(self) -> TreeModifier: :return: An object allowing to modify the internal cache. This can be used to change the tree's contents. When done, make sure you call ``set_done`` on the tree modifier, or serialization behaviour will be incorrect. - See the ``TreeModifier`` for more information on how to alter the cache""" + See :class:`TreeModifier` for more information on how to alter the cache. + """ return TreeModifier(self._cache) def traverse( @@ -326,8 +340,10 @@ def traverse( ignore_self: int = 1, as_edge: bool = False, ) -> Union[Iterator[IndexObjUnion], Iterator[TraversedTreeTup]]: - """For documentation, see util.Traversable._traverse() - Trees are set to visit_once = False to gain more performance in the traversal""" + """For documentation, see util.Traversable._traverse(). + + Trees are set to ``visit_once = False`` to gain more performance in the traversal. + """ # """ # # To typecheck instead of using cast. @@ -392,7 +408,7 @@ def __contains__(self, item: Union[IndexObjUnion, PathLike]) -> bool: # END handle item is index object # compatibility - # treat item as repo-relative path + # Treat item as repo-relative path. else: path = self.path for info in self._cache: @@ -405,10 +421,12 @@ def __reversed__(self) -> Iterator[IndexObjUnion]: return reversed(self._iter_convert_to_object(self._cache)) # type: ignore def _serialize(self, stream: "BytesIO") -> "Tree": - """Serialize this tree into the stream. Please note that we will assume - our tree data to be in a sorted state. If this is not the case, serialization - will not generate a correct tree representation as these are assumed to be sorted - by algorithms""" + """Serialize this tree into the stream. Assumes sorted tree data. + + .. note:: We will assume our tree data to be in a sorted state. If this is not + the case, serialization will not generate a correct tree representation as + these are assumed to be sorted by algorithms. + """ tree_to_stream(self._cache, stream.write) return self @@ -419,6 +437,5 @@ def _deserialize(self, stream: "BytesIO") -> "Tree": # END tree -# finalize map definition +# Finalize map definition. Tree._map_id_to_type[Tree.tree_id] = Tree -# diff --git a/git/objects/util.py b/git/objects/util.py index 992a53d9c..d2c1c0158 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -3,7 +3,9 @@ # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ -"""Module for general utility functions""" + +"""Module for general utility functions.""" + # flake8: noqa F401 @@ -62,10 +64,10 @@ class TraverseNT(NamedTuple): src: Union["Traversable", None] -T_TIobj = TypeVar("T_TIobj", bound="TraversableIterableObj") # for TraversableIterableObj.traverse() +T_TIobj = TypeVar("T_TIobj", bound="TraversableIterableObj") # For TraversableIterableObj.traverse() TraversedTup = Union[ - Tuple[Union["Traversable", None], "Traversable"], # for commit, submodule + Tuple[Union["Traversable", None], "Traversable"], # For commit, submodule "TraversedTreeTup", ] # for tree.traverse() @@ -92,12 +94,14 @@ class TraverseNT(NamedTuple): def mode_str_to_int(modestr: Union[bytes, str]) -> int: """ - :param modestr: string like 755 or 644 or 100644 - only the last 6 chars will be used + :param modestr: + String like 755 or 644 or 100644 - only the last 6 chars will be used. + :return: String identifying a mode compatible to the mode methods ids of the stat module regarding the rwx permissions for user, group and other, - special flags and file system flags, i.e. whether it is a symlink - for example.""" + special flags and file system flags, such as whether it is a symlink. + """ mode = 0 for iteration, char in enumerate(reversed(modestr[-6:])): char = cast(Union[str, int], char) @@ -110,12 +114,13 @@ def get_object_type_by_name( object_type_name: bytes, ) -> Union[Type["Commit"], Type["TagObject"], Type["Tree"], Type["Blob"]]: """ - :return: type suitable to handle the given object type name. + :return: A type suitable to handle the given object type name. Use the type to create new instances. :param object_type_name: Member of TYPES - :raise ValueError: In case object_type_name is unknown""" + :raise ValueError: If object_type_name is unknown + """ if object_type_name == b"commit": from . import commit @@ -138,9 +143,9 @@ def get_object_type_by_name( def utctz_to_altz(utctz: str) -> int: """Convert a git timezone offset into a timezone offset west of - UTC in seconds (compatible with time.altzone). + UTC in seconds (compatible with :attr:`time.altzone`). - :param utctz: git utc timezone string, i.e. +0200 + :param utctz: git utc timezone string, e.g. +0200 """ int_utctz = int(utctz) seconds = (abs(int_utctz) // 100) * 3600 + (abs(int_utctz) % 100) * 60 @@ -148,9 +153,9 @@ def utctz_to_altz(utctz: str) -> int: def altz_to_utctz_str(altz: float) -> str: - """Convert a timezone offset west of UTC in seconds into a git timezone offset string + """Convert a timezone offset west of UTC in seconds into a Git timezone offset string. - :param altz: timezone offset in seconds west of UTC + :param altz: Timezone offset in seconds west of UTC """ hours = abs(altz) // 3600 minutes = (abs(altz) % 3600) // 60 @@ -159,8 +164,11 @@ def altz_to_utctz_str(altz: float) -> str: def verify_utctz(offset: str) -> str: - """:raise ValueError: if offset is incorrect - :return: offset""" + """ + :raise ValueError: If offset is incorrect + + :return: offset + """ fmt_exc = ValueError("Invalid timezone offset format: %s" % offset) if len(offset) != 5: raise fmt_exc @@ -194,7 +202,7 @@ def dst(self, dt: Union[datetime, None]) -> timedelta: def from_timestamp(timestamp: float, tz_offset: float) -> datetime: - """Converts a timestamp + tz_offset into an aware datetime instance.""" + """Convert a timestamp + tz_offset into an aware datetime instance.""" utc_dt = datetime.fromtimestamp(timestamp, utc) try: local_dt = utc_dt.astimezone(tzoffset(tz_offset)) @@ -205,16 +213,18 @@ def from_timestamp(timestamp: float, tz_offset: float) -> datetime: def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]: """ - Parse the given date as one of the following + Parse the given date as one of the following: - * aware datetime instance + * Aware datetime instance * Git internal format: timestamp offset * RFC 2822: Thu, 07 Apr 2005 22:13:13 +0200. * ISO 8601 2005-04-07T22:13:13 - The T can be a space as well + The T can be a space as well. :return: Tuple(int(timestamp_UTC), int(offset)), both in seconds since epoch + :raise ValueError: If the format could not be understood + :note: Date can also be YYYY.MM.DD, MM/DD/YYYY and DD.MM.YYYY. """ if isinstance(string_date, datetime): @@ -225,7 +235,7 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]: else: raise ValueError(f"string_date datetime object without tzinfo, {string_date}") - # git time + # Git time try: if string_date.count(" ") == 1 and string_date.rfind(":") == -1: timestamp, offset_str = string_date.split() @@ -234,21 +244,21 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]: timestamp_int = int(timestamp) return timestamp_int, utctz_to_altz(verify_utctz(offset_str)) else: - offset_str = "+0000" # local time by default + offset_str = "+0000" # Local time by default. if string_date[-5] in "-+": offset_str = verify_utctz(string_date[-5:]) string_date = string_date[:-6] # skip space as well # END split timezone info offset = utctz_to_altz(offset_str) - # now figure out the date and time portion - split time + # Now figure out the date and time portion - split time. date_formats = [] splitter = -1 if "," in string_date: date_formats.append("%a, %d %b %Y") splitter = string_date.rfind(" ") else: - # iso plus additional + # ISO plus additional date_formats.append("%Y-%m-%d") date_formats.append("%Y.%m.%d") date_formats.append("%m/%d/%Y") @@ -258,15 +268,15 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]: if splitter == -1: splitter = string_date.rfind(" ") # END handle 'T' and ' ' - # END handle rfc or iso + # END handle RFC or ISO assert splitter > -1 - # split date and time - time_part = string_date[splitter + 1 :] # skip space + # Split date and time. + time_part = string_date[splitter + 1 :] # Skip space. date_part = string_date[:splitter] - # parse time + # Parse time. tstruct = time.strptime(time_part, "%H:%M:%S") for fmt in date_formats: @@ -291,7 +301,7 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]: # END exception handling # END for each fmt - # still here ? fail + # Still here ? fail. raise ValueError("no format matched") # END handle format except Exception as e: @@ -299,7 +309,7 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]: # END handle exceptions -# precompiled regex +# Precompiled regexes _re_actor_epoch = re.compile(r"^.+? (.*) (\d+) ([+-]\d+).*$") _re_only_actor = re.compile(r"^.+? (.*)$") @@ -309,7 +319,8 @@ def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]: author Tom Preston-Werner 1191999972 -0700 - :return: [Actor, int_seconds_since_epoch, int_timezone_offset]""" + :return: [Actor, int_seconds_since_epoch, int_timezone_offset] + """ actor, epoch, offset = "", "0", "0" m = _re_actor_epoch.search(line) if m: @@ -327,12 +338,12 @@ def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]: class ProcessStreamAdapter(object): - - """Class wireing all calls to the contained Process instance. + """Class wiring all calls to the contained Process instance. Use this type to hide the underlying process to provide access only to a specified stream. The process is usually wrapped into an AutoInterrupt class to kill - it if the instance goes out of scope.""" + it if the instance goes out of scope. + """ __slots__ = ("_proc", "_stream") @@ -346,11 +357,12 @@ def __getattr__(self, attr: str) -> Any: @runtime_checkable class Traversable(Protocol): - """Simple interface to perform depth-first or breadth-first traversals - into one direction. + in one direction. + Subclasses only need to implement one function. - Instances of the Subclass must be hashable + + Instances of the Subclass must be hashable. Defined subclasses = [Commit, Tree, SubModule] """ @@ -363,7 +375,7 @@ def _get_intermediate_items(cls, item: Any) -> Sequence["Traversable"]: """ Returns: Tuple of items connected to the given item. - Must be implemented in subclass + Must be implemented in subclass. class Commit:: (cls, Commit) -> Tuple[Commit, ...] class Submodule:: (cls, Submodule) -> Iterablelist[Submodule] @@ -393,22 +405,22 @@ def _list_traverse( Submodule -> IterableList['Submodule'] Tree -> IterableList[Union['Submodule', 'Tree', 'Blob']] """ - # Commit and Submodule have id.__attribute__ as IterableObj - # Tree has id.__attribute__ inherited from IndexObject + # Commit and Submodule have id.__attribute__ as IterableObj. + # Tree has id.__attribute__ inherited from IndexObject. if isinstance(self, Has_id_attribute): id = self._id_attribute_ else: - id = "" # shouldn't reach here, unless Traversable subclass created with no _id_attribute_ - # could add _id_attribute_ to Traversable, or make all Traversable also Iterable? + id = "" # Shouldn't reach here, unless Traversable subclass created with no _id_attribute_. + # Could add _id_attribute_ to Traversable, or make all Traversable also Iterable? if not as_edge: out: IterableList[Union["Commit", "Submodule", "Tree", "Blob"]] = IterableList(id) out.extend(self.traverse(as_edge=as_edge, *args, **kwargs)) return out - # overloads in subclasses (mypy doesn't allow typing self: subclass) + # Overloads in subclasses (mypy doesn't allow typing self: subclass). # Union[IterableList['Commit'], IterableList['Submodule'], IterableList[Union['Submodule', 'Tree', 'Blob']]] else: - # Raise deprecationwarning, doesn't make sense to use this + # Raise DeprecationWarning, it doesn't make sense to use this. out_list: IterableList = IterableList(self.traverse(*args, **kwargs)) return out_list @@ -434,35 +446,37 @@ def _traverse( ignore_self: int = 1, as_edge: bool = False, ) -> Union[Iterator[Union["Traversable", "Blob"]], Iterator[TraversedTup]]: - """:return: iterator yielding of items found when traversing self - :param predicate: f(i,d) returns False if item i at depth d should not be included in the result + """:return: Iterator yielding items found when traversing self + + :param predicate: f(i,d) returns False if item i at depth d should not be + included in the result. :param prune: - f(i,d) return True if the search should stop at item i at depth d. - Item i will not be returned. + f(i,d) return True if the search should stop at item i at depth d. Item i + will not be returned. :param depth: - define at which level the iteration should not go deeper - if -1, there is no limit - if 0, you would effectively only get self, the root of the iteration + Defines at which level the iteration should not go deeper if -1, there is no + limit if 0, you would effectively only get self, the root of the iteration i.e. if 1, you would only get the first level of predecessors/successors :param branch_first: if True, items will be returned branch first, otherwise depth first :param visit_once: - if True, items will only be returned once, although they might be encountered - several times. Loops are prevented that way. + if True, items will only be returned once, although they might be + encountered several times. Loops are prevented that way. :param ignore_self: - if True, self will be ignored and automatically pruned from - the result. Otherwise it will be the first item to be returned. - If as_edge is True, the source of the first edge is None + if True, self will be ignored and automatically pruned from the result. + Otherwise it will be the first item to be returned. If as_edge is True, the + source of the first edge is None :param as_edge: if True, return a pair of items, first being the source, second the - destination, i.e. tuple(src, dest) with the edge spanning from - source to destination""" + destination, i.e. tuple(src, dest) with the edge spanning from source to + destination + """ """ Commit -> Iterator[Union[Commit, Tuple[Commit, Commit]] @@ -473,11 +487,12 @@ def _traverse( ignore_self=True is_edge=True -> Iterator[item] ignore_self=True is_edge=False --> Iterator[item] ignore_self=False is_edge=True -> Iterator[item] | Iterator[Tuple[src, item]] - ignore_self=False is_edge=False -> Iterator[Tuple[src, item]]""" + ignore_self=False is_edge=False -> Iterator[Tuple[src, item]] + """ visited = set() stack: Deque[TraverseNT] = deque() - stack.append(TraverseNT(0, self, None)) # self is always depth level 0 + stack.append(TraverseNT(0, self, None)) # self is always depth level 0. def addToStack( stack: Deque[TraverseNT], @@ -497,7 +512,7 @@ def addToStack( # END addToStack local method while stack: - d, item, src = stack.pop() # depth of item, item, item_source + d, item, src = stack.pop() # Depth of item, item, item_source if visit_once and item in visited: continue @@ -506,7 +521,7 @@ def addToStack( visited.add(item) rval: Union[TraversedTup, "Traversable", "Blob"] - if as_edge: # if as_edge return (src, item) unless rrc is None (e.g. for first item) + if as_edge: # If as_edge return (src, item) unless rrc is None (e.g. for first item). rval = (src, item) else: rval = item @@ -518,7 +533,7 @@ def addToStack( if not skipStartItem and predicate(rval, d): yield rval - # only continue to next level if this is appropriate ! + # Only continue to next level if this is appropriate! nd = d + 1 if depth > -1 and nd > depth: continue @@ -529,24 +544,30 @@ def addToStack( @runtime_checkable class Serializable(Protocol): - - """Defines methods to serialize and deserialize objects from and into a data stream""" + """Defines methods to serialize and deserialize objects from and into a data stream.""" __slots__ = () # @abstractmethod def _serialize(self, stream: "BytesIO") -> "Serializable": - """Serialize the data of this object into the given data stream - :note: a serialized object would ``_deserialize`` into the same object + """Serialize the data of this object into the given data stream. + + :note: A serialized object would ``_deserialize`` into the same object. + :param stream: a file-like object - :return: self""" + + :return: self + """ raise NotImplementedError("To be implemented in subclass") # @abstractmethod def _deserialize(self, stream: "BytesIO") -> "Serializable": - """Deserialize all information regarding this object from the stream + """Deserialize all information regarding this object from the stream. + :param stream: a file-like object - :return: self""" + + :return: self + """ raise NotImplementedError("To be implemented in subclass") diff --git a/git/refs/__init__.py b/git/refs/__init__.py index 1486dffe6..18ea2013c 100644 --- a/git/refs/__init__.py +++ b/git/refs/__init__.py @@ -1,5 +1,5 @@ # flake8: noqa -# import all modules in order, fix the names they require +# Import all modules in order, fix the names they require. from .symbolic import * from .reference import * from .head import * diff --git a/git/refs/head.py b/git/refs/head.py index 26efc6cb9..194f51e78 100644 --- a/git/refs/head.py +++ b/git/refs/head.py @@ -5,7 +5,7 @@ from .symbolic import SymbolicReference from .reference import Reference -# typinng --------------------------------------------------- +# typing --------------------------------------------------- from typing import Any, Sequence, Union, TYPE_CHECKING @@ -28,12 +28,12 @@ def strip_quotes(string: str) -> str: class HEAD(SymbolicReference): - - """Special case of a Symbolic Reference as it represents the repository's + """Special case of a SymbolicReference representing the repository's HEAD reference.""" _HEAD_NAME = "HEAD" _ORIG_HEAD_NAME = "ORIG_HEAD" + __slots__ = () def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME): @@ -45,7 +45,8 @@ def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME): def orig_head(self) -> SymbolicReference: """ :return: SymbolicReference pointing at the ORIG_HEAD, which is maintained - to contain the previous value of HEAD""" + to contain the previous value of HEAD. + """ return SymbolicReference(self.repo, self._ORIG_HEAD_NAME) def reset( @@ -71,7 +72,7 @@ def reset( :param working_tree: If True, the working tree will be forcefully adjusted to match the given commit, possibly overwriting uncommitted changes without warning. - If working_tree is True, index must be true as well + If `working_tree` is True, `index` must be True as well. :param paths: Single path or list of paths relative to the git root directory @@ -80,14 +81,15 @@ def reset( :param kwargs: Additional arguments passed to git-reset. - :return: self""" + :return: self + """ mode: Union[str, None] mode = "--soft" if index: mode = "--mixed" - # it appears, some git-versions declare mixed and paths deprecated - # see http://github.com/Byron/GitPython/issues#issue/2 + # Tt appears some git versions declare mixed and paths deprecated. + # See http://github.com/Byron/GitPython/issues#issue/2. if paths: mode = None # END special case @@ -104,7 +106,7 @@ def reset( self.repo.git.reset(mode, commit, "--", paths, **kwargs) except GitCommandError as e: # git nowadays may use 1 as status to indicate there are still unstaged - # modifications after the reset + # modifications after the reset. if e.status != 1: raise # END handle exception @@ -113,7 +115,6 @@ def reset( class Head(Reference): - """A Head is a named reference to a Commit. Every Head instance contains a name and a Commit object. @@ -129,33 +130,35 @@ class Head(Reference): >>> head.commit.hexsha - '1c09f116cbc2cb4100fb6935bb162daa4723f455'""" + '1c09f116cbc2cb4100fb6935bb162daa4723f455' + """ _common_path_default = "refs/heads" k_config_remote = "remote" - k_config_remote_ref = "merge" # branch to merge from remote + k_config_remote_ref = "merge" # Branch to merge from remote. @classmethod def delete(cls, repo: "Repo", *heads: "Union[Head, str]", force: bool = False, **kwargs: Any) -> None: - """Delete the given heads + """Delete the given heads. :param force: If True, the heads will be deleted even if they are not yet merged into the main development stream. - Default False""" + Default False + """ flag = "-d" if force: flag = "-D" repo.git.branch(flag, *heads) def set_tracking_branch(self, remote_reference: Union["RemoteReference", None]) -> "Head": - """ - Configure this branch to track the given remote reference. This will alter - this branch's configuration accordingly. + """Configure this branch to track the given remote reference. This will + alter this branch's configuration accordingly. :param remote_reference: The remote reference to track or None to untrack - any references - :return: self""" + any references. + :return: self + """ from .remote import RemoteReference if remote_reference is not None and not isinstance(remote_reference, RemoteReference): @@ -180,7 +183,7 @@ def set_tracking_branch(self, remote_reference: Union["RemoteReference", None]) def tracking_branch(self) -> Union["RemoteReference", None]: """ :return: The remote_reference we are tracking, or None if we are - not a tracking branch""" + not a tracking branch.""" from .remote import RemoteReference reader = self.config_reader() @@ -193,22 +196,24 @@ def tracking_branch(self) -> Union["RemoteReference", None]: return RemoteReference(self.repo, remote_refpath) # END handle have tracking branch - # we are not a tracking branch + # We are not a tracking branch. return None def rename(self, new_path: PathLike, force: bool = False) -> "Head": - """Rename self to a new path + """Rename self to a new path. :param new_path: Either a simple name or a path, i.e. new_name or features/new_name. - The prefix refs/heads is implied + The prefix refs/heads is implied. :param force: If True, the rename will succeed even if a head with the target name already exists. :return: self - :note: respects the ref log as git commands are used""" + + :note: Respects the ref log as git commands are used. + """ flag = "-m" if force: flag = "-M" @@ -218,19 +223,20 @@ def rename(self, new_path: PathLike, force: bool = False) -> "Head": return self def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]: - """Checkout this head by setting the HEAD to this reference, by updating the index - to reflect the tree we point to and by updating the working tree to reflect - the latest index. + """Check out this head by setting the HEAD to this reference, by updating the + index to reflect the tree we point to and by updating the working tree to + reflect the latest index. The command will fail if changed working tree files would be overwritten. :param force: If True, changes to the index and the working tree will be discarded. - If False, GitCommandError will be raised in that situation. + If False, :class:`GitCommandError ` will be + raised in that situation. :param kwargs: - Additional keyword arguments to be passed to git checkout, i.e. - b='new_branch' to create a new branch at the given spot. + Additional keyword arguments to be passed to git checkout, e.g. + ``b="new_branch"`` to create a new branch at the given spot. :return: The active branch after the checkout operation, usually self unless @@ -241,7 +247,8 @@ def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]: :note: By default it is only allowed to checkout heads - everything else will leave the HEAD detached which is allowed and possible, but remains - a special state that some tools might not be able to handle.""" + a special state that some tools might not be able to handle. + """ kwargs["f"] = force if kwargs["f"] is False: kwargs.pop("f") @@ -265,13 +272,15 @@ def _config_parser(self, read_only: bool) -> SectionConstraint[GitConfigParser]: def config_reader(self) -> SectionConstraint[GitConfigParser]: """ :return: A configuration parser instance constrained to only read - this instance's values""" + this instance's values. + """ return self._config_parser(read_only=True) def config_writer(self) -> SectionConstraint[GitConfigParser]: """ :return: A configuration writer instance with read-and write access - to options of this head""" + to options of this head. + """ return self._config_parser(read_only=False) # } END configuration diff --git a/git/refs/log.py b/git/refs/log.py index 9b02051d3..21c757ccd 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -38,18 +38,17 @@ class RefLogEntry(Tuple[str, str, Actor, Tuple[int, int], str]): - - """Named tuple allowing easy access to the revlog data fields""" + """Named tuple allowing easy access to the revlog data fields.""" _re_hexsha_only = re.compile(r"^[0-9A-Fa-f]{40}$") __slots__ = () def __repr__(self) -> str: - """Representation of ourselves in git reflog format""" + """Representation of ourselves in git reflog format.""" return self.format() def format(self) -> str: - """:return: a string suitable to be placed in a reflog file""" + """:return: A string suitable to be placed in a reflog file.""" act = self.actor time = self.time return "{} {} {} <{}> {!s} {}\t{}\n".format( @@ -64,30 +63,31 @@ def format(self) -> str: @property def oldhexsha(self) -> str: - """The hexsha to the commit the ref pointed to before the change""" + """The hexsha to the commit the ref pointed to before the change.""" return self[0] @property def newhexsha(self) -> str: - """The hexsha to the commit the ref now points to, after the change""" + """The hexsha to the commit the ref now points to, after the change.""" return self[1] @property def actor(self) -> Actor: - """Actor instance, providing access""" + """Actor instance, providing access.""" return self[2] @property def time(self) -> Tuple[int, int]: """time as tuple: - * [0] = int(time) - * [1] = int(timezone_offset) in time.altzone format""" + * [0] = ``int(time)`` + * [1] = ``int(timezone_offset)`` in :attr:`time.altzone` format + """ return self[3] @property def message(self) -> str: - """Message describing the operation that acted on the reference""" + """Message describing the operation that acted on the reference.""" return self[4] @classmethod @@ -109,8 +109,11 @@ def new( @classmethod def from_line(cls, line: bytes) -> "RefLogEntry": """:return: New RefLogEntry instance from the given revlog line. - :param line: line bytes without trailing newline - :raise ValueError: If line could not be parsed""" + + :param line: Line bytes without trailing newline + + :raise ValueError: If `line` could not be parsed + """ line_str = line.decode(defenc) fields = line_str.split("\t", 1) if len(fields) == 1: @@ -141,13 +144,13 @@ def from_line(cls, line: bytes) -> "RefLogEntry": class RefLog(List[RefLogEntry], Serializable): - """A reflog contains RefLogEntrys, each of which defines a certain state of the head in question. Custom query methods allow to retrieve log entries by date or by other criteria. - Reflog entries are ordered, the first added entry is first in the list, the last - entry, i.e. the last change of the head or reference, is last in the list.""" + Reflog entries are ordered. The first added entry is first in the list. The last + entry, i.e. the last change of the head or reference, is last in the list. + """ __slots__ = ("_path",) @@ -158,7 +161,7 @@ def __new__(cls, filepath: Union[PathLike, None] = None) -> "RefLog": def __init__(self, filepath: Union[PathLike, None] = None): """Initialize this instance with an optional filepath, from which we will initialize our data. The path is also used to write changes back using - the write() method""" + the write() method.""" self._path = filepath if filepath is not None: self._read_from_file() @@ -168,7 +171,7 @@ def _read_from_file(self) -> None: try: fmap = file_contents_ro_filepath(self._path, stream=True, allow_mmap=True) except OSError: - # it is possible and allowed that the file doesn't exist ! + # It is possible and allowed that the file doesn't exist! return # END handle invalid log @@ -183,31 +186,35 @@ def _read_from_file(self) -> None: @classmethod def from_file(cls, filepath: PathLike) -> "RefLog": """ - :return: a new RefLog instance containing all entries from the reflog + :return: A new RefLog instance containing all entries from the reflog at the given filepath - :param filepath: path to reflog - :raise ValueError: If the file could not be read or was corrupted in some way""" + :param filepath: Path to reflog + :raise ValueError: If the file could not be read or was corrupted in some way + """ return cls(filepath) @classmethod def path(cls, ref: "SymbolicReference") -> str: """ - :return: string to absolute path at which the reflog of the given ref + :return: String to absolute path at which the reflog of the given ref instance would be found. The path is not guaranteed to point to a valid file though. - :param ref: SymbolicReference instance""" + :param ref: SymbolicReference instance + """ return osp.join(ref.repo.git_dir, "logs", to_native_path(ref.path)) @classmethod def iter_entries(cls, stream: Union[str, "BytesIO", mmap]) -> Iterator[RefLogEntry]: """ :return: Iterator yielding RefLogEntry instances, one for each line read - sfrom the given stream. - :param stream: file-like object containing the revlog in its native format - or string instance pointing to a file to read""" + from the given stream. + + :param stream: File-like object containing the revlog in its native format + or string instance pointing to a file to read. + """ new_entry = RefLogEntry.from_line if isinstance(stream, str): - # default args return mmap on py>3 + # Default args return mmap since Python 3. _stream = file_contents_ro_filepath(stream) assert isinstance(_stream, mmap) else: @@ -223,23 +230,23 @@ def iter_entries(cls, stream: Union[str, "BytesIO", mmap]) -> Iterator[RefLogEnt @classmethod def entry_at(cls, filepath: PathLike, index: int) -> "RefLogEntry": """ - :return: RefLogEntry at the given index + :return: RefLogEntry at the given index. - :param filepath: full path to the index file from which to read the entry + :param filepath: Full path to the index file from which to read the entry. - :param index: python list compatible index, i.e. it may be negative to - specify an entry counted from the end of the list + :param index: Python list compatible index, i.e. it may be negative to + specify an entry counted from the end of the list. - :raise IndexError: If the entry didn't exist + :raise IndexError: If the entry didn't exist. .. note:: This method is faster as it only parses the entry at index, skipping all other lines. Nonetheless, the whole file has to be read if - the index is negative + the index is negative. """ with open(filepath, "rb") as fp: if index < 0: return RefLogEntry.from_line(fp.readlines()[index].strip()) - # read until index is reached + # Read until index is reached. for i in range(index + 1): line = fp.readline() @@ -254,7 +261,8 @@ def entry_at(cls, filepath: PathLike, index: int) -> "RefLogEntry": def to_file(self, filepath: PathLike) -> None: """Write the contents of the reflog instance to a file at the given filepath. - :param filepath: path to file, parent directories are assumed to exist""" + :param filepath: Path to file, parent directories are assumed to exist. + """ lfd = LockedFD(filepath) assure_directory_exists(filepath, is_file=True) @@ -279,19 +287,21 @@ def append_entry( ) -> "RefLogEntry": """Append a new log entry to the revlog at filepath. - :param config_reader: configuration reader of the repository - used to obtain - user information. May also be an Actor instance identifying the committer directly or None. - :param filepath: full path to the log file - :param oldbinsha: binary sha of the previous commit - :param newbinsha: binary sha of the current commit - :param message: message describing the change to the reference - :param write: If True, the changes will be written right away. Otherwise - the change will not be written + :param config_reader: Configuration reader of the repository - used to obtain + user information. May also be an Actor instance identifying the committer + directly or None. + :param filepath: Full path to the log file. + :param oldbinsha: Binary sha of the previous commit. + :param newbinsha: Binary sha of the current commit. + :param message: Message describing the change to the reference. + :param write: If True, the changes will be written right away. Otherwise the + change will not be written. - :return: RefLogEntry objects which was appended to the log + :return: RefLogEntry objects which was appended to the log. - :note: As we are append-only, concurrent access is not a problem as we - do not interfere with readers.""" + :note: As we are append-only, concurrent access is not a problem as we do not + interfere with readers. + """ if len(oldbinsha) != 20 or len(newbinsha) != 20: raise ValueError("Shas need to be given in binary format") @@ -325,9 +335,10 @@ def append_entry( return entry def write(self) -> "RefLog": - """Write this instance's data to the file we are originating from + """Write this instance's data to the file we are originating from. - :return: self""" + :return: self + """ if self._path is None: raise ValueError("Instance was not initialized with a path, use to_file(...) instead") # END assert path @@ -337,10 +348,11 @@ def write(self) -> "RefLog": # } END interface # { Serializable Interface + def _serialize(self, stream: "BytesIO") -> "RefLog": write = stream.write - # write all entries + # Write all entries. for e in self: write(e.format().encode(defenc)) # END for each entry @@ -348,5 +360,6 @@ def _serialize(self, stream: "BytesIO") -> "RefLog": def _deserialize(self, stream: "BytesIO") -> "RefLog": self.extend(self.iter_entries(stream)) - # } END serializable interface return self + + # } END serializable interface diff --git a/git/refs/reference.py b/git/refs/reference.py index dea1af68c..ca265c0e4 100644 --- a/git/refs/reference.py +++ b/git/refs/reference.py @@ -22,7 +22,7 @@ def require_remote_ref_path(func: Callable[..., _T]) -> Callable[..., _T]: - """A decorator raising a TypeError if we are not a valid remote, based on the path""" + """A decorator raising a TypeError if we are not a valid remote, based on the path.""" def wrapper(self: T_References, *args: Any) -> _T: if not self.is_remote(): @@ -38,27 +38,30 @@ def wrapper(self: T_References, *args: Any) -> _T: class Reference(SymbolicReference, LazyMixin, IterableObj): + """A named reference to any object. - """Represents a named reference to any object. Subclasses may apply restrictions though, - i.e. Heads can only point to commits.""" + Subclasses may apply restrictions though, e.g., a :class:`Head ` + can only point to commits. + """ __slots__ = () + _points_to_commits_only = False _resolve_ref_on_create = True _common_path_default = "refs" def __init__(self, repo: "Repo", path: PathLike, check_path: bool = True) -> None: - """Initialize this instance - - :param repo: Our parent repository - :param path: - Path relative to the .git/ directory pointing to the ref in question, i.e. - refs/heads/master - :param check_path: if False, you can provide any path. Otherwise the path must start with the - default path prefix of this type.""" + """Initialize this instance. + + :param repo: Our parent repository. + :param path: Path relative to the .git/ directory pointing to the ref in + question, e.g. ``refs/heads/master``. + :param check_path: If False, you can provide any path. Otherwise the path must + start with the default path prefix of this type. + """ if check_path and not str(path).startswith(self._common_path_default + "/"): raise ValueError(f"Cannot instantiate {self.__class__.__name__!r} from path {path}") - self.path: str # SymbolicReference converts to string atm + self.path: str # SymbolicReference converts to string at the moment. super(Reference, self).__init__(repo, path) def __str__(self) -> str: @@ -72,9 +75,10 @@ def set_object( object: Union[Commit_ish, "SymbolicReference", str], logmsg: Union[str, None] = None, ) -> "Reference": - """Special version which checks if the head-log needs an update as well + """Special version which checks if the head-log needs an update as well. - :return: self""" + :return: self + """ oldbinsha = None if logmsg is not None: head = self.repo.head @@ -104,13 +108,13 @@ def set_object( return self - # NOTE: Don't have to overwrite properties as the will only work without a the log + # NOTE: No need to overwrite properties, as the will only work without a the log. @property def name(self) -> str: """:return: (shortest) Name of this reference - it may contain path components""" - # first two path tokens are can be removed as they are - # refs/heads or refs/tags or refs/remotes + # The first two path tokens can be removed as they are + # refs/heads or refs/tags or refs/remotes. tokens = self.path.split("/") if len(tokens) < 3: return self.path # could be refs/HEAD @@ -132,23 +136,27 @@ def iter_items( # { Remote Interface - @property # type: ignore ## mypy cannot deal with properties with an extra decorator (2021-04-21) + @property # type: ignore # mypy cannot deal with properties with an extra decorator (2021-04-21). @require_remote_ref_path def remote_name(self) -> str: """ :return: Name of the remote we are a reference of, such as 'origin' for a reference - named 'origin/master'""" + named 'origin/master'. + """ tokens = self.path.split("/") # /refs/remotes// return tokens[2] - @property # type: ignore ## mypy cannot deal with properties with an extra decorator (2021-04-21) + @property # type: ignore # mypy cannot deal with properties with an extra decorator (2021-04-21). @require_remote_ref_path def remote_head(self) -> str: - """:return: Name of the remote head itself, i.e. master. + """ + :return: Name of the remote head itself, e.g. master. + :note: The returned name is usually not qualified enough to uniquely identify - a branch""" + a branch. + """ tokens = self.path.split("/") return "/".join(tokens[3:]) diff --git a/git/refs/remote.py b/git/refs/remote.py index ec10c5a1b..e4b1f4392 100644 --- a/git/refs/remote.py +++ b/git/refs/remote.py @@ -21,8 +21,7 @@ class RemoteReference(Head): - - """Represents a reference pointing to a remote head.""" + """A reference pointing to a remote head.""" _common_path_default = Head._remote_common_path_default @@ -35,7 +34,7 @@ def iter_items( *args: Any, **kwargs: Any, ) -> Iterator["RemoteReference"]: - """Iterate remote references, and if given, constrain them to the given remote""" + """Iterate remote references, and if given, constrain them to the given remote.""" common_path = common_path or cls._common_path_default if remote is not None: common_path = join_path(common_path, str(remote)) @@ -49,15 +48,16 @@ def iter_items( # "type: ignore". (See https://github.com/python/typing/issues/241) @classmethod def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None: # type: ignore - """Delete the given remote references + """Delete the given remote references. :note: kwargs are given for comparability with the base class method as we - should not narrow the signature.""" + should not narrow the signature. + """ repo.git.branch("-d", "-r", *refs) - # the official deletion method will ignore remote symbolic refs - these + # The official deletion method will ignore remote symbolic refs - these # are generally ignored in the refs/ folder. We don't though - # and delete remainders manually + # and delete remainders manually. for ref in refs: try: os.remove(os.path.join(repo.common_dir, ref.path)) @@ -71,5 +71,5 @@ def delete(cls, repo: "Repo", *refs: "RemoteReference", **kwargs: Any) -> None: @classmethod def create(cls, *args: Any, **kwargs: Any) -> NoReturn: - """Used to disable this method""" + """Raises TypeError. Defined so the create method is disabled.""" raise TypeError("Cannot explicitly create remote references") diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index 5ff84367d..7da957e2f 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -48,7 +48,7 @@ def _git_dir(repo: "Repo", path: Union[PathLike, None]) -> PathLike: - """Find the git dir that's appropriate for the path""" + """Find the git dir that is appropriate for the path.""" name = f"{path}" if name in ["HEAD", "ORIG_HEAD", "FETCH_HEAD", "index", "logs"]: return repo.git_dir @@ -56,14 +56,16 @@ def _git_dir(repo: "Repo", path: Union[PathLike, None]) -> PathLike: class SymbolicReference(object): + """Special case of a reference that is symbolic. - """Represents a special case of a reference such that this reference is symbolic. - It does not point to a specific commit, but to another Head, which itself - specifies a commit. + This does not point to a specific commit, but to another + :class:`Head `, which itself specifies a commit. - A typical example for a symbolic reference is HEAD.""" + A typical example for a symbolic reference is ``HEAD``. + """ __slots__ = ("repo", "path") + _resolve_ref_on_create = False _points_to_commits_only = True _common_path_default = "" @@ -97,7 +99,8 @@ def name(self) -> str: """ :return: In case of symbolic references, the shortest assumable name - is the path itself.""" + is the path itself. + """ return str(self.path) @property @@ -110,8 +113,11 @@ def _get_packed_refs_path(cls, repo: "Repo") -> str: @classmethod def _iter_packed_refs(cls, repo: "Repo") -> Iterator[Tuple[str, str]]: - """Returns an iterator yielding pairs of sha1/path pairs (as strings) for the corresponding refs. - :note: The packed refs file will be kept open as long as we iterate""" + """Return an iterator yielding pairs of sha1/path pairs (as strings) + for the corresponding refs. + + :note: The packed refs file will be kept open as long as we iterate. + """ try: with open(cls._get_packed_refs_path(repo), "rt", encoding="UTF-8") as fp: for line in fp: @@ -133,8 +139,8 @@ def _iter_packed_refs(cls, repo: "Repo") -> Iterator[Tuple[str, str]]: continue # END parse comment - # skip dereferenced tag object entries - previous line was actual - # tag reference for it + # Skip dereferenced tag object entries - previous line was actual + # tag reference for it. if line[0] == "^": continue @@ -153,7 +159,9 @@ def dereference_recursive(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> """ :return: hexsha stored in the reference at the given ref_path, recursively dereferencing all intermediate references as required - :param repo: the repository containing the reference at ref_path""" + + :param repo: The repository containing the reference at ref_path + """ while True: hexsha, ref_path = cls._get_ref_info(repo, ref_path) @@ -163,7 +171,11 @@ def dereference_recursive(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> @staticmethod def _check_ref_name_valid(ref_path: PathLike) -> None: - # Based on the rules described in https://git-scm.com/docs/git-check-ref-format/#_description + """Check a ref name for validity. + + This is based on the rules described in: + https://git-scm.com/docs/git-check-ref-format/#_description + """ previous: Union[str, None] = None one_before_previous: Union[str, None] = None for c in str(ref_path): @@ -210,9 +222,12 @@ def _check_ref_name_valid(ref_path: PathLike) -> None: def _get_ref_info_helper( cls, repo: "Repo", ref_path: Union[PathLike, None] ) -> Union[Tuple[str, None], Tuple[None, str]]: - """Return: (str(sha), str(target_ref_path)) if available, the sha the file at - rela_path points to, or None. target_ref_path is the reference we - point to, or None""" + """ + :return: (str(sha), str(target_ref_path)) if available, the sha the file at + rela_path points to, or None. + + target_ref_path is the reference we point to, or None. + """ if ref_path: cls._check_ref_name_valid(ref_path) @@ -221,18 +236,18 @@ def _get_ref_info_helper( try: with open(os.path.join(repodir, str(ref_path)), "rt", encoding="UTF-8") as fp: value = fp.read().rstrip() - # Don't only split on spaces, but on whitespace, which allows to parse lines like + # Don't only split on spaces, but on whitespace, which allows to parse lines like: # 60b64ef992065e2600bfef6187a97f92398a9144 branch 'master' of git-server:/path/to/repo tokens = value.split() assert len(tokens) != 0 except OSError: - # Probably we are just packed, find our entry in the packed refs file + # Probably we are just packed. Find our entry in the packed refs file. # NOTE: We are not a symbolic ref if we are in a packed file, as these - # are excluded explicitly + # are excluded explicitly. for sha, path in cls._iter_packed_refs(repo): if path != ref_path: continue - # sha will be used + # sha will be used. tokens = sha, path break # END for each packed ref @@ -240,11 +255,11 @@ def _get_ref_info_helper( if tokens is None: raise ValueError("Reference at %r does not exist" % ref_path) - # is it a reference ? + # Is it a reference? if tokens[0] == "ref:": return (None, tokens[1]) - # its a commit + # It's a commit. if repo.re_hexsha_only.match(tokens[0]): return (tokens[0], None) @@ -252,25 +267,31 @@ def _get_ref_info_helper( @classmethod def _get_ref_info(cls, repo: "Repo", ref_path: Union[PathLike, None]) -> Union[Tuple[str, None], Tuple[None, str]]: - """Return: (str(sha), str(target_ref_path)) if available, the sha the file at - rela_path points to, or None. target_ref_path is the reference we - point to, or None""" + """ + :return: (str(sha), str(target_ref_path)) if available, the sha the file at + rela_path points to, or None. + + target_ref_path is the reference we point to, or None. + """ return cls._get_ref_info_helper(repo, ref_path) def _get_object(self) -> Commit_ish: """ :return: The object our ref currently refers to. Refs can be cached, they will - always point to the actual object as it gets re-created on each query""" - # have to be dynamic here as we may be a tag which can point to anything - # Our path will be resolved to the hexsha which will be used accordingly + always point to the actual object as it gets re-created on each query. + """ + # We have to be dynamic here as we may be a tag which can point to anything. + # Our path will be resolved to the hexsha which will be used accordingly. return Object.new_from_sha(self.repo, hex_to_bin(self.dereference_recursive(self.repo, self.path))) def _get_commit(self) -> "Commit": """ :return: - Commit object we point to, works for detached and non-detached - SymbolicReferences. The symbolic reference will be dereferenced recursively.""" + Commit object we point to. This works for detached and non-detached + :class:`SymbolicReference` instances. The symbolic reference will be + dereferenced recursively. + """ obj = self._get_object() if obj.type == "tag": obj = obj.object @@ -286,12 +307,13 @@ def set_commit( commit: Union[Commit, "SymbolicReference", str], logmsg: Union[str, None] = None, ) -> "SymbolicReference": - """As set_object, but restricts the type of object to be a Commit + """As set_object, but restricts the type of object to be a Commit. :raise ValueError: If commit is not a Commit object or doesn't point to a commit - :return: self""" - # check the type - assume the best if it is a base-string + :return: self + """ + # Check the type - assume the best if it is a base-string. invalid_type = False if isinstance(commit, Object): invalid_type = commit.type != Commit.type @@ -309,7 +331,7 @@ def set_commit( raise ValueError("Need commit, got %r" % commit) # END handle raise - # we leave strings to the rev-parse method below + # We leave strings to the rev-parse method below. self.set_object(commit, logmsg) return self @@ -320,14 +342,18 @@ def set_object( logmsg: Union[str, None] = None, ) -> "SymbolicReference": """Set the object we point to, possibly dereference our symbolic reference first. - If the reference does not exist, it will be created + If the reference does not exist, it will be created. - :param object: a refspec, a SymbolicReference or an Object instance. SymbolicReferences - will be dereferenced beforehand to obtain the object they point to + :param object: A refspec, a :class:`SymbolicReference` or an + :class:`Object ` instance. + :class:`SymbolicReference` instances will be dereferenced beforehand to + obtain the object they point to. :param logmsg: If not None, the message will be used in the reflog entry to be - written. Otherwise the reflog is not altered - :note: plain SymbolicReferences may not actually point to objects by convention - :return: self""" + written. Otherwise the reflog is not altered. + :note: Plain :class:`SymbolicReference` instances may not actually point to + objects by convention. + :return: self + """ if isinstance(object, SymbolicReference): object = object.object # @ReservedAssignment # END resolve references @@ -349,7 +375,9 @@ def set_object( object = property(_get_object, set_object, doc="Return the object our ref currently refers to") # type: ignore def _get_reference(self) -> "SymbolicReference": - """:return: Reference Object we point to + """ + :return: Reference Object we point to + :raise TypeError: If this symbolic reference is detached, hence it doesn't point to a reference, but to a commit""" sha, target_ref_path = self._get_ref_info(self.repo, self.path) @@ -367,18 +395,23 @@ def set_reference( will be set which effectively detaches the reference if it was a purely symbolic one. - :param ref: SymbolicReference instance, Object instance or refspec string - Only if the ref is a SymbolicRef instance, we will point to it. Everything - else is dereferenced to obtain the actual object. + :param ref: + A :class:`SymbolicReference` instance, + an :class:`Object ` instance, or a refspec string. + Only if the ref is a :class:`SymbolicReference` instance, we will point to + it. Everything else is dereferenced to obtain the actual object. + :param logmsg: If set to a string, the message will be used in the reflog. Otherwise, a reflog entry is not written for the changed reference. The previous commit of the entry will be the commit we point to now. - See also: log_append() + See also: :meth:`log_append` :return: self + :note: This symbolic reference will not be dereferenced. For that, see - ``set_object(...)``""" + :meth:`set_object`. + """ write_value = None obj = None if isinstance(ref, SymbolicReference): @@ -388,7 +421,7 @@ def set_reference( write_value = ref.hexsha elif isinstance(ref, str): try: - obj = self.repo.rev_parse(ref + "^{}") # optionally deref tags + obj = self.repo.rev_parse(ref + "^{}") # Optionally dereference tags. write_value = obj.hexsha except (BadObject, BadName) as e: raise ValueError("Could not extract object from %s" % ref) from e @@ -428,7 +461,7 @@ def set_reference( return self - # aliased reference + # Aliased reference reference: Union["Head", "TagReference", "RemoteReference", "Reference"] reference = property(_get_reference, set_reference, doc="Returns the Reference we point to") # type: ignore ref = reference @@ -437,7 +470,8 @@ def is_valid(self) -> bool: """ :return: True if the reference is valid, hence it can be read and points to - a valid object or reference.""" + a valid object or reference. + """ try: self.object except (OSError, ValueError): @@ -450,7 +484,8 @@ def is_detached(self) -> bool: """ :return: True if we are a detached reference, hence we point to a specific commit - instead to another reference""" + instead to another reference. + """ try: self.ref return False @@ -460,10 +495,11 @@ def is_detached(self) -> bool: def log(self) -> "RefLog": """ :return: RefLog for this reference. Its last entry reflects the latest change - applied to this reference + applied to this reference. .. note:: As the log is parsed every time, its recommended to cache it for use - instead of calling this method repeatedly. It should be considered read-only.""" + instead of calling this method repeatedly. It should be considered read-only. + """ return RefLog.from_file(RefLog.path(self)) def log_append( @@ -472,16 +508,17 @@ def log_append( message: Union[str, None], newbinsha: Union[bytes, None] = None, ) -> "RefLogEntry": - """Append a logentry to the logfile of this ref + """Append a logentry to the logfile of this ref. - :param oldbinsha: binary sha this ref used to point to - :param message: A message describing the change + :param oldbinsha: Binary sha this ref used to point to. + :param message: A message describing the change. :param newbinsha: The sha the ref points to now. If None, our current commit sha - will be used - :return: added RefLogEntry instance""" - # NOTE: we use the committer of the currently active commit - this should be + will be used. + :return: The added :class:`RefLogEntry ` instance. + """ + # NOTE: We use the committer of the currently active commit - this should be # correct to allow overriding the committer on a per-commit level. - # See https://github.com/gitpython-developers/GitPython/pull/146 + # See https://github.com/gitpython-developers/GitPython/pull/146. try: committer_or_reader: Union["Actor", "GitConfigParser"] = self.commit.committer except ValueError: @@ -496,19 +533,24 @@ def log_append( return RefLog.append_entry(committer_or_reader, RefLog.path(self), oldbinsha, newbinsha, message) def log_entry(self, index: int) -> "RefLogEntry": - """:return: RefLogEntry at the given index - :param index: python list compatible positive or negative index + """ + :return: RefLogEntry at the given index + + :param index: Python list compatible positive or negative index .. note:: This method must read part of the reflog during execution, hence - it should be used sparringly, or only if you need just one index. - In that case, it will be faster than the ``log()`` method""" + it should be used sparingly, or only if you need just one index. + In that case, it will be faster than the ``log()`` method. + """ return RefLog.entry_at(RefLog.path(self), index) @classmethod def to_full_path(cls, path: Union[PathLike, "SymbolicReference"]) -> PathLike: """ :return: string with a full repository-relative path which can be used to initialize - a Reference instance, for instance by using ``Reference.from_path``""" + a Reference instance, for instance by using + :meth:`Reference.from_path `. + """ if isinstance(path, SymbolicReference): path = path.path full_ref_path = path @@ -520,21 +562,22 @@ def to_full_path(cls, path: Union[PathLike, "SymbolicReference"]) -> PathLike: @classmethod def delete(cls, repo: "Repo", path: PathLike) -> None: - """Delete the reference at the given path + """Delete the reference at the given path. :param repo: - Repository to delete the reference from + Repository to delete the reference from. :param path: - Short or full path pointing to the reference, i.e. refs/myreference - or just "myreference", hence 'refs/' is implied. - Alternatively the symbolic reference to be deleted""" + Short or full path pointing to the reference, e.g. ``refs/myreference`` + or just ``myreference``, hence ``refs/`` is implied. + Alternatively the symbolic reference to be deleted. + """ full_ref_path = cls.to_full_path(path) abs_path = os.path.join(repo.common_dir, full_ref_path) if os.path.exists(abs_path): os.remove(abs_path) else: - # check packed refs + # Check packed refs. pack_file_path = cls._get_packed_refs_path(repo) try: with open(pack_file_path, "rb") as reader: @@ -545,10 +588,10 @@ def delete(cls, repo: "Repo", path: PathLike) -> None: line = line_bytes.decode(defenc) _, _, line_ref = line.partition(" ") line_ref = line_ref.strip() - # keep line if it is a comment or if the ref to delete is not - # in the line + # Keep line if it is a comment or if the ref to delete is not + # in the line. # If we deleted the last line and this one is a tag-reference object, - # we drop it as well + # we drop it as well. if (line.startswith("#") or full_ref_path != line_ref) and ( not dropped_last_line or dropped_last_line and not line.startswith("^") ): @@ -557,21 +600,21 @@ def delete(cls, repo: "Repo", path: PathLike) -> None: continue # END skip comments and lines without our path - # drop this line + # Drop this line. made_change = True dropped_last_line = True - # write the new lines + # Write the new lines. if made_change: - # write-binary is required, otherwise windows will - # open the file in text mode and change LF to CRLF ! + # Binary writing is required, otherwise Windows will + # open the file in text mode and change LF to CRLF! with open(pack_file_path, "wb") as fd: fd.writelines(line.encode(defenc) for line in new_lines) except OSError: - pass # it didn't exist at all + pass # It didn't exist at all. - # delete the reflog + # Delete the reflog. reflog_path = RefLog.path(cls(repo, full_ref_path)) if os.path.isfile(reflog_path): os.remove(reflog_path) @@ -587,16 +630,18 @@ def _create( force: bool, logmsg: Union[str, None] = None, ) -> T_References: - """internal method used to create a new symbolic reference. - If resolve is False, the reference will be taken as is, creating + """Internal method used to create a new symbolic reference. + + If `resolve` is False, the reference will be taken as is, creating a proper symbolic reference. Otherwise it will be resolved to the corresponding object and a detached symbolic reference will be created - instead""" + instead. + """ git_dir = _git_dir(repo, path) full_ref_path = cls.to_full_path(path) abs_ref_path = os.path.join(git_dir, full_ref_path) - # figure out target data + # Figure out target data. target = reference if resolve: target = repo.rev_parse(str(reference)) @@ -630,22 +675,22 @@ def create( force: bool = False, **kwargs: Any, ) -> T_References: - """Create a new symbolic reference, hence a reference pointing , to another reference. + """Create a new symbolic reference: a reference pointing to another reference. :param repo: - Repository to create the reference in + Repository to create the reference in. :param path: - full path at which the new symbolic reference is supposed to be - created at, i.e. "NEW_HEAD" or "symrefs/my_new_symref" + Full path at which the new symbolic reference is supposed to be + created at, e.g. ``NEW_HEAD`` or ``symrefs/my_new_symref``. :param reference: - The reference to which the new symbolic reference should point to. + The reference which the new symbolic reference should point to. If it is a commit-ish, the symbolic ref will be detached. :param force: - if True, force creation even if a symbolic reference with that name already exists. - Raise OSError otherwise + If True, force creation even if a symbolic reference with that name already exists. + Raise :class:`OSError` otherwise. :param logmsg: If not None, the message to append to the reflog. Otherwise no reflog @@ -657,23 +702,26 @@ def create( If a (Symbolic)Reference with the same name but different contents already exists. - :note: This does not alter the current HEAD, index or Working Tree""" + :note: This does not alter the current HEAD, index or working tree. + """ return cls._create(repo, path, cls._resolve_ref_on_create, reference, force, logmsg) def rename(self, new_path: PathLike, force: bool = False) -> "SymbolicReference": - """Rename self to a new path + """Rename self to a new path. :param new_path: - Either a simple name or a full path, i.e. new_name or features/new_name. - The prefix refs/ is implied for references and will be set as needed. - In case this is a symbolic ref, there is no implied prefix + Either a simple name or a full path, e.g. ``new_name`` or ``features/new_name``. + The prefix ``refs/`` is implied for references and will be set as needed. + In case this is a symbolic ref, there is no implied prefix. :param force: If True, the rename will succeed even if a head with the target name - already exists. It will be overwritten in that case + already exists. It will be overwritten in that case. :return: self - :raise OSError: In case a file at path but a different contents already exists""" + + :raise OSError: If a file at path but with different contents already exists. + """ new_path = self.to_full_path(new_path) if self.path == new_path: return self @@ -682,15 +730,15 @@ def rename(self, new_path: PathLike, force: bool = False) -> "SymbolicReference" cur_abs_path = os.path.join(_git_dir(self.repo, self.path), self.path) if os.path.isfile(new_abs_path): if not force: - # if they point to the same file, its not an error + # If they point to the same file, it's not an error. with open(new_abs_path, "rb") as fd1: f1 = fd1.read().strip() with open(cur_abs_path, "rb") as fd2: f2 = fd2.read().strip() if f1 != f2: raise OSError("File at path %r already exists" % new_abs_path) - # else: we could remove ourselves and use the otherone, but - # but clarity we just continue as usual + # else: We could remove ourselves and use the other one, but... + # ...for clarity, we just continue as usual. # END not force handling os.remove(new_abs_path) # END handle existing target file @@ -713,10 +761,10 @@ def _iter_items( common_path = cls._common_path_default rela_paths = set() - # walk loose refs - # Currently we do not follow links + # Walk loose refs. + # Currently we do not follow links. for root, dirs, files in os.walk(join_path_native(repo.common_dir, common_path)): - if "refs" not in root.split(os.sep): # skip non-refs subfolders + if "refs" not in root.split(os.sep): # Skip non-refs subfolders. refs_id = [d for d in dirs if d == "refs"] if refs_id: dirs[0:] = ["refs"] @@ -730,14 +778,14 @@ def _iter_items( # END for each file in root directory # END for each directory to walk - # read packed refs + # Read packed refs. for _sha, rela_path in cls._iter_packed_refs(repo): if rela_path.startswith(str(common_path)): rela_paths.add(rela_path) # END relative path matches common path # END packed refs reading - # return paths in sorted order + # Yield paths in sorted order. for path in sorted(rela_paths): try: yield cls.from_path(repo, path) @@ -753,37 +801,45 @@ def iter_items( *args: Any, **kwargs: Any, ) -> Iterator[T_References]: - """Find all refs in the repository + """Find all refs in the repository. :param repo: is the Repo :param common_path: - Optional keyword argument to the path which is to be shared by all - returned Ref objects. - Defaults to class specific portion if None assuring that only - refs suitable for the actual class are returned. + Optional keyword argument to the path which is to be shared by all returned + Ref objects. + Defaults to class specific portion if None, ensuring that only refs suitable + for the actual class are returned. :return: - git.SymbolicReference[], each of them is guaranteed to be a symbolic - ref which is not detached and pointing to a valid ref + A list of :class:`SymbolicReference`, each guaranteed to be a symbolic ref + which is not detached and pointing to a valid ref. - List is lexicographically sorted - The returned objects represent actual subclasses, such as Head or TagReference""" + The list is lexicographically sorted. The returned objects are instances of + concrete subclasses, such as :class:`Head ` or + :class:`TagReference `. + """ return (r for r in cls._iter_items(repo, common_path) if r.__class__ is SymbolicReference or not r.is_detached) @classmethod def from_path(cls: Type[T_References], repo: "Repo", path: PathLike) -> T_References: """ - :param path: full .git-directory-relative path name to the Reference to instantiate - :note: use to_full_path() if you only have a partial path of a known Reference Type + Make a symbolic reference from a path. + + :param path: Full ``.git``-directory-relative path name to the Reference to instantiate. + + :note: Use :meth:`to_full_path` if you only have a partial path of a known Reference type. + :return: - Instance of type Reference, Head, or Tag - depending on the given path""" + Instance of type :class:`Reference `, + :class:`Head `, or :class:`Tag `, + depending on the given path. + """ if not path: raise ValueError("Cannot create Reference from %r" % path) - # Names like HEAD are inserted after the refs module is imported - we have an import dependency - # cycle and don't want to import these names in-function + # Names like HEAD are inserted after the refs module is imported - we have an + # import dependency cycle and don't want to import these names in-function. from . import HEAD, Head, RemoteReference, TagReference, Reference for ref_type in ( diff --git a/git/refs/tag.py b/git/refs/tag.py index d32d91bcf..3c269d9ba 100644 --- a/git/refs/tag.py +++ b/git/refs/tag.py @@ -18,10 +18,9 @@ class TagReference(Reference): - - """Class representing a lightweight tag reference which either points to a commit - ,a tag object or any other object. In the latter case additional information, - like the signature or the tag-creator, is available. + """A lightweight tag reference which either points to a commit, a tag object or any + other object. In the latter case additional information, like the signature or the + tag-creator, is available. This tag object will always point to a commit object, but may carry additional information in a tag object:: @@ -29,9 +28,11 @@ class TagReference(Reference): tagref = TagReference.list_items(repo)[0] print(tagref.commit.message) if tagref.tag is not None: - print(tagref.tag.message)""" + print(tagref.tag.message) + """ __slots__ = () + _common_default = "tags" _common_path_default = Reference._common_path_default + "/" + _common_default @@ -39,11 +40,12 @@ class TagReference(Reference): def commit(self) -> "Commit": # type: ignore[override] # LazyMixin has unrelated commit method """:return: Commit object the tag ref points to - :raise ValueError: if the tag points to a tree or blob""" + :raise ValueError: If the tag points to a tree or blob + """ obj = self.object while obj.type != "commit": if obj.type == "tag": - # it is a tag object which carries the commit as an object - we can point to anything + # It is a tag object which carries the commit as an object - we can point to anything. obj = obj.object else: raise ValueError( @@ -59,16 +61,13 @@ def commit(self) -> "Commit": # type: ignore[override] # LazyMixin has unrelat def tag(self) -> Union["TagObject", None]: """ :return: Tag object this tag ref points to or None in case - we are a light weight tag""" + we are a lightweight tag""" obj = self.object if obj.type == "tag": return obj return None - # make object read-only - # It should be reasonably hard to adjust an existing tag - - # object = property(Reference._get_object) + # Make object read-only. It should be reasonably hard to adjust an existing tag. @property def object(self) -> Commit_ish: # type: ignore[override] return Reference._get_object(self) @@ -86,30 +85,31 @@ def create( """Create a new tag reference. :param path: - The name of the tag, i.e. 1.0 or releases/1.0. - The prefix refs/tags is implied + The name of the tag, e.g. ``1.0`` or ``releases/1.0``. + The prefix ``refs/tags`` is implied. :param ref: - A reference to the Object you want to tag. The Object can be a commit, tree or - blob. + A reference to the :class:`Object ` you want to + tag. The Object can be a commit, tree or blob. :param logmsg: If not None, the message will be used in your tag object. This will also - create an additional tag object that allows to obtain that information, i.e.:: + create an additional tag object that allows to obtain that information, e.g.:: tagref.tag.message :param message: - Synonym for :param logmsg: - Included for backwards compatibility. :param logmsg is used in preference if both given. + Synonym for the `logmsg` parameter. + Included for backwards compatibility. `logmsg` takes precedence if both are passed. :param force: - If True, to force creation of a tag even though that tag already exists. + If True, force creation of a tag even though that tag already exists. :param kwargs: - Additional keyword arguments to be passed to git-tag + Additional keyword arguments to be passed to git-tag. - :return: A new TagReference""" + :return: A new TagReference. + """ if "ref" in kwargs and kwargs["ref"]: reference = kwargs["ref"] @@ -130,9 +130,9 @@ def create( @classmethod def delete(cls, repo: "Repo", *tags: "TagReference") -> None: # type: ignore[override] - """Delete the given existing tag or tags""" + """Delete the given existing tag or tags.""" repo.git.tag("-d", *tags) -# provide an alias +# Provide an alias. Tag = TagReference diff --git a/git/remote.py b/git/remote.py index fc2b2ceba..76352087a 100644 --- a/git/remote.py +++ b/git/remote.py @@ -4,7 +4,8 @@ # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ -# Module implementing a remote object allowing easy access to git remotes +"""Module implementing a remote object allowing easy access to git remotes.""" + import logging import re @@ -80,9 +81,13 @@ def add_progress( progress: Union[RemoteProgress, "UpdateProgress", Callable[..., RemoteProgress], None], ) -> Any: """Add the --progress flag to the given kwargs dict if supported by the - git command. If the actual progress in the given progress instance is not - given, we do not request any progress - :return: possibly altered kwargs""" + git command. + + :note: If the actual progress in the given progress instance is not + given, we do not request any progress. + + :return: possibly altered kwargs + """ if progress is not None: v = git.version_info[:2] if v >= (1, 7): @@ -113,18 +118,16 @@ def to_progress_instance(progress: RemoteProgress) -> RemoteProgress: def to_progress_instance( progress: Union[Callable[..., Any], RemoteProgress, None] ) -> Union[RemoteProgress, CallableRemoteProgress]: - """Given the 'progress' return a suitable object derived from - RemoteProgress(). - """ - # new API only needs progress as a function + """Given the 'progress' return a suitable object derived from RemoteProgress.""" + # New API only needs progress as a function. if callable(progress): return CallableRemoteProgress(progress) - # where None is passed create a parser that eats the progress + # Where None is passed create a parser that eats the progress. elif progress is None: return RemoteProgress() - # assume its the old API with an instance of RemoteProgress. + # Assume its the old API with an instance of RemoteProgress. return progress @@ -152,6 +155,7 @@ class PushInfo(IterableObj, object): "_remote", "summary", ) + _id_attribute_ = "pushinfo" ( @@ -187,8 +191,10 @@ def __init__( old_commit: Optional[str] = None, summary: str = "", ) -> None: - """Initialize a new instance - local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None""" + """Initialize a new instance. + + local_ref: HEAD | Head | RemoteReference | TagReference | Reference | SymbolicReference | None + """ self.flags = flags self.local_ref = local_ref self.remote_ref_string = remote_ref_string @@ -204,9 +210,11 @@ def old_commit(self) -> Union[str, SymbolicReference, Commit_ish, None]: def remote_ref(self) -> Union[RemoteReference, TagReference]: """ :return: - Remote Reference or TagReference in the local repository corresponding - to the remote_ref_string kept in this instance.""" - # translate heads to a local remote, tags stay as they are + Remote :class:`Reference ` or + :class:`TagReference ` in the local repository + corresponding to the :attr:`remote_ref_string` kept in this instance. + """ + # Translate heads to a local remote. Tags stay as they are. if self.remote_ref_string.startswith("refs/tags"): return TagReference(self._remote.repo, self.remote_ref_string) elif self.remote_ref_string.startswith("refs/heads"): @@ -222,11 +230,11 @@ def remote_ref(self) -> Union[RemoteReference, TagReference]: @classmethod def _from_line(cls, remote: "Remote", line: str) -> "PushInfo": """Create a new PushInfo instance as parsed from line which is expected to be like - refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes""" + refs/heads/master:refs/heads/master 05d2687..1d0568e as bytes.""" control_character, from_to, summary = line.split("\t", 3) flags = 0 - # control character handling + # Control character handling try: flags |= cls._flag_map[control_character] except KeyError as e: @@ -243,7 +251,7 @@ def _from_line(cls, remote: "Remote", line: str) -> "PushInfo": else: from_ref = Reference.from_path(remote.repo, from_ref_string) - # commit handling, could be message or commit info + # Commit handling, could be message or commit info old_commit: Optional[str] = None if summary.startswith("["): if "[rejected]" in summary: @@ -260,13 +268,13 @@ def _from_line(cls, remote: "Remote", line: str) -> "PushInfo": flags |= cls.NEW_HEAD # uptodate encoded in control character else: - # fast-forward or forced update - was encoded in control character, - # but we parse the old and new commit + # Fast-forward or forced update - was encoded in control character, + # but we parse the old and new commit. split_token = "..." if control_character == " ": split_token = ".." old_sha, _new_sha = summary.split(" ")[0].split(split_token) - # have to use constructor here as the sha usually is abbreviated + # Have to use constructor here as the sha usually is abbreviated. old_commit = old_sha # END message handling @@ -278,9 +286,7 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn: # -> class PushInfoList(IterableList[PushInfo]): - """ - IterableList of PushInfo objects. - """ + """IterableList of PushInfo objects.""" def __new__(cls) -> "PushInfoList": return cast(PushInfoList, IterableList.__new__(cls, "push_infos")) @@ -290,15 +296,12 @@ def __init__(self) -> None: self.error: Optional[Exception] = None def raise_if_error(self) -> None: - """ - Raise an exception if any ref failed to push. - """ + """Raise an exception if any ref failed to push.""" if self.error: raise self.error class FetchInfo(IterableObj, object): - """ Carries information about the results of a fetch operation of a single head:: @@ -315,6 +318,7 @@ class FetchInfo(IterableObj, object): """ __slots__ = ("ref", "old_commit", "flags", "note", "remote_ref_path") + _id_attribute_ = "fetchinfo" ( @@ -341,9 +345,7 @@ class FetchInfo(IterableObj, object): @classmethod def refresh(cls) -> Literal[True]: - """This gets called by the refresh function (see the top level - __init__). - """ + """This gets called by the refresh function (see the top level __init__).""" # clear the old values in _flag_map try: del cls._flag_map["t"] @@ -371,9 +373,7 @@ def __init__( old_commit: Union[Commit_ish, None] = None, remote_ref_path: Optional[PathLike] = None, ) -> None: - """ - Initialize a new instance - """ + """Initialize a new instance.""" self.ref = ref self.flags = flags self.note = note @@ -410,12 +410,13 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo": ' ' means a fast-forward fetch line is the corresponding line from FETCH_HEAD, like - acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo""" + acb0fa8b94ef421ad60c8507b634759a472cd56c not-for-merge branch '0.1.7RC' of /tmp/tmpya0vairemote_repo + """ match = cls._re_fetch_result.match(line) if match is None: raise ValueError("Failed to parse line: %r" % line) - # parse lines + # Parse lines. remote_local_ref_str: str ( control_character, @@ -432,7 +433,7 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo": except ValueError as e: # unpack error raise ValueError("Failed to parse FETCH_HEAD line: %r" % fetch_line) from e - # parse flags from control_character + # Parse flags from control_character. flags = 0 try: flags |= cls._flag_map[control_character] @@ -440,7 +441,8 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo": raise ValueError("Control character %r unknown as parsed from line %r" % (control_character, line)) from e # END control char exception handling - # parse operation string for more info - makes no sense for symbolic refs, but we parse it anyway + # Parse operation string for more info. + # This makes no sense for symbolic refs, but we parse it anyway. old_commit: Union[Commit_ish, None] = None is_tag_operation = False if "rejected" in operation: @@ -460,45 +462,45 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo": old_commit = repo.rev_parse(operation.split(split_token)[0]) # END handle refspec - # handle FETCH_HEAD and figure out ref type + # Handle FETCH_HEAD and figure out ref type. # If we do not specify a target branch like master:refs/remotes/origin/master, # the fetch result is stored in FETCH_HEAD which destroys the rule we usually - # have. In that case we use a symbolic reference which is detached + # have. In that case we use a symbolic reference which is detached. ref_type: Optional[Type[SymbolicReference]] = None if remote_local_ref_str == "FETCH_HEAD": ref_type = SymbolicReference elif ref_type_name == "tag" or is_tag_operation: - # the ref_type_name can be branch, whereas we are still seeing a tag operation. It happens during - # testing, which is based on actual git operations + # The ref_type_name can be branch, whereas we are still seeing a tag operation. + # It happens during testing, which is based on actual git operations. ref_type = TagReference elif ref_type_name in ("remote-tracking", "branch"): - # note: remote-tracking is just the first part of the 'remote-tracking branch' token. - # We don't parse it correctly, but its enough to know what to do, and its new in git 1.7something + # Note: remote-tracking is just the first part of the 'remote-tracking branch' token. + # We don't parse it correctly, but its enough to know what to do, and it's new in git 1.7something. ref_type = RemoteReference elif "/" in ref_type_name: - # If the fetch spec look something like this '+refs/pull/*:refs/heads/pull/*', and is thus pretty - # much anything the user wants, we will have trouble to determine what's going on - # For now, we assume the local ref is a Head + # If the fetch spec look something like this '+refs/pull/*:refs/heads/pull/*', + # and is thus pretty much anything the user wants, we will have trouble + # determining what's going on. For now, we assume the local ref is a Head. ref_type = Head else: raise TypeError("Cannot handle reference type: %r" % ref_type_name) # END handle ref type - # create ref instance + # Create ref instance. if ref_type is SymbolicReference: remote_local_ref = ref_type(repo, "FETCH_HEAD") else: - # determine prefix. Tags are usually pulled into refs/tags, they may have subdirectories. - # It is not clear sometimes where exactly the item is, unless we have an absolute path as indicated - # by the 'ref/' prefix. Otherwise even a tag could be in refs/remotes, which is when it will have the - # 'tags/' subdirectory in its path. + # Determine prefix. Tags are usually pulled into refs/tags, they may have subdirectories. + # It is not clear sometimes where exactly the item is, unless we have an absolute path as + # indicated by the 'ref/' prefix. Otherwise even a tag could be in refs/remotes, which is + # when it will have the 'tags/' subdirectory in its path. # We don't want to test for actual existence, but try to figure everything out analytically. ref_path: Optional[PathLike] = None remote_local_ref_str = remote_local_ref_str.strip() if remote_local_ref_str.startswith(Reference._common_path_default + "/"): - # always use actual type if we get absolute paths - # Will always be the case if something is fetched outside of refs/remotes (if its not a tag) + # Always use actual type if we get absolute paths. + # Will always be the case if something is fetched outside of refs/remotes (if its not a tag). ref_path = remote_local_ref_str if ref_type is not TagReference and not remote_local_ref_str.startswith( RemoteReference._common_path_default + "/" @@ -506,14 +508,14 @@ def _from_line(cls, repo: "Repo", line: str, fetch_line: str) -> "FetchInfo": ref_type = Reference # END downgrade remote reference elif ref_type is TagReference and "tags/" in remote_local_ref_str: - # even though its a tag, it is located in refs/remotes + # Even though it's a tag, it is located in refs/remotes. ref_path = join_path(RemoteReference._common_path_default, remote_local_ref_str) else: ref_path = join_path(ref_type._common_path_default, remote_local_ref_str) # END obtain refpath - # even though the path could be within the git conventions, we make - # sure we respect whatever the user wanted, and disabled path checking + # Even though the path could be within the git conventions, we make + # sure we respect whatever the user wanted, and disabled path checking. remote_local_ref = ref_type(repo, ref_path, check_path=False) # END create ref instance @@ -527,16 +529,17 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> NoReturn: # -> class Remote(LazyMixin, IterableObj): - """Provides easy read and write access to a git remote. Everything not part of this interface is considered an option for the current remote, allowing constructs like remote.pushurl to query the pushurl. - NOTE: When querying configuration, the configuration accessor will be cached - to speed up subsequent accesses.""" + :note: When querying configuration, the configuration accessor will be cached + to speed up subsequent accesses. + """ __slots__ = ("repo", "name", "_config_reader") + _id_attribute_ = "name" unsafe_git_fetch_options = [ @@ -557,22 +560,23 @@ class Remote(LazyMixin, IterableObj): ] def __init__(self, repo: "Repo", name: str) -> None: - """Initialize a remote instance + """Initialize a remote instance. :param repo: The repository we are a remote of - :param name: the name of the remote, i.e. 'origin'""" + :param name: The name of the remote, e.g. 'origin' + """ self.repo = repo self.name = name self.url: str def __getattr__(self, attr: str) -> Any: """Allows to call this instance like - remote.special( \\*args, \\*\\*kwargs) to call git-remote special self.name""" + remote.special( \\*args, \\*\\*kwargs) to call git-remote special self.name.""" if attr == "_config_reader": return super(Remote, self).__getattr__(attr) - # sometimes, probably due to a bug in python itself, we are being called - # even though a slot of the same name exists + # Sometimes, probably due to a bug in Python itself, we are being called + # even though a slot of the same name exists. try: return self._config_reader.get(attr) except cp.NoOptionError: @@ -584,8 +588,8 @@ def _config_section_name(self) -> str: def _set_cache_(self, attr: str) -> None: if attr == "_config_reader": - # NOTE: This is cached as __getattr__ is overridden to return remote config values implicitly, such as - # in print(r.pushurl) + # NOTE: This is cached as __getattr__ is overridden to return remote config + # values implicitly, such as in print(r.pushurl). self._config_reader = SectionConstraint(self.repo.config_reader("repository"), self._config_section_name()) else: super(Remote, self)._set_cache_(attr) @@ -608,12 +612,13 @@ def __hash__(self) -> int: def exists(self) -> bool: """ :return: True if this is a valid, existing remote. - Valid remotes have an entry in the repository's configuration""" + Valid remotes have an entry in the repository's configuration. + """ try: self.config_reader.get("url") return True except cp.NoOptionError: - # we have the section at least ... + # We have the section at least... return True except cp.NoSectionError: return False @@ -635,12 +640,12 @@ def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator["Remote def set_url( self, new_url: str, old_url: Optional[str] = None, allow_unsafe_protocols: bool = False, **kwargs: Any ) -> "Remote": - """Configure URLs on current remote (cf command git remote set_url) + """Configure URLs on current remote (cf command git remote set_url). This command manages URLs on the remote. - :param new_url: string being the URL to add as an extra remote URL - :param old_url: when set, replaces this URL with new_url for the remote + :param new_url: String being the URL to add as an extra remote URL + :param old_url: When set, replaces this URL with new_url for the remote :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext :return: self """ @@ -655,12 +660,12 @@ def set_url( return self def add_url(self, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote": - """Adds a new url on current remote (special case of git remote set_url) + """Adds a new url on current remote (special case of git remote set_url). This command adds new URLs to a given remote, making it possible to have multiple URLs for a single remote. - :param url: string being the URL to add as an extra remote URL + :param url: String being the URL to add as an extra remote URL :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext :return: self """ @@ -672,7 +677,7 @@ def delete_url(self, url: str, **kwargs: Any) -> "Remote": This command deletes new URLs to a given remote, making it possible to have multiple URLs for a single remote. - :param url: string being the URL to delete from the remote + :param url: String being the URL to delete from the remote :return: self """ return self.set_url(url, delete=True) @@ -700,7 +705,7 @@ def urls(self) -> Iterator[str]: yield line.split(": ")[-1] except GitCommandError as _ex: if any(msg in str(_ex) for msg in ["correct access rights", "cannot run ssh"]): - # If ssh is not setup to access this repository, see issue 694 + # If ssh is not setup to access this repository, see issue 694. remote_details = self.repo.git.config("--get-all", "remote.%s.url" % self.name) assert isinstance(remote_details, str) for line in remote_details.split("\n"): @@ -715,8 +720,9 @@ def refs(self) -> IterableList[RemoteReference]: """ :return: IterableList of RemoteReference objects. It is prefixed, allowing - you to omit the remote path portion, i.e.:: - remote.refs.master # yields RemoteReference('/refs/remotes/origin/master')""" + you to omit the remote path portion, e.g.:: + remote.refs.master # yields RemoteReference('/refs/remotes/origin/master') + """ out_refs: IterableList[RemoteReference] = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) out_refs.extend(RemoteReference.list_items(self.repo, remote=self.name)) return out_refs @@ -745,7 +751,7 @@ def stale_refs(self) -> IterableList[Reference]: if not line.startswith(token): continue ref_name = line.replace(token, "") - # sometimes, paths start with a full ref name, like refs/tags/foo, see #260 + # Sometimes, paths start with a full ref name, like refs/tags/foo. See #260. if ref_name.startswith(Reference._common_path_default + "/"): out_refs.append(Reference.from_path(self.repo, ref_name)) else: @@ -757,7 +763,7 @@ def stale_refs(self) -> IterableList[Reference]: @classmethod def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool = False, **kwargs: Any) -> "Remote": - """Create a new remote to the given repository + """Create a new remote to the given repository. :param repo: Repository instance that is to receive the new remote :param name: Desired name of the remote @@ -765,7 +771,8 @@ def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext :param kwargs: Additional arguments to be passed to the git-remote add command :return: New Remote instance - :raise GitCommandError: in case an origin with that name already exists""" + :raise GitCommandError: in case an origin with that name already exists + """ scmd = "add" kwargs["insert_kwargs_after"] = scmd url = Git.polish_url(url) @@ -774,29 +781,30 @@ def create(cls, repo: "Repo", name: str, url: str, allow_unsafe_protocols: bool repo.git.remote(scmd, "--", name, url, **kwargs) return cls(repo, name) - # add is an alias + # `add` is an alias. @classmethod def add(cls, repo: "Repo", name: str, url: str, **kwargs: Any) -> "Remote": return cls.create(repo, name, url, **kwargs) @classmethod def remove(cls, repo: "Repo", name: str) -> str: - """Remove the remote with the given name + """Remove the remote with the given name. - :return: the passed remote name to remove + :return: The passed remote name to remove """ repo.git.remote("rm", name) if isinstance(name, cls): name._clear_cache() return name - # alias + # `rm` is an alias. rm = remove def rename(self, new_name: str) -> "Remote": - """Rename self to the given new_name + """Rename self to the given new_name. - :return: self""" + :return: self + """ if self.name == new_name: return self @@ -808,13 +816,12 @@ def rename(self, new_name: str) -> "Remote": def update(self, **kwargs: Any) -> "Remote": """Fetch all changes for this remote, including new branches which will - be forced in ( in case your local remote branch is not part the new remote branches - ancestry anymore ). - - :param kwargs: - Additional arguments passed to git-remote update + be forced in (in case your local remote branch is not part the new remote + branch's ancestry anymore). - :return: self""" + :param kwargs: Additional arguments passed to git-remote update + :return: self + """ scmd = "update" kwargs["insert_kwargs_after"] = scmd self.repo.git.remote(scmd, self.name, **kwargs) @@ -828,15 +835,15 @@ def _get_fetch_info_from_stderr( ) -> IterableList["FetchInfo"]: progress = to_progress_instance(progress) - # skip first line as it is some remote info we are not interested in + # Skip first line as it is some remote info we are not interested in. output: IterableList["FetchInfo"] = IterableList("name") - # lines which are no progress are fetch info lines - # this also waits for the command to finish - # Skip some progress lines that don't provide relevant information + # Lines which are no progress are fetch info lines. + # This also waits for the command to finish. + # Skip some progress lines that don't provide relevant information. fetch_info_lines = [] - # Basically we want all fetch info lines which appear to be in regular form, and thus have a - # command character. Everything else we ignore, + # Basically we want all fetch info lines which appear to be in regular form, and + # thus have a command character. Everything else we ignore. cmds = set(FetchInfo._flag_map.keys()) progress_handler = progress.new_message_handler() @@ -861,7 +868,7 @@ def _get_fetch_info_from_stderr( fetch_info_lines.append(line) continue - # read head information + # Read head information. fetch_head = SymbolicReference(self.repo, "FETCH_HEAD") with open(fetch_head.abspath, "rb") as fp: fetch_head_info = [line.decode(defenc) for line in fp.readlines()] @@ -899,10 +906,10 @@ def _get_push_info( ) -> PushInfoList: progress = to_progress_instance(progress) - # read progress information from stderr - # we hope stdout can hold all the data, it should ... - # read the lines manually as it will use carriage returns between the messages - # to override the previous one. This is why we read the bytes manually + # Read progress information from stderr. + # We hope stdout can hold all the data, it should... + # Read the lines manually as it will use carriage returns between the messages + # to override the previous one. This is why we read the bytes manually. progress_handler = progress.new_message_handler() output: PushInfoList = PushInfoList() @@ -925,8 +932,8 @@ def stdout_handler(line: str) -> None: try: proc.wait(stderr=stderr_text) except Exception as e: - # This is different than fetch (which fails if there is any std_err - # even if there is an output) + # This is different than fetch (which fails if there is any stderr + # even if there is an output). if not output: raise elif stderr_text: @@ -936,7 +943,7 @@ def stdout_handler(line: str) -> None: return output def _assert_refspec(self) -> None: - """Turns out we can't deal with remotes if the refspec is missing""" + """Turns out we can't deal with remotes if the refspec is missing.""" config = self.config_reader unset = "placeholder" try: @@ -958,38 +965,46 @@ def fetch( allow_unsafe_options: bool = False, **kwargs: Any, ) -> IterableList[FetchInfo]: - """Fetch the latest changes for this remote + """Fetch the latest changes for this remote. :param refspec: A "refspec" is used by fetch and push to describe the mapping between remote ref and local ref. They are combined with a colon in - the format :, preceded by an optional plus sign, +. - For example: git fetch $URL refs/heads/master:refs/heads/origin means + the format ``:``, preceded by an optional plus sign, ``+``. + For example: ``git fetch $URL refs/heads/master:refs/heads/origin`` means "grab the master branch head from the $URL and store it as my origin - branch head". And git push $URL refs/heads/master:refs/heads/to-upstream + branch head". And ``git push $URL refs/heads/master:refs/heads/to-upstream`` means "publish my master branch head as to-upstream branch at $URL". See also git-push(1). - Taken from the git manual + Taken from the git manual, gitglossary(7). Fetch supports multiple refspecs (as the underlying git-fetch does) - supplying a list rather than a string for 'refspec' will make use of this facility. - :param progress: See 'push' method - :param verbose: Boolean for verbose output + + :param progress: See :meth:`push` method. + + :param verbose: Boolean for verbose output. + :param kill_after_timeout: To specify a timeout in seconds for the git command, after which the process should be killed. It is set to None by default. - :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext - :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack - :param kwargs: Additional arguments to be passed to git-fetch + + :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext. + + :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack. + + :param kwargs: Additional arguments to be passed to git-fetch. + :return: IterableList(FetchInfo, ...) list of FetchInfo instances providing detailed information about the fetch results :note: As fetch does not provide progress information to non-ttys, we cannot make - it available here unfortunately as in the 'push' method.""" + it available here unfortunately as in the :meth:`push` method. + """ if refspec is None: # No argument refspec, then ensure the repo's config has a fetch refspec. self._assert_refspec() @@ -1028,13 +1043,14 @@ def pull( """Pull changes from the given branch, being the same as a fetch followed by a merge of branch with your local branch. - :param refspec: see :meth:`fetch` method - :param progress: see :meth:`push` method - :param kill_after_timeout: see :meth:`fetch` method + :param refspec: See :meth:`fetch` method + :param progress: See :meth:`push` method + :param kill_after_timeout: See :meth:`fetch` method :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack :param kwargs: Additional arguments to be passed to git-pull - :return: Please see :meth:`fetch` method""" + :return: Please see :meth:`fetch` method + """ if refspec is None: # No argument refspec, then ensure the repo's config has a fetch refspec. self._assert_refspec() @@ -1067,33 +1083,42 @@ def push( ) -> PushInfoList: """Push changes from source branch in refspec to target branch in refspec. - :param refspec: see 'fetch' method + :param refspec: See :meth:`fetch` method. + :param progress: Can take one of many value types: - * None to discard progress information + * None to discard progress information. * A function (callable) that is called with the progress information. Signature: ``progress(op_code, cur_count, max_count=None, message='')``. `Click here `__ for a description of all arguments given to the function. - * An instance of a class derived from ``git.RemoteProgress`` that - overrides the ``update()`` function. + * An instance of a class derived from :class:`git.RemoteProgress` that + overrides the :meth:`update ` method. :note: No further progress information is returned after push returns. + :param kill_after_timeout: To specify a timeout in seconds for the git command, after which the process should be killed. It is set to None by default. - :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext - :param allow_unsafe_options: Allow unsafe options to be used, like --receive-pack - :param kwargs: Additional arguments to be passed to git-push + + :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext. + + :param allow_unsafe_options: + Allow unsafe options to be used, like --receive-pack. + + :param kwargs: Additional arguments to be passed to git-push. + :return: - A ``PushInfoList`` object, where each list member + A :class:`PushInfoList` object, where each list member represents an individual head which had been updated on the remote side. - If the push contains rejected heads, these will have the PushInfo.ERROR bit set - in their flags. + If the push contains rejected heads, these will have the + :attr:`PushInfo.ERROR` bit set in their flags. If the operation fails completely, the length of the returned PushInfoList will be 0. - Call ``.raise_if_error()`` on the returned object to raise on any failure.""" + Call :meth:`raise_if_error ` on the returned + object to raise on any failure. + """ kwargs = add_progress(kwargs, self.repo.git, progress) refspec = Git._unpack_args(refspec or []) @@ -1121,7 +1146,8 @@ def config_reader(self) -> SectionConstraint[GitConfigParser]: """ :return: GitConfigParser compatible object able to read options for only our remote. - Hence you may simple type config.get("pushurl") to obtain the information""" + Hence you may simple type config.get("pushurl") to obtain the information. + """ return self._config_reader def _clear_cache(self) -> None: @@ -1135,15 +1161,17 @@ def _clear_cache(self) -> None: def config_writer(self) -> SectionConstraint: """ :return: GitConfigParser compatible object able to write options for this remote. + :note: You can only own one writer at a time - delete it to release the configuration file and make it usable by others. To assure consistent results, you should only query options through the writer. Once you are done writing, you are free to use the config reader - once again.""" + once again. + """ writer = self.repo.config_writer() - # clear our cache to assure we re-read the possibly changed configuration + # Clear our cache to ensure we re-read the possibly changed configuration. self._clear_cache() return SectionConstraint(writer, self._config_section_name()) diff --git a/git/repo/__init__.py b/git/repo/__init__.py index 23c18db85..f1eac3311 100644 --- a/git/repo/__init__.py +++ b/git/repo/__init__.py @@ -1,3 +1,5 @@ -"""Initialize the Repo package""" +"""Initialize the Repo package.""" + # flake8: noqa + from .base import Repo as Repo diff --git a/git/repo/base.py b/git/repo/base.py index 23136a1d1..da81698d8 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -3,7 +3,9 @@ # # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ + from __future__ import annotations + import logging import os import re @@ -164,10 +166,10 @@ def __init__( search_parent_directories: bool = False, expand_vars: bool = True, ) -> None: - """Create a new Repo instance + """Create a new Repo instance. :param path: - the path to either the root git directory or the bare git repo:: + The path to either the root git directory or the bare git repo:: repo = Repo("/Users/mtrier/Development/git-python") repo = Repo("/Users/mtrier/Development/git-python.git") @@ -175,21 +177,26 @@ def __init__( repo = Repo("$REPOSITORIES/Development/git-python.git") repo = Repo("C:\\Users\\mtrier\\Development\\git-python\\.git") - - In *Cygwin*, path may be a `'cygdrive/...'` prefixed path. - - If it evaluates to false, :envvar:`GIT_DIR` is used, and if this also evals to false, - the current-directory is used. + - In *Cygwin*, path may be a ``cygdrive/...`` prefixed path. + - If it evaluates to false, :envvar:`GIT_DIR` is used, and if this also + evals to false, the current-directory is used. + :param odbt: Object DataBase type - a type which is constructed by providing the directory containing the database objects, i.e. .git/objects. It will be used to access all object data + :param search_parent_directories: - if True, all parent directories will be searched for a valid repo as well. + If True, all parent directories will be searched for a valid repo as well. + + Please note that this was the default behaviour in older versions of + GitPython, which is considered a bug though. - Please note that this was the default behaviour in older versions of GitPython, - which is considered a bug though. :raise InvalidGitRepositoryError: :raise NoSuchPathError: - :return: git.Repo""" + + :return: git.Repo + """ epath = path or os.getenv("GIT_DIR") if not epath: @@ -271,7 +278,7 @@ def __init__( try: self._bare = self.config_reader("repository").getboolean("core", "bare") except Exception: - # lets not assume the option exists, although it should + # Let's not assume the option exists, although it should. pass try: @@ -280,8 +287,8 @@ def __init__( except OSError: self._common_dir = "" - # adjust the wd in case we are actually bare - we didn't know that - # in the first place + # Adjust the working directory in case we are actually bare - we didn't know + # that in the first place. if self._bare: self._working_tree_dir = None # END working dir handling @@ -289,7 +296,7 @@ def __init__( self.working_dir: PathLike = self._working_tree_dir or self.common_dir self.git = self.GitCommandWrapperType(self.working_dir) - # special handling, in special times + # Special handling, in special times. rootpath = osp.join(self.common_dir, "objects") if issubclass(odbt, GitCmdObjectDB): self.odb = odbt(rootpath, self.git) @@ -311,12 +318,10 @@ def __del__(self) -> None: def close(self) -> None: if self.git: self.git.clear_cache() - # Tempfiles objects on Windows are holding references to - # open files until they are collected by the garbage - # collector, thus preventing deletion. - # TODO: Find these references and ensure they are closed - # and deleted synchronously rather than forcing a gc - # collection. + # Tempfiles objects on Windows are holding references to open files until + # they are collected by the garbage collector, thus preventing deletion. + # TODO: Find these references and ensure they are closed and deleted + # synchronously rather than forcing a gc collection. if is_win: gc.collect() gitdb.util.mman.collect() @@ -351,14 +356,18 @@ def _set_description(self, descr: str) -> None: @property def working_tree_dir(self) -> Optional[PathLike]: - """:return: The working tree directory of our git repository. If this is a bare repository, None is returned.""" + """ + :return: The working tree directory of our git repository. + If this is a bare repository, None is returned. + """ return self._working_tree_dir @property def common_dir(self) -> PathLike: """ :return: The git dir that holds everything except possibly HEAD, - FETCH_HEAD, ORIG_HEAD, COMMIT_EDITMSG, index, and logs/.""" + FETCH_HEAD, ORIG_HEAD, COMMIT_EDITMSG, index, and logs/. + """ return self._common_dir or self.git_dir @property @@ -368,30 +377,36 @@ def bare(self) -> bool: @property def heads(self) -> "IterableList[Head]": - """A list of ``Head`` objects representing the branch heads in - this repo + """A list of ``Head`` objects representing the branch heads in this repo. - :return: ``git.IterableList(Head, ...)``""" + :return: ``git.IterableList(Head, ...)`` + """ return Head.list_items(self) @property def references(self) -> "IterableList[Reference]": """A list of Reference objects representing tags, heads and remote references. - :return: IterableList(Reference, ...)""" + :return: IterableList(Reference, ...) + """ return Reference.list_items(self) - # alias for references + # Alias for references. refs = references - # alias for heads + # Alias for heads. branches = heads @property def index(self) -> "IndexFile": - """:return: IndexFile representing this repository's index. - :note: This property can be expensive, as the returned ``IndexFile`` will be - reinitialized. It's recommended to re-use the object.""" + """ + :return: :class:`IndexFile ` representing this + repository's index. + + :note: This property can be expensive, as the returned + :class:`IndexFile ` will be reinitialized. + It is recommended to reuse the object. + """ return IndexFile(self) @property @@ -401,14 +416,17 @@ def head(self) -> "HEAD": @property def remotes(self) -> "IterableList[Remote]": - """A list of Remote objects allowing to access and manipulate remotes + """A list of Remote objects allowing to access and manipulate remotes. - :return: ``git.IterableList(Remote, ...)``""" + :return: ``git.IterableList(Remote, ...)`` + """ return Remote.list_items(self) def remote(self, name: str = "origin") -> "Remote": """:return: Remote with the specified name - :raise ValueError: if no remote with such a name exists""" + + :raise ValueError: If no remote with such a name exists + """ r = Remote(self, name) if not r.exists(): raise ValueError("Remote named '%s' didn't exist" % name) @@ -420,12 +438,15 @@ def remote(self, name: str = "origin") -> "Remote": def submodules(self) -> "IterableList[Submodule]": """ :return: git.IterableList(Submodule, ...) of direct submodules - available from the current head""" + available from the current head + """ return Submodule.list_items(self) def submodule(self, name: str) -> "Submodule": """:return: Submodule with the given name - :raise ValueError: If no such submodule exists""" + + :raise ValueError: If no such submodule exists + """ try: return self.submodules[name] except IndexError as e: @@ -433,38 +454,47 @@ def submodule(self, name: str) -> "Submodule": # END exception handling def create_submodule(self, *args: Any, **kwargs: Any) -> Submodule: - """Create a new submodule + """Create a new submodule. :note: See the documentation of Submodule.add for a description of the - applicable parameters - :return: created submodules""" + applicable parameters. + + :return: The created submodules. + """ return Submodule.add(self, *args, **kwargs) def iter_submodules(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]: """An iterator yielding Submodule instances, see Traversable interface - for a description of args and kwargs + for a description of args and kwargs. - :return: Iterator""" + :return: Iterator + """ return RootModule(self).traverse(*args, **kwargs) def submodule_update(self, *args: Any, **kwargs: Any) -> Iterator[Submodule]: """Update the submodules, keeping the repository consistent as it will - take the previous state into consideration. For more information, please - see the documentation of RootModule.update""" + take the previous state into consideration. + + :note: For more information, please see the documentation of + :meth:`RootModule.update `. + """ return RootModule(self).update(*args, **kwargs) # }END submodules @property def tags(self) -> "IterableList[TagReference]": - """A list of ``Tag`` objects that are available in this repo + """A list of ``Tag`` objects that are available in this repo. - :return: ``git.IterableList(TagReference, ...)``""" + :return: ``git.IterableList(TagReference, ...)`` + """ return TagReference.list_items(self) def tag(self, path: PathLike) -> TagReference: """:return: TagReference Object, reference pointing to a Commit or Tag - :param path: path to the tag reference, i.e. 0.1.5 or tags/0.1.5""" + + :param path: path to the tag reference, i.e. 0.1.5 or tags/0.1.5 + """ full_path = self._to_full_tag_path(path) return TagReference(self, full_path) @@ -486,15 +516,19 @@ def create_head( logmsg: Optional[str] = None, ) -> "Head": """Create a new head within the repository. - For more documentation, please see the Head.create method. - :return: newly created Head Reference""" + :note: For more documentation, please see the + :meth:`Head.create ` method. + + :return: Newly created :class:`Head ` Reference + """ return Head.create(self, path, commit, logmsg, force) def delete_head(self, *heads: "Union[str, Head]", **kwargs: Any) -> None: - """Delete the given heads + """Delete the given heads. - :param kwargs: Additional keyword arguments to be passed to git-branch""" + :param kwargs: Additional keyword arguments to be passed to git-branch + """ return Head.delete(self, *heads, **kwargs) def create_tag( @@ -506,9 +540,12 @@ def create_tag( **kwargs: Any, ) -> TagReference: """Create a new tag reference. - For more documentation, please see the TagReference.create method. - :return: TagReference object""" + :note: For more documentation, please see the + :meth:`TagReference.create ` method. + + :return: :class:`TagReference ` object + """ return TagReference.create(self, path, ref, message, force, **kwargs) def delete_tag(self, *tags: TagReference) -> None: @@ -518,10 +555,11 @@ def delete_tag(self, *tags: TagReference) -> None: def create_remote(self, name: str, url: str, **kwargs: Any) -> Remote: """Create a new remote. - For more information, please see the documentation of the Remote.create - methods + For more information, please see the documentation of the + :meth:`Remote.create ` method. - :return: Remote reference""" + :return: :class:`Remote ` reference + """ return Remote.create(self, name, url, **kwargs) def delete_remote(self, remote: "Remote") -> str: @@ -531,8 +569,8 @@ def delete_remote(self, remote: "Remote") -> str: def _get_config_path(self, config_level: Lit_config_levels, git_dir: Optional[PathLike] = None) -> str: if git_dir is None: git_dir = self.git_dir - # we do not support an absolute path of the gitconfig on windows , - # use the global config instead + # We do not support an absolute path of the gitconfig on Windows. + # Use the global config instead. if is_win and config_level == "system": config_level = "global" @@ -561,17 +599,20 @@ def config_reader( ) -> GitConfigParser: """ :return: - GitConfigParser allowing to read the full git configuration, but not to write it + :class:`GitConfigParser ` allowing to read the + full git configuration, but not to write it. The configuration will include values from the system, user and repository configuration files. :param config_level: - For possible values, see config_writer method - If None, all applicable levels will be used. Specify a level in case - you know which file you wish to read to prevent reading multiple files. - :note: On windows, system configuration cannot currently be read as the path is - unknown, instead the global path will be used.""" + For possible values, see the :meth:`config_writer` method. If None, all + applicable levels will be used. Specify a level in case you know which file + you wish to read to prevent reading multiple files. + + :note: On Windows, system configuration cannot currently be read as the path is + unknown, instead the global path will be used. + """ return self._config_reader(config_level=config_level) def _config_reader( @@ -592,23 +633,26 @@ def _config_reader( def config_writer(self, config_level: Lit_config_levels = "repository") -> GitConfigParser: """ :return: - GitConfigParser allowing to write values of the specified configuration file level. - Config writers should be retrieved, used to change the configuration, and written - right away as they will lock the configuration file in question and prevent other's - to write it. + A :class:`GitConfigParser ` allowing to write + values of the specified configuration file level. Config writers should be + retrieved, used to change the configuration, and written right away as they + will lock the configuration file in question and prevent other's to write + it. :param config_level: - One of the following values - system = system wide configuration file - global = user level configuration file - repository = configuration file for this repository only""" + One of the following values: + + * ``"system"`` = system wide configuration file + * ``"global"`` = user level configuration file + * ``"`repository"`` = configuration file for this repository only + """ return GitConfigParser(self._get_config_path(config_level), read_only=False, repo=self, merge_includes=False) def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit: - """The Commit object for the specified revision + """The Commit object for the specified revision. :param rev: revision specifier, see git-rev-parse for viable options. - :return: ``git.Commit`` + :return: :class:`git.Commit ` """ if rev is None: return self.head.commit @@ -616,22 +660,27 @@ def commit(self, rev: Union[str, Commit_ish, None] = None) -> Commit: def iter_trees(self, *args: Any, **kwargs: Any) -> Iterator["Tree"]: """:return: Iterator yielding Tree objects - :note: Takes all arguments known to iter_commits method""" + + :note: Accepts all arguments known to the :meth:`iter_commits` method. + """ return (c.tree for c in self.iter_commits(*args, **kwargs)) def tree(self, rev: Union[Tree_ish, str, None] = None) -> "Tree": - """The Tree object for the given treeish revision + """The Tree object for the given tree-ish revision. + Examples:: repo.tree(repo.heads[0]) - :param rev: is a revision pointing to a Treeish ( being a commit or tree ) + :param rev: is a revision pointing to a Treeish (being a commit or tree) + :return: ``git.Tree`` :note: If you need a non-root level tree, find it by iterating the root tree. Otherwise it cannot know about its path relative to the repository root and subsequent - operations might have unexpected results.""" + operations might have unexpected results. + """ if rev is None: return self.head.commit.tree return self.rev_parse(str(rev) + "^{tree}") @@ -642,37 +691,40 @@ def iter_commits( paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any, ) -> Iterator[Commit]: - """A list of Commit objects representing the history of a given ref/commit + """A list of Commit objects representing the history of a given ref/commit. :param rev: - revision specifier, see git-rev-parse for viable options. + Revision specifier, see git-rev-parse for viable options. If None, the active branch will be used. :param paths: - is an optional path or a list of paths; if set only commits that include the path - or paths will be returned + An optional path or a list of paths; if set only commits that include the + path or paths will be returned :param kwargs: - Arguments to be passed to git-rev-list - common ones are - max_count and skip + Arguments to be passed to git-rev-list - common ones are max_count and skip. - :note: to receive only commits between two named revisions, use the - "revA...revB" revision specifier + :note: To receive only commits between two named revisions, use the + ``"revA...revB"`` revision specifier. - :return: ``git.Commit[]``""" + :return: ``git.Commit[]`` + """ if rev is None: rev = self.head.commit return Commit.iter_items(self, rev, paths, **kwargs) def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]: - """Find the closest common ancestor for the given revision (e.g. Commits, Tags, References, etc) + """Find the closest common ancestor for the given revision (Commits, Tags, References, etc.). :param rev: At least two revs to find the common ancestor for. - :param kwargs: Additional arguments to be passed to the repo.git.merge_base() command which does all the work. - :return: A list of Commit objects. If --all was not specified as kwarg, the list will have at max one Commit, - or is empty if no common merge base exists. - :raises ValueError: If not at least two revs are provided + :param kwargs: Additional arguments to be passed to the + ``repo.git.merge_base()`` command which does all the work. + :return: A list of :class:`Commit ` objects. If + ``--all`` was not passed as a keyword argument, the list will have at max + one :class:`Commit `, or is empty if no common + merge base exists. + :raises ValueError: If not at least two revs are provided. """ if len(rev) < 2: raise ValueError("Please specify at least two revs, got only %i" % len(rev)) @@ -697,7 +749,7 @@ def merge_base(self, *rev: TBD, **kwargs: Any) -> List[Union[Commit_ish, None]]: return res def is_ancestor(self, ancestor_rev: "Commit", rev: "Commit") -> bool: - """Check if a commit is an ancestor of another + """Check if a commit is an ancestor of another. :param ancestor_rev: Rev which should be an ancestor :param rev: Rev to test against ancestor_rev @@ -754,9 +806,10 @@ def _set_daemon_export(self, value: object) -> None: del _set_daemon_export def _get_alternates(self) -> List[str]: - """The list of alternates for this repo from which objects can be retrieved + """The list of alternates for this repo from which objects can be retrieved. - :return: list of strings being pathnames of alternates""" + :return: List of strings being pathnames of alternates + """ if self.git_dir: alternates_path = osp.join(self.git_dir, "objects", "info", "alternates") @@ -767,16 +820,18 @@ def _get_alternates(self) -> List[str]: return [] def _set_alternates(self, alts: List[str]) -> None: - """Sets the alternates + """Sets the alternates. :param alts: is the array of string paths representing the alternates at which git should look for objects, i.e. /home/user/repo/.git/objects :raise NoSuchPathError: + :note: The method does not check for the existence of the paths in alts - as the caller is responsible.""" + as the caller is responsible. + """ alternates_path = osp.join(self.common_dir, "objects", "info", "alternates") if not alts: if osp.isfile(alternates_path): @@ -801,27 +856,28 @@ def is_dirty( ) -> bool: """ :return: - ``True``, the repository is considered dirty. By default it will react + ``True`` if the repository is considered dirty. By default it will react like a git-status without untracked files, hence it is dirty if the - index or the working copy have changes.""" + index or the working copy have changes. + """ if self._bare: # Bare repositories with no associated working directory are # always considered to be clean. return False - # start from the one which is fastest to evaluate + # Start from the one which is fastest to evaluate. default_args = ["--abbrev=40", "--full-index", "--raw"] if not submodules: default_args.append("--ignore-submodules") if path: default_args.extend(["--", str(path)]) if index: - # diff index against HEAD + # diff index against HEAD. if osp.isfile(self.index.path) and len(self.git.diff("--cached", *default_args)): return True # END index handling if working_tree: - # diff index against working tree + # diff index against working tree. if len(self.git.diff(*default_args)): return True # END working tree handling @@ -841,14 +897,15 @@ def untracked_files(self) -> List[str]: are relative to the current working directory of the git command. :note: - ignored files will not appear here, i.e. files mentioned in .gitignore + Ignored files will not appear here, i.e. files mentioned in ``.gitignore``. :note: - This property is expensive, as no cache is involved. To process the result, please - consider caching it yourself.""" + This property is expensive, as no cache is involved. To process the result, + please consider caching it yourself. + """ return self._get_untracked_files() def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]: - # make sure we get all files, not only untracked directories + # Make sure we get all files, not only untracked directories. proc = self.git.status(*args, porcelain=True, untracked_files=True, as_process=True, **kwargs) # Untracked files prefix in porcelain mode prefix = "?? " @@ -868,11 +925,13 @@ def _get_untracked_files(self, *args: Any, **kwargs: Any) -> List[str]: return untracked_files def ignored(self, *paths: PathLike) -> List[str]: - """Checks if paths are ignored via .gitignore - Doing so using the "git check-ignore" method. + """Checks if paths are ignored via .gitignore. + + This does so using the ``git check-ignore`` method. :param paths: List of paths to check whether they are ignored or not - :return: subset of those paths which are ignored + + :return: Subset of those paths which are ignored """ try: proc: str = self.git.check_ignore(*paths) @@ -892,23 +951,25 @@ def active_branch(self) -> Head: """The name of the currently active branch. :raises TypeError: If HEAD is detached - :return: Head to the active branch""" + :return: Head to the active branch + """ # reveal_type(self.head.reference) # => Reference return self.head.reference def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterator["BlameEntry"]: """Iterator for blame information for the given file at the given revision. - Unlike .blame(), this does not return the actual file's contents, only - a stream of BlameEntry tuples. + Unlike :meth:`blame`, this does not return the actual file's contents, only a + stream of :class:`BlameEntry` tuples. - :param rev: revision specifier, see git-rev-parse for viable options. - :return: lazy iterator of BlameEntry tuples, where the commit - indicates the commit to blame for the line, and range - indicates a span of line numbers in the resulting file. + :param rev: Revision specifier, see git-rev-parse for viable options. - If you combine all line number ranges outputted by this command, you - should get a continuous range spanning all line numbers in the file. + :return: Lazy iterator of :class:`BlameEntry` tuples, where the commit indicates + the commit to blame for the line, and range indicates a span of line numbers + in the resulting file. + + If you combine all line number ranges outputted by this command, you should get + a continuous range spanning all line numbers in the file. """ data: bytes = self.git.blame(rev, "--", file, p=True, incremental=True, stdout_as_string=False, **kwargs) @@ -917,7 +978,7 @@ def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterat stream = (line for line in data.split(b"\n") if line) while True: try: - line = next(stream) # when exhausted, causes a StopIteration, terminating this function + line = next(stream) # When exhausted, causes a StopIteration, terminating this function. except StopIteration: return split_line = line.split() @@ -927,7 +988,7 @@ def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterat orig_lineno = int(orig_lineno_b) if hexsha not in commits: # Now read the next few lines and build up a dict of properties - # for this commit + # for this commit. props: Dict[bytes, bytes] = {} while True: try: @@ -936,13 +997,13 @@ def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterat return if line == b"boundary": # "boundary" indicates a root commit and occurs - # instead of the "previous" tag + # instead of the "previous" tag. continue tag, value = line.split(b" ", 1) props[tag] = value if tag == b"filename": - # "filename" formally terminates the entry for --incremental + # "filename" formally terminates the entry for --incremental. orig_filename = value break @@ -963,10 +1024,10 @@ def blame_incremental(self, rev: str | HEAD, file: str, **kwargs: Any) -> Iterat commits[hexsha] = c else: # Discard all lines until we find "filename" which is - # guaranteed to be the last line + # guaranteed to be the last line. while True: try: - line = next(stream) # will fail if we reach the EOF unexpectedly + line = next(stream) # Will fail if we reach the EOF unexpectedly. except StopIteration: return tag, value = line.split(b" ", 1) @@ -991,12 +1052,15 @@ def blame( ) -> List[List[Commit | List[str | bytes] | None]] | Iterator[BlameEntry] | None: """The blame information for the given file at the given revision. - :param rev: revision specifier, see git-rev-parse for viable options. + :param rev: Revision specifier, see git-rev-parse for viable options. + :return: list: [git.Commit, list: []] + A list of lists associating a Commit object with a list of lines that changed within the given commit. The Commit objects will be given in order - of appearance.""" + of appearance. + """ if incremental: return self.blame_incremental(rev, file, **kwargs) rev_opts = rev_opts or [] @@ -1028,9 +1092,9 @@ class InfoTD(TypedDict, total=False): is_binary = True else: # As we don't have an idea when the binary data ends, as it could contain multiple newlines - # in the process. So we rely on being able to decode to tell us what is is. + # in the process. So we rely on being able to decode to tell us what it is. # This can absolutely fail even on text files, but even if it does, we should be fine treating it - # as binary instead + # as binary instead. parts = self.re_whitespace.split(line_str, 1) firstpart = parts[0] is_binary = False @@ -1133,32 +1197,32 @@ def init( expand_vars: bool = True, **kwargs: Any, ) -> "Repo": - """Initialize a git repository at the given path if specified + """Initialize a git repository at the given path if specified. :param path: - is the full path to the repo (traditionally ends with /.git) - or None in which case the repository will be created in the current - working directory + The full path to the repo (traditionally ends with /.git) or None in + which case the repository will be created in the current working directory :param mkdir: - if specified will create the repository directory if it doesn't - already exists. Creates the directory with a mode=0755. - Only effective if a path is explicitly given + If specified, will create the repository directory if it doesn't + already exist. Creates the directory with a mode=0755. + Only effective if a path is explicitly given. :param odbt: Object DataBase type - a type which is constructed by providing the directory containing the database objects, i.e. .git/objects. - It will be used to access all object data + It will be used to access all object data. :param expand_vars: - if specified, environment variables will not be escaped. This + If specified, environment variables will not be escaped. This can lead to information disclosure, allowing attackers to - access the contents of environment variables + access the contents of environment variables. :param kwargs: - keyword arguments serving as additional options to the git-init command + Keyword arguments serving as additional options to the git-init command. - :return: ``git.Repo`` (the newly created repo)""" + :return: ``git.Repo`` (the newly created repo) + """ if path: path = expand_path(path, expand_vars) if mkdir and path and not osp.exists(path): @@ -1184,7 +1248,7 @@ def _clone( ) -> "Repo": odbt = kwargs.pop("odbt", odb_default_type) - # when pathlib.Path or other classbased path is passed + # When pathlib.Path or other classbased path is passed if not isinstance(path, str): path = str(path) @@ -1236,21 +1300,21 @@ def _clone( log.debug("Cmd(%s)'s unused stdout: %s", cmdline, stdout) finalize_process(proc, stderr=stderr) - # our git command could have a different working dir than our actual - # environment, hence we prepend its working dir if required + # Our git command could have a different working dir than our actual + # environment, hence we prepend its working dir if required. if not osp.isabs(path): path = osp.join(git._working_dir, path) if git._working_dir is not None else path repo = cls(path, odbt=odbt) - # retain env values that were passed to _clone() + # Retain env values that were passed to _clone(). repo.git.update_environment(**git.environment()) - # adjust remotes - there may be operating systems which use backslashes, + # Adjust remotes - there may be operating systems which use backslashes, # These might be given as initial paths, but when handling the config file # that contains the remote from which we were clones, git stops liking it # as it will escape the backslashes. Hence we undo the escaping just to be - # sure + # sure. if repo.remotes: with repo.remotes[0].config_writer as writer: writer.set_value("url", Git.polish_url(repo.remotes[0].url)) @@ -1268,20 +1332,22 @@ def clone( ) -> "Repo": """Create a clone from this repository. - :param path: is the full path of the new repo (traditionally ends with ./.git). - :param progress: See 'git.remote.Remote.push'. - :param multi_options: A list of Clone options that can be provided multiple times. One - option per list item which is passed exactly as specified to clone. - For example ['--config core.filemode=false', '--config core.ignorecase', + :param path: The full path of the new repo (traditionally ends with + ``./.git``). + :param progress: See :meth:`git.remote.Remote.push`. + :param multi_options: A list of Clone options that can be provided multiple times. + One option per list item which is passed exactly as specified to clone. + For example: ['--config core.filemode=false', '--config core.ignorecase', '--recurse-submodule=repo1_path', '--recurse-submodule=repo2_path'] - :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext - :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack + :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext. + :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack. :param kwargs: * odbt = ObjectDatabase Type, allowing to determine the object database - implementation used by the returned Repo instance - * All remaining keyword arguments are given to the git-clone command + implementation used by the returned Repo instance. + * All remaining keyword arguments are given to the git-clone command. - :return: ``git.Repo`` (the newly cloned repo)""" + :return: :class:`Repo` (the newly cloned repo) + """ return self._clone( self.git, self.common_dir, @@ -1306,22 +1372,32 @@ def clone_from( allow_unsafe_options: bool = False, **kwargs: Any, ) -> "Repo": - """Create a clone from the given URL + """Create a clone from the given URL. + + :param url: Valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS + + :param to_path: Path to which the repository should be cloned to. + + :param progress: See :meth:`git.remote.Remote.push`. - :param url: valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS - :param to_path: Path to which the repository should be cloned to - :param progress: See 'git.remote.Remote.push'. :param env: Optional dictionary containing the desired environment variables. + Note: Provided variables will be used to update the execution environment for `git`. If some variable is not specified in `env` and is defined in `os.environ`, value from `os.environ` will be used. If you want to unset some variable, consider providing empty string as its value. - :param multi_options: See ``clone`` method - :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext - :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack - :param kwargs: see the ``clone`` method - :return: Repo instance pointing to the cloned directory""" + + :param multi_options: See :meth:`clone` method. + + :param allow_unsafe_protocols: Allow unsafe protocols to be used, like ext. + + :param allow_unsafe_options: Allow unsafe options to be used, like --upload-pack. + + :param kwargs: See the :meth:`clone` method. + + :return: :class:`Repo` instance pointing to the cloned directory. + """ git = cls.GitCommandWrapperType(os.getcwd()) if env is not None: git.update_environment(**env) @@ -1346,18 +1422,23 @@ def archive( ) -> Repo: """Archive the tree at the given revision. - :param ostream: file compatible stream object to which the archive will be written as bytes - :param treeish: is the treeish name/id, defaults to active branch - :param prefix: is the optional prefix to prepend to each filename in the archive - :param kwargs: Additional arguments passed to git-archive + :param ostream: file compatible stream object to which the archive will be written as bytes. + + :param treeish: is the treeish name/id, defaults to active branch. + + :param prefix: is the optional prefix to prepend to each filename in the archive. + + :param kwargs: Additional arguments passed to git-archive: * Use the 'format' argument to define the kind of format. Use specialized ostreams to write any format supported by python. * You may specify the special **path** keyword, which may either be a repository-relative path to a directory or file to place into the archive, or a list or tuple of multiple paths. - :raise GitCommandError: in case something went wrong - :return: self""" + :raise GitCommandError: If something went wrong. + + :return: self + """ if treeish is None: treeish = self.head.commit if prefix and "prefix" not in kwargs: @@ -1374,7 +1455,8 @@ def archive( def has_separate_working_tree(self) -> bool: """ :return: True if our git_dir is not at the root of our working_tree_dir, but a .git file with a - platform agnositic symbolic link. Our git_dir will be wherever the .git file points to + platform agnositic symbolic link. Our git_dir will be wherever the .git file points to. + :note: bare repositories will always return False here """ if self.bare: @@ -1382,7 +1464,7 @@ def has_separate_working_tree(self) -> bool: if self.working_tree_dir: return osp.isfile(osp.join(self.working_tree_dir, ".git")) else: - return False # or raise Error? + return False # Or raise Error? rev_parse = rev_parse @@ -1394,7 +1476,7 @@ def currently_rebasing_on(self) -> Commit | None: """ :return: The commit which is currently being replayed while rebasing. - None if we are not currently rebasing. + None if we are not currently rebasing. """ if self.git_dir: rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD") diff --git a/git/repo/fun.py b/git/repo/fun.py index ae35aa81e..f0bb4cd1f 100644 --- a/git/repo/fun.py +++ b/git/repo/fun.py @@ -1,5 +1,7 @@ -"""Package with general repository related functions""" +"""Module with general repository-related functions.""" + from __future__ import annotations + import os import stat from pathlib import Path diff --git a/git/types.py b/git/types.py index 21276b5f1..22bb91c16 100644 --- a/git/types.py +++ b/git/types.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # This module is part of GitPython and is released under # the BSD License: https://opensource.org/license/bsd-3-clause/ + # flake8: noqa import os @@ -75,9 +76,11 @@ def assert_never(inp: NoReturn, raise_error: bool = True, exc: Union[Exception, None] = None) -> None: """For use in exhaustive checking of literal or Enum in if/else chain. + Should only be reached if all members not handled OR attempt to pass non-members through chain. If all members handled, type is Empty. Otherwise, will cause mypy error. + If non-members given, should cause mypy error at variable creation. If raise_error is True, will also raise AssertionError or the Exception passed to exc. diff --git a/git/util.py b/git/util.py index ccadb5881..d6a20b3e3 100644 --- a/git/util.py +++ b/git/util.py @@ -144,7 +144,7 @@ def _read_env_flag(name: str, default: bool) -> bool: def unbare_repo(func: Callable[..., T]) -> Callable[..., T]: """Methods with this decorator raise :class:`.exc.InvalidGitRepositoryError` if they - encounter a bare repository""" + encounter a bare repository.""" from .exc import InvalidGitRepositoryError @@ -164,8 +164,9 @@ def wrapper(self: "Remote", *args: Any, **kwargs: Any) -> T: def cwd(new_dir: PathLike) -> Generator[PathLike, None, None]: """Context manager to temporarily change directory. - This is similar to contextlib.chdir introduced in Python 3.11, but the context - manager object returned by a single call to this function is not reentrant.""" + This is similar to :func:`contextlib.chdir` introduced in Python 3.11, but the + context manager object returned by a single call to this function is not reentrant. + """ old_dir = os.getcwd() os.chdir(new_dir) try: @@ -191,8 +192,10 @@ def patch_env(name: str, value: str) -> Generator[None, None, None]: def rmtree(path: PathLike) -> None: """Remove the given directory tree recursively. - :note: We use :func:`shutil.rmtree` but adjust its behaviour to see whether files that - couldn't be deleted are read-only. Windows will not remove them in that case.""" + :note: We use :func:`shutil.rmtree` but adjust its behaviour to see whether files + that couldn't be deleted are read-only. Windows will not remove them in that + case. + """ def handler(function: Callable, path: PathLike, _excinfo: Any) -> None: """Callback for :func:`shutil.rmtree`. Works either as ``onexc`` or ``onerror``.""" @@ -224,9 +227,10 @@ def rmfile(path: PathLike) -> None: def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 * 1024) -> int: """Copy all data from the source stream into the destination stream in chunks - of size chunk_size + of size chunk_size. - :return: amount of bytes written""" + :return: Number of bytes written + """ br = 0 while True: chunk = source.read(chunk_size) @@ -240,7 +244,7 @@ def stream_copy(source: BinaryIO, destination: BinaryIO, chunk_size: int = 512 * def join_path(a: PathLike, *p: PathLike) -> PathLike: R"""Join path tokens together similar to osp.join, but always use - '/' instead of possibly '\' on windows.""" + '/' instead of possibly '\' on Windows.""" path = str(a) for b in p: b = str(b) @@ -269,7 +273,7 @@ def to_native_path_linux(path: PathLike) -> str: __all__.append("to_native_path_windows") to_native_path = to_native_path_windows else: - # no need for any work on linux + # No need for any work on Linux. def to_native_path_linux(path: PathLike) -> str: return str(path) @@ -277,19 +281,22 @@ def to_native_path_linux(path: PathLike) -> str: def join_path_native(a: PathLike, *p: PathLike) -> PathLike: - R""" - As join path, but makes sure an OS native path is returned. This is only - needed to play it safe on my dear windows and to assure nice paths that only - use '\'""" + R"""Like join_path, but makes sure an OS native path is returned. + + This is only needed to play it safe on Windows and to assure nice paths that only + use '\'. + """ return to_native_path(join_path(a, *p)) def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool: - """Assure that the directory pointed to by path exists. + """Make sure that the directory pointed to by path exists. + + :param is_file: If True, ``path`` is assumed to be a file and handled correctly. + Otherwise it must be a directory. - :param is_file: If True, path is assumed to be a file and handled correctly. - Otherwise it must be a directory - :return: True if the directory was created, False if it already existed""" + :return: True if the directory was created, False if it already existed. + """ if is_file: path = osp.dirname(path) # END handle file @@ -365,9 +372,9 @@ def _cygexpath(drive: Optional[str], path: str) -> str: def cygpath(path: str) -> str: - """Use :meth:`git.cmd.Git.polish_url()` instead, that works on any environment.""" - path = str(path) # ensure is str and not AnyPath. - # Fix to use Paths when 3.5 dropped. or to be just str if only for urls? + """Use :meth:`git.cmd.Git.polish_url` instead, that works on any environment.""" + path = str(path) # Ensure is str and not AnyPath. + # Fix to use Paths when 3.5 dropped. Or to be just str if only for URLs? if not path.startswith(("/cygdrive", "//", "/proc/cygdrive")): for regex, parser, recurse in _cygpath_parsers: match = regex.match(path) @@ -477,11 +484,10 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]: - """ - Parse any command line argument and if on of the element is an URL with a + """Parse any command line argument and if one of the elements is an URL with a username and/or password, replace them by stars (in-place). - If nothing found just returns the command line as-is. + If nothing is found, this just returns the command line as-is. This should be used for every log line that print a command line, as well as exception messages. @@ -491,7 +497,7 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]: new_cmdline.append(to_parse) try: url = urlsplit(to_parse) - # Remove password from the URL if present + # Remove password from the URL if present. if url.password is None and url.username is None: continue @@ -501,7 +507,7 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]: url = url._replace(netloc=url.netloc.replace(url.username, "*****")) new_cmdline[index] = urlunsplit(url) except ValueError: - # This is not a valid URL + # This is not a valid URL. continue return new_cmdline @@ -555,14 +561,15 @@ def _parse_progress_line(self, line: AnyStr) -> None: or git-fetch. - Lines that do not contain progress info are stored in :attr:`other_lines`. - - Lines that seem to contain an error (i.e. start with error: or fatal:) are stored - in :attr:`error_lines`.""" + - Lines that seem to contain an error (i.e. start with ``error:`` or ``fatal:``) + are stored in :attr:`error_lines`. + """ # handle # Counting objects: 4, done. # Compressing objects: 50% (1/2) # Compressing objects: 100% (2/2) # Compressing objects: 100% (2/2), done. - if isinstance(line, bytes): # mypy argues about ternary assignment + if isinstance(line, bytes): # mypy argues about ternary assignment. line_str = line.decode("utf-8") else: line_str = line @@ -572,14 +579,14 @@ def _parse_progress_line(self, line: AnyStr) -> None: self.error_lines.append(self._cur_line) return - # find escape characters and cut them away - regex will not work with - # them as they are non-ascii. As git might expect a tty, it will send them + # Find escape characters and cut them away - regex will not work with + # them as they are non-ASCII. As git might expect a tty, it will send them. last_valid_index = None for i, c in enumerate(reversed(line_str)): if ord(c) < 32: # its a slice index last_valid_index = -i - 1 - # END character was non-ascii + # END character was non-ASCII # END for each character in line if last_valid_index is not None: line_str = line_str[:last_valid_index] @@ -600,7 +607,7 @@ def _parse_progress_line(self, line: AnyStr) -> None: op_code = 0 _remote, op_name, _percent, cur_count, max_count, message = match.groups() - # get operation id + # Get operation ID. if op_name == "Counting objects": op_code |= self.COUNTING elif op_name == "Compressing objects": @@ -616,7 +623,7 @@ def _parse_progress_line(self, line: AnyStr) -> None: elif op_name == "Checking out files": op_code |= self.CHECKING_OUT else: - # Note: On windows it can happen that partial lines are sent + # Note: On Windows it can happen that partial lines are sent. # Hence we get something like "CompreReceiving objects", which is # a blend of "Compressing objects" and "Receiving objects". # This can't really be prevented, so we drop the line verbosely @@ -624,11 +631,11 @@ def _parse_progress_line(self, line: AnyStr) -> None: # commands at some point. self.line_dropped(line_str) # Note: Don't add this line to the other lines, as we have to silently - # drop it + # drop it. return None # END handle op code - # figure out stage + # Figure out stage. if op_code not in self._seen_ops: self._seen_ops.append(op_code) op_code |= self.BEGIN @@ -655,8 +662,9 @@ def _parse_progress_line(self, line: AnyStr) -> None: def new_message_handler(self) -> Callable[[str], None]: """ :return: - a progress handler suitable for handle_process_output(), passing lines on to this Progress - handler in a suitable format""" + A progress handler suitable for handle_process_output(), passing lines on to + this Progress handler in a suitable format + """ def handler(line: AnyStr) -> None: return self._parse_progress_line(line.rstrip()) @@ -675,7 +683,7 @@ def update( max_count: Union[str, float, None] = None, message: str = "", ) -> None: - """Called whenever the progress changes + """Called whenever the progress changes. :param op_code: Integer allowing to be compared against Operation IDs and stage IDs. @@ -683,11 +691,12 @@ def update( Stage IDs are BEGIN and END. BEGIN will only be set once for each Operation ID as well as END. It may be that BEGIN and END are set at once in case only one progress message was emitted due to the speed of the operation. - Between BEGIN and END, none of these flags will be set + Between BEGIN and END, none of these flags will be set. Operation IDs are all held within the OP_MASK. Only one Operation ID will be active per call. - :param cur_count: Current absolute count of items + + :param cur_count: Current absolute count of items. :param max_count: The maximum count of items we expect. It may be None in case there is @@ -697,12 +706,13 @@ def update( In case of the 'WRITING' operation, it contains the amount of bytes transferred. It may possibly be used for other purposes as well. - You may read the contents of the current line in self._cur_line""" + You may read the contents of the current line in ``self._cur_line``. + """ pass class CallableRemoteProgress(RemoteProgress): - """An implementation forwarding updates to any callable""" + """An implementation forwarding updates to any callable.""" __slots__ = "_callable" @@ -724,7 +734,7 @@ class Actor(object): name_email_regex = re.compile(r"(.*) <(.*?)>") # ENVIRONMENT VARIABLES - # read when creating new commits + # These are read when creating new commits. env_author_name = "GIT_AUTHOR_NAME" env_author_email = "GIT_AUTHOR_EMAIL" env_committer_name = "GIT_COMMITTER_NAME" @@ -758,11 +768,13 @@ def __repr__(self) -> str: @classmethod def _from_string(cls, string: str) -> "Actor": """Create an Actor from a string. - :param string: is the string, which is expected to be in regular git format - John Doe + :param string: The string, which is expected to be in regular git format:: - :return: Actor""" + John Doe + + :return: Actor + """ m = cls.name_email_regex.search(string) if m: name, email = m.groups() @@ -771,7 +783,7 @@ def _from_string(cls, string: str) -> "Actor": m = cls.name_only_regex.search(string) if m: return Actor(m.group(1), None) - # assume best and use the whole string as name + # Assume the best and use the whole string as name. return Actor(string, None) # END special case name # END handle name/email matching @@ -784,7 +796,7 @@ def _main_actor( config_reader: Union[None, "GitConfigParser", "SectionConstraint"] = None, ) -> "Actor": actor = Actor("", "") - user_id = None # We use this to avoid multiple calls to getpass.getuser() + user_id = None # We use this to avoid multiple calls to getpass.getuser(). def default_email() -> str: nonlocal user_id @@ -822,20 +834,21 @@ def committer(cls, config_reader: Union[None, "GitConfigParser", "SectionConstra :return: Actor instance corresponding to the configured committer. It behaves similar to the git implementation, such that the environment will override configuration values of config_reader. If no value is set at all, it will be - generated + generated. + :param config_reader: ConfigReader to use to retrieve the values from in case - they are not set in the environment""" + they are not set in the environment. + """ return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader) @classmethod def author(cls, config_reader: Union[None, "GitConfigParser", "SectionConstraint"] = None) -> "Actor": - """Same as committer(), but defines the main author. It may be specified in the environment, - but defaults to the committer""" + """Same as committer(), but defines the main author. It may be specified in the + environment, but defaults to the committer.""" return cls._main_actor(cls.env_author_name, cls.env_author_email, config_reader) class Stats(object): - """ Represents stat information as presented by git at the end of a merge. It is created from the output of a diff operation. @@ -859,7 +872,8 @@ class Stats(object): In addition to the items in the stat-dict, it features additional information:: - files = number of changed files as int""" + files = number of changed files as int + """ __slots__ = ("total", "files") @@ -871,7 +885,8 @@ def __init__(self, total: Total_TD, files: Dict[PathLike, Files_TD]): def _list_from_string(cls, repo: "Repo", text: str) -> "Stats": """Create a Stat object from output retrieved by git-diff. - :return: git.Stat""" + :return: git.Stat + """ hsh: HSH_TD = { "total": {"insertions": 0, "deletions": 0, "lines": 0, "files": 0}, @@ -895,14 +910,14 @@ def _list_from_string(cls, repo: "Repo", text: str) -> "Stats": class IndexFileSHA1Writer(object): - """Wrapper around a file-like object that remembers the SHA1 of the data written to it. It will write a sha when the stream is closed or if the asked for explicitly using write_sha. - Only useful to the indexfile + Only useful to the index file. - :note: Based on the dulwich project""" + :note: Based on the dulwich project. + """ __slots__ = ("f", "sha1") @@ -929,13 +944,13 @@ def tell(self) -> int: class LockFile(object): - """Provides methods to obtain, check for, and release a file based lock which should be used to handle concurrent access to the same file. As we are a utility class to be derived from, we only use protected methods. - Locks will automatically be released on destruction""" + Locks will automatically be released on destruction. + """ __slots__ = ("_file_path", "_owns_lock") @@ -951,14 +966,18 @@ def _lock_file_path(self) -> str: return "%s.lock" % (self._file_path) def _has_lock(self) -> bool: - """:return: True if we have a lock and if the lockfile still exists - :raise AssertionError: if our lock-file does not exist""" + """ + :return: True if we have a lock and if the lockfile still exists + + :raise AssertionError: If our lock-file does not exist + """ return self._owns_lock def _obtain_lock_or_raise(self) -> None: - """Create a lock file as flag for other instances, mark our instance as lock-holder + """Create a lock file as flag for other instances, mark our instance as lock-holder. - :raise IOError: if a lock was already present or a lock file could not be written""" + :raise IOError: If a lock was already present or a lock file could not be written + """ if self._has_lock(): return lock_file = self._lock_file_path() @@ -978,15 +997,15 @@ def _obtain_lock_or_raise(self) -> None: def _obtain_lock(self) -> None: """The default implementation will raise if a lock cannot be obtained. - Subclasses may override this method to provide a different implementation""" + Subclasses may override this method to provide a different implementation.""" return self._obtain_lock_or_raise() def _release_lock(self) -> None: - """Release our lock if we have one""" + """Release our lock if we have one.""" if not self._has_lock(): return - # if someone removed our file beforhand, lets just flag this issue + # If someone removed our file beforehand, lets just flag this issue # instead of failing, to make it more usable. lfp = self._lock_file_path() try: @@ -997,13 +1016,13 @@ def _release_lock(self) -> None: class BlockingLockFile(LockFile): - """The lock file will block until a lock could be obtained, or fail after a specified timeout. :note: If the directory containing the lock was removed, an exception will be raised during the blocking period, preventing hangs as the lock - can never be obtained.""" + can never be obtained. + """ __slots__ = ("_check_interval", "_max_block_time") @@ -1013,13 +1032,14 @@ def __init__( check_interval_s: float = 0.3, max_block_time_s: int = sys.maxsize, ) -> None: - """Configure the instance + """Configure the instance. :param check_interval_s: Period of time to sleep until the lock is checked the next time. - By default, it waits a nearly unlimited time + By default, it waits a nearly unlimited time. - :param max_block_time_s: Maximum amount of seconds we may lock""" + :param max_block_time_s: Maximum amount of seconds we may lock. + """ super(BlockingLockFile, self).__init__(file_path) self._check_interval = check_interval_s self._max_block_time = max_block_time_s @@ -1027,7 +1047,9 @@ def __init__( def _obtain_lock(self) -> None: """This method blocks until it obtained the lock, or raises IOError if it ran out of time or if the parent directory was not available anymore. - If this method returns, you are guaranteed to own the lock""" + + If this method returns, you are guaranteed to own the lock. + """ starttime = time.time() maxtime = starttime + float(self._max_block_time) while True: @@ -1059,7 +1081,6 @@ def _obtain_lock(self) -> None: class IterableList(List[T_IterableObj]): - """ List of iterable objects allowing to query an object by id or by named index:: @@ -1070,13 +1091,14 @@ class IterableList(List[T_IterableObj]): Iterable parent objects = [Commit, SubModule, Reference, FetchInfo, PushInfo] Iterable via inheritance = [Head, TagReference, RemoteReference] - ] + It requires an id_attribute name to be set which will be queried from its contained items to have a means for comparison. A prefix can be specified which is to be used in case the id returned by the items always contains a prefix that does not matter to the user, so it - can be left out.""" + can be left out. + """ __slots__ = ("_id_attr", "_prefix") @@ -1088,7 +1110,7 @@ def __init__(self, id_attr: str, prefix: str = "") -> None: self._prefix = prefix def __contains__(self, attr: object) -> bool: - # first try identity match for performance + # First try identity match for performance. try: rval = list.__contains__(self, attr) if rval: @@ -1097,9 +1119,9 @@ def __contains__(self, attr: object) -> bool: pass # END handle match - # otherwise make a full name search + # Otherwise make a full name search. try: - getattr(self, cast(str, attr)) # use cast to silence mypy + getattr(self, cast(str, attr)) # Use cast to silence mypy. return True except (AttributeError, TypeError): return False @@ -1148,7 +1170,7 @@ def __delitem__(self, index: Union[SupportsIndex, int, slice, str]) -> None: class IterableClassWatcher(type): - """Metaclass that watches""" + """Metaclass that watches.""" def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None: for base in bases: @@ -1164,24 +1186,26 @@ def __init__(cls, name: str, bases: Tuple, clsdict: Dict) -> None: class Iterable(metaclass=IterableClassWatcher): - """Defines an interface for iterable items which is to assure a uniform - way to retrieve and iterate items within the git repository""" + way to retrieve and iterate items within the git repository.""" __slots__ = () + _id_attribute_ = "attribute that most suitably identifies your instance" @classmethod def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any: """ Deprecated, use IterableObj instead. + Find all items of this type - subclasses can specify args and kwargs differently. If no args are given, subclasses are obliged to return all items if no additional arguments arg given. :note: Favor the iter_items method as it will - :return: list(Item,...) list of item instances""" + :return: list(Item,...) list of item instances + """ out_list: Any = IterableList(cls._id_attribute_) out_list.extend(cls.iter_items(repo, *args, **kwargs)) return out_list @@ -1189,19 +1213,23 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any: @classmethod def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Any: # return typed to be compatible with subtypes e.g. Remote - """For more information about the arguments, see list_items - :return: iterator yielding Items""" + """For more information about the arguments, see list_items. + + :return: Iterator yielding Items + """ raise NotImplementedError("To be implemented by Subclass") @runtime_checkable class IterableObj(Protocol): """Defines an interface for iterable items which is to assure a uniform - way to retrieve and iterate items within the git repository + way to retrieve and iterate items within the git repository. - Subclasses = [Submodule, Commit, Reference, PushInfo, FetchInfo, Remote]""" + Subclasses = [Submodule, Commit, Reference, PushInfo, FetchInfo, Remote] + """ __slots__ = () + _id_attribute_: str @classmethod @@ -1213,7 +1241,8 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_I :note: Favor the iter_items method as it will - :return: list(Item,...) list of item instances""" + :return: list(Item,...) list of item instances + """ out_list: IterableList = IterableList(cls._id_attribute_) out_list.extend(cls.iter_items(repo, *args, **kwargs)) return out_list @@ -1221,9 +1250,11 @@ def list_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> IterableList[T_I @classmethod @abstractmethod def iter_items(cls, repo: "Repo", *args: Any, **kwargs: Any) -> Iterator[T_IterableObj]: # Iterator[T_IterableObj]: - # return typed to be compatible with subtypes e.g. Remote - """For more information about the arguments, see list_items - :return: iterator yielding Items""" + # Return-typed to be compatible with subtypes e.g. Remote. + """For more information about the arguments, see list_items. + + :return: Iterator yielding Items + """ raise NotImplementedError("To be implemented by Subclass")