http://qs1969.pair.com?node_id=36561
Category: Miscellaneous
Author/Contact Info
Description: This is my first pass at a solution to an interesting problem,, how to have modules that smoothly version themselves based on some criteria, which can have multiple versions loaded in parallel.

This version is pretty simple, the version that you get depends on an environment variable My test script is called test.pl and looks like this:

#! /usr/bin/perl # See comments in Versioning about warnings use strict; require versioned_module; tst(0); tst(1); tst(0); tst(1); sub tst { $ENV{IS_PROD} = shift; import versioned_module("report_version"); report_version(); }
The module it uses is versioned_module.pm:
package versioned_module; use Versioner; @ISA = qw(Versioner);
And this has two implementations, Prod/versioned_module.pvm and Devel/versioned_module.pvm that just happen to be the same:
use strict; use vars qw(@EXPORT_OK); @EXPORT_OK = qw(report_version); sub report_version { my $ver = __PACKAGE__; print "Got version $ver\n"; }
And when you run the test, you indeed get it switching versions back and forth depending on the environment variable.

This idea can, of course, be made more complex. :-)

UPDATE
A few improvements. It now works smoothly for OO modules as well without a custom import, see later post.

package Versioner;
use strict;
use Carp;
require Exporter;
use vars qw(%is_loaded $VERSION);
$VERSION = 0.07;

sub decide_version {
  my $class = shift;
  my $ver = $ENV{IS_PROD} ? "Prod" : "Devel";
  return "${ver}::$class";
}

# This should not be inherited
sub Versioner::private::get_implementation {
  my $class = shift;
  my $file = $class;
  $file =~ s/::/\//g;
  $file .= ".pvm";
  foreach my $dir (@INC) {
    if (-e "$dir/$file") {
      $file = "$dir/$file";
      local $/;
      local *FH;
      open (FH, $file) or
        confess("Cannot load '$file' for '$class': $!");
      return qq(
          package $class;
          \n#line 1 "{load '$file' for $class}"\n)
        . <FH>;
    }
  }
  confess(
    "Cannot find $file to implement $class in \@INC\n"
    . "  (\@INC contains: @INC)\n"
  );
}

sub import {
  my $class = shift;
  return if $class eq 'Versioner'; # I only version others!
  my $load = UNIVERSAL::can($class, "decide_version") or
    croak("Class $class does not inherit from Versioner");
  my $implement = $load->($class);
  unless ($is_loaded{$implement}) {
    # Oops, need to load
    eval Versioner::private::get_implementation($implement);
    if ($@) {
      croak("Cannot implement $class with $implement: $@");
    }
    no strict 'refs';
    push @{"${implement}::ISA"}, "Exporter";
    $is_loaded{$implement}++;
  }
  # Inherit?
  no strict 'refs';
  unless (grep {$implement eq $_} @{"${class}::ISA"}) {
    @{"${class}::ISA"} = $class->inherit_version($implement);
  }
  # And transparently replace this with the right import
  my $meth = UNIVERSAL::can($implement, "import");
  unshift @_, $implement;
  goto &$meth;
}

sub inherit_version {
  return (__PACKAGE__, $_[1]);
}

1;
__END__

=head1 NAME

Versioner - Implements an import method for versioned modules.

=head1 SYNOPSIS

Write a module Some/Module.pm:

  package Some::Module;
  use Versioner;
  @ISA = qw(Versioner);
  1; # End of module

Place your implementations in C<$base/Prod/Some/Module.pvm> and
C<$base/Devel/Some/Module.pvm> where C<$base> is any directory
in C<@INC>.  The environment variable C<IS_PROD> determines
which version you will run.

=head1 DESCRIPTION

The Versioner module implements a default C<import> method that
causes a base module to be implemented by the appropriate
version of the module.  Which version is chosen is determined
by the C<decide_ver> method.  The default implementation looks
at the environment variable C<IS_PROD> and puts "Prod::" or
"Devel::" in front of the package name as appropriate.  Once
the package is known, the implementation is loaded if has not
been already, inheritance is set up if it is needed, and then
the C<import> method of that version is called.  This will
default to the C<import> method from L<Exporter|Exporter>.

If the module needs to be loaded, first the package name of the
implementation is turned into a file-name by converting "::" to
"/" and putting .pvm (Perl Versioned Module) at the end.  Then
C<@INC> is searched for the implementation.  If it is found it
is compiled with L<strict|strict> on in the correct package,
and then made to inherit from L<Exporter|Exporter>.  Modules
that don't want L<strict|strict> are free to unimport it with
C<no>.

Here are the methods that can be overridden

=over 5

=item B<decide_version>

This method takes the class being versioned and returns the
class that should implement it.  The default implementation
prepends "Prod::" or "Devel::" depending on the value of
C<$ENV{IS_PROD}>.  This is convenient for people who develop in
a development branch and regularly cut that over to production.

Should you set up a custom version, carefully consider how you
will manage it.  Mixing multiple versions of the same module
is a considerable increase in complexity, so pick a scheme that
you are confident you can manage.

=item B<inherit_version>

This method takes the class being versioned and the version
chosen, and returns what I<@ISA> should be for that class.  By
default this returns C<("Versioner", $implementation)>.

=head2 Applicability of L<Exporter|Exporter>

Aside from the detail that the implementation should not
declare its  own package or load and inherit from
L<Exporter|Exporter>, this loading mechanism should be totally
transparent.  You can even define your own custom C<import>
methods and they will not see the loading.  But if you do that
you will probably want to call C<export_to_level> with:

  __PACKAGE__->export_to_level($level, @what);

so that you can avoid specifying the package anywhere in the
implementation.

You should be able to convert the versioned implementation into
an equivalent module by adding the right package declaration,
putting in C<use strict>, and appending C<Exporter> to C<@ISA>.
(Declaring it if need be.)  The only catch should be that the
versioned module will inherit C<decide_version> and
C<inherit_version> from this module.

=back

=head1 BUGS

L<Versioner|Versioner> has a number of limitations.  The
primary one is that it will not work properly if the
implementations declare their own package, nor can it easily
test for that.  Secondly it can only version pure Perl modules.
You are on your own for xsubs.  A third problem is that while
you can load a different implementation over an old one, it
does not try to unload the previous one.  Finally loading a
different implementation over an old can cause warnings.  The
following custom import method will solve that:

  sub import {
      local $^W;
      __PACKAGE__->export_to_level(1, @_);
  }

However using this can slow the loading process, particuarly
for OO modules that didn't plan to import anything.  Also no
easy way presents itself to locally turn warnings off while
having the versioning completely transparent to the
implementation.

=head1 AUTHOR INFORMATION

Copyright 2000 by Ben Tilly.  All rights reserved.

This module is free software; you can redistribute it under
the same terms as Perl itself.

Address bug reports and comments to ben_tilly@hotmail.com.

=cut
Replies are listed 'Best First'.
RE (tilly) 1: Versioned modules
by tilly (Archbishop) on Oct 13, 2000 at 18:45 UTC
    I did a bit of cleanup. I did not follow tye's suggestion because from both my reading of p5p and from my own tests, using "/" as a directory separator works portably. It might not look pretty, but it works.

    The biggest improvement is that you automatically are going to inherit correctly. My new test script is:

    #! /usr/bin/perl # See comments in Versioning about warnings use strict; require versioned_module; tst(0); tst(1); tst(0); tst(1); sub tst { $ENV{IS_PROD} = shift; import versioned_module("report_version"); report_version("Procedural"); report_version versioned_module("OO"); }
    and the versioned modules look like:
    use vars qw(@EXPORT_OK); @EXPORT_OK = qw(report_version); sub report_version { my $arg = pop(); my $ver = __PACKAGE__; print "Got version $ver through $arg interface\n"; }
(tye)RE: Versioned modules
by tye (Sage) on Oct 13, 2000 at 17:27 UTC

    Not all versions of Perl use "/" for a directory separator. Please use File::Spec to construct your file specification.

            - tye (but my friends call me "Tye")
      All versions that I have available understand "/" for a directory separator, and that includes an archaic version of ActiveState.

      Not all versions that I can find on machines around here have File::Spec installed. (Exceptions, an old machine with 5.004_04, and the aforementioned archaic version of ActiveState.)

      Therefore for me constructing the filename with "/" is more portable than using File::Spec and catfile. However I only tested on Windows NT, Linux, and Solaris.