File Coverage

blib/lib/Module/Patch.pm
Criterion Covered Total %
statement 132 166 79.5
branch 58 110 52.7
condition 13 34 38.2
subroutine 13 14 92.8
pod 1 3 33.3
total 217 327 66.3


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