aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLoek Le Blansch <loek.le-blansch.pv@renesas.com>2025-10-30 08:11:35 +0100
committerLoek Le Blansch <loek.le-blansch.pv@renesas.com>2025-10-30 08:11:35 +0100
commitb18d0b52a06bd7d36af0768235ac6f7a07cde813 (patch)
tree6f4a25fa7c594b61a01c40c15416b1645e0d752f
parentedc7fa6838f700ab51f4f7ba1820b412eed66888 (diff)
more docs
-rw-r--r--doc/api/index.rst2
-rw-r--r--doc/conf.py2
-rw-r--r--doc/dev/index.rst4
-rw-r--r--doc/dev/processor.rst114
-rw-r--r--patchtree/cli.py13
-rw-r--r--patchtree/config.py49
-rw-r--r--patchtree/context.py71
-rw-r--r--patchtree/diff.py7
8 files changed, 164 insertions, 98 deletions
diff --git a/doc/api/index.rst b/doc/api/index.rst
index b4678f8..feb1740 100644
--- a/doc/api/index.rst
+++ b/doc/api/index.rst
@@ -1,3 +1,5 @@
+.. _api:
+
API reference
=============
diff --git a/doc/conf.py b/doc/conf.py
index 07823d6..0dc01c8 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -18,7 +18,7 @@ extensions = [
]
templates_path = []
exclude_patterns = []
-html_theme = "alabaster"
+html_theme = "sphinx_rtd_theme"
html_static_path = []
autodoc_mock_imports = [project]
diff --git a/doc/dev/index.rst b/doc/dev/index.rst
index 66bd679..523e6a9 100644
--- a/doc/dev/index.rst
+++ b/doc/dev/index.rst
@@ -109,6 +109,10 @@ The configuration file is a Python file sourced from ``ptconfig.py`` relative to
This file is a regular Python source file and can contain any arbitrary code.
Any global definitions with an identical name to a member variable of the :any:`Config` class will override the global configuration instance's value.
+The main method in which patchtree is configured is by creating subclasses of its internal classes and overriding its methods.
+In order to facilitate this, the type of most classes is read from the :any:`Config` dataclass instead of being instantiated directly.
+Please take a look at the :ref:`api` for more info.
+
For example:
.. code:: none
diff --git a/doc/dev/processor.rst b/doc/dev/processor.rst
index 0a19ab8..018d216 100644
--- a/doc/dev/processor.rst
+++ b/doc/dev/processor.rst
@@ -16,28 +16,14 @@ Note that some processors may take positional arguments, while others may use ke
Identity
********
-The identity processor is used to "touch" files or add arbitrary identifiers to patchset source filenames through its arguments.
+The identity processor is used to "touch" files, change the mode of existing files, or add arbitrary identifiers to patchset source filenames by passing arbitrary arguments.
-.. list-table::
- :stub-columns: 1
-
- * - Class
- - :any:`ProcessIdentity`
- * - Identifier
- - ``id``
-
-Input
- Ignored.
-
-Output
- The *content* of the target file.
-
- .. note::
-
- Changing the patchset input's mode *will* affect the target file mode!
-
-Arguments
- Any arguments passed to this processor are ignored.
+:Class: :any:`ProcessIdentity`
+:Identifier: ``id``
+:Input: Ignored.
+:Output:
+ A file with the *content* of the target file and *mode* of the patchset input.
+:Arguments: Any arguments passed to this processor are ignored.
.. _process_cocci:
@@ -51,22 +37,11 @@ The Coccinelle processor uses Coccinelle to apply patch(es) in the SmPL (Semanti
In order to use this processor, Coccinelle must be installed and ``spatch`` must be available in ``$PATH``.
-.. list-table::
- :stub-columns: 1
-
- * - Class
- - :any:`ProcessCoccinelle`
- * - Identifier
- - ``cocci``
-
-Input
- Coccinelle's SmPL input.
-
-Output
- The contents of the target file after being processed by Coccinelle (not the diff returned by Coccinelle).
-
-Arguments
- Reserved.
+:Class: :any:`ProcessCoccinelle`
+:Identifier: ``cocci``
+:Input: Coccinelle's SmPL input.
+:Output: The contents of the target file after being processed by Coccinelle (not the diff returned by Coccinelle).
+:Arguments: Reserved.
.. _process_jinja:
@@ -74,30 +49,19 @@ Arguments
Jinja template
**************
-The Jinja processor passes the inputs through the Jinja2 templating engine.
+The Jinja processor passes the input through the Jinja2 templating engine.
+
+:Class: :any:`ProcessJinja2`
+:Identifier: ``jinja``
+:Input: Jinja template code.
+:Output: The input after being processed by Jinja.
+:Arguments: Reserved.
.. note::
Template variables are generated through the :any:`get_template_vars <ProcessJinja2.get_template_vars>` method.
This method returns an empty dict by default, and is meant to be implemented by implementing a custom class that derives from ProcessJinja2 and registering it through the :ref:`configuration file <ptconfig>`.
-.. list-table::
- :stub-columns: 1
-
- * - Class
- - :any:`ProcessJinja2`
- * - Identifier
- - ``jinja``
-
-Input
- Jinja template code.
-
-Output
- The input after being processed by Jinja.
-
-Arguments
- Reserved.
-
.. _process_exe:
**********
@@ -106,26 +70,17 @@ Executable
The executable processor runs the input as an executable, passes the target file to its standard input, and returns its standard output.
-.. list-table::
- :stub-columns: 1
-
- * - Class
- - :any:`ProcessExec`
- * - Identifier
- - ``exec``
-
-Input
+:Class: :any:`ProcessExec`
+:Identifier: ``exec``
+:Input:
Executable script.
.. important::
- The executable must contain a shebang line to specify what interpreter to use.
-
-Output
- Any content written to the standard output by the executable.
+ The executable *must* contain a shebang line to specify what interpreter to use.
-Arguments
- Reserved.
+:Output: Any content written to the standard output by the executable.
+:Arguments: Reserved.
.. _process_merge:
@@ -135,21 +90,12 @@ Merge
The merge processor merges the input with the target file, such that changes are combined with the target instead of replacing the target.
-.. list-table::
- :stub-columns: 1
-
- * - Class
- - :any:`ProcessMerge`
- * - Identifier
- - ``merge``
-
-Input
- Content to merge.
-
-Output
- Merged changes.
+:Class: :any:`ProcessMerge`
+:Identifier: ``merge``
+:Input: Content to merge.
+:Output: Merged changes.
+:Arguments: Positional.
-Arguments (positional)
1. Merge strategy:
``ignore``
diff --git a/patchtree/cli.py b/patchtree/cli.py
index bef0c0d..c5672f0 100644
--- a/patchtree/cli.py
+++ b/patchtree/cli.py
@@ -8,6 +8,11 @@ from .config import Config
def load_config() -> Config:
+ """
+ Create a new instance of the :any:`Config` dataclass and attempt to overwrite any members using the global
+ variables defined in ptconfig.py (if it exists).
+ """
+
init = {}
cfg = Path("ptconfig.py")
if cfg.exists():
@@ -18,6 +23,10 @@ def load_config() -> Config:
def path_dir(path: str) -> Path:
+ """
+ Argparse helper type for a pathlib Path to a directory.
+ """
+
out = Path(path)
if not out.is_dir():
raise ArgumentTypeError(f"not a directory: `{path}'")
@@ -25,6 +34,10 @@ def path_dir(path: str) -> Path:
def parse_arguments(config: Config) -> Context:
+ """
+ Parse command-line arguments and return the application context.
+ """
+
parser = config.argument_parser(
prog="patchtree",
description="patch file generator",
diff --git a/patchtree/config.py b/patchtree/config.py
index 2f2373e..48f11fb 100644
--- a/patchtree/config.py
+++ b/patchtree/config.py
@@ -21,14 +21,24 @@ DEFAULT_PROCESSORS: dict[str, type[Process]] = {
class Header:
"""
- Patch output header.
+ Patch output header generator.
+
+ The header is formatted as
+
+ * shebang (optional)
+ * patchtree version info
+ * extra version info (empty by default)
+ * license (empty by default)
"""
config: Config
context: Context
name = "patchtree"
+ """Program name shown in version info."""
+
license = None
+ """License text (optional)."""
def __init__(self, config: Config, context: Context):
self.config = config
@@ -40,8 +50,13 @@ class Header:
self.write_license()
def write_shebang(self):
+ """
+ Write a shebang line to apply the output patch if the --shebang option was passed.
+ """
+
if not self.config.output_shebang:
return
+
cmd = [
"/usr/bin/env",
"-S",
@@ -51,13 +66,28 @@ class Header:
self.context.output.write(f"#!{cmdline}\n")
def write_version(self):
+ """
+ Write the patchtree name and version number.
+ """
+
version = metadata.version("patchtree")
self.context.output.write(f"{self.name} output (version {version})\n")
def write_version_extra(self):
+ """
+ Write extra version information (empty).
+
+ This method is meant to be implemented by subclasses of Header defined in the ptconfig.py of
+ patchsets.
+ """
+
pass
def write_license(self):
+ """
+ Write a license if it is defined.
+ """
+
if self.license is None:
return
self.context.output.write(f"{self.license}\n")
@@ -72,29 +102,32 @@ class Config:
"""
context: type[Context] = Context
- """Context class type."""
+ """Context class type. Override this to add custom context variables."""
patch: type[Patch] = Patch
"""Patch class type."""
argument_parser: type[ArgumentParser] = ArgumentParser
- """ArgumentParser class type."""
+ """ArgumentParser class type. Override this to add custom arguments."""
process_delimiter: str = "#"
- """String used to delimit processors in patch source filenames."""
+ """
+ String used to delimit processors in patch source filenames.
+
+ See: :ref:`processors`.
+ """
processors: dict[str, type[Process]] = field(default_factory=lambda: DEFAULT_PROCESSORS)
"""Maps processor specification string to :type:`Process` class type."""
header: type[Header] = Header
- """Header class type."""
+ """Header class type. Override this to modify the patch header format."""
diff_context: int = 3
- """Lines of context to include in the default diffs."""
+ """Lines of context to include in the diffs."""
output_shebang: bool = False
- """Whether to output a shebang line with the ``git patch`` command to apply
- the patch."""
+ """Whether to output a shebang line with the ``git patch`` command to apply the patch."""
default_patch_sources: list[Path] = field(default_factory=list)
"""List of default sources."""
diff --git a/patchtree/context.py b/patchtree/context.py
index cca419a..ef32e97 100644
--- a/patchtree/context.py
+++ b/patchtree/context.py
@@ -18,22 +18,48 @@ ZIP_CREATE_SYSTEM_UNX = 3
class FS:
+ """Target filesystem interface."""
+
target: Path
def __init__(self, target: Path):
self.target = target
def get_dir(self, dir: str) -> list[str]:
+ """
+ List all items in a subdirectory of the target.
+
+ :returns: A list of all item names.
+ """
+
raise NotImplementedError()
def get_content(self, file: str) -> str | None:
+ """
+ Get the content of a file relative to the target.
+
+ :returns:
+ * The file content if it exists.
+ * None if the file does not exist.
+ """
+
raise NotImplementedError()
def get_mode(self, file: str) -> int:
+ """
+ Get the mode of a file relative to the target.
+
+ :returns:
+ * The mode as returned by stat(3)'s ``stat.st_mode``
+ * 0 if the file does not exist
+ """
+
raise NotImplementedError()
class DiskFS(FS):
+ """Implementation of :any:`FS` for a regular directory. Reads directly from the disk."""
+
def __init__(self, target):
super(DiskFS, self).__init__(target)
@@ -59,16 +85,30 @@ class DiskFS(FS):
class ZipFS(FS):
+ """Implementation of :any:`FS` for zip files. Reads directly from the archive."""
+
zip: ZipFile
+ """Underlying zip file."""
+
files: dict[Path, ZipInfo] = {}
+ """Map of path -> ZipInfo for all files in the archive."""
def __init__(self, target):
super(ZipFS, self).__init__(target)
self.zip = ZipFile(str(target))
for info in self.zip.infolist():
self.files[Path(info.filename)] = info
+ # todo: index implicit directories in tree
def get_info(self, path: str) -> ZipInfo | None:
+ """
+ Get the ZipInfo for a file in the archive
+
+ :returns:
+ * The ZipInfo for the file at ``path``
+ * None if the file does not exist
+ """
+
return self.files.get(Path(path), None)
def get_dir(self, dir: str) -> list[str]:
@@ -95,15 +135,40 @@ class ZipFS(FS):
except:
return ""
+ def is_implicit_dir(self, file: str) -> bool:
+ """
+ Check if there is an implicit directory at ``file``.
+
+ Some zip files may not include entries for all directories if they already define entries for files or
+ subdirectories within. This function checks if any path that is a subdirectory of ``file`` exists.
+
+ :returns: ``True`` if there is a directory at ``file``, else ``False``.
+ """
+
+ parent = Path(file)
+ for child in self.files:
+ if parent in child.parents:
+ return True
+ return False
+
def get_mode(self, file: str) -> int:
+ MODE_NONEXISTANT = 0
+ MODE_FILE = 0o644 | S_IFREG
+ MODE_DIR = 0o755 | S_IFDIR
+
info = self.get_info(file)
if info is None:
- return 0
+ # if self.is_implicit_dir(file):
+ # return MODE_DIR
+ return MODE_NONEXISTANT
+
if info.create_system == ZIP_CREATE_SYSTEM_UNX:
return (info.external_attr >> 16) & 0xFFFF
+
if info.is_dir():
- return 0o755 | S_IFDIR
- return 0o644 | S_IFREG
+ return MODE_DIR
+
+ return MODE_FILE
class Context:
diff --git a/patchtree/diff.py b/patchtree/diff.py
index 7b55353..3dec600 100644
--- a/patchtree/diff.py
+++ b/patchtree/diff.py
@@ -19,15 +19,18 @@ class File:
class Diff:
"""
- Produce a regular diff from the (possibly absent) original file to the file in the patch input tree. This
- effectively overwrites whatever exists in the target sources with the file in the patch input tree.
+ Produce a regular diff from the (possibly absent) original file to the file in the patch input tree.
"""
config: Config
file: str
+ """Path to file relative to target dir."""
a: File
+ """Original file."""
+
b: File
+ """Target file."""
def __init__(self, config: Config, file: str):
self.config = config