Get Started with PatchStatistics

>>> from fitzzftw.patch.static import colors
>>> colors.switch_to_testmode(False)

The patcher.PatchStatistics class is responsible for collecting information during the patching process and presenting it to the user.

Initialization and Properties

When you create a new instance, you define the verbosity level. All counters start at zero:

>>> from fitzzftw.patch.patcher import PatchStatistics
>>> stats = PatchStatistics()

You can access the current state through several read-only properties:

>>> stats
PatchStatistics(verbosity: 0)
>>> stats.verbosity
0
>>> stats.total_files
0
>>> stats.lines_added
0
>>> stats.lines_removed
0
>>> stats.files_modified
0
>>> stats.files_created
0
>>> stats.files_deleted
0

Basic Output

The PatchStatistics.print() method generates a summary based on the gathered data. Even without any files added, it provides a basic status report:

>>> stats.print()
Files processed: 0
Runtime: ... s

Higher Verbosity

Changing the verbosity level doesn’t affect the data, but it will change what print() eventually shows (once data is present).

>>> stat1 = PatchStatistics(verbosity=1)
>>> stat1
PatchStatistics(verbosity: 1)
>>> stat1.print()
Files processed: 0
Lines processed: 0
Runtime: ... s
>>> stat_error = PatchStatistics()
>>> from fitzzftw.patch.lines import HeadLine
>>> from fitzzftw.patch.container import DiffCodeFile
>>> h1 = HeadLine("--- a/test.py")
>>> diff_error = DiffCodeFile(h1)
>>> stat_error.add_file(diff_error)
Traceback (most recent call last):
    ...
fitzzftw.patch.exceptions.FtwPatchError: New Header not found!

FtwPatch Class

The patcher.FtwPatch class is the high-level controller of the module. It coordinates the parsing of the patch file and the application of changes to the target directory using a safe staging mechanism.

Environment Setup and Path Initialization for the Tests
>>> from pathlib import Path
>>> from fitzzftw.develtool.testinfra import TestHomeEnvironment


>>> env = TestHomeEnvironment(Path("doc/source/devel/testhome"))
>>> env.setup()
>>> env.input_readonly = True
>>> env.do_not_clean = False

Initialization and Properties

>>> from fitzzftw.patch.ftw_patch import FtwPatch
>>> from argparse import Namespace

1. Preparation and Configuration

To initialize the patcher, we provide an options object which fullfill the ArgParsOptions protocol, containing all necessary settings.

We use the dummy_patch_file created in the setup

>>> Path("patch.diff").touch()
>>> options = Namespace(
...     patch_file=Path("patch.diff"),
...     target_directory=Path("."),
...     strip_count=0,
...     normalize_whitespace=False,
...     ignore_blank_lines=False,
...     ignore_all_whitespace=False,
...     dry_run=False,
...     verbose=0,
...     backup_ext=".bak",
...     backup_path=Path(".")
... )
>>> patcher = FtwPatch(options)
>>> patcher
FtwPatch(backup_ext='.bak', backup_path='.')

2. Executing the Patch (FtwPatch.apply() method)

The apply() method executes the patching logic. It returns 0 if the process was successful.

Using the options defined above

>>> patcher.apply(options)

Verifying Dry-Run Behavior

A key feature of FtwPatch is the ability to simulate changes. When options.dry_run is set to True, the internal staging area is prepared, but no changes are written back to the target files.

Create a test file

>>> test_file = env.copy2cwd("hello.py")

>>> test_file = Path("hello.py")
>>> test_file.write_text("print('Old')\n")
13

Define a patch for this file

>>> _ =env.copy2cwd("hello.patch", "test.patch")

Set up dry_run

>>> options.patch_file = Path("test.patch")
>>> options.dry_run = True
>>> simulation = FtwPatch(options)
>>> simulation.apply(options)

The file remains unchanged

>>> test_file.read_text()
"print('Old')\n"

Inspecting Patcher Properties

Once initialized, the FtwPatch instance provides read-only access to its configuration and the results of the parsing process through various properties.

Basic paths used by the patcher:

Accessing the core paths

>>> patcher.patch_file_path.name
'test.patch'
>>> patcher.target_directory.as_posix()
'.'

strip_count

The strip_count property returns the number of leading path components that are stripped from the file names found in the patch file (ro).

Accessing the strip count configuration

>>> patcher.strip_count
0

The patcher also exposes the normalization settings derived from the options:

>>> patcher.normalize_whitespace
False
>>> patcher.ignore_blank_lines
False
>>> patcher.ignore_all_whitespace
False

Accessing Parsed Data

The parsed_files property provides access to the structured data before or after the patch is applied. This is useful for generating reports or verifying the patch content programmatically.

In our previous setup, we used a patch with one file: ‘hello.py’

>>> len(simulation.parsed_files)
1
>>> diff_file = simulation.parsed_files[0]
>>> diff_file.orig_header.content
'hello.py'
>>> len(diff_file.hunks)
1

Verbosity and Logging

The verbosity level determines how much information is printed to the console during the execution of the apply() method.

>>> patcher.verbose
0

Inspecting the Patcher

You can check the current configuration of an FtwPatch instance through its read-only properties (ro).

Check the configuration via properties

>>> patcher = FtwPatch(options)
>>> patcher.dry_run
True
>>> patcher.verbose
0

Verify paths are handled correctly

>>> isinstance(patcher.patch_file_path, Path)
True

Advanced Configuration

The patcher can handle different whitespace styles. This is useful when dealing with files from different operating systems. Enabling these options changes how the patch is parsed.

Enable whitespace normalization

>>> options.normalize_whitespace = True
>>> advanced_patcher = FtwPatch(options)

The patcher now uses advanced parsing logic

>>> advanced_patcher.apply(options)

Simulation vs. Real Execution

The apply() method is a procedure that performs the patching process. You can switch between a safe simulation and the actual write process.

  1. Simulation Mode (Safety first)

>>> options.dry_run = True
>>> simulation = FtwPatch(options)
>>> simulation.apply(options)
  1. Real Application (Default behavior)

>>> options.dry_run = False
>>> real_patcher = FtwPatch(options)

This executes the final commit to the file system

>>> real_patcher.apply(options)

FtwPatch.apply()

When applying changes, the patcher automatically creates backups of the modified files. If no specific backup directory is provided, the backup is created in the same directory as the original file using an extension.

>>> options.backup_ext = '.bak'
>>> options.backup=True
>>> options
Namespace(patch_file=...Path('test.patch'),
    target_directory=...Path('.'),
    strip_count=0,
    normalize_whitespace=True,
    ignore_blank_lines=False,
    ignore_all_whitespace=False,
    dry_run=False,
    verbose=0,
    backup_ext='.bak',
    backup_path=...Path('.'),
    backup=True)
>>> Path('hello.py').read_text()
"print('New')\n"

Handling Patch Failures (Safety First)

A key feature of FtwPatch is its integrity check. If the context of a patch (a “hunk”) does not exactly match the target file, the process aborts immediately.

This prevents the tool from applying changes to the wrong lines, ensuring your source code remains consistent.

>>> patcher.apply(options)
Traceback (most recent call last):
    ...
fitzzftw.patch.exceptions.PatchParseError: Hunk mismatch at line 1.
... not match the hunk's context.

Verification of Atomicity

Because the patch failed the integrity check, no changes were written to the disk, and no backup file was created. The operation is atomic.

The backup should NOT exist because nothing was changed

>>> backup_file = Path("hello.py.bak")
>>> backup_file.exists()
False

The original file remains untouched

>>> orig_file = Path(patcher._patch_files[0].orig_header.content)
>>> orig_file.read_text()
"print('New')\n"

Error Handling

The FtwPatch class ensures data integrity. If a patch is malformed or files are missing, it raises an exception to prevent partial changes.

Triggering an error with a missing file

>>> options.patch_file = Path("missing.patch")
>>> FtwPatch(options).apply(options)
Traceback (most recent call last):
    ...
FileNotFoundError: Patch file not found at ...Path('missing.patch')

Full Cycle: Patching a Python Source File

To demonstrate the full power of FtwPatch.apply(), we will perform a complete patching cycle: creating a source file, defining a unified diff, and applying it.

  1. Setup the Source File We create a simple Python file with a few lines of code.

>>> env.clean_home()
>>> source_path = env.copy2cwd("app.py")
>>> deleted_path = env.copy2cwd("app_old_config.py", "old_config.py")
  1. Create the Patch File We define a patch that changes the greeting and adds a new function. Notice the use of standard Unified Diff prefixes.

>>> patch_path = env.copy2cwd("changes_multi.diff","changes.diff")
  1. Apply the Patch Now we use FtwPatch to apply these changes. We will enable backup generation to see the safety mechanism in action.

>>> from argparse import Namespace
>>> from datetime import datetime
>>> run_options = Namespace(
...     patch_file=patch_path,
...     target_directory=Path("."),
...     strip_count=1,
...     normalize_whitespace=False,
...     ignore_blank_lines=False,
...     ignore_all_whitespace=False,
...     dry_run=False,
...     verbose=1,
...     backup=True,
...     backup_ext=".orig",
...     dt_now = datetime.now(),
...     backup_path = Path("."),
... )
>>> patcher = FtwPatch(run_options)
>>> patcher.apply(run_options)

Traceback (most recent call last):
    ...
fitzzftw.patch.exceptions.PatchParseError: Hunk starting at line 0 exceeds file bounds. File has 0 lines.
  1. Verify the Results The original file should now contain the new content, and a backup file app.py.orig should exist.

>>> print(source_path.read_text())
def greet():
    print('Hello World')

def farewell():
    print('Goodbye')

if __name__ == '__main__':
    greet()


>>> Path("app.py.orig").exists()
True
>>> "print('Hello')" in Path("app.py.orig").read_text()
True

Copy the patched file to a persistant directory.

>>> _ = env.cwd2doc_inc("app.py")
Before and After
Original Files (Input)
app.py (original)
1def greet():
2    print('Hello')
3
4if __name__ == '__main__':
5    greet()
changes.diff
--- a/app.py
+++ b/app.py
@@ -1,5 +1,9 @@
 def greet():
-    print('Hello')
+    print('Hello World')
 
+def farewell():
+    print('Goodbye')
+
 if __name__ == '__main__':
     greet()
Patched File (Output)
app.py (patched)
1def greet():
2    print('Hello World')
3
4def farewell():
5    print('Goodbye')
6
7if __name__ == '__main__':
8    greet()
>>> Path("old_config.py.orig").exists()
True
>>> deleted_path.exists()
False
>>> Path("utils.py").exists()
True
>>> Path("utils.py.orig").exists()
False
>>> stat2 = PatchStatistics(2)
>>> stat3 = PatchStatistics(3)
>>> stat4 = PatchStatistics(4)
>>> stat4.print()
Runtime: ... s
>>> stat5 = PatchStatistics(5)
>>> for diff_ in patcher.parsed_files:
...     stats.add_file(diff_)
...     stat1.add_file(diff_)
...     stat2.add_file(diff_)
...     stat3.add_file(diff_)
...     stat4.add_file(diff_)
...     stat5.add_file(diff_)
>>> stats.print()
Files processed: 3
Runtime: ... s
>>> stat1.print()
Files processed: 3
Lines processed: 11
Runtime: ... s
>>> stats.lines_added
7
>>> stats.lines_removed
4
>>> stats.files_modified
1
>>> stats.files_created
1
>>> stats.files_deleted
1
>>> colors.switch_to_testmode()
>>> stat2.print()
grn>Files created:  1<reset
ylw>Files modified: 1<reset
red>Files deleted:  1<reset
trm>Runtime: ... s<reset
>>> stat3.print()
grn>Files created:   1<reset
ylw>Files modified:  1<reset
red>Files deleted:   1<reset
trm>Lines processed: 11<reset
trm>Runtime: ... s<reset
>>> stat4.print()
grn>Files created:   b/utils.py<reset
ylw>Files modified:  b/app.py<reset
red>Files deleted:   a/old_config.py<reset
trm>Runtime: ... s<reset
>>> stat5.print()
grn>File created:   b/utils.py<reset
grn>        Lines added: 3<reset
ylw>File modified:   b/app.py<reset
grn>        Lines added: 4<reset
red>        Lines deleted: 1<reset
red>File deleted:   a/old_config.py<reset
red>        Lines deleted: 3<reset
trm>Runtime: ... s<reset
>>> stat6 = PatchStatistics(6)
>>> for diff_ in patcher.parsed_files:
...     stat6.add_file(diff_)
ylw>MODIFIED b/app.py (+4, -1)<reset
grn>CREATED  b/utils.py (+3, -0)<reset
red>DELETED  a/old_config.py (+0, -3)<reset
>>> env.clean_home()
>>> source_path = env.copy2cwd("app.py")
>>> deleted_path = env.copy2cwd("app_old_config.py", "old_config.py")
>>> patch_path = env.copy2cwd("changes_multi.diff","changes.diff")
>>> run_options.backup_path=Path("my_backup")

>> run_options

>>> patcher_run = FtwPatch(run_options)
>>> patcher_run.run()
trm>Files processed: 3
Lines processed: 11<reset
trm>Runtime: ... s<reset

>> Path(“my_backup/app.py.orig”).exists() True >>> “print(‘Hello’)” in Path(“my_backup/app.py.orig”).read_text() True

>>> Path("my_backup/old_config.py.orig").exists()
True
Cleanup Testenvironment
>>> env.input_readonly=False

>>> env.do_not_clean = False

>>> env.teardown()

>>> env.clean_home()