Beefy Boxes and Bandwidth Generously Provided by pair Networks
Clear questions and runnable code
get the best and fastest answer
 
PerlMonks  

RFC: Preventing a module from loading

by Corion (Patriarch)
on Feb 16, 2003 at 11:41 UTC ( [id://235707]=perlmeditation: print w/replies, xml ) Need Help??

I like modules that provide a dynamic fallback and degrade gracefully if some prerequisites are not available instead of requiring modules when they can do well without them.

But there is a problem - on my development machine, I have all these optional modules installed, but I want to test the behaviour of my code without the optional modules. So I want to set up tests where the optional modules seem not available. My preferred syntax for this is a pragma-like syntax :

use Test::Without::Modules qw( HTML::Template ); use Test::Without::Modules qr/^POE::/;

So, most of the magic will have to be installed in a sub called "import()" within my (to be written) module.

When you want to muck around with module loading, the only way in Perl seems to be to add a code reference into @INC. That code reference either returns a filehandle, from which the text will be loaded, or undef, which means that the next entry in @INC will be tried.

Things that didn't work :

BEGIN { @SAVED_INC = @INC; }; sub import { @INC = sub { # return undef if it's a blocked module # Look if the module is in @SAVED_INC # Return a filehandle to it }; };

This first variant worked quite well, until I came up to Digest::MD5, which wants to load XS code. And the XS code loader looks through @INC, it dosen't respect coderefs in @INC, and thus, the loading of Digest::MD5 fails. Or rather, Digest::MD5 has a fallback to Digest::Perl::MD5, which I didn't have installed. So this way will not work as soon as we use any module which uses XS code.

So I had to keep all existing directories in @INC, but there was no way to prevent Perl to look through the rest of @INC if my handler returned undef for a blocked module :

BEGIN { @SAVED_INC = @INC; }; sub import { @INC = sub { # return undef if it's a blocked module }; };

demerphq then suggested that I forget about a handler in @INC and muck instead with %INC and a custom import method, that would die whenever that module was imported into a new namespace.

sub import { $INC{$module} = 1; *{$module."::import"} = sub { die 'ugh'; }; };

But this version didn't work, because one could still require the module, and most checks whether a module is available rely on the meme

eval { require Optional::Module }; if ($@) { # module is not available };

But this put me on the right track, I would simply create a faked module on the fly, and return this faked module whenever I want to prevent a module from loading. I don't need to handle the case that a module is allowed, as the rest of @INC will take care of that.

sub import { unshift @INC, sub { # return dummy module filehandle if it's a blocked module }; };

There are now some technical pitfalls. First, IO::String does not work in an @INC-handler, seemingly Perl wants a real filehandle (or at least, Acme::Intraweb and PAR do it that way as well), so I have to create a tempfile for every faked module. That's not a real concern as my module is intended for testing anyway - efficiency is of no importance.

Second, what if a module has already been loaded? Then Perl won't go through @INC at all. So we have to scrub %INC as well and clean it of the unwanted modules, in case they have already been loaded.

After these tries, the algorithm to prevent a module from loading now looks like the following :

use vars qw( %forbidden ); sub import { my ($self,@forbidden_modules) = @_; scrub( $module ) for @forbidden_modules; unshift @INC, sub { my (undef,$filename,undef) = @_; if (exists $forbidden{$filename}) { # return faked, failing module }; }; };

The complete module is appended below. If you have suggestions about the naming convention or the usage interface, I'd like to hear about them. If you have any hint on how to make my module into a lexical pragma (warnings.pm and strict.pm didn't offer a hint to me), I'll be even more interested.

package Test::Without::Module; use strict; use File::Temp; use Carp qw( croak ); use vars qw( %forbidden $VERSION ); $VERSION = 0.01; sub import { my ($self,@forbidden_modules) = @_; $forbidden{$_} = $_ for @forbidden_modules; # Scrub %INC, so that loaded modules disappear my ($module); for $module (@forbidden_modules) { scrub( $module ); }; # Move our handler to the front of the list @INC = grep { $_ ne \&fake_module } @INC; unshift @INC, \&fake_module; }; sub fake_module { my ($self,$module_file,$member_only) = @_; warn $@ if $@; my $modulename = file2module($module_file); # Deliver a faked, nonworking module if (grep { $modulename =~ $_ } keys %forbidden) { my $fh = File::Temp::tmpfile(); print $fh <<MODULE; package $modulename; =head1 NAME $modulename =head1 SYNOPSIS !!! THIS IS A FAKED VERSION OF $modulename !!! !!! IT WAS CREATED BY Test::Without::Module !!! !!! IT SHOULD NEVER END UP IN YOUR lib/ OR site/lib/ !!! =cut sub import { undef }; 0; MODULE seek $fh, 0,0; return $fh; }; }; sub unimport { my ($self,@list) = @_; my $module; for $module (@list) { if (exists $forbidden{$module}) { delete $forbidden{$module}; scrub( $module ); } else { croak "Can't allow non-forbidden module $module"; }; }; }; sub file2module { my ($mod) = @_; $mod =~ s!/!::!g; $mod =~ s!\.pm$!!; $mod; }; sub scrub { my ($module) = @_; my $key; for $key (keys %INC) { delete $INC{$key} if (file2module($key) =~ $module); }; }; 1; __END__ =head1 NAME Test::Without::Module - Test fallback behaviour in absence of modules =head1 SYNOPSIS =for example begin use Test::Without::Module qw( File::Temp ); # Now, loading of File::Temp fails : eval { require File::Temp; }; warn $@ if $@; # Now it works again eval q{ no Test::Without::Module qw( File::Temp ) }; eval { require File::Temp; }; print "Found File::Temp" unless $@; =for example end =head1 DESCRIPTION This module allows you to deliberately hide modules from a program even though they are installed. This is mostly useful for testing modu +les that have a fallback when a certain dependency module is not installed +. =head2 EXPORT None. All magic is done via C<use Test::Without::Module> and C<no Test::Without::Module>. =begin testing no warnings 'once'; eval 'use Test::Without::Module qw( File::Temp )'; eval 'no Test::Without::Module qw( File::Temp )'; is_deeply( [keys %Test::Without::Module::forbidden],[],"Module list" + ); eval { require File::Temp; }; is( $@, '', "unimport" ); =end testing =head1 BUGS =over 4 =item * There is no lexicalic scoping (yet) =back =head1 AUTHOR Max Maischein, E<lt>corion@cpan.orgE<gt> =head1 SEE ALSO L<Acme::Intraweb>, L<PAR>, L<perlfunc> =cut
perl -MHTTP::Daemon -MHTTP::Response -MLWP::Simple -e ' ; # The $d = new HTTP::Daemon and fork and getprint $d->url and exit;#spider ($c = $d->accept())->get_request(); $c->send_response( new #in the HTTP::Response(200,$_,$_,qq(Just another Perl hacker\n))); ' # web

Update: Fixed calling of scrub. That's what you get for last minute changes !

Replies are listed 'Best First'.
(jeffa) Re: RFC: Preventing a module from loading
by jeffa (Bishop) on Feb 16, 2003 at 15:47 UTC
    Unless i am missing something (i most always am), would Test::MockObject be useful here?
    use Test::More qw(no_plan); use Test::MockObject; # use Roman; # <- i don't have Roman.pm installed my $roman = Test::MockObject->new(); $roman->set_true($_) for qw(roman isroman arabic); ok($roman->isroman('IV')); $roman->set_false('isroman'); ok($roman->isroman('42'));
    or am i missing something?

    jeffa

    L-LL-L--L-LL-L--L-LL-L--
    -R--R-RR-R--R-RR-R--R-RR
    B--B--B--B--B--B--B--B--
    H---H---H---H---H---H---
    (the triplet paradiddle with high-hat)
    

      In a certain way, my module implements the reverse. Test::MockObject implements faked modules, that always load (or rather are already loaded). My module makes sure that a given module cannot be found via require or use, and that all such calls fail.

      This module is solution to the following problem :

      I have a module, CGI::Wiki::Simple, which can optionally use HTML::Template. But HTML::Template might not be there, and then I want to fallback onto CGI::Wiki::Simple::NoTemplates, a subclass that provides hardcoded pages. But how do I test that this fallback works ?

      #!/usr/bin/perl -w use strict; use Test::More tests => 3; use Test::Without::Module qw( HTML::Template ); BEGIN{ use_ok('CGI::Wiki::Simple') }; my $wiki = CGI::Wiki::Simple->new( PARAMS => { store => {} } # dummy store ); isa_ok($wiki, 'CGI::Wiki::Simple', "The created wiki"); isa_ok($wiki, 'CGI::Wiki::Simple::NoTemplates', "The created wiki");
      perl -MHTTP::Daemon -MHTTP::Response -MLWP::Simple -e ' ; # The $d = new HTTP::Daemon and fork and getprint $d->url and exit;#spider ($c = $d->accept())->get_request(); $c->send_response( new #in the HTTP::Response(200,$_,$_,qq(Just another Perl hacker\n))); ' # web
        You might consider prefixing the JAPH sig in your post with a <hr> or something so it doesn't look like part of your sample code. I don't mean to nit-pick but it did trip me up.

        Thanks for this idea and code by the way; ++.

Re: RFC: Preventing a module from loading
by mirod (Canon) on Feb 16, 2003 at 13:25 UTC

    Very cool! I have to test it more thouroughly, but this is really something that will be helpful for me. Except that it means that now I have to write more tests ;--(

Re: RFC: Preventing a module from loading
by Aristotle (Chancellor) on Feb 18, 2003 at 18:06 UTC

    You can avoid the temporary files, as I demonstrated at Coderefs in @INC (was: Building a Simple Perl Module Database). Your approach seems like an awful lot of work though - especially as it will fail to work correctly if any module mucks with @INC too much (such as by unshifting an extra path).

    Wouldn't something along the lines of the following work better?

    #### untested #### package Test::Without::Module; use constant REQUIRE_ERROR => q/Can't locate %s.pm in @INC (@INC conta +ins: %s)/; my @dont_load_rx; sub import { my $class = shift; push @dont_load_rx, map qr/$_/, @_; } # prototype("CORE::require") eq ";$" *CORE::GLOBAL::require = sub (;$) { local $_ = $_[0]; for my $forbidden (@dont_load) { /$forbidden/ or next; s!::!/!; # possibly OS dependent? require Carp; Carp::croak(sprintf REQUIRE_ERROR, $_, "@INC"); } goto CORE::require; }; 1;
    Update: switched from die to croak, used goto to chain to the core require, to make the module more transparent to the outside world.

    Makeshifts last the longest.

      That idea is much cleaner and removes much of the clutter I've amassed from the above way to reaching my goal. But it dosen't completely work, as perls parsing gets in the way :

      I use this as the subsitute - neither the prototype of $ nor the prototype of * (suggested here did make a difference. *CORE::GLOBAL::require = sub (*) { my $forbidden = get_forbidden_list; local $_ = $_[0]; for my $forbidden_module (keys %{$forbidden}) { /$forbidden_module/ or next; s!::!/!g; require Carp; Carp::croak(sprintf REQUIRE_ERROR, $_, "@INC"); } print "Loading original via @_\n"; goto CORE::require; };

      The following test dies with Can't locate Test::Without::Module in @INC..., which suggests to me that somehow my faked require dosen't quite parse like the real thing to perl (or rather, the bareword module name isn't seen as such anymore by the real require) :

      #!/usr/bin/perl -w use strict; use Symbol qw( delete_package ); use Test::More tests => 6; BEGIN { use_ok( "Test::Without::Module", qw( Digest::MD5 )); }; { use Test::Without::Module qw( Digest::MD5 ); eval { require Digest::MD5 }; ok( $@ ne '', "Loading raised error"); like( $@, qr!^Can't locate Digest/MD5.pm in \@INC!, "Hid module"); is_deeply( [sort keys %{Test::Without::Module::get_forbidden_list()} +],[ qw[ Digest::MD5 ]],"Module list" ); delete_package( 'Digest::MD5' ); };

      It gets weirder - I can prevent the original require from seeing @_ by using my $name = $_[0] as the first line of the sub instead. So somehow, this beautiful solution dosen't work for me and Perl 5.6.1 :-((


      perl -MHTTP::Daemon -MHTTP::Response -MLWP::Simple -e ' ; # The $d = new HTTP::Daemon and fork and getprint $d->url and exit;#spider ($c = $d->accept())->get_request(); $c->send_response( new #in the HTTP::Response(200,$_,$_,qq(Just another Perl hacker\n))); ' # web
        Maybe you could post the new code as well so I can have a stab at running the test suite against it and possibly fixing it?

        Makeshifts last the longest.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://235707]
Front-paged by cchampion
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others having an uproarious good time at the Monastery: (6)
As of 2024-04-19 07:28 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found