diff options
| author | Loek Le Blansch <loek.le-blansch.pv@renesas.com> | 2025-10-30 10:22:46 +0100 |
|---|---|---|
| committer | Loek Le Blansch <loek.le-blansch.pv@renesas.com> | 2025-10-30 10:22:46 +0100 |
| commit | 2c084c84e1d8668afd9e8aa37bc3c651a06b55ce (patch) | |
| tree | 61ae8febe136269704af39b0f0dfbe0a4502e336 | |
| parent | 0afe5cfb0091e749f28145cdd201f69d255a40c3 (diff) | |
add more API documentation
| -rw-r--r-- | patchtree/config.py | 17 | ||||
| -rw-r--r-- | patchtree/context.py | 56 | ||||
| -rw-r--r-- | patchtree/diff.py | 18 | ||||
| -rw-r--r-- | patchtree/patch.py | 19 | ||||
| -rw-r--r-- | patchtree/process.py | 27 |
5 files changed, 103 insertions, 34 deletions
diff --git a/patchtree/config.py b/patchtree/config.py index a1a2b0f..a4d0912 100644 --- a/patchtree/config.py +++ b/patchtree/config.py @@ -57,11 +57,7 @@ class Header: if not self.config.output_shebang: return - cmd = [ - "/usr/bin/env", - "-S", - *self.context.get_apply_cmd(), - ] + cmd = ["/usr/bin/env", "-S", *self.context.get_apply_cmd()] cmdline = " ".join(cmd) self.context.output.write(f"#!{cmdline}\n") @@ -118,7 +114,14 @@ class Config: """ processors: dict[str, type[Process]] = field(default_factory=lambda: DEFAULT_PROCESSORS) - """Maps processor specification string to :type:`Process` class type.""" + """ + Maps processor specification string to :type:`Process` class type. + + .. note:: + + If this member is defined in the configuration file, it is automatically merged with the default dict, + with the configuration file keys taking priority. + """ header: type[Header] = Header """Header class type. Override this to modify the patch header format.""" @@ -133,7 +136,7 @@ class Config: """List of default sources.""" default_root: str | None = None - """Default value of the -C argument.""" + """Default value of the ``-C``/``--root`` argument.""" def __post_init__(self): self.processors = {**DEFAULT_PROCESSORS, **self.processors} diff --git a/patchtree/context.py b/patchtree/context.py index 8635e92..23817f1 100644 --- a/patchtree/context.py +++ b/patchtree/context.py @@ -172,17 +172,31 @@ class ZipFS(FS): class Context: + """Global app context / state holder.""" + + inputs: list[Path] = [] + """A list of patchset inputs (relative to the current working directory).""" + + root: Path """ - Global app context / state holder. + Patchset root folder. All patchset input paths will be treated relative to this folder. + + .. note:: + + The ``root`` member only changes the appearance of paths. All internal logic uses the "real" paths. """ + target: Path + """Path to target.""" + fs: FS + """Target file system interface.""" + output: IO + """Output stream for writing the clean patch.""" - root: Path - target: Path - inputs: list[Path] = [] in_place: bool + """Whether to apply the changes directly to the target instead of outputting the .patch file.""" config: Config @@ -201,6 +215,8 @@ class Context: self.apply(True) def close(self): + """Finish writing the clean patch file and close it.""" + # patch must have a trailing newline self.output.write("\n") self.output.flush() @@ -211,6 +227,9 @@ class Context: self.output.close() def collect_inputs(self, options: Namespace) -> list[Path]: + """ + Collect a list of patchset inputs depending on the globbing, patchset root and provided input path(s). + """ inputs: set[Path] = set() if len(inputs) == 0 and options.root is not None: @@ -235,15 +254,23 @@ class Context: return list(inputs) def get_dir(self, dir: str) -> list[str]: + """Get a target directory's content (see :any:`FS.get_dir()`)""" return self.fs.get_dir(dir) def get_content(self, file: str) -> bytes | str | None: + """Get a target file's content (see :any:`FS.get_content()`)""" return self.fs.get_content(file) def get_mode(self, file: str) -> int: + """Get a target file's mode (see :any:`FS.get_mode()`)""" return self.fs.get_mode(file) def get_fs(self) -> FS: + """ + Open the selected target, taking into account the --in-place option. + + :returns: Target filesystem interface. + """ target = self.target if not target.exists(): @@ -260,6 +287,11 @@ class Context: raise Exception("cannot read `{target}'") def get_output(self, options: Namespace) -> IO: + """ + Open the output stream, taking into account the --in-place and --out options. + + :returns: Output stream. + """ if self.in_place: if options.out is not None: print("warning: --out is ignored when using --in-place", file=stderr) @@ -274,16 +306,22 @@ class Context: return stdout def get_apply_cmd(self) -> list[str]: - cmd = [ - "git", - "apply", - "--allow-empty", - ] + """ + Create a command argument vector for applying the current patch. + + :returns: Command argument vector. + """ + + cmd = ["git", "apply", "--allow-empty"] if self.config.diff_context == 0: cmd.append("--unidiff-zero") return cmd def apply(self, reverse: bool) -> None: + """ + Apply the patch in ``self.output`` and update the cache or reverse the patch in the cache. + """ + location = cast(DiskFS, self.fs).target cache = location.joinpath(".patchtree.diff") cmd = self.get_apply_cmd() diff --git a/patchtree/diff.py b/patchtree/diff.py index 9b68350..5a76487 100644 --- a/patchtree/diff.py +++ b/patchtree/diff.py @@ -11,12 +11,26 @@ if TYPE_CHECKING: @dataclass class File: content: str | bytes | None + """The file's contents, or ``None`` if it does not exist.""" + mode: int + """The file's mode as returned by stat(3)'s ``stat.st_mode``.""" def is_binary(self) -> bool: return isinstance(self.content, bytes) def lines(self) -> list[str]: + """ + Get a list of lines in this file. + + :returns: + * A list of strings for each line in the file + * An empty list if the file is empty or nonexistent + + .. note:: + + This function only works for text files. Use :any:`File.is_binary` to check this safely. + """ assert not isinstance(self.content, bytes) return (self.content or "").splitlines() @@ -31,10 +45,10 @@ class Diff: """Path to file relative to target dir.""" a: File - """Original file.""" + """Original (before) file.""" b: File - """Target file.""" + """Target (after) file.""" def __init__(self, config: Config, file: str): self.config = config diff --git a/patchtree/patch.py b/patchtree/patch.py index c393ca1..85d056d 100644 --- a/patchtree/patch.py +++ b/patchtree/patch.py @@ -12,11 +12,18 @@ if TYPE_CHECKING: class Patch: + """A single patched file.""" + config: Config + patch: Path + """The patchset input location.""" file: str + """The name of the patched file in the target.""" + processors: list[tuple[type[Process], Process.Args]] = [] + """A list of processors to apply to the input before diffing.""" def __init__(self, config: Config, patch: Path): self.patch = patch @@ -33,15 +40,13 @@ class Patch: for arg in argv: key, value, *_ = (*arg.split("=", 1), None) args.argd[key] = value - self.processors.insert( - 0, - ( - proc_cls, - args, - ), - ) + self.processors.insert(0, (proc_cls, args)) def write(self, context: Context) -> None: + """ + Apply all processors, compare to the target and write the delta to :any:`Context.output`. + """ + if context.root is not None: self.file = str(Path(self.file).relative_to(context.root)) diff --git a/patchtree/process.py b/patchtree/process.py index 3c500dd..bd6b802 100644 --- a/patchtree/process.py +++ b/patchtree/process.py @@ -24,11 +24,21 @@ class Process: @dataclass class Args: + """ + Processor filename arguments. + + See :ref:`processors`. + """ + name: str + """The name the processor was called with.""" argv: list[str] = field(default_factory=list) + """The arguments passed to the processor""" argd: dict[str, str | None] = field(default_factory=dict) + """The key/value arguments passed to the processor""" args: Args + """Arguments passed to this processor.""" def __init__(self, context: Context, args: Args): self.args = args @@ -38,9 +48,9 @@ class Process: """ Transform the input file. - :param a: content of file to patch - :param b: content of patch input (in patch tree) - :returns: processed file + :param a: Content of file to patch. + :param b: Content of patch input in patch tree or output of previous processor. + :returns: Processed file. """ raise NotImplementedError() @@ -64,6 +74,7 @@ class ProcessJinja2(Process): def transform(self, a, b): template_vars = self.get_template_vars() assert b.content is not None + assert not isinstance(b.content, bytes) b.content = self.environment.from_string(b.content).render(**template_vars) return b @@ -91,6 +102,8 @@ class ProcessCoccinelle(Process): raise Exception("too many arguments") def transform(self, a, b): + assert not isinstance(a.content, bytes) + assert not isinstance(b.content, bytes) content_a = a.content or "" content_b = b.content or "" @@ -147,6 +160,7 @@ class ProcessExec(Process): def transform(self, a, b): assert b.content is not None + assert not isinstance(b.content, bytes) fd, exec = mkstemp() with fdopen(fd, "wt") as f: @@ -172,12 +186,7 @@ class ProcessMerge(Process): add_lines = set(lines_b) - set(lines_a) - b.content = "\n".join( - ( - *lines_a, - *add_lines, - ) - ) + b.content = "\n".join((*lines_a, *add_lines)) return b |