The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Package::Butcher - When you absolutely have to load that damned package.

ALPHA CODE

You've been warned. It also has an embarrassingly poor test suite. It was hacked together in an emergency while sitting in a hospital waiting for my daughter to be born. Sue me.

VERSION

Version 0.02

SYNOPSIS

    my $butcher = Package::Butcher->new(
        {
            package     => 'Dummy',
            do_not_load => [qw/Cannot::Load Cannot::Load2 NoSuch::List::MoreUtils/],
            predeclare  => 'uniq',
            subs => {
                this     => sub { 7 },
                that     => sub { 3 },
                existing => sub { 'replaced existing' },
            },
            method_chains => [
                [
                    'Cannot::Load' => qw/foo bar baz this that/ => sub {
                        my $args = join ', ' => @_;
                        return "end chain: $args";
                    },
                ],
            ],
        }
    );
    $butcher->use(@optional_import_list);

DESCRIPTION

Sometimes you need to load a module which won't otherwise load. Unit testing is a good reason. Unfortunately, some modules are just very, very difficult to load. This module is a nasty hack with a name designed to make this clear. It's here to provide a standard set of tools to let you load these problem modules.

USAGE

To use this module, let's consider the following awful module:

    package Dummy;

    use strict;
    use Cannot::Load;
    use NoSuch::List::MoreUtils 'uniq';
    use DBI;

    use base 'Exporter';
    our @EXPORT_OK = qw(existing);

    sub existing { 'should never see this' }

    # this strange construct forces a syntax error
    sub filter {
        uniq map {lc} split /\W+/, shift;
    }

    sub employees {
        my @connect =
          ( 'dbi:Pg:dbname=ourdb', '', '', { AutoCommit => 0 } );
        return DBI->connect(@connect)
          ->selectall_arrayref(
            'SELECT id, name, position FROM employees ORDER BY id');
    }

    sub recipes {
        my @connect = ( 'dbi:Pg:dbname=ourdb', '', '', { AutoCommit => 0 } );
        return DBI->connect(@connect)
          ->selectall_arrayref('SELECT id, name FROM recipes');
    } 

    1;

You probably cannot load this. You don't have Cannot::Load or NoSuch::List::MoreUtils available. What's worse, even if you try to stub them out and fake this, the employees and recipes methods might be frustrating. We'll use this as an example of how to use Package::Butcher.

METHODS

new

The constructor for Package::Butcher takes a hashref with several allowed keys. For example, the following will allow the Dummy package above to load:

    my $dummy = Package::Butcher->new({
        package => 'Dummy',
        do_not_load =>
          [qw/Cannot::Load NoSuch::List::MoreUtils DBI/],
        predeclare => 'uniq',
        subs       => {
            existing       => sub { 'replaced existing' },
            reverse_string => sub {
                my $arg = shift;
                return scalar reverse $arg;
            },
        },
        method_chains => [
            [
                'Cannot::Load' => qw/foo bar baz this that/ => sub {
                    my $args = join ', ' => @_;
                    return "end chain: $args";
                },
            ],
            [
                'DBI' => qw/connect selectall_arrayref/ => sub {
                    my $sql = shift;
                    return (
                        $sql =~ /\brecipes\b/
                        ? [
                            [qw/1 bob secretary/], 
                            [qw/2 alice ceo/],
                            [qw/3 ovid idiot/],
                          ]
                        : [ [ 1, 'Tartiflette' ], [ 2, 'Eggs Benedict' ], ];
                 },
             ],
        ],
    });

Here are the allowed keys to the constructor:

  • package

    The name of the package to be butchered.

     package => 'Hard::To::Load::Package'
  • do_not_load

    Packages which must not be loaded. This is useful when there are a bunch of use or require statements in the code which cause the target code to try and load packages which may not be loadable.

     do_not_load => [
        'Apache::Never::Loads',
        'Module::I::Do::Not::Have::Installed',
        'Win32::Anything',
     ]
  • predeclare

    Sometimes you need to simply predeclare a method or subroutine to ensure it parses correctly, even if you don't need to execute that function (for example, if you're replacing a subroutine which contains the offending code). To do this, you can simply "predeclare a function or arrayref of functions with optional prototypes.

     predeclare => [ 'uniq (@)', 'some_other_function' ]
  • subs

    This should point to a hashref of subroutine names and sub bodies. These will be added to the package, overwriting any subroutines already there:

     subs => {
         existing       => sub { 'replaced existing' },
         reverse_string => sub {
             my $arg = shift;
             return scalar reverse $arg;
         },
     },

    Note that any subroutinine listed in the subs section will automatically be predeclared.

  • method_chains

    Method "chains" are frequent in bad code (and even in some good code). This is when you see a class with a list of chained methods getting called. For example:

     return DBI->connect(@connect)
       ->selectall_arrayref(
         'SELECT id, name, position FROM employees ORDER BY id');

    The butcher allows you to declare a method chain and a subref which will be executed. The structure is like this:

     method_chains => [
        [ $class1, @list_of_methods1, sub { @body } ],
        [ $class2, @list_of_methods2, sub { @body } ],
        [ $class3, @list_of_methods3, sub { @body } ],
     ],

    For the DBI example above, assuming this was the only method chain in the code, you would have something like:

     method_chains => [
        [ 'DBI', qw/connect selectall_arrayref/, \&some_sub ],
     ],

    See Package::Butcher::Inflator code to see how this works.

  • import_on_use

    This defaults to false and you should hopefully not need it.

    As a general rule, if you call $butcher->use, the package's import method will be called after you use the class to allow us to inject the new code before importing. This means that if a class exports a 'foo' method and you've replaced it with your own, you are generally guaranteed to get your replacement when you call:

     $butcher->use('foo');

    However, if you class requires that the import method be called at the at time the class is "use"d, then you can specify this in the constructor:

     import_on_use => 1,

use

 my $butcher = Package::Butcher->new({ package ... });
 $butcher->use(@import_list);

Once constructed, this method will "use" the package in question. You may pass it the same import list that the package you're butchering takes. Note that if you override import, you're on your own.

require

 my $butcher = Package::Butcher->new({ package ... });
 $butcher->require;

Like use, but does a require.

AUTHOR

Curtis 'Ovid' Poe, <ovid at cpan.org>

BUGS

Please report any bugs or feature requests to bug-package-butcher at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Package-Butcher. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Package::Butcher

You can also look for information at:

ACKNOWLEDGEMENTS

Flavio Glock for help with a parsing error.

LICENSE AND COPYRIGHT

Copyright 2011 Curtis 'Ovid' Poe.

This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.