NAME
Module::Patch - Patch package with a set of patches
VERSION
This document describes version 0.277 of Module::Patch (from Perl distribution Module-Patch), released on 2024-04-17.
SYNOPSIS
In this example, we're patching HTTP::Tiny to add automatic retrying. Module::Patch can be used in two ways: either directly or via creating your own patch module based on Module::Patch.
Using Module::Patch directly
use Module::Patch qw(patch_package);
use Log::ger;
my $handle = patch_package('HTTP::Tiny', [
{
action => 'wrap',
mod_version => qr/^0\.*/,
sub_name => 'request',
code => sub {
my $ctx = shift;
my $orig = $ctx->{orig};
my ($self, $method, $url) = @_;
my $retries = 0;
my $res;
while (1) {
$res = $orig->(@_);
return $res if $res->{status} !~ /\A[5]/; # only retry 5xx responses
last if $retries >= $config{-retries};
$retries++;
log_trace "Failed requesting $url ($res->{status} - $res->{reason}), retrying" .
($config{-delay} ? " in $config{-delay} second(s)" : "") .
" ($retries of $config{-retries}) ...";
sleep $config{-delay};
}
$res;
};
},
]);
# do stuffs with HTTP::Tiny
my $res = HTTP::Tiny->new->request(...);
...
# unpatch, restore original subroutines/methods
undef $handle;
Creating patch module by subclassing Module::Patch
In your patch module lib/HTTP/Tiny/Patch/Retry.pm:
package HTTP::Tiny::Patch::Retry;
use parent qw(Module::Patch);
use Log::ger;
our %config;
sub patch_data {
return {
v => 3,
config => {
-delay => {
summary => 'Number of seconds to wait between retries',
schema => 'nonnegint*',
default => 2,
},
-retries => {
summary => 'Maximum number of retries to perform consecutively on a request (0=disable retry)',
schema => 'nonnegint*',
default => 3,
},
},
patches => [
{
action => 'wrap',
mod_version => qr/^0\.*/,
sub_name => 'request',
code => sub {
my $ctx = shift;
my $orig = $ctx->{orig};
my ($self, $method, $url) = @_;
my $retries = 0;
my $res;
while (1) {
$res = $orig->(@_);
return $res if $res->{status} !~ /\A[5]/; # only retry 5xx responses
last if $retries >= $config{-retries};
$retries++;
log_trace "Failed requesting $url ($res->{status} - $res->{reason}), retrying" .
($config{-delay} ? " in $config{-delay} second(s)" : "") .
" ($retries of $config{-retries}) ...";
sleep $config{-delay};
}
$res;
};
},
], # patched
# other Module::Patch import options can be specified here
# -load_target => 1,
# -warn_target_loaded => 1,
# -force => 0,
};
}
1;
Using your patch module in Perl:
use HTTP::Tiny::Patch::Retry
-retries => 4, # optional, default is 3 as per configuration
-delay => 5, # optional, default is 2 as per configuration
;
# do stuffs with HTTP::Tiny
my $res = HTTP::Tiny->new->request(...);
# unpatch, restore original subroutines/methods in target module (HTTP::Tiny)
HTTP::Tiny::Patch::Retry->unimport;
To patch locally:
use HTTP::Tiny::Patch::Retry ();
sub get_data {
HTTP::Tiny::Patch::Retry->import;
my $res = HTTP::TIny->new->request(...);
HTTP::Tiny::Patch::Retry->unimport;
$res;
}
Using your patch module on the command-line:
% perl -MHTTP::Tiny::Patch::Retry -E'my $res = HTTP::Tiny->new->request(...); ...'
DESCRIPTION
Module::Patch is basically a convenient way to define and bundle a set of patches. Actual patching is done by Monkey::Patch::Action, which provides lexically scoped patching.
There are two ways to use this module:
subclass it
This is used for convenient bundling of patches. You create a patch module (a module that monkey-patches other module by adding/replacing/wrapping/deleting subroutines of target module) by subclassing Module::Patch and providing the patches specification in patch_data() method.
Patch module should be named Some::Module::Patch::YourCategory. YourCategory should be a keyword or phrase (verb + obj) that describes what the patch does. For example, HTTP::Daemon::Patch::IPv6, LWP::UserAgent::Patch::LogResponse.
Patch module should be use()'d, or require()'d + import()'ed instead of just require()'d, because the patching is done in import().
require/import it directly
Module::Patch provides patch_package which is the actual routine to do the patching.
PATCH DATA SPECIFICATION
Patch data must be stored in patch_data()
subroutine. It must be a DefHash (i.e. a regular Perl hash) with the following known properties:
v => int
Must be 3 (current version).
patches => array
Will be passed to
patch_package()
.config => hash
A hash of name and config specifications. Config specification is another DefHash and can contain the following properties:
schema
(a Sah schema),default
(default value).after_read_config => coderef
A hook to run after patch module is imported and configuration has been read.
before_patch => coderef
after_patch => coderef
before_unpatch => coderef
after_unpatch => coderef
FUNCTIONS
import
If imported directly, will export @exports as arguments and export requested symbols.
If imported from subclass, will take %opts as arguments and run patch_package() on caller package. %opts include:
-load_target => BOOL (default 1)
Load target modules. Set to 0 if package is already defined in other files and cannot be require()-ed.
-warn_target_loaded => BOOL (default 1)
If set to false, do not warn if target modules are loaded before the patch module. By default, it warns to prevent users making the mistake of importing subroutines from target modules before they are patched.
-force => BOOL
Will be passed to patch_package's \%opts.
patch_package
Usage:
my $handle = patch_package($package, $patches_spec, \%opts);
Patch target package $package
with a set of patches.
$patches_spec
is an arrayref containing a series of patches specifications. Each patch specification is a hashref containing these keys: action
(string, required; either 'wrap', 'add', 'replace', 'add_or_replace', 'delete'), mod_version
(string/regex or array of string/regex, can be ':all' to mean all versions; optional; defaults to ':all'). sub_name
(string/regex or array of string/regex, subroutine(s) to patch, can be ':all' to mean all subroutine, ':public' to mean all public subroutines [those not prefixed by _
], ':private' to mean all private), code
(coderef, not required if action
is 'delete').
Die if there is conflict with other patch modules, for example if target module has been patched 'delete' and another patch wants to 'wrap' it.
Known options:
force => BOOL (default 0)
Force patching even if target module version does not match. The default is to warn and skip patching.
FAQ
Why patch? Why not subclass the target module?
Sometimes the target module is not easily subclassable or at all. But even if the target module is subclassable, all client code must explicitly use the subclass. Patching allows us to modify behavior of a target module without changing any client code that use that module.
Why create a patch module? Why not submit patches to the module's author?
Not all patches are potentials for inclusion into the upstream (target module). But even if a patch is, creating and releasing a patch module can get you something working sooner (while you wait for the original author to respond to your patch, if ever).
This module does not work! The target module does not get patched!
It probably does. Some of the common mistakes are:
Not storing the handle
You do this:
patch_package(...);
instead of this:
my $handle = patch_package(...);
Since the handle is used to revert the patch, if you do not store
$handle
, you are basically patching and immediately reverting the patch.Importing before patching
If
Target::Module
exports symbols, and you patch one of the default exports, the users need to patch before importing. Otherwise he/she will get the unpatched version. For example, this won't work:use Target::Module; # by default export foo use Target::Module::Patch::Foo; # patches foo foo(); # user gets the unpatched version
While this does:
use Target::Module::Patch::Foo; # patches foo use Target::Module; # by default export foo foo(); # user gets the patched version
Since 0.16, Module::Patch already warns this (unless
-load_target
or-warn_target_loaded
is set to false).
HOMEPAGE
Please visit the project's homepage at https://metacpan.org/release/Module-Patch.
SOURCE
Source repository is at https://github.com/perlancar/perl-Module-Patch.
SEE ALSO
Pod::Weaver::Plugin::ModulePatch
Some examples of patch modules that use Module::Patch by subclassing it: Net::HTTP::Methods::Patch::LogRequest, LWP::UserAgent::Patch::HTTPSHardTimeout, HTTP::Tiny::Patch::Cache, HTTP::Tiny::Patch::Retry.
Some examples of modules that use Module::Patch directly: Log::ger::For::Class.
AUTHOR
perlancar <perlancar@cpan.org>
CONTRIBUTOR
Steven Haryanto <stevenharyanto@gmail.com>
CONTRIBUTING
To contribute, you can send patches by email/via RT, or send pull requests on GitHub.
Most of the time, you don't need to build the distribution yourself. You can simply modify the code, then test via:
% prove -l
If you want to build the distribution (e.g. to try to install it locally on your system), you can install Dist::Zilla, Dist::Zilla::PluginBundle::Author::PERLANCAR, Pod::Weaver::PluginBundle::Author::PERLANCAR, and sometimes one or two other Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond that are considered a bug and can be reported to me.
COPYRIGHT AND LICENSE
This software is copyright (c) 2024, 2019, 2018, 2017, 2016, 2015, 2014, 2013, 2012 by perlancar <perlancar@cpan.org>.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
BUGS
Please report any bugs or feature requests on the bugtracker website https://rt.cpan.org/Public/Dist/Display.html?Name=Module-Patch
When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature.