diff options
author | Chad Granum <exodist7@gmail.com> | 2016-01-17 23:17:55 -0800 |
---|---|---|
committer | Chad Granum <exodist7@gmail.com> | 2016-01-17 23:17:55 -0800 |
commit | 44efc4d505992ca317d29a5ef1e0d38b98d1e3cb (patch) | |
tree | 9bf3fb886eeb2fb8a05a6af06d7623fc40ac055f | |
parent | 93ace0a047ce8556afdb80522389fb0eab015a1d (diff) |
Testing, Bugfixes, and Exporter.pm clone(ish)
-rw-r--r-- | lib/Importer.pm | 191 | ||||
-rw-r--r-- | lib/Importer/Exporter.pm | 110 | ||||
-rw-r--r-- | lib/Importer/Exporter/Heavy.pm | 68 | ||||
-rw-r--r-- | t/units.t | 444 |
4 files changed, 678 insertions, 135 deletions
diff --git a/lib/Importer.pm b/lib/Importer.pm index 83669e0..44d5696 100644 --- a/lib/Importer.pm +++ b/lib/Importer.pm @@ -4,6 +4,8 @@ use warnings; our $VERSION = 0.002; +our @EXPORT_OK = qw/exporter_import/; + my %SIG_TO_SLOT = ( '&' => 'CODE', '$' => 'SCALAR', @@ -17,14 +19,30 @@ my %IMPORTED; # This will be used to check if an import arg is a version number my %NUMERIC = map { $_ => 1 } 0 .. 9; -sub _version_check { - my ($mod, $caller, @versions) = @_; +# If a consumer just wants subs then we can optimize the import. This is used +# as a lookup table to find non-optimal sigils. Can;t just look for '&' since a +# sub can be listed without a sigil, so alpha-numerics may also be checked +# against thi stable, and we want those to be considered optimal. +my %NON_OPTIMAL = ( '$' => 1, '@' => 1, '%' => 1, '*' => 1 ); - eval <<" EOT" or die $@; -#line $caller->[2] "$caller->[1]" -\$mod->VERSION(\$_) for \@versions; -1; - EOT +sub exporter_import { + my $from = shift; + + my @caller = caller(0); + + return unless @_; + + my $file = _mod_to_file($from); + _load_file(\@caller, $file) unless $INC{$file}; + + return if _optimal_import($from, $caller[0], @_); + + my $self = __PACKAGE__->new( + from => $from, + caller => \@caller, + ); + + $self->do_import($caller[0], @_); } sub import { @@ -38,12 +56,16 @@ sub import { my ($from, @args) = @_; + my $file = _mod_to_file($from); + _load_file(\@caller, $file) unless $INC{$file}; + + return if _optimal_import($from, $caller[0], @args); + my $self = $class->new( from => $from, caller => \@caller, ); - $self->load_from() unless $INC{$self->from_file()}; $self->do_import($caller[0], @args); } @@ -61,12 +83,16 @@ sub import_into { @caller = caller(0); } + my $file = _mod_to_file($from); + _load_file(\@caller, $file) unless $INC{$file}; + + return if _optimal_import($from, $into, @args); + my $self = $class->new( from => $from, caller => \@caller, ); - $self->load_from() unless $INC{$self->from_file()}; $self->do_import($into, @args); } @@ -168,12 +194,7 @@ sub from { $_[0]->{from} } sub from_file { my $self = shift; - unless($self->{from_file}) { - my $file = $self->{from}; - $file =~ s{::}{/}g; - $file .= '.pm'; - return $self->{from_file} = $file; - } + $self->{from_file} ||= _mod_to_file($self->{from}); return $self->{from_file}; } @@ -187,10 +208,7 @@ sub load_from { my $caller = $self->get_caller; - eval <<" EOT" || die $@; -#line $caller->[2] "$caller->[1]" -require \$from_file; - EOT + _load_file($caller, $from_file); } sub get_caller { @@ -242,17 +260,27 @@ sub reload_menu { my $from = $self->from; - my ($export, $export_ok, $export_tags, $export_fail, $generate); + my ($export, $export_ok, $export_tags, $export_fail, $generate, $export_gen, $export_anon, $new_style); if ($from->can('IMPORTER_MENU')) { # Hook, other exporter modules can define this method to be compatible with # Importer.pm + $new_style = 1; + my %got = $from->IMPORTER_MENU($into, $self->get_caller); $export = $got{export} || []; $export_ok = $got{export_ok} || []; $export_tags = $got{export_tags} || {}; $export_fail = $got{export_fail} || []; - $generate = $got{generate}; + $export_anon = $got{export_anon} || {}; + + $export_gen = $got{export_gen}; + $generate = $got{generate}; + + $self->croak("'$from' provides both 'generate' and 'export_gen' in its IMPORTER_MENU (They are exclusive, module must pick 1)") + if $export_gen && $generate; + + $export_gen ||= {}; } else { no strict 'refs'; @@ -261,18 +289,49 @@ sub reload_menu { $export_ok = \@{"$from\::EXPORT_OK"}; $export_tags = \%{"$from\::EXPORT_TAGS"}; $export_fail = \@{"$from\::EXPORT_FAIL"}; + $export_gen = \%{"$from\::EXPORT_GEN"}; + $export_anon = \%{"$from\::EXPORT_ANON"}; } - my $exports = { map { - my ($sig, $name) = (m/^(\W?)(.*)$/); + $generate ||= sub { + my $symbol = shift; + my ($sig, $name) = ($symbol =~ m/^(\W?)(.*)$/); + $sig ||= '&'; + + my $do = $export_gen->{"${sig}${name}"}; + $do ||= $export_gen->{$name} if !$sig || $sig eq '&'; + + return undef unless $do; + + $from->$do($into, $symbol); + } if $export_gen && keys %$export_gen; + + my $lookup = {}; + my $exports = {}; + for my $sym (@$export, @$export_ok, keys %$export_gen, keys %$export_anon) { + my ($sig, $name) = ($sym =~ m/^(\W?)(.*)$/); $sig ||= '&'; - my $slot = $SIG_TO_SLOT{$sig} || 'CODE'; + my $slot = $SIG_TO_SLOT{$sig}; + + $lookup->{"${sig}${name}"} = 1; + $lookup->{$name} = 1 if $sig eq '&'; + + next if $export_gen->{"${sig}${name}"}; + next if $sig eq '&' && $export_gen->{$name}; no strict 'refs'; no warnings 'once'; - ("${sig}${name}" => $slot eq 'SCALAR' ? \${"$from\::$_"} : *{"$from\::$_"}{$slot}); - } @$export, @$export_ok }; + $exports->{"${sig}${name}"} = $export_anon->{$sym} || ($slot eq 'SCALAR' ? \${"$from\::$name"} : *{"$from\::$name"}{$slot}); + } + my $f_import = $new_style || $from->can('import'); + $self->croak("'$from' does not provide any exports") + unless $new_style + || keys %$exports + || $from->isa('Exporter') + || ($INC{'Exporter.pm'} && $f_import && $f_import == \&Exporter::import); + + # Do not cleanup or normalize the list added to the DEFAULT tag, legacy.... my $tags = { %$export_tags, 'DEFAULT' => [ @$export ], @@ -282,12 +341,10 @@ sub reload_menu { map { my ($sig, $name) = (m/^(\W?)(.*)$/); $sig ||= '&'; - ("${sig}${name}" => 1) + ("${sig}${name}" => 1, $sig eq '&' ? ($name => 1) : ()) } @$export_fail } : undef; - my $lookup = { map { $_ => 1 } @$export, @$export_ok }; - $self->{menu_for} = $into; return $self->{menu} = { lookup => $lookup, @@ -311,7 +368,8 @@ sub parse_args { my @import; my @versions; - while(my $arg = shift @args) { + while(my $full_arg = shift @args) { + my $arg = $full_arg; my $lead = substr($arg, 0, 1); my ($spec, $exc); @@ -366,12 +424,16 @@ sub parse_args { } # Normalize list, always have a sigil - @list = map {m/^\W/ ? $_ : "\&$_" } @list; + my %seen; + @list = grep !$seen{$_}++, map {m/^\W/ ? $_ : "\&$_" } @list; if ($exc) { $exclude{$_} = 1 for @list; } else { + $self->croak("Cannot use '-as' to rename multiple symbols included by: $full_arg") + if $spec->{'-as'} && @list > 1; + push @import => [$_, $spec] for @list; } } @@ -431,7 +493,7 @@ sub _set_symbols { for my $set (@$import) { my ($symbol, $spec) = @$set; - my ($sig, $name) = ($symbol =~ m/^(\W?)(.*)$/); + my ($sig, $name) = ($symbol =~ m/^(\W)(.*)$/); # Find the thing we are actually shoving in a new namespace my $ref = $menu->{exports}->{$symbol}; @@ -440,7 +502,7 @@ sub _set_symbols { # Exporter.pm supported listing items in @EXPORT that are not actually # available for export. So if it is listed (lookup) but nothing is # there (!$ref) we simply skip it. - croak "$from does not export $symbol" unless $ref || $menu->{lookup}->{$name} || $menu->{lookup}->{$symbol}; + croak "$from does not export $symbol" unless $ref || $menu->{lookup}->{"${sig}${name}"}; next unless $ref; # Figure out the name they actually want it under @@ -458,6 +520,69 @@ sub _set_symbols { } } +######################################################### +## The rest of these are utility functions, not methods! + +sub _version_check { + my ($mod, $caller, @versions) = @_; + + eval <<" EOT" or die $@; +#line $caller->[2] "$caller->[1]" +\$mod->VERSION(\$_) for \@versions; +1; + EOT +} + +sub _mod_to_file { + my $file = shift; + $file =~ s{::}{/}g; + $file .= '.pm'; + return $file; +} + +sub _load_file { + my ($caller, $file) = @_; + + eval <<" EOT" || die $@; +#line $caller->[2] "$caller->[1]" +require \$file; + EOT +} + +sub _optimal_import { + my ($from, $into, @args) = @_; + + my %final; + no strict 'refs'; + return 0 if @{"$from\::EXPORT_FAIL"}; + @args = @{"$from\::EXPORT"} unless @args; + my %allowed = map +($_ => 1), @{"$from\::EXPORT"}, @{"$from\::EXPORT_OK"}; + use strict 'refs'; + + for my $arg (@args) { + # Get sigil, or first letter of name + my $sig = substr($arg, 0, 1); + + # Return if non-sub sigil + return 0 if $NON_OPTIMAL{$sig}; + + # Strip sigil (if sub) + my $name = $arg; + substr($name, 0, 1, '') if $sig eq '&'; + + # Check if the name is allowed (with or without sigil) + return 0 unless $allowed{$name} || $allowed{$arg}; + + no strict 'refs'; + $final{$name} = \&{"$from\::$name"}; + } + + no strict 'refs'; + (*{"$into\::$_"} = $final{$_}, push @{$IMPORTED{$into}} => $_) for keys %final; + + return 1; +} + 1; __END__ diff --git a/lib/Importer/Exporter.pm b/lib/Importer/Exporter.pm new file mode 100644 index 0000000..db1cb2a --- /dev/null +++ b/lib/Importer/Exporter.pm @@ -0,0 +1,110 @@ +package Importer::Exporter; +use strict; +use warnings; + +use Importer Importer => ( + exporter_import => { -as => 'import' }, + exporter_import => { -as => 'export' }, +); + +our @EXPORT_OK = qw/import/; + +sub export_fail { shift; @_ } + +sub export_to_level { + my $from = shift; + my ($level) = @_; + Importer->import_into($from, $level + 1, @_); +} + +sub require_version { + my ($self, $wanted) = @_; + my $pkg = ref $self || $self; + return ${pkg}->VERSION($wanted); +} + +my $push_tags = sub { + my $from = shift; + my ($var, @tags) = @_; + + no strict 'refs'; + my $export = \%{"$from\::$var"}; + my $export_tags = \%{"$from\::EXPORT_TAGS"}; + use strict 'refs'; + + my @nontag = (); + for my $tag (@tags ? @tags : keys %$export_tags) { + my $tag_list = $export_tags->{$tag}; + $tag_list ? push @$export => @$tag_list : push @nontag => $tag; + }; + + return unless @nontag && $^W; + + require Carp; + Carp::carp(join(", ", @nontag) . " are not tags of $from"); +}; + +sub export_tags { + my $from = shift; + $from->$push_tags('EXPORT', @_); +} + +sub export_ok_tags { + my $from = shift; + $from->$push_tags('EXPORT_OK', @_); +} + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +Importer::Exporter - DO NOT USE THIS + +=head1 DESCRIPTION + +This is what L<Exporter> would look like if it used L<Importer> to get the job +done. Works with C<use base 'Importer::Exporter';> as well as +C<use Importer::Exporter qw/import/>. + +=head1 *** EXPERIMENTAL *** + +This module is still experimental. Anything can change at any time. Testing is +currently VERY insufficient. + +=head1 SOURCE + +The source code repository for symbol can be found at +F<http://github.com/exodist/Importer>. + +=head1 MAINTAINERS + +=over 4 + +=item Chad Granum E<lt>exodist@cpan.orgE<gt> + +=back + +=head1 AUTHORS + +=over 4 + +=item Chad Granum E<lt>exodist@cpan.orgE<gt> + +=back + +=head1 COPYRIGHT + +Copyright 2015 Chad Granum E<lt>exodist7@gmail.comE<gt>. + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See F<http://dev.perl.org/licenses/> + +=cut diff --git a/lib/Importer/Exporter/Heavy.pm b/lib/Importer/Exporter/Heavy.pm new file mode 100644 index 0000000..1977ccf --- /dev/null +++ b/lib/Importer/Exporter/Heavy.pm @@ -0,0 +1,68 @@ +package Importer::Exporter::Heavy; +use warnings; + +use Importer::Exporter(); + +*{"heavy_$_"} = Importer::Exporter->can($_) for qw{ + export_fail + export + export_to_level + require_version + export_tags + export_ok_tags +}; + + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +Importer::Exporter::Heavy - DO NOT USE THIS + +=head1 DESCRIPTION + +This is what L<Exporter::Heavy> would look like if it used L<Importer> to get +the job done. + +=head1 *** EXPERIMENTAL *** + +This module is still experimental. Anything can change at any time. Testing is +currently VERY insufficient. + +=head1 SOURCE + +The source code repository for symbol can be found at +F<http://github.com/exodist/Importer>. + +=head1 MAINTAINERS + +=over 4 + +=item Chad Granum E<lt>exodist@cpan.orgE<gt> + +=back + +=head1 AUTHORS + +=over 4 + +=item Chad Granum E<lt>exodist@cpan.orgE<gt> + +=back + +=head1 COPYRIGHT + +Copyright 2015 Chad Granum E<lt>exodist7@gmail.comE<gt>. + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See F<http://dev.perl.org/licenses/> + +=cut @@ -8,10 +8,15 @@ my $CLASS = 'Importer'; sub dies(&) { my $code = shift; - local $@; - eval { $code->(); 1 } and return undef; + my $err; + { + local $@; + eval { $code->(); 1 } and return undef; + $err = $@; + } - return $@ || 1; + $@ = $err; + return $err || 1; } sub warns(&) { @@ -131,7 +136,7 @@ subtest from_file => sub { subtest load_from => sub { my $one = $CLASS->new(from => 'Some::Fake::Module::AFSGEWGWE::FASDF', caller => ['main', 'fake.pl', 42]); - + like( dies { $one->load_from }, qr{Can't locate.*at fake\.pl line 42}, @@ -184,87 +189,283 @@ subtest carp_and_croak => sub { ); }; -done_testing; +subtest menu => sub { + my $menu; -__END__ + no warnings 'redefine'; + local *Importer::reload_menu = sub { + my $self = shift; + my ($into) = @_; + $self->{menu} = $menu; + $self->{menu_for} = $into; + return $menu; + }; -sub menu { - my $self = shift; - my ($into) = @_; + $menu = { a => 1 }; + my $one = $CLASS->new(from => 'fake'); + is_deeply($one->menu('fake2'), $menu, "returned menu"); - $self->croak("menu() requires the name of the destination package") - unless $into; + my $old = $menu; + $menu = { b => 2 }; - my $for = $self->{menu_for}; - delete $self->{menu} if $for && $for ne $into; - return $self->{menu} || $self->reload_menu($into); -} + is_deeply($one->menu('fake2'), $old, "cached"); -sub reload_menu { - my $self = shift; - my ($into) = @_; + is_deeply($one->menu('fake3'), $menu, "refreshed with different destination"); - $self->croak("reload_menu() requires the name of the destination package") - unless $into; + my $line; + like( + dies { $line = __LINE__; $one->menu() }, + qr/menu\(\) requires the name of the destination package at ${\__FILE__} line $line/, + "Need 'into' package" + ); +}; - my $from = $self->from; +subtest reload_menu => sub { + my $one = $CLASS->new(from => 'fake'); - my ($export, $export_ok, $export_tags, $export_fail, $generate); - if ($from->can('IMPORTER_MENU')) { - # Hook, other exporter modules can define this method to be compatible with - # Importer.pm - - my %got = $from->IMPORTER_MENU($into, $self->get_caller); - $export = $got{export} || []; - $export_ok = $got{export_ok} || []; - $export_tags = $got{export_tags} || {}; - $export_fail = $got{export_fail} || []; - $generate = $got{generate}; - } - else { - no strict 'refs'; - no warnings 'once'; - $export = \@{"$from\::EXPORT"}; - $export_ok = \@{"$from\::EXPORT_OK"}; - $export_tags = \%{"$from\::EXPORT_TAGS"}; - $export_fail = \@{"$from\::EXPORT_FAIL"}; - } + my $line; + like( + dies { $line = __LINE__; $one->reload_menu() }, + qr/menu\(\) requires the name of the destination package at ${\__FILE__} line $line/, + "Need 'into' package" + ); - my $exports = { map { - my ($sig, $name) = (m/^(\W?)(.*)$/); - $sig ||= '&'; - my $slot = $SIG_TO_SLOT{$sig} || 'CODE'; + subtest empty => sub { + { + no warnings 'once'; + require Exporter; + @Fake::Exporter1::ISA = ('Exporter'); + *Fake::Exporter2::import = Exporter->can('import'); + *Fake::Exporter3::IMPORTER_MENU = sub { () }; + *Fake::Exporter4::IMPORTER_MENU = sub { (generate => sub { 1 }, export_gen => { a => 1 }) }; + } - no strict 'refs'; - no warnings 'once'; - ("${sig}${name}" => $slot eq 'SCALAR' ? \${"$from\::$_"} : *{"$from\::$_"}{$slot}); - } @$export, @$export_ok }; + like( + dies { $line = __LINE__; $CLASS->new(from => 'Fake::Exporter4')->reload_menu('fake') }, + qr/'Fake::Exporter4' provides both 'generate' and 'export_gen' in its IMPORTER_MENU \(They are exclusive, module must pick 1\) at ${\__FILE__} line $line/, + "Bad IMPORT_MENU" + ); + + like( + dies { $line = __LINE__; $CLASS->new(from => 'Fake::Exporter5')->reload_menu('fake') }, + qr/'Fake::Exporter5' does not provide any exports at ${\__FILE__} line $line/, + "No exports, not an exporter" + ); + + my ($menu1, $menu2, $menu3); + ok(!dies { $menu1 = $CLASS->new(from => 'Fake::Exporter1')->reload_menu('fake') }, "Package isa Exporter with no exports") || diag $@; + ok(!dies { $menu2 = $CLASS->new(from => 'Fake::Exporter2')->reload_menu('fake') }, "Package uses Exporter qw/import/") || diag $@; + ok(!dies { $menu3 = $CLASS->new(from => 'Fake::Exporter3')->reload_menu('fake') }, "Package provides IMPORTER_MENU") || diag $@; + + is_deeply( + [$menu1, $menu1, $menu2], + [$menu2, $menu3, $menu3], + "All empty menus are the same" + ); + + is_deeply( + $menu1, + { + lookup => {}, + exports => {}, + tags => { DEFAULT => [] }, + fail => undef, + generate => undef, + }, + "Got valid, but empty menu" + ); + }; + + subtest IMPORTER_MENU => sub { + { + package Fake::ExporterI; + sub IMPORTER_MENU { + ::is_deeply( + \@_, + ['Fake::ExporterI', 'fake', ['fake', 'fake.pl', 42]], + "Got input args" + ); + return ( + export => [qw/foo &bar $ZAP %ZAP @ZAP/], + export_ok => [qw/baz ick missing/], + export_tags => {b => [qw/bar baz/]}, + export_fail => [qw/ick/], + export_anon => { x => \&__x, z => \&__z }, + export_gen => { + 'gena' => sub { + sub { 'a' } + }, + '&genb' => sub { + sub { 'b' } + }, + }, + ); + } + + sub foo { 'foo' } + sub bar { 'bar' } + sub baz { 'baz' } + sub ick { 'ick' } + sub __x { 'x' } + sub __z { 'z' } + + # These are here to insure 'exports' does not pull them in, they are listed as generate + sub gena { sub { 'oops, should not see this' } } + sub genb { sub { 'oops, should not see this' } } + + # These are here to insure 'exports' does not pull them in, refs were provided by anon + sub x { sub { 'oops, should not see this' } } + sub y { sub { 'oops, should not see this' } } + + package Fake::ExporterI2; - my $tags = { - %$export_tags, - 'DEFAULT' => [ @$export ], + sub IMPORTER_MENU { + return ( + generate => \&generate, + ); + } + + sub generate { sub { 'a pie' } } + } + + my $one = $CLASS->new(from => 'Fake::ExporterI', caller => ['fake', 'fake.pl', 42]); + my $menu = $one->reload_menu('fake'); + is($one->{menu_for}, 'fake', "remember who it was generated for"); + ok(my $gen = delete $menu->{generate}, "got a generate function"); + + is_deeply( + $menu, + { + lookup => {qw/ + foo 1 &foo 1 + bar 1 &bar 1 + baz 1 &baz 1 + ick 1 &ick 1 + missing 1 &missing 1 + x 1 &x 1 + z 1 &z 1 + gena 1 &gena 1 + genb 1 &genb 1 + + $ZAP 1 %ZAP 1 @ZAP 1 + /}, + exports => { + '&foo' => Fake::ExporterI->can('foo'), + '&bar' => Fake::ExporterI->can('bar'), + '&baz' => Fake::ExporterI->can('baz'), + '&ick' => Fake::ExporterI->can('ick'), + '&x' => Fake::ExporterI->can('__x'), + '&z' => Fake::ExporterI->can('__z'), + + '&missing' => undef, + + '$ZAP' => \$Fake::ExporterI::ZAP, + '@ZAP' => \@Fake::ExporterI::ZAP, + '%ZAP' => \%Fake::ExporterI::ZAP, + }, + tags => { + b => [qw/bar baz/], + DEFAULT => [qw/foo &bar $ZAP %ZAP @ZAP/], + }, + fail => { '&ick' => 1, ick => 1 }, + }, + "Got menu" + ); + + is($gen->('gena')->(), 'a', "generated a"); + is($gen->('genb')->(), 'b', "generated b"); + + $one = $CLASS->new(from => 'Fake::ExporterI2', caller => ['fake', 'fake.pl', 42]); + $menu = $one->reload_menu('fake'); + is($menu->{generate}, \&Fake::ExporterI2::generate, "can provide custom generate") }; - my $fail = @$export_fail ? { - map { - my ($sig, $name) = (m/^(\W?)(.*)$/); - $sig ||= '&'; - ("${sig}${name}" => 1) - } @$export_fail - } : undef; - - my $lookup = { map { $_ => 1 } @$export, @$export_ok }; - - $self->{menu_for} = $into; - return $self->{menu} = { - lookup => $lookup, - exports => $exports, - tags => $tags, - fail => $fail, - generate => $generate, + subtest OLD_STYLE => sub { + { + package Fake::ExporterE; + our @EXPORT = qw/foo &bar $ZAP %ZAP @ZAP/; + our @EXPORT_OK = qw/baz ick missing/; + our %EXPORT_TAGS = (b => [qw/bar baz/]); + our @EXPORT_FAIL = qw/ick/; + our %EXPORT_ANON = (x => \&__x, z => \&__z); + our %EXPORT_GEN = ( + 'gena' => sub { + sub { 'a' } + }, + '&genb' => sub { + sub { 'b' } + }, + ); + + sub foo { 'foo' } + sub bar { 'bar' } + sub baz { 'baz' } + sub ick { 'ick' } + sub __x { 'x' } + sub __z { 'z' } + + # These are here to insure 'exports' does not pull them in, they are listed as generate + sub gena { sub { 'oops, should not see this' } } + sub genb { sub { 'oops, should not see this' } } + + # These are here to insure 'exports' does not pull them in, refs were provided by anon + sub x { sub { 'oops, should not see this' } } + sub y { sub { 'oops, should not see this' } } + } + + my $one = $CLASS->new(from => 'Fake::ExporterE', caller => ['fake', 'fake.pl', 42]); + my $menu = $one->reload_menu('fake'); + is($one->{menu_for}, 'fake', "remember who it was generated for"); + ok(my $gen = delete $menu->{generate}, "got a generate function"); + + is_deeply( + $menu, + { + lookup => {qw/ + foo 1 &foo 1 + bar 1 &bar 1 + baz 1 &baz 1 + ick 1 &ick 1 + missing 1 &missing 1 + x 1 &x 1 + z 1 &z 1 + gena 1 &gena 1 + genb 1 &genb 1 + + $ZAP 1 %ZAP 1 @ZAP 1 + /}, + exports => { + '&foo' => Fake::ExporterE->can('foo'), + '&bar' => Fake::ExporterE->can('bar'), + '&baz' => Fake::ExporterE->can('baz'), + '&ick' => Fake::ExporterE->can('ick'), + '&x' => Fake::ExporterE->can('__x'), + '&z' => Fake::ExporterE->can('__z'), + + '&missing' => undef, + + '$ZAP' => \$Fake::ExporterE::ZAP, + '@ZAP' => \@Fake::ExporterE::ZAP, + '%ZAP' => \%Fake::ExporterE::ZAP, + }, + tags => { + b => [qw/bar baz/], + DEFAULT => [qw/foo &bar $ZAP %ZAP @ZAP/], + }, + fail => { '&ick' => 1, ick => 1 }, + }, + "Got menu" + ); + + is($gen->('gena')->(), 'a', "generated a"); + is($gen->('genb')->(), 'b', "generated b"); }; -} +}; + +done_testing; + +__END__ sub parse_args { @@ -280,7 +481,8 @@ sub parse_args { my @import; my @versions; - while(my $arg = shift @args) { + while(my $full_arg = shift @args) { + my $arg = $full_arg; my $lead = substr($arg, 0, 1); my ($spec, $exc); @@ -341,6 +543,9 @@ sub parse_args { $exclude{$_} = 1 for @list; } else { + $self->croak("Cannot use '-as' to rename multiple symbols included by: $full_arg") + if $spec->{'-as'} && @list > 1; + push @import => [$_, $spec] for @list; } } @@ -348,7 +553,6 @@ sub parse_args { return ($into, \@versions, \%exclude, \@import); } - sub _handle_fail { my $self = shift; my ($into, $import) = @_; @@ -401,7 +605,7 @@ sub _set_symbols { for my $set (@$import) { my ($symbol, $spec) = @$set; - my ($sig, $name) = ($symbol =~ m/^(\W?)(.*)$/); + my ($sig, $name) = ($symbol =~ m/^(\W)(.*)$/); # Find the thing we are actually shoving in a new namespace my $ref = $menu->{exports}->{$symbol}; @@ -428,51 +632,87 @@ sub _set_symbols { } } -sub do_import { - my $self = shift; - - my ($into, $versions, $exclude, $import) = $self->parse_args(@_); - - # Exporter supported multiple version numbers being listed... - _version_check($self->from, $self->get_caller, @$versions) if @$versions; +######################################################### +## The rest of these are utility functions, not methods! - return unless @$import; +sub _version_check { + my ($mod, $caller, @versions) = @_; - $self->_handle_fail($into, $import) if $self->menu($into)->{fail}; - $self->_set_symbols($into, $exclude, $import); + eval <<" EOT" or die $@; +#line $caller->[2] "$caller->[1]" +\$mod->VERSION(\$_) for \@versions; +1; + EOT } -sub do_unimport { - my $self = shift; +sub _mod_to_file { + my $file = shift; + $file =~ s{::}{/}g; + $file .= '.pm'; + return $file; +} - my $from = $self->from; - my $imported = $IMPORTED{$from} || $self->croak("'$from' does not have any imports to remove"); +sub _load_file { + my ($caller, $file) = @_; - my %allowed = map { $_ => 1 } @$imported; + eval <<" EOT" || die $@; +#line $caller->[2] "$caller->[1]" +require \$file; + EOT +} - my @args = @_ ? @_ : @$imported; +sub _optimal_import { + my ($from, $into, @args) = @_; + my %final; no strict 'refs'; - my $stash = \%{"$from\::"}; + return 0 if @{"$from\::EXPORT_FAIL"}; + @args = @{"$from\::EXPORT"} unless @args; + my %allowed = map +($_ => 1), @{"$from\::EXPORT"}, @{"$from\::EXPORT_OK"}; use strict 'refs'; - for my $name (@args) { - $name =~ s/^&//; + for my $arg (@args) { + # Get sigil, or first letter of name + my $sig = substr($arg, 0, 1); - $self->croak("Sub '$name' was not imported using " . ref($self)) unless $allowed{$name}; + # Return if non-sub sigil + return 0 if $NON_OPTIMAL{$sig}; - no warnings 'once'; - my $glob = delete $stash->{$name}; - local *GLOBCLONE = *$glob; + # Strip sigil (if sub) + my $name = $arg; + substr($name, 0, 1, '') if $sig eq '&'; - for my $type (qw/SCALAR HASH ARRAY FORMAT IO/) { - next unless defined(*{$glob}{$type}); - no strict 'refs'; - *{"$from\::$name"} = *{$glob}{$type} - } + # Check if the name is allowed (with or without sigil) + return 0 unless $allowed{$name} || $allowed{$arg}; + + no strict 'refs'; + $final{$name} = \&{"$from\::$name"}; } + + no strict 'refs'; + (*{"$into\::$_"} = $final{$_}, push @{$IMPORTED{$into}} => $_) for keys %final; + + return 1; } +sub exporter_import { + my $from = shift; + + my @caller = caller(0); + + return unless @_; + + my $file = _mod_to_file($from); + _load_file(\@caller, $file) unless $INC{$file}; + + return if _optimal_import($from, $caller[0], @_); + + my $self = __PACKAGE__->new( + from => $from, + caller => \@caller, + ); + + $self->do_import($caller[0], @_); +} -1; |