diff options
| author | Loek Le Blansch <loek.le-blansch.pv@renesas.com> | 2025-10-24 13:04:37 +0200 |
|---|---|---|
| committer | Loek Le Blansch <loek.le-blansch.pv@renesas.com> | 2025-10-24 13:04:37 +0200 |
| commit | 30cdc59df8d3c6f27d11fa015174962db65a277b (patch) | |
| tree | 4f928ad6568e13fa6befe124c5f79acd5cbdd773 | |
| parent | b819354e3d7b2181995ac11510da2226564eebfb (diff) | |
python black
| -rw-r--r-- | .pre-commit-config.yaml | 4 | ||||
| -rw-r--r-- | patchtree/cli.py | 95 | ||||
| -rw-r--r-- | patchtree/config.py | 70 | ||||
| -rw-r--r-- | patchtree/context.py | 79 | ||||
| -rw-r--r-- | patchtree/diff.py | 81 | ||||
| -rw-r--r-- | patchtree/patch.py | 135 | ||||
| -rw-r--r-- | patchtree/process.py | 86 | ||||
| -rw-r--r-- | setup.py | 1 |
8 files changed, 293 insertions, 258 deletions
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 40a64e2..b21ca20 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,3 +11,7 @@ repos: hooks: - id: codespell args: [--quiet-level=2] + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 25.9.0 + hooks: + - id: black diff --git a/patchtree/cli.py b/patchtree/cli.py index b9cd952..5ec8213 100644 --- a/patchtree/cli.py +++ b/patchtree/cli.py @@ -5,62 +5,67 @@ from pathlib import Path from .context import Context from .config import Config + def load_config() -> Config: - init = {} - cfg = Path("ptconfig.py") - if cfg.exists(): - exec(cfg.read_text(), init) - valid_fields = [field.name for field in fields(Config)] - init = {key: value for key, value in init.items() if key in valid_fields} - return Config(**init) + init = {} + cfg = Path("ptconfig.py") + if cfg.exists(): + exec(cfg.read_text(), init) + valid_fields = [field.name for field in fields(Config)] + init = {key: value for key, value in init.items() if key in valid_fields} + return Config(**init) + def parse_arguments(config: Config) -> Context: - parser = config.argument_parser( - prog='patchtree', - description='patch file generator', - ) - parser.add_argument( - '-o', '--out', - help="output file (stdout by default)", - type=str, - ) - parser.add_argument('target', help="target directory or archive") - parser.add_argument("patch", help="patch input glob(s)", nargs="+") - - options = parser.parse_args() - - try: - return config.context(options) - except Exception as e: - parser.error(str(e)) + parser = config.argument_parser( + prog="patchtree", + description="patch file generator", + ) + parser.add_argument( + "-o", + "--out", + help="output file (stdout by default)", + type=str, + ) + parser.add_argument("target", help="target directory or archive") + parser.add_argument("patch", help="patch input glob(s)", nargs="+") + + options = parser.parse_args() + + try: + return config.context(options) + except Exception as e: + parser.error(str(e)) + def main(): - config = load_config() + config = load_config() - context = parse_arguments(config) + context = parse_arguments(config) - file_set: set[Path] = set() - for pattern in context.options.patch: - for path in Path('.').glob(pattern): - if not path.is_file(): - continue - file_set.add(path) - files = sorted(file_set) + file_set: set[Path] = set() + for pattern in context.options.patch: + for path in Path(".").glob(pattern): + if not path.is_file(): + continue + file_set.add(path) + files = sorted(file_set) - if len(files) == 0: - print("no files to patch!", file=stderr) - return 0 + if len(files) == 0: + print("no files to patch!", file=stderr) + return 0 - config.header(context) + config.header(context) - for file in files: - patch = config.patch(config, file) - patch.write(context) + for file in files: + patch = config.patch(config, file) + patch.write(context) - context.output.flush() - context.output.close() + context.output.flush() + context.output.close() + + return 0 - return 0 if __name__ == "__main__": - exit(main()) + exit(main()) diff --git a/patchtree/config.py b/patchtree/config.py index b4772a9..065b03c 100644 --- a/patchtree/config.py +++ b/patchtree/config.py @@ -8,49 +8,55 @@ from .process import * from .diff import * DEFAULT_PROCESSORS: dict[str, type[Process]] = { - "jinja": ProcessJinja2, - "cocci": ProcessCoccinelle, - "smpl": ProcessCoccinelle, + "jinja": ProcessJinja2, + "cocci": ProcessCoccinelle, + "smpl": ProcessCoccinelle, } DEFAULT_DIFFS: dict[str, type[Diff]] = { - ".gitignore": IgnoreDiff, + ".gitignore": IgnoreDiff, } + class Header: - context: Context - name = "patchtree" - license = None + context: Context + name = "patchtree" + license = None + + def __init__(self, context: Context): + self.context = context - def __init__(self, context: Context): - self.context = context + self.write_version() + self.write_version_extra() + self.write_license() - self.write_version() - self.write_version_extra() - self.write_license() + def write_version(self): + version = metadata.version("patchtree") + self.context.output.write(f"{self.name} output (version {version})\n") - def write_version(self): - version = metadata.version("patchtree") - self.context.output.write(f"{self.name} output (version {version})\n") + def write_version_extra(self): + pass - def write_version_extra(self): - pass + def write_license(self): + if self.license is None: + return + self.context.output.write(f"{self.license}\n") - def write_license(self): - if self.license is None: - return - self.context.output.write(f"{self.license}\n") @dataclass class Config: - context: type[Context] = Context - patch: type[Patch] = Patch - argument_parser: type[ArgumentParser] = ArgumentParser - process_delimiter: str = "#" - processors: dict[str, type[Process]] = field(default_factory=lambda: DEFAULT_PROCESSORS) - diff_strategies: dict[str, type[Diff]] = field(default_factory=lambda: DEFAULT_DIFFS) - header: type[Header] = Header - - def __post_init__(self): - self.processors = {**DEFAULT_PROCESSORS, **self.processors} - self.diff_strategies = {**DEFAULT_DIFFS, **self.diff_strategies} + context: type[Context] = Context + patch: type[Patch] = Patch + argument_parser: type[ArgumentParser] = ArgumentParser + process_delimiter: str = "#" + processors: dict[str, type[Process]] = field( + default_factory=lambda: DEFAULT_PROCESSORS + ) + diff_strategies: dict[str, type[Diff]] = field( + default_factory=lambda: DEFAULT_DIFFS + ) + header: type[Header] = Header + + def __post_init__(self): + self.processors = {**DEFAULT_PROCESSORS, **self.processors} + self.diff_strategies = {**DEFAULT_DIFFS, **self.diff_strategies} diff --git a/patchtree/context.py b/patchtree/context.py index 993ec64..8fa7abd 100644 --- a/patchtree/context.py +++ b/patchtree/context.py @@ -5,43 +5,44 @@ from zipfile import is_zipfile, Path as ZipPath from os import path from sys import stdout + class Context: - """ - Generic "context" struct that Diff and the main() function depend on for - application context. - """ - - fs: Union[DiskPath, ZipPath] - output: IO = stdout - options: Namespace - - def __init__(self, options: Namespace): - self.options = options - - target = options.target - if not path.exists(target): - raise Exception(f"cannot open `{target}'") - - if options.out is not None: - if options.out == "-": - self.output = stdout - else: - self.output = open(options.out, "w+") - - if path.isdir(target): - self.fs = DiskPath(target) - elif is_zipfile(target): - self.fs = ZipPath(target) - else: - raise Exception("cannot read `{target}'") - - def get_dir(self, dir: str) -> list[str]: - here = self.fs.joinpath(dir) - print(here) - return [path.name for path in here.iterdir()] - - def get_content(self, file: str) -> str | None: - here = self.fs.joinpath(file) - if not here.exists(): - return None - return here.read_bytes().decode() + """ + Generic "context" struct that Diff and the main() function depend on for + application context. + """ + + fs: Union[DiskPath, ZipPath] + output: IO = stdout + options: Namespace + + def __init__(self, options: Namespace): + self.options = options + + target = options.target + if not path.exists(target): + raise Exception(f"cannot open `{target}'") + + if options.out is not None: + if options.out == "-": + self.output = stdout + else: + self.output = open(options.out, "w+") + + if path.isdir(target): + self.fs = DiskPath(target) + elif is_zipfile(target): + self.fs = ZipPath(target) + else: + raise Exception("cannot read `{target}'") + + def get_dir(self, dir: str) -> list[str]: + here = self.fs.joinpath(dir) + print(here) + return [path.name for path in here.iterdir()] + + def get_content(self, file: str) -> str | None: + here = self.fs.joinpath(file) + if not here.exists(): + return None + return here.read_bytes().decode() diff --git a/patchtree/diff.py b/patchtree/diff.py index 5613858..9058b02 100644 --- a/patchtree/diff.py +++ b/patchtree/diff.py @@ -1,49 +1,56 @@ from difflib import unified_diff -class Diff: - """ - The base Diff class just produces a regular diff from the (possibly absent) - SDK10 file. This effectively adds a new file or replaces the SDK10 source file - with the file in the patch directory. - """ - - file: str - - content_a: str | None - content_b: str = "" - def __init__(self, file: str): - self.file = file +class Diff: + """ + The base Diff class just produces a regular diff from the (possibly absent) + SDK10 file. This effectively adds a new file or replaces the SDK10 source file + with the file in the patch directory. + """ - def compare(self) -> str: - fromfile = f"a/{self.file}" - tofile = f"b/{self.file}" + file: str - if self.content_a is None: - fromfile = "/dev/null" - self.content_a = "" + content_a: str | None + content_b: str = "" - a = self.content_a.splitlines() - b = self.content_b.splitlines() - diff = unified_diff(a, b, fromfile, tofile, lineterm="") - return "\n".join(diff) + "\n" + def __init__(self, file: str): + self.file = file - def diff(self) -> str: - return self.compare() + def compare(self) -> str: + fromfile = f"a/{self.file}" + tofile = f"b/{self.file}" -class IgnoreDiff(Diff): - """ - IgnoreDiff is slightly different and is used to ensure all the lines in the - patch source ignore file are present in the SDK version. This ensures no - duplicate ignore lines exist after patching. - """ + if self.content_a is None: + fromfile = "/dev/null" + self.content_a = "" - def diff(self): - lines_a = (self.content_a or "").splitlines() - lines_b = self.content_b.splitlines() + a = self.content_a.splitlines() + b = self.content_b.splitlines() + diff = unified_diff(a, b, fromfile, tofile, lineterm="") + return "\n".join(diff) + "\n" - add_lines = set(lines_b) - set(lines_a) + def diff(self) -> str: + return self.compare() - self.content_b = "\n".join((*lines_a, *add_lines,)) - return self.compare() +class IgnoreDiff(Diff): + """ + IgnoreDiff is slightly different and is used to ensure all the lines in the + patch source ignore file are present in the SDK version. This ensures no + duplicate ignore lines exist after patching. + """ + + def diff(self): + lines_a = (self.content_a or "").splitlines() + lines_b = self.content_b.splitlines() + + add_lines = set(lines_b) - set(lines_a) + + self.content_b = "\n".join( + ( + *lines_a, + *add_lines, + ) + ) + + return self.compare() diff --git a/patchtree/patch.py b/patchtree/patch.py index cc0c1c6..415cf26 100644 --- a/patchtree/patch.py +++ b/patchtree/patch.py @@ -6,71 +6,74 @@ from pathlib import Path from .diff import Diff if TYPE_CHECKING: - from .process import Process - from .context import Context - from .config import Config + from .process import Process + from .context import Context + from .config import Config + class Patch: - config: Config - patch: Path - - file: str - file_name: str = "" - file_type: str = "" - processors: list[str] = [] - - def __init__(self, config: Config, patch: Path): - self.patch = patch - self.config = config - - self.file_name = patch.name - - # find preprocessors - idx = self.file_name.find(config.process_delimiter) - if idx >= 0: - self.processors = self.file_name[idx:].split(config.process_delimiter) - self.processors = [template.strip() for template in self.processors] - self.processors = [template for template in self.processors if len(template) > 0] - self.processors.reverse() - self.file_name = self.file_name[:idx] - - # save the path to the target file - self.file = str(patch.parent.joinpath(self.file_name)) - - # find and split at file extension - idx = self.file_name.find(".") - if idx >= 0: - self.file_type = self.file_name[idx:] - self.file_name = self.file_name[:idx] - - def get_diff(self) -> type[Diff]: - return self.config.diff_strategies.get(self.file_type, Diff) - - def get_processors(self) -> list[type[Process]]: - processors = [] - for processor in self.processors: - if processor not in self.config.processors: - continue - processors.append(self.config.processors[processor]) - return processors - - def write(self, context: Context) -> None: - diff_class = self.get_diff() - processor_classes = self.get_processors() - - diff = diff_class(self.file) - - # read file A contents - content_a = context.get_content(self.file) - - # read file B contents - content_b = self.patch.read_text() - for processor_class in processor_classes: - processor = processor_class(context) - content_b = processor.transform(content_a, content_b) - - diff.content_a = content_a - diff.content_b = content_b - - delta = diff.diff() - context.output.write(delta) + config: Config + patch: Path + + file: str + file_name: str = "" + file_type: str = "" + processors: list[str] = [] + + def __init__(self, config: Config, patch: Path): + self.patch = patch + self.config = config + + self.file_name = patch.name + + # find preprocessors + idx = self.file_name.find(config.process_delimiter) + if idx >= 0: + self.processors = self.file_name[idx:].split(config.process_delimiter) + self.processors = [template.strip() for template in self.processors] + self.processors = [ + template for template in self.processors if len(template) > 0 + ] + self.processors.reverse() + self.file_name = self.file_name[:idx] + + # save the path to the target file + self.file = str(patch.parent.joinpath(self.file_name)) + + # find and split at file extension + idx = self.file_name.find(".") + if idx >= 0: + self.file_type = self.file_name[idx:] + self.file_name = self.file_name[:idx] + + def get_diff(self) -> type[Diff]: + return self.config.diff_strategies.get(self.file_type, Diff) + + def get_processors(self) -> list[type[Process]]: + processors = [] + for processor in self.processors: + if processor not in self.config.processors: + continue + processors.append(self.config.processors[processor]) + return processors + + def write(self, context: Context) -> None: + diff_class = self.get_diff() + processor_classes = self.get_processors() + + diff = diff_class(self.file) + + # read file A contents + content_a = context.get_content(self.file) + + # read file B contents + content_b = self.patch.read_text() + for processor_class in processor_classes: + processor = processor_class(context) + content_b = processor.transform(content_a, content_b) + + diff.content_a = content_a + diff.content_b = content_b + + delta = diff.diff() + context.output.write(delta) diff --git a/patchtree/process.py b/patchtree/process.py index 0e86f2e..b8b87bd 100644 --- a/patchtree/process.py +++ b/patchtree/process.py @@ -7,55 +7,63 @@ from subprocess import Popen from pathlib import Path if TYPE_CHECKING: - from .context import Context + from .context import Context + class Process: - context: Context + context: Context + + def __init__(self, context: Context): + self.context = context - def __init__(self, context: Context): - self.context = context + def transform(self, content_a: str | None, content_b: str) -> str: + return content_b - def transform(self, content_a: str | None, content_b: str) -> str: - return content_b class ProcessJinja2(Process): - environment: Environment = Environment( - trim_blocks=True, - lstrip_blocks=True, - ) + environment: Environment = Environment( + trim_blocks=True, + lstrip_blocks=True, + ) + + def transform(self, content_a, content_b) -> str: + template_vars = self.get_template_vars() + return self.environment.from_string(content_b).render(**template_vars) - def transform(self, content_a, content_b) -> str: - template_vars = self.get_template_vars() - return self.environment.from_string(content_b).render(**template_vars) + def get_template_vars(self) -> dict[str, Any]: + return {} - def get_template_vars(self) -> dict[str, Any]: - return {} class ProcessCoccinelle(Process): - def transform(self, content_a, content_b) -> str: - content_a = content_a or "" - - if len(content_b.strip()) == 0: - return content_a - - temp_a = Path(mkstemp()[1]) - temp_b = Path(mkstemp()[1]) - temp_sp = Path(mkstemp()[1]) - - temp_a.write_text(content_a) - temp_sp.write_text(content_b) - cmd = ( - "spatch", "--very-quiet", "--no-show-diff", - "--sp-file", str(temp_sp), - str(temp_a), "-o", str(temp_b), - ) - coccinelle = Popen(cmd) - coccinelle.wait() + def transform(self, content_a, content_b) -> str: + content_a = content_a or "" + + if len(content_b.strip()) == 0: + return content_a + + temp_a = Path(mkstemp()[1]) + temp_b = Path(mkstemp()[1]) + temp_sp = Path(mkstemp()[1]) + + temp_a.write_text(content_a) + temp_sp.write_text(content_b) + cmd = ( + "spatch", + "--very-quiet", + "--no-show-diff", + "--sp-file", + str(temp_sp), + str(temp_a), + "-o", + str(temp_b), + ) + coccinelle = Popen(cmd) + coccinelle.wait() - content_b = temp_b.read_text() + content_b = temp_b.read_text() - temp_a.unlink() - temp_b.unlink() - temp_sp.unlink() + temp_a.unlink() + temp_b.unlink() + temp_sp.unlink() - return content_b + return content_b @@ -1,2 +1,3 @@ from setuptools import setup + setup() |