File Coverage

blib/lib/Parse/CPAN/Packages.pm
Criterion Covered Total %
statement 132 132 100.0
branch 26 34 76.4
condition 5 12 41.6
subroutine 35 35 100.0
pod 14 16 87.5
total 212 229 92.5


line stmt bran cond sub pod time code
1             package Parse::CPAN::Packages;
2 1     1   1040 use Moo;
  1         11821  
  1         4  
3 1     1   1566 use CPAN::DistnameInfo;
  1         698  
  1         24  
4 1     1   528 use Compress::Zlib;
  1         52676  
  1         201  
5 1     1   429 use Path::Class ();
  1         25022  
  1         25  
6 1     1   7 use File::Slurp 'read_file';
  1         1  
  1         56  
7 1     1   363 use Parse::CPAN::Packages::Distribution;
  1         3  
  1         40  
8 1     1   451 use Parse::CPAN::Packages::Package;
  1         4  
  1         38  
9 1     1   6 use Types::Standard qw( HashRef Maybe Str );
  1         1  
  1         4  
10 1     1   942 use version;
  1         1338  
  1         9  
11             our $VERSION = '2.40';
12              
13             has 'filename' => ( is => 'rw', isa => Str );
14             has 'mirror_dir' => ( is => 'lazy', isa => Maybe [Str] );
15              
16             has 'details' => ( is => 'rw', isa => HashRef, default => sub { {} } );
17             has 'data' => ( is => 'rw', isa => HashRef, default => sub { {} } );
18             has 'dists' => ( is => 'rw', isa => HashRef, default => sub { {} } );
19             has 'latestdists' => ( is => 'rw', isa => HashRef, default => sub { {} } );
20              
21             sub BUILDARGS {
22 6     6 0 7237 my ( $class, @args ) = @_;
23 6 100       35 return {@args} if @args > 1;
24 5         95 return { filename => $args[0] };
25             }
26              
27             sub BUILD {
28 6     6 0 154 my $self = shift;
29 6         133 my $filename = $self->filename;
30              
31             # read the file then parse it if present
32 6 50       539 $self->parse( $filename ) if $filename;
33              
34 6         86 return $self;
35             }
36              
37             sub _build_mirror_dir {
38 5     5   706 my ( $self ) = @_;
39 5 100       93 return if $self->filename =~ /\n/;
40 3 50       71 return if !-f $self->filename;
41 3         130 my $dir = Path::Class::file( $self->filename )->dir->parent;
42 3         987 return $dir->stringify;
43             }
44              
45             # read the file into memory and return it
46             sub _slurp_details {
47 6     6   8 my ( $self, $filename ) = @_;
48 6   50     18 $filename ||= '02packages.details.txt.gz';
49              
50 6 100       28 return $filename if $filename =~ /Description:/;
51 5 100       21 return Compress::Zlib::memGunzip( $filename ) if $filename =~ /^\037\213/;
52              
53 4         9 my @read_params = ( $filename );
54 4 100       16 push @read_params, ( binmode => ':raw' ) if $filename =~ /\.gz/;
55              
56 4         30 my $data = read_file( @read_params );
57              
58 4 100       539 return Compress::Zlib::memGunzip( $data ) if $filename =~ /\.gz/;
59 2         20 return $data;
60             }
61              
62             for my $subname ( qw(file url description columns intended_for written_by line_count last_updated) ) {
63 1     1   438 no strict 'refs';
  1         23  
  1         580  
64 8     8   627 *{$subname} = sub { return shift->{preamble}{$subname} };
65             }
66              
67             sub parse {
68 6     6 1 9 my ( $self, $filename ) = @_;
69              
70             # read the preamble
71 6         23 my @details = split "\n", $self->_slurp_details( $filename );
72 6         579 while ( @details ) {
73 54         59 local $_ = shift @details;
74 54 100       139 last if /^\s*$/;
75 48 50       162 next unless /^([^:]+):\s*(.*)/;
76 48         100 my ( $key, $value ) = ( lc( $1 ), $2 );
77 48         52 $key =~ tr/-/_/;
78 48         145 $self->{preamble}{$key} = $value;
79             }
80              
81             # run though each line of the file
82 6         16 for my $line ( @details ) {
83              
84             # make a package object from the line
85 54         146 my ( $package_name, $package_version, $prefix ) = split ' ', $line;
86 54         104 $self->add_quick( $package_name, $package_version, $prefix );
87             }
88             }
89              
90             sub add_quick {
91 54     54 1 63 my ( $self, $package_name, $package_version, $prefix ) = @_;
92              
93             # create a distribution object (or get an existing one)
94 54         80 my $dist = $self->distribution_from_prefix( $prefix );
95              
96             # create the package object
97 54         1069 my $m = Parse::CPAN::Packages::Package->new(
98             {
99             package => $package_name,
100             version => $package_version,
101             distribution => $dist
102             }
103             );
104              
105             # make the package have the distribion and the distribution
106             # have the package. Yes, this creates a cirtular reference. eek!
107 54         4994 $dist->add_package( $m );
108              
109             # record this distribution and package
110 54         837 $self->add_distribution( $dist );
111 54         91 $self->add_package( $m );
112             }
113              
114             sub distribution_from_prefix {
115 54     54 1 45 my ( $self, $prefix ) = @_;
116              
117             # see if we have one of these already and return it if we do.
118 54         91 my $d = $self->distribution( $prefix );
119 54 100       841 return $d if $d;
120              
121             # create a new one otherwise
122 42         144 my $i = CPAN::DistnameInfo->new( $prefix );
123 42         2512 $d = Parse::CPAN::Packages::Distribution->new(
124             {
125             prefix => $prefix,
126             dist => $i->dist,
127             version => $i->version,
128             maturity => $i->maturity,
129             filename => $i->filename,
130             cpanid => $i->cpanid,
131             distvname => $i->distvname,
132             mirror_dir => $self->mirror_dir,
133             }
134             );
135 42         1943 return $d;
136             }
137              
138             sub add_package {
139 54     54 1 51 my ( $self, $package ) = @_;
140              
141             # store it
142 54         874 $self->data->{ $package->package } = $package;
143              
144 54         2900 return $self;
145             }
146              
147             sub package {
148 6     6 1 8224 my ( $self, $package_name ) = @_;
149 6         137 return $self->data->{$package_name};
150             }
151              
152             sub packages {
153 7     7 1 5376 my $self = shift;
154 7         37 return values %{ $self->data };
  7         212  
155             }
156              
157             sub add_distribution {
158 54     54 1 65 my ( $self, $dist ) = @_;
159              
160 54         81 $self->_store_distribution( $dist );
161 54         1707 $self->_ensure_latest_distribution( $dist );
162             }
163              
164             sub _store_distribution {
165 54     54   55 my ( $self, $dist ) = @_;
166              
167 54         895 $self->dists->{ $dist->prefix } = $dist;
168             }
169              
170             sub _ensure_latest_distribution {
171 54     54   59 my ( $self, $new ) = @_;
172              
173 54         825 my $latest = $self->latest_distribution( $new->dist );
174 54 100       881 if ( !$latest ) {
175 36         59 $self->_set_latest_distribution( $new );
176 36         769 return;
177             }
178 18         420 my $new_version = $new->version;
179 18         813 my $latest_version = $latest->version;
180 18         79 my ( $newv, $latestv );
181              
182 18         21 eval {
183 1     1   5 no warnings;
  1         1  
  1         107  
184 18   50     141 $newv = version->new( $new_version || 0 );
185 18   50     80 $latestv = version->new( $latest_version || 0 );
186             };
187              
188 18 50       39 $self->_set_latest_distribution( $new ) if $self->_dist_is_latest( $newv, $latestv, $new_version, $latest_version );
189              
190 18         44 return;
191             }
192              
193             sub _dist_is_latest {
194 18     18   31 my ( $self, $newv, $latestv, $new_version, $latest_version ) = @_;
195 18 50 33     445 return 1 if $newv && $latestv && $newv > $latestv;
      33        
196 1     1   4 no warnings;
  1         1  
  1         253  
197 18 50       126 return 1 if $new_version > $latest_version;
198 18         41 return 0;
199             }
200              
201             sub distribution {
202 55     55 1 470 my ( $self, $dist ) = @_;
203 55         937 return $self->dists->{$dist};
204             }
205              
206             sub distributions {
207 1     1 1 2 my $self = shift;
208 1         2 return values %{ $self->dists };
  1         25  
209             }
210              
211             sub _set_latest_distribution {
212 36     36   37 my ( $self, $dist ) = @_;
213 36 50       548 return unless $dist->dist;
214 36         679 $self->latestdists->{ $dist->dist } = $dist;
215             }
216              
217             sub latest_distribution {
218 55     55 1 1325 my ( $self, $dist ) = @_;
219 55 50       117 return unless $dist;
220 55         882 return $self->latestdists->{$dist};
221             }
222              
223             sub latest_distributions {
224 2     2 1 824 my $self = shift;
225 2         2 return values %{ $self->latestdists };
  2         50  
226             }
227              
228             sub package_count {
229 1     1 1 613 my $self = shift;
230 1         3 return scalar scalar $self->packages;
231             }
232              
233             sub distribution_count {
234 1     1 1 426 my $self = shift;
235 1         37 return scalar $self->distributions;
236             }
237              
238             sub latest_distribution_count {
239 1     1 1 414 my $self = shift;
240 1         4 return scalar $self->latest_distributions;
241             }
242              
243             1;
244              
245             __END__
246              
247             =head1 NAME
248              
249             Parse::CPAN::Packages - Parse 02packages.details.txt.gz
250              
251             =head1 SYNOPSIS
252              
253             use Parse::CPAN::Packages;
254              
255             # must have downloaded
256             my $p = Parse::CPAN::Packages->new("02packages.details.txt.gz");
257             # either a filename as above or pass in the contents of the file
258             # (uncompressed)
259             my $p = Parse::CPAN::Packages->new($packages_details_contents);
260              
261             my $m = $p->package("Acme::Colour");
262             # $m is a Parse::CPAN::Packages::Package object
263             print $m->package, "\n"; # Acme::Colour
264             print $m->version, "\n"; # 1.00
265              
266             my $d = $m->distribution();
267             # $d is a Parse::CPAN::Packages::Distribution object
268             print $d->prefix, "\n"; # L/LB/LBROCARD/Acme-Colour-1.00.tar.gz
269             print $d->dist, "\n"; # Acme-Colour
270             print $d->version, "\n"; # 1.00
271             print $d->maturity, "\n"; # released
272             print $d->filename, "\n"; # Acme-Colour-1.00.tar.gz
273             print $d->cpanid, "\n"; # LBROCARD
274             print $d->distvname, "\n"; # Acme-Colour-1.00
275              
276             # all the package objects
277             my @packages = $p->packages;
278              
279             # all the distribution objects
280             my @distributions = $p->distributions;
281              
282             # the latest distribution
283             $d = $p->latest_distribution("Acme-Colour");
284             is($d->prefix, "L/LB/LBROCARD/Acme-Colour-1.00.tar.gz");
285             is($d->version, "1.00");
286              
287             # all the latest distributions
288             my @distributions = $p->latest_distributions;
289              
290             =head1 DESCRIPTION
291              
292             The Comprehensive Perl Archive Network (CPAN) is a very useful
293             collection of Perl code. It has several indices of the files that it
294             hosts, including a file named "02packages.details.txt.gz" in the
295             "modules" directory. This file contains lots of useful information and
296             this module provides a simple interface to the data contained within.
297              
298             In a future release L<Parse::CPAN::Packages::Package> and
299             L<Parse::CPAN::Packages::Distribution> might have more information.
300              
301             =head2 Methods
302              
303             =over
304              
305             =item new
306              
307             Creates a new instance from a details file.
308              
309             The constructor can be passed either the path to the
310             C<02packages.details.txt.gz> file, a path to an ungzipped version of
311             this file, or a scalar containing the entire uncompressed contents of
312             the file.
313              
314             Note that this module does not concern itself with downloading this
315             file. You should do this yourself. For example:
316              
317             use LWP::Simple qw(get);
318             my $data = get("http://www.cpan.org/modules/02packages.details.txt.gz");
319             my $p = Parse::CPAN::Packages->new($data);
320              
321             If you have a configured L<CPAN>, then there's usually already a
322             cached file available:
323              
324             use CPAN;
325             $CPAN::Be_Silent = 1;
326             CPAN::HandleConfig->load;
327             my $file = $CPAN::Config->{keep_source_where} . "/modules/02packages.details.txt.gz";
328             my $p = Parse::CPAN::Packages->new($file);
329              
330             =item package($packagename)
331              
332             Returns a C<Parse::CPAN::Packages::Package> that represents the
333             named package.
334              
335             my $p = Parse::CPAN::Packages->new($gzfilename);
336             my $package = $p->package("Acme::Colour");
337              
338             =item packages()
339              
340             Returns a list of B<Parse::CPAN::Packages::Package> objects
341             representing all the packages that were extracted from the file.
342              
343             =item package_count()
344              
345             Returns the number of packages stored.
346              
347             =item distribution($filename)
348              
349             Returns a B<Parse::CPAN::Packages::Distribution> object that
350             represents the filename passed:
351              
352             my $p = Parse::CPAN::Packages->new($gzfilename);
353             my $dist = $p->distribution('L/LB/LBROCARD/Acme-Colour-1.00.tar.gz');
354              
355             =item distributions()
356              
357             Returns a list of B<Parse::CPAN::Packages::Distribution> objects
358             representing all the known distributions.
359              
360             =item distribution_count()
361              
362             Returns the number of distributions stored.
363              
364             =item latest_distribution($distname)
365              
366             Returns the C<Parse::CPAN::Packages::Distribution> object that
367             represents the latest distribution for the named disribution passed,
368             that is to say it returns the distribution that has the highest
369             version number (as determined by version.pm or number comparison if
370             that fails):
371              
372             my $p = Parse::CPAN::Packages->new($gzfilename);
373             my $dist = $p->distribution('Acme-Color');
374              
375             =item latest_distrbutions()
376              
377             Returns a list of B<Parse::CPAN::Packages::Distribution> objects
378             representing all the latest distributions.
379              
380             =item latest_distribution_count()
381              
382             Returns the number of distributions stored.
383              
384             =back
385              
386             =head2 Preamble Methods
387              
388             These methods return the information from the preamble
389             at the start of the file. They return undef if for any reason
390             no matching preamble line was found.
391              
392             =over
393              
394             =item file()
395              
396             =item url()
397              
398             =item description()
399              
400             =item columns()
401              
402             =item intended_for()
403              
404             =item written_by()
405              
406             =item line_count()
407              
408             =item last_updated()
409              
410             =back
411              
412             =head2 Addtional Methods
413              
414             These are additional methods that you may find useful.
415              
416             =over
417              
418             =item parse($filename)
419              
420             Parses the filename. Works in a similar fashion to the the
421             constructor (i.e. you can pass it a filename for a
422             compressed/1uncompressed file, a uncompressed scalar containing the
423             file. You can also pass nothing to indicate to load the compressed
424             file from the current working directory.)
425              
426             Note that each time this function is run the packages and distribtions
427             found will be C<added> to the current list of packages.
428              
429             =item add_quick($package_name, $package_version, $prefix)
430              
431             Quick way of adding a new package and distribution.
432              
433             =item add_package($package_obj)
434              
435             Adds a package. Note that you'll probably want to add the
436             corrisponding distribution for that package too (it's not done
437             automatically.)
438              
439             =item add_distribution($distribution_obj)
440              
441             Adds a distribution. Note that you'll probably want to add the
442             corresponding packages for that distribution too (it's not done
443             automatically.)
444              
445             =item distribution_from_prefix($prefix)
446              
447             Returns a distribution given a prefix.
448              
449             =item latest_distributions
450              
451             Returns all the latest distributions:
452              
453             my @distributions = $p->latest_distributions;
454              
455             =cut
456              
457             =back
458              
459             =head1 AUTHOR
460              
461             Leon Brocard <acme@astray.com>
462              
463             =head1 COPYRIGHT
464              
465             Copyright (C) 2004-9, Leon Brocard
466              
467             =head1 LICENSE
468              
469             This module is free software; you can redistribute it or modify it under
470             the same terms as Perl itself.
471              
472             =head1 BUGS
473              
474             This module leaks memory as packages hold distributions and
475             distributions hold packages. No attempt has been made to fix this as
476             it's not anticpated that this will be used in long running programs
477             that will dispose of the objects once created.
478              
479             The old interface for C<new> where if you passed no arguments it would
480             look for a C<02packages.details.txt.gz> in your current directory is
481             no longer supported.
482              
483             =head1 TODO
484              
485             delete_* methods. merge_into method. Documentation for other modules.
486              
487             =head1 SEE ALSO
488              
489             L<CPAN::DistInfoname>, L<Parse::CPAN::Packages::Writer>.