File Coverage

blib/lib/PDL/Demos.pm
Criterion Covered Total %
statement 40 74 54.0
branch 10 24 41.6
condition 3 11 27.2
subroutine 9 16 56.2
pod 9 9 100.0
total 71 134 52.9


line stmt bran cond sub pod time code
1             package PDL::Demos;
2              
3 1     1   95571 use strict;
  1         1  
  1         32  
4 1     1   4 use warnings;
  1         2  
  1         53  
5 1     1   5 use Carp;
  1         1  
  1         86  
6 1     1   6 use Exporter;
  1         3  
  1         1238  
7             require File::Spec;
8              
9             our @ISA="Exporter";
10             our @EXPORT = qw/comment act actnw/;
11              
12             sub comment($) {
13 0     0 1 0 local $SIG{__DIE__} = \&Carp::confess;
14 0         0 print "----\n";
15 0         0 print $_[0];
16 0         0 my $prompt = "---- (press enter)";
17 0 0       0 defined($PERLDL::TERM) ? $PERLDL::TERM->readline($prompt) : ( print $prompt, <> );
18             }
19              
20             sub act($) {
21 0     0 1 0 local $SIG{__DIE__} = \&Carp::confess;
22 0         0 actnw($_[0], (caller)[0]);
23 0         0 my $prompt = "---- (press enter)";
24 0 0       0 defined($PERLDL::TERM) ? $PERLDL::TERM->readline($prompt) : ( print $prompt, <> );
25             }
26              
27             sub _eval_pkg {
28 0     0   0 my ($txt, $pack) = @_;
29 0         0 eval "package $pack; no strict; use PDL; $txt";
30             }
31             sub actnw($) {
32 0     0 1 0 local $SIG{__DIE__} = \&Carp::confess;
33 0         0 my ($script, $pack) = @_;
34 0         0 print "---- Code:";
35 0         0 print $script;
36 0         0 print "---- Output:\n";
37 0   0     0 _eval_pkg($script, $pack // (caller)[0]);
38 0         0 print "----\n";
39 0 0       0 print "----\nOOPS!!! Something went wrong, please make a bug report!: $@\n----\n" if $@;
40             }
41              
42             my ($searched, @found);
43             my @d = qw(PDL Demos);
44             sub list {
45 2 100   2 1 190689 return @found if $searched;
46 1         3 $searched = 1;
47 1         7 my %found_already;
48 1         3 foreach my $path ( @INC ) {
49 8 100       423 next if !-d (my $dir = File::Spec->catdir( $path, @d ));
50 3 50       9 my @c = do { opendir my $dirfh, $dir or die "$dir: $!"; grep !/^\./, readdir $dirfh };
  3         135  
  3         251  
51 3   66     675 for my $f (grep /\.pm$/ && -f File::Spec->catfile( $dir, $_ ), @c) {
52 18         49 $f =~ s/\.pm//;
53 18         45 my $found_mod = join "::", @d, $f;
54 18 100       51 next if $found_already{$found_mod}++;
55 6         15 push @found, $found_mod;
56             }
57 3         527 for my $t (grep -d $_->[1], map [$_, File::Spec->catdir( $dir, $_ )], @c) {
58 0         0 my ($subname, $subd) = @$t;
59             # one extra level
60 0 0       0 my @c = do { opendir my $dirfh, $subd or die "$subd: $!"; grep !/^\./, readdir $dirfh };
  0         0  
  0         0  
61 0   0     0 for my $f (grep /\.pm$/ && -f File::Spec->catfile( $subd, $_ ), @c) {
62 0         0 $f =~ s/\.pm//;
63 0         0 my $found_mod = join "::", @d, $subname, $f;
64 0 0       0 next if $found_already{$found_mod}++;
65 0         0 push @found, $found_mod;
66             }
67             }
68             }
69 1         10 @found;
70             }
71              
72             my ($kw_loaded, %kw2info); # info = [kw, description, module]
73             sub _load_keywords {
74 3 100   3   14 return if $kw_loaded;
75 1         2 $kw_loaded = 1;
76 1         4 my @modules = grep eval "require $_; 1", __PACKAGE__->list;
77 1         26 my %mod2i = map +($_ => [$_->info]), grep $_->can('info'), @modules;
78 1         5 %kw2info = map +($mod2i{$_}[0] => [@{$mod2i{$_}}, $_]), keys %mod2i;
  3         14  
79             }
80 1     1 1 1293 sub keywords { _load_keywords(); keys %kw2info; }
  1         6  
81             sub info {
82 2     2 1 4426 _load_keywords();
83 2   50     8 my $info = $kw2info{$_[1]} || die "unknown demo $_[1]\n";
84 2         10 @$info;
85             }
86             sub demo {
87 1     1 1 5104 my $pkg = ($_[0]->info($_[1]))[2];
88 1 50       12 $pkg->can('demo') ? $pkg->demo : [comment=>"No demo data found for $_[1]\n"];
89             }
90             sub _proxy {
91 0     0     my $method = shift;
92 0           my $pkg = ($_[0]->info($_[1]))[2];
93 0 0         return if !$pkg->can($method);
94 0           _eval_pkg($pkg->$method, (caller 1)[0]);
95             }
96 0     0 1   sub init { _proxy('init', @_) }
97 0     0 1   sub done { _proxy('done', @_) }
98              
99             =head1 NAME
100              
101             PDL::Demos - PDL demo infrastructure
102              
103             =head1 SYNOPSIS
104              
105             # in a demo, if text-orientated
106             package PDL::Demos::Blah;
107             sub info { ('blah', 'Longer description of demo') }
108             sub init { 'use PDL::Graphics::Simple;' }
109             my @demo = (
110             [comment => "Welcome to the Blah demo"],
111             [act => <<'EOF'],
112             print "PDL can make n-dimensional sequences:\n";
113             print $x = sequence(2,3);
114             EOF
115             );
116             sub demo { @demo }
117             sub done { "# return things to previous state\n" }
118              
119             # a GUI-orientated one
120             package PDL::Demos::GUIBlah;
121             use GUIBlah; # so demo won't show up in list if GUIBlah not installed
122             sub info { ('blahgui', 'GUIBlah demo') }
123             sub demo {[actnw => q|
124             # starting up the GUI demo app
125             |.__PACKAGE__.q|::run();
126             |]}
127             sub run { # this is just a convention, but a good one
128             # ...
129             }
130              
131             # iterate a demo of your own module - call it PDL::Demos::(something)
132             make && perl -Mblib -S perldl # run "demo" and it will see your demo
133              
134             # in a CLI or REPL
135             use PDL::Demos;
136             sub demo {
137             if (!$_[0]) {
138             require List::Util;
139             my @kw = sort grep $_ ne 'pdl', PDL::Demos->keywords;
140             my $maxlen = List::Util::max(map length, @kw);
141             print "Use:\n";
142             printf " demo %-${maxlen}s # %s\n", @$_[0,1] for map [PDL::Demos->info($_)], 'pdl', @kw;
143             return;
144             }
145             no strict;
146             PDL::Demos->init($_[0]);
147             $_->[0]->($_->[1]) for PDL::Demos->demo($_[0]);
148             PDL::Demos->done($_[0]);
149             }
150              
151             =head1 DESCRIPTION
152              
153             Provides utilities to make demos for PDL modules.
154              
155             PDL demos should be in the C namespace so that they can
156             be auto-discovered.
157              
158             Please ensure that your demo module is included in a CPAN distribution
159             and add it to the appropriate metadata (e.g. C and
160             C).
161              
162             =head1 METHODS
163              
164             =head2 list
165              
166             Class method; goes through C<@INC> finding all modules starting with
167             C (with up to two C<::>-separated words). Cached after
168             first run. Does not distinguish demo modules that did not load.
169              
170             =head2 keywords
171              
172             Returns the list of keywords (first element of C return-list)
173             of all found modules that loaded successfully and implement an C
174             method. Caches results.
175              
176             =head2 info
177              
178             Given a keyword, returns the result of calling C on the relevant
179             module plus the module name (three elements) or throws exception if
180             unknown keyword.
181              
182             =head2 init
183              
184             Given a keyword, Cs the result of calling C on the relevant
185             module if it has one, or throws exception if unknown keyword.
186              
187             =head2 demo
188              
189             Given a keyword, returns the result of calling C on the relevant
190             module or throws exception if unknown keyword.
191              
192             =head2 done
193              
194             Given a keyword, Cs the result of calling C on the relevant
195             module if it has one, or throws exception if unknown keyword.
196              
197             =head1 DEMO MODULE METHODS
198              
199             Each demo module must provide these class methods:
200              
201             =over
202              
203             =item info
204              
205             Return a two-element list of strings: a single keyword (probably
206             lower-case), and a short description of the demo. Both will be displayed
207             when a user enters C without giving a name.
208              
209             =item demo
210              
211             Returns a list of array-refs of two elements: a L
212             provided by this module, and an argument for it.
213              
214             =item init
215              
216             Return a string of Perl code which will be evaluated in the package
217             running the demo. Use this e.g. for C statements that import
218             functions needed in your demo.
219              
220             =back
221              
222             =head1 FUNCTIONS
223              
224             These are all exported.
225              
226             =head2 comment
227              
228             Prints its argument, prompts user to press enter before returning.
229              
230             =head2 actnw
231              
232             The argument must be a string containing valid Perl code. The string
233             is printed with a separator, then evaluated as Perl code in the
234             package running the demo, with C loaded. Doesn't prompt, so use
235             this for e.g. GUI demos that return when the user tells them to.
236              
237             Multiline code string should start with a newline.
238              
239             =head2 act
240              
241             As above, but prompts before returning.
242              
243             =head1 ERROR HANDLING
244              
245             Check the prerequisites (e.g. optional Perl modules) for your demo in
246             your demo module and not only in the code string you pass to the
247             C routine. If the code in your demo module dies, then the demo
248             will not be offered in the demo overview. Fatal errors in the init
249             routine will be printed and mess up the output layout. Also, error
250             messages might be difficult to understand if users just want to run
251             the demo.
252              
253             If you want to show the demo in the overview though it can't run in
254             the current situation, then make sure that your C method informs
255             the user what is missing, and where they can obtain it.
256              
257             =head1 AUTHOR
258              
259             Copyright (C) 1998 Tuomas J. Lukka.
260             Tweaks by Ed J for PDL 2.077, 2022.
261              
262             =cut
263              
264             1;