aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLoek Le Blansch <loek.le-blansch.pv@renesas.com>2025-10-30 10:22:46 +0100
committerLoek Le Blansch <loek.le-blansch.pv@renesas.com>2025-10-30 10:22:46 +0100
commit2c084c84e1d8668afd9e8aa37bc3c651a06b55ce (patch)
tree61ae8febe136269704af39b0f0dfbe0a4502e336
parent0afe5cfb0091e749f28145cdd201f69d255a40c3 (diff)
add more API documentation
-rw-r--r--patchtree/config.py17
-rw-r--r--patchtree/context.py56
-rw-r--r--patchtree/diff.py18
-rw-r--r--patchtree/patch.py19
-rw-r--r--patchtree/process.py27
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