File Coverage

blib/lib/Module/Patch.pm
Criterion Covered Total %
statement 138 172 80.2
branch 58 110 52.7
condition 17 43 39.5
subroutine 13 14 92.8
pod 1 3 33.3
total 227 342 66.3


line stmt bran cond sub pod time code
1             package Module::Patch;
2              
3 2     2   775936 use 5.010001;
  2         9  
4 2     2   12 use strict 'subs', 'vars';
  2         4  
  2         137  
5 2     2   15 use warnings;
  2         4  
  2         162  
6 2     2   3914 use Log::ger;
  2         134  
  2         13  
7              
8 2     2   1673 use Monkey::Patch::Action qw();
  2         9388  
  2         60  
9 2     2   1160 use Package::Stash;
  2         18828  
  2         106  
10 2     2   893 use Package::Util::Lite qw(package_exists);
  2         1398  
  2         1863  
11              
12             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
13             our $DATE = '2024-04-17'; # DATE
14             our $DIST = 'Module-Patch'; # DIST
15             our $VERSION = '0.278'; # VERSION
16              
17             our @EXPORT_OK = qw(patch_package);
18              
19             sub is_loaded {
20 6     6 0 28 my $mod = shift;
21              
22 6         25 (my $mod_pm = "$mod.pm") =~ s!::!/!g;
23 6 100       43 exists($INC{$mod_pm}) && $INC{$mod_pm};
24             }
25              
26             my%loaded_by_us;
27              
28             sub import {
29 6     6   18812 my $self = shift;
30              
31 6         24 my $caller = caller;
32              
33 6 50       240 if ($self eq __PACKAGE__) {
34             # we are not subclassed, provide exports
35 0         0 for my $exp (@_) {
36             die "$exp is not exported by ".__PACKAGE__
37 0 0       0 unless grep { $_ eq $exp } @EXPORT_OK;
  0         0  
38 0         0 *{"$caller\::$exp"} = \&{$exp};
  0         0  
  0         0  
39             }
40             } else {
41             # patch already applied, ignore
42 6 50       9 return if ${"$self\::handles"};
  6         85  
43              
44             # get patch data from patch_data()
45 6 100       12 unless (${"$self\::patch_data_cached"}) {
  6         34  
46 3         14 ${"$self\::patch_data_cached"} = $self->patch_data;
  3         48  
47             }
48              
49             # we are subclassed, patch caller with patch data
50 6         17 my %opts = @_;
51              
52 6         12 my $load;
53 6 50       24 if (exists $opts{-load_target}) {
54 0         0 $load = $opts{-load_target};
55 0         0 delete $opts{-load_target};
56             }
57 6   33     21 $load //= ${"$self\::patch_data_cached"}->{-load_target};
  6         64  
58 6   50     43 $load //= 1;
59              
60 6         10 my $force;
61 6 100       19 if (exists $opts{-force}) {
62 1         4 $force = $opts{-force};
63 1         4 delete $opts{-force};
64             }
65 6   66     27 $force //= ${"$self\::patch_data_cached"}->{-force};
  5         23  
66 6   100     21 $force //= 0;
67              
68 6         9 my $warn;
69 6 50       14 if (exists $opts{-warn_target_loaded}) {
70 0         0 $warn = $opts{-warn_target_loaded};
71 0         0 delete $opts{-warn_target_loaded};
72             }
73 6   33     17 $warn //= ${"$self\::patch_data_cached"}->{"-warn_target_loaded"};
  6         29  
74 6   50     22 $warn //= 1;
75              
76 6 50       9 my $pdata = ${"$self\::patch_data_cached"} or
  6         24  
77             die "BUG: $self: No patch data supplied";
78 6   50     19 my $v = $pdata->{v} // 1;
79 6         11 my $curv = 3;
80 6 50 33     34 if ($v == 1 || $v == 2) {
    50          
81 0         0 my $mpv;
82 0 0       0 if ($v == 1) {
    0          
83 0         0 $mpv = "0.06 or earlier";
84             } elsif ($v == 2) {
85 0         0 $mpv = "0.07-0.09";
86             }
87 0   0     0 die "$self ".( ${"$self\::VERSION" } // "?" ).
  0   0     0  
88             " requires Module::Patch $mpv (patch_data format v=$v),".
89             " this is Module::Patch ".($Module::Patch::VERSION // '?').
90             " (v=$curv), please install an older version of ".
91             "Module::Patch or upgrade $self";
92             } elsif ($v == 3) {
93             # ok, current version
94             } else {
95 0         0 die "BUG: $self: Unknown patch_data format version ($v), ".
96             "only v=$curv supported by this version of Module::Patch";
97             }
98              
99 6         13 my $target = $self;
100 6 50       56 $target =~ s/(?<=\w)::[Pp]atch::\w+$//
101             or die "BUG: $self: Bad patch module name '$target', it should ".
102             "end with '::Patch::YourCategory'";
103              
104 6 100       20 if (is_loaded($target)) {
105 5 50       16 if (!$loaded_by_us{$target}) {
106 0 0 0     0 if ($load && $warn) {
107 0         0 warn "$target is loaded before ".__PACKAGE__.", this is ".
108             "not recommended since $target might export subs ".
109             "before " . __PACKAGE__." gets the chance to patch " .
110             "them";
111             }
112             }
113             } else {
114 1 50       3 if ($load) {
115 1     1   102 eval "package $caller; use $target"; ## no critic: BuiltinFunctions::ProhibitStringyEval
  1         553  
  1         47  
  1         29  
116 1 50       6 die if $@;
117 1         3 $loaded_by_us{$target}++;
118             } else {
119 0 0       0 if ($warn) {
120 0         0 warn "$target does not exist and we are told not to load ".
121             "it, skipped patching";
122             }
123 0         0 return;
124             }
125             }
126              
127             # read patch module's configs
128 2     2   16 no warnings 'once';
  2         7  
  2         3308  
129 6   100     24 my $pcdata = $pdata->{config} // {};
130 6         12 my $config = \%{"$self\::config"};
  6         28  
131 6         28 while (my ($k, $v) = each %$pcdata) {
132 6 50       24 die "Invalid configuration defined by $self\::patch_data(): ".
133             "$k: must start with dash" unless $k =~ /\A-/;
134 6         16 $config->{$k} = $v->{default};
135 6 100       87 if (exists $opts{$k}) {
136 1         2 $config->{$k} = $opts{$k};
137 1         5 delete $opts{$k};
138             }
139             }
140              
141 6 100       18 if (keys %opts) {
142 1         24 die "$self: Unknown option(s): ".join(", ", keys %opts);
143             }
144              
145 5 50       14 if ($pdata->{after_read_config}) {
146 0         0 $pdata->{after_read_config}->();
147             }
148              
149 5 50       13 if ($pdata->{before_patch}) {
150 0         0 $pdata->{before_patch}->();
151             }
152              
153 5         27 log_trace "Module::Patch: patching $target with $self ...";
154 4         20 ${"$self\::handles"} = patch_package(
155             $target, $pdata->{patches},
156 5   33     67 {force=>$force, patch_module=>ref($self) || $self});
157              
158 4 50       39 if ($pdata->{after_patch}) {
159 0         0 $pdata->{after_patch}->();
160             }
161              
162             }
163             }
164              
165             sub unimport {
166 4     4   10823 my $self = shift;
167              
168 4 50       16 if ($self eq __PACKAGE__) {
169             # we are not subclassed, do nothing
170             } else {
171 4 50       6 my $pdata = ${"$self\::patch_data_cached"} or
  4         50  
172             die "BUG: $self: No patch data supplied";
173              
174 4 50       20 if ($pdata->{before_unpatch}) {
175 0         0 $pdata->{before_unpatch}->();
176             }
177              
178 4         16 my $handles = ${"$self\::handles"};
  4         15  
179 4         20 log_trace "Module::Patch: Unpatching $self ...";
180 4         15 undef ${"$self\::handles"};
  4         16  
181             # do we need to undef ${"$self\::config"}?, i'm thinking not really
182              
183 4 50       29 if ($pdata->{after_unpatch}) {
184 0         0 $pdata->{after_unpatch}->();
185             }
186              
187             }
188             }
189              
190             sub patch_data {
191 0     0 0 0 die "BUG: patch_data() should be provided by subclass";
192             }
193              
194             sub patch_package {
195 5     5 1 48 my ($package0, $patches_spec, $opts) = @_;
196 5   50     13 $opts //= {};
197              
198 5         10 my $handles = {};
199 5 50       21 for my $target (ref($package0) eq 'ARRAY' ? @$package0 : ($package0)) {
200 5 50       21 die "FATAL: Target module '$target' not loaded"
201             unless package_exists($target);
202 5         96 my $target_version = ${"$target\::VERSION"};
  5         18  
203 5         5 my $target_subs;
204              
205 5         10 my $i = 0;
206             PATCH:
207 5         11 for my $pspec (@$patches_spec) {
208 7         13 my $act = $pspec->{action};
209 7 50       28 my $errp = ($opts->{patch_module} ? "$opts->{patch_module}: ":"").
210             "patch[$i]"; # error prefix
211 7 50       20 $act or die "BUG: $errp: no action supplied";
212 7 50       38 $act =~ /\A(wrap|add|replace|add_or_replace|delete)\z/ or die
213             "BUG: $errp: action '$pspec->{action}' unknown";
214 7 50       19 if ($act eq 'delete') {
215 0 0       0 $pspec->{code} and die "BUG: $errp: for action 'delete', ".
216             "code must not be supplied";
217             } else {
218 7 50       17 $pspec->{code} or die "BUG: $errp: code not supplied";
219             }
220              
221             my $sub_names = ref($pspec->{sub_name}) eq 'ARRAY' ?
222 7 50       25 [@{ $pspec->{sub_name} }] : [$pspec->{sub_name}];
  0         0  
223 7         16 for (@$sub_names) {
224 7 50       19 $_ = qr/.*/ if $_ eq ':all';
225 7 50       15 $_ = qr/^_/ if $_ eq ':private';
226 7 50       15 $_ = qr/^[^_]/ if $_ eq ':public';
227 7 50       20 die "BUG: $errp: unknown tag in sub_name $_" if /^:/;
228             }
229              
230 7         13 my @s;
231 7         10 for my $sub_name (@$sub_names) {
232 7 50       15 if (ref($sub_name) eq 'Regexp') {
233 0 0       0 unless ($target_subs) {
234 0         0 $target_subs = [Package::Stash->new($target)->list_all_symbols("CODE")];
235             }
236 0         0 for (@$target_subs) {
237 0 0 0     0 push @s, $_ if $_ !~~ @s && $_ =~ $sub_name;
238             }
239             } else {
240 7         47 push @s, $sub_name;
241             }
242             }
243              
244 7 100 66     36 unless (!defined($pspec->{mod_version}) ||
245             $pspec->{mod_version} eq ':all') {
246 5 50 33     21 defined($target_version) && length($target_version)
247             or die "FATAL: Target package '$target' does not have ".
248             "\$VERSION";
249 5         9 my $mod_versions = $pspec->{mod_version};
250 5 100       33 $mod_versions = ref($mod_versions) eq 'ARRAY' ?
251             [@$mod_versions] : [$mod_versions];
252 5         12 for (@$mod_versions) {
253 7 50       19 $_ = qr/.*/ if $_ eq ':all';
254 7 50       18 die "BUG: $errp: unknown tag in mod_version $_"
255             if /^:/;
256             }
257              
258 5 100       13 unless (grep {
259 7 50       39 ref($_) eq 'Regexp' ? $target_version =~ $_ : $target_version eq $_
260             } @$mod_versions) {
261             warn "$errp: Target module version $target_version ".
262             "does not match [".join(", ", @$mod_versions)."], ".
263             ($opts->{force} ?
264 2 100       114 "patching anyway (force)":"skipped"). ".";
265 2 100       37 next PATCH unless $opts->{force};
266             }
267             }
268              
269 6         15 for my $s (@s) {
270             #log_trace("Patching %s ...", $s);
271             $handles->{"$target\::$s"} =
272             Monkey::Patch::Action::patch_package(
273 6         25 $target, $s, $act, $pspec->{code});
274             }
275              
276 5         560 $i++;
277             } # for $pspec
278             } # for $target
279 4         10 $handles;
280             }
281              
282             1;
283             # ABSTRACT: Patch package with a set of patches
284              
285             __END__