Skip to content

Commit 6efbeed

Browse files
committed
new readme
1 parent 23e3336 commit 6efbeed

2 files changed

Lines changed: 73 additions & 52 deletions

File tree

README.md

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,31 +62,47 @@ Benefits:
6262
- no need to copy parent callers or entire dependency chains
6363
- easier validation in live test workflows
6464

65-
## Core API
65+
## Test-hotfix use case
6666

67-
- `inject_patch(pkg, patch_list)`: overwrite functions inside a package namespace or environment
68-
- `undo_patch(pkg, names = NULL)`: restore backed-up originals for patched bindings
69-
- `test_patched_dir(pkg, test_path)`: run `testthat` tests against the modified namespace
70-
- `apply_hotfix_file(file, pkg = NULL)`: load a hotfix script and inject the included patch list
67+
`hotpatchR` is built for the common legacy scenario where a package version is fixed in place
68+
and you want to validate a runtime correction using existing package tests.
69+
With `inject_patch()`, you can replace a broken internal function.
7170

72-
## Example usage
71+
A typical test-hotfix flow looks like this:
7372

7473
```r
7574
library(hotpatchR)
7675

77-
pkg_env <- new.env()
78-
pkg_env$broken_child <- function() "I am broken"
79-
pkg_env$parent_caller <- function() pkg_env$broken_child()
80-
lockEnvironment(pkg_env)
81-
lockBinding("broken_child", pkg_env)
82-
lockBinding("parent_caller", pkg_env)
76+
baseline <- hotpatchR:::dummy_parent_func("test")
77+
print(baseline)
78+
#> "Parent output -> I am the BROKEN child. Input: test"
79+
80+
inject_patch(
81+
pkg = "hotpatchR",
82+
patch_list = list(dummy_child_func = function(x) {
83+
paste("I am the FIXED child! Input:", x)
84+
})
85+
)
86+
87+
patched_result <- hotpatchR:::dummy_parent_func("test")
88+
print(patched_result)
89+
#> "Parent output -> I am the FIXED child! Input: test"
8390

84-
inject_patch(pkg_env, list(broken_child = function() "I am FIXED"))
8591

86-
pkg_env$parent_caller()
87-
#> "I am FIXED"
92+
#Eventually, you can reverse the patch to restore the original behavior if needed:
93+
undo_patch(pkg = "hotpatchR", names = "dummy_child_func")
94+
restored_result <- hotpatchR:::dummy_parent_func("test")
95+
print(restored_result)
96+
#> "Parent output -> I am the BROKEN child. Input: test"
8897
```
8998

99+
## Core API
100+
101+
- `inject_patch(pkg, patch_list)`: overwrite functions inside a package namespace or environment
102+
- `undo_patch(pkg, names = NULL)`: restore backed-up originals for patched bindings
103+
- `test_patched_dir(pkg, test_path)`: run `testthat` tests against the modified namespace
104+
- `apply_hotfix_file(file, pkg = NULL)`: load a hotfix script and inject the included patch list
105+
90106
## How it works
91107

92108
`inject_patch()` unlocks the binding inside the target namespace, assigns the replacement function,
@@ -95,5 +111,4 @@ normal internal function resolution.
95111

96112
## Vignettes and docs
97113

98-
See `vignettes/hotpatchR-intro.Rmd` for a deeper explanation of the namespace trap,
99-
rollback workflows, and example hotfix scripts.
114+
See `vignettes/hotpatchR-intro.Rmd` for a deeper explanation of the namespace trap, rollback workflows, and example hotfix scripts.

vignettes/hotpatchR-intro.Rmd

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,53 +20,57 @@ This vignette explains the `hotpatchR` design and why runtime namespace patching
2020

2121
## The legacy package lockdown problem
2222

23-
A loaded R package lives in a locked namespace. That means internal functions are resolved from inside the package bubble.
24-
When one internal function calls another, R does not look in the global environment.
23+
A loaded R package lives in a locked namespace. Internal functions are resolved from inside the package bubble, so calling a hidden helper from the global environment does not change the package's internal behavior.
2524

26-
So if you fix `a_freq_j()` in the global environment, `tt_to_tlgrtf()` inside the package still calls the original broken version.
27-
28-
This is the namespace trap that makes legacy hotfixing so difficult.
25+
This is the namespace trap that makes legacy hotfixing difficult: a visible exported function may still invoke a broken internal helper even after you have sourced a fixed version elsewhere.
2926

3027
## Why the global workaround fails
3128

3229
The usual workaround is:
3330

34-
- identify the broken function
35-
- find all parent callers in the package
36-
- copy the broken function and every dependent caller into a hotfix script
31+
- identify the broken internal function
32+
- find every package caller that depends on it
33+
- copy the broken internals into a hotfix script
3734
- source the script into the global environment
38-
- run tests to confirm the patched path
35+
- run tests and hope the package resolves the new bindings
3936

40-
This is painful because a bug in one internal function can require copying many internal callers, even when only one implementation actually needs to change.
37+
That workflow is brittle because a bug in one hidden helper can force you to patch many callers, even when only one implementation needs to change.
4138

4239
## hotpatchR philosophy
4340

4441
Instead of pulling functions out into the global environment, `hotpatchR` performs surgical edits inside the package namespace.
4542
That means:
4643

47-
- fix only the broken function
48-
- internal callers automatically resolve to the updated implementation
44+
- fix only the broken internal function
45+
- exported callers automatically resolve to the updated implementation
4946
- the package remains loaded and otherwise unchanged
5047

5148
## Basic workflow
5249

50+
The package includes a real example of this pattern with an exported parent function and an internal child helper.
51+
5352
```{r example}
54-
# Simulate a locked package environment.
55-
pkg_env <- new.env()
56-
pkg_env$broken_child <- function() "I am broken"
57-
pkg_env$parent_caller <- function() pkg_env$broken_child()
58-
lockEnvironment(pkg_env)
59-
lockBinding("broken_child", pkg_env)
60-
lockBinding("parent_caller", pkg_env)
61-
62-
# Apply the patch.
63-
fixed_child <- function() "I am FIXED"
64-
inject_patch(pkg_env, list(broken_child = fixed_child))
65-
66-
pkg_env$parent_caller()
53+
library(hotpatchR)
54+
55+
baseline <- dummy_parent_func("test")
56+
print(baseline)
57+
#> "Parent output -> I am the BROKEN child. Input: test"
58+
59+
inject_patch(
60+
pkg = "hotpatchR",
61+
patch_list = list(
62+
dummy_child_func = function(x) {
63+
paste("I am the FIXED child! Input:", x)
64+
}
65+
)
66+
)
67+
68+
patched_result <- dummy_parent_func("test")
69+
print(patched_result)
70+
#> "Parent output -> I am the FIXED child! Input: test"
6771
```
6872

69-
If `pkg_env` were a real package namespace, the same internal caller would now use the patched `broken_child()`.
73+
This shows the real package behavior: `dummy_parent_func()` is exported, while `dummy_child_func()` is a hidden internal helper that gets replaced inside the `hotpatchR` namespace.
7074

7175
## How inject_patch works
7276

@@ -77,39 +81,41 @@ If `pkg_env` were a real package namespace, the same internal caller would now u
7781
3. assigns the replacement function into that environment
7882
4. re-locks the binding
7983

80-
Because the replacement function's parent environment is set to the target namespace, it can still call other internal objects from the same package.
84+
Because the replacement function can be defined with the package namespace as its parent, it still has access to the package's internal helpers.
8185

8286
## Rolling back a patch
8387

8488
If you need to restore the original binding, `undo_patch()` reverses the previous change.
8589

8690
```{r undo-example}
87-
undo_patch(pkg_env, "broken_child")
88-
pkg_env$parent_caller()
91+
undo_patch(pkg = "hotpatchR", names = "dummy_child_func")
92+
restored_result <- dummy_parent_func("test")
93+
print(restored_result)
94+
#> "Parent output -> I am the BROKEN child. Input: test"
8995
```
9096

9197
## Hotfix scripts
9298

93-
`apply_hotfix_file()` is a convenience wrapper for scripting hotfix application. A compatible hotfix script should define:
99+
`apply_hotfix_file()` is a convenience wrapper for scripted hotfix application. A compatible hotfix file should define:
94100

95101
- `pkg` (optional, if not passed explicitly)
96102
- `patch_list`, a named list of replacement functions
97103

98-
Example hotfix file:
104+
Example hotfix file for this package:
99105

100106
```r
101-
pkg <- "junco"
107+
pkg <- "hotpatchR"
102108
patch_list <- list(
103-
a_freq_j = function(...) {
104-
# fixed implementation
109+
dummy_child_func = function(x) {
110+
paste("I am the FIXED child! Input:", x)
105111
}
106112
)
107113
```
108114

109115
Then apply it with:
110116

111117
```r
112-
apply_hotfix_file("dev/junco_hotfix_v0-1-1.R")
118+
apply_hotfix_file("dev/hotpatchR_hotfix.R")
113119
```
114120

115121
## Next steps

0 commit comments

Comments
 (0)