File Coverage

blib/lib/Parse/CPAN/Packages/Fast.pm
Criterion Covered Total %
statement 165 187 88.2
branch 39 70 55.7
condition 4 12 33.3
subroutine 31 35 88.5
pod 3 10 30.0
total 242 314 77.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             # -*- perl -*-
3              
4             #
5             # Author: Slaven Rezic
6             #
7             # Copyright (C) 2009,2010,2012,2013,2014,2015,2024 Slaven Rezic. All rights reserved.
8             # This program is free software; you can redistribute it and/or
9             # modify it under the same terms as Perl itself.
10             #
11             # WWW: https://github.com/eserte/parse-cpan-packages-fast
12             #
13              
14 3     3   658028 use strict;
  3         8  
  3         135  
15 3     3   35 use warnings;
  3         7  
  3         192  
16              
17 3     3   2820 use CPAN::DistnameInfo ();
  3         4439  
  3         320  
18              
19             ######################################################################
20              
21             {
22             package Parse::CPAN::Packages::Fast;
23              
24             our $VERSION = '0.09_50';
25             $VERSION =~ s{_}{};
26              
27 3     3   1428 use IO::Uncompress::Gunzip qw($GunzipError);
  3         107475  
  3         431  
28 3     3   1614 use CPAN::Version ();
  3         4465  
  3         184  
29              
30             # Note: this function is possibly interactive, i.e. if CPAN.pm was
31             # never configured, or if CPAN's Config.pm needs reconfiguration.
32             sub _default_packages_file_interactive {
33 0     0   0 my($class) = @_;
34 0         0 require CPAN;
35 3     3   21 no warnings 'once';
  3         6  
  3         417  
36 0         0 local $CPAN::Be_Silent = 1;
37 0         0 CPAN::HandleConfig->load;
38 0         0 my $packages_file = $CPAN::Config->{keep_source_where} . "/modules/02packages.details.txt.gz";
39 0         0 $packages_file;
40             }
41              
42             # Note: this function is guaranteed to be non-interactive, but it
43             # is using just default locations to look at the CPAN config, or
44             # the 02packages files.
45             sub _default_packages_file_batch {
46 2     2   270387 my($class) = @_;
47              
48 2         4 my $home_cpandir = do {
49 3     3   17 no warnings 'uninitialized'; # HOME may be uninitialized on some systems e.g. Windows
  3         5  
  3         3858  
50 2         7 "$ENV{HOME}/.cpan";
51             };
52 2 50       9 if (!$INC{"CPAN/MyConfig.pm"}) {
53 2         4 my $home_myconfig = "$home_cpandir/CPAN/MyConfig.pm";
54 2 50       72 if (-r $home_myconfig) {
55 2         7 local @INC = ($home_cpandir);
56 2         4 eval { require CPAN::MyConfig };
  2         1272  
57             }
58             }
59 2 50 33     17 if ($INC{"CPAN/MyConfig.pm"} && $CPAN::Config->{keep_source_where}) {
60 2         5 my $packages_file = $CPAN::Config->{keep_source_where} . "/modules/02packages.details.txt.gz";
61 2 50 33     85 if (-r $packages_file && -s $packages_file) {
62 2         12 return $packages_file;
63             }
64             }
65              
66             # Cannot find a usable CPAN::MyConfig, try a default location
67 0         0 my $packages_file = "$home_cpandir/sources/modules/02packages.details.txt.gz";
68 0 0 0     0 if (-r $packages_file && -s $packages_file) {
69 0         0 return $packages_file;
70             }
71              
72 0         0 undef;
73             }
74              
75             *_default_packages_file = \&_default_packages_file_interactive;
76              
77             sub new {
78 3     3 1 278200 my($class, $packages_file) = @_;
79              
80 3 50       15 if (!$packages_file) {
81 0         0 $packages_file = $class->_default_packages_file;
82 0 0       0 if (!$packages_file) {
83 0         0 die "packages file not specified and cannot be determined from CPAN.pm configuration";
84             }
85             }
86              
87 3         24 my %pkg_to_dist;
88             my %dist_to_pkgs;
89 3         0 my %pkg_ver;
90              
91 3         0 my $FH;
92 3 50       25 if ($packages_file !~ m{\.gz$}) { # assume uncompressed file
93 0 0       0 open $FH, '<', $packages_file
94             or die "Can't open $packages_file: $!";
95             } else {
96 3 50       37 $FH = IO::Uncompress::Gunzip->new($packages_file)
97             or die "Can't open $packages_file: $GunzipError";
98             }
99             # overread header
100 3         8602 while(<$FH>) {
101 20 100       1463 last if /^$/;
102             }
103             # read payload
104 3         13 while(<$FH>) {
105 524984         42284446 my($pkg, $ver, $dist) = split;
106 524984         2247912 $pkg_to_dist{$pkg} = $dist;
107 524984         1345598 $pkg_ver{$pkg} = $ver;
108 524984         763896 push @{ $dist_to_pkgs{$dist} }, $pkg;
  524984         2202718  
109             }
110            
111 3         217 bless { pkg_to_dist => \%pkg_to_dist,
112             dist_to_pkgs => \%dist_to_pkgs,
113             pkg_ver => \%pkg_ver,
114             }, $class;
115             }
116              
117             sub package {
118 1     1 0 19 my($self, $package_name) = @_;
119 1 50       9 return undef if !exists $self->{pkg_ver}{$package_name};
120 1         28 Parse::CPAN::Packages::Fast::Package->new($package_name, $self);
121             }
122              
123             sub packages {
124 1     1 1 486 my $self = shift;
125 1         4 keys %{ $self->{pkg_ver} };
  1         206756  
126             }
127              
128             sub package_count {
129 1     1 0 1396 my $self = shift;
130 1         2 scalar keys %{ $self->{pkg_ver} };
  1         15  
131             }
132              
133             sub distribution {
134 0     0 1 0 my($self, $distribution_name) = @_;
135 0 0       0 die "Distribution $distribution_name does not exist" if !exists $self->{dist_to_pkgs}{$distribution_name}; # XXX die or not?
136 0         0 Parse::CPAN::Packages::Fast::Distribution->new($distribution_name, $self);
137             }
138              
139             sub distributions {
140 1     1 0 2 my $self = shift;
141 1         2 map { Parse::CPAN::Packages::Fast::Distribution->new($_, $self) } keys %{ $self->{dist_to_pkgs} };
  43599         93057  
  1         27559  
142             }
143              
144             sub distribution_count {
145 1     1 0 2 my $self = shift;
146 1         4 my @dists = $self->distributions;
147 1         16332 scalar @dists;
148             }
149              
150             sub latest_distribution {
151 4     4 0 1731 my($self, $distribution_name) = @_;
152 4 100       23 return undef if !defined $distribution_name;
153 3         7 my @candidates;
154 3         8 for my $candidate (keys %{ $self->{dist_to_pkgs} }) {
  3         52213  
155 87200 100       354259 if ($candidate =~ m{^./../.*/\Q$distribution_name}) {
156             # Possibly pure CPAN::DistnameInfo is somewhat faster
157             # than Parse::CPAN::Packages::Fast::Distribution (no
158             # inside-out handling, no additional DESTROY)
159 6         77 my $d = CPAN::DistnameInfo->new($candidate);
160 3     3   31 no warnings 'uninitialized'; # Some distributions have no parseable dist name
  3         11  
  3         3505  
161 6 50       887 if ($d->dist eq $distribution_name) {
162 6         73 push @candidates, $d;
163             }
164             }
165             }
166 3 50       25351 return if !@candidates; # XXX die or not?
167 3         21 my $best_candidate = pop @candidates;
168 3         28 my $best_candidate_version = $best_candidate->version;
169 3         32 for my $candidate (@candidates) {
170 3         147 my $this_version = $candidate->version;
171 3 50       102 if (CPAN::Version->vlt($best_candidate_version, $this_version)) {
172 0         0 $best_candidate = $candidate;
173 0         0 $best_candidate_version = $this_version;
174             }
175             }
176 3         181 Parse::CPAN::Packages::Fast::Distribution->new($best_candidate->pathname, $self);
177             }
178              
179             sub latest_distributions {
180 2     2 0 7 my $self = shift;
181 2         8 my %latest_dist;
182 2         4 for my $pathname (keys %{ $self->{dist_to_pkgs} }) {
  2         51749  
183 87198         213865 my $d = Parse::CPAN::Packages::Fast::Distribution->new($pathname, $self);
184 87198         234747 my $dist = $d->dist;
185 87198 100       409898 next if !defined $dist;
186 87136 100       280249 if (!exists $latest_dist{$dist}) {
187 82396         300137 $latest_dist{$dist} = $d;
188             } else {
189 4740 100       20429 if (CPAN::Version->vlt($latest_dist{$dist}->version, $d->version)) {
190 2026         139369 $latest_dist{$dist} = $d;
191             }
192             }
193             }
194 2         136024 values %latest_dist;
195             }
196              
197             sub latest_distribution_count {
198 1     1 0 3 my $self = shift;
199 1         6 my @dists = $self->latest_distributions;
200 1         652 scalar @dists;
201             }
202              
203             # Addition: fast module lookup without loading whole packages file
204             # Not yet official!
205             sub _module_lookup {
206 13 50   13   94645 die "Usage?" if @_ != 4;
207 13         55 my($class, $module, $orig_packages_file, $cache_file) = @_;
208 13         1739 require Search::Dict;
209 13         2228 my $pwhfh = $class->_get_plain_packages_fh($orig_packages_file, $cache_file);
210 13         33 my $skey = "$module ";
211 13 50       55 return if Search::Dict::look($pwhfh, $skey, 0, 1) == -1;
212 13         9673 while () {
213 13         126 my $got = <$pwhfh>;
214 13 100       81 if (index($got, $skey) == 0) {
215 12         24 chomp $got;
216 12         69 my($pkg, $ver, $dist) = split /\s+/, $got;
217             return {
218 12         308 package => $pkg,
219             version => $ver,
220             dist => $dist,
221             };
222             }
223 1 50       48 return if lc(substr($got, 0, length($skey))) gt lc($skey);
224 0 0       0 return if eof($pwhfh);
225             }
226             }
227            
228             sub _get_plain_packages_fh {
229 13 50   13   38 die "Usage?" if @_ != 3;
230 13         35 my(undef, $orig_packages_file, $cache_file) = @_;
231 13 50       391 die "$orig_packages_file does not exist" if !-e $orig_packages_file;
232 13 100 66     350 if (!-e $cache_file || -M $cache_file > -M $orig_packages_file) {
233 1 50       33 my $ifh = IO::Uncompress::Gunzip->new($orig_packages_file)
234             or die "Can't open $orig_packages_file: $GunzipError";
235 1         3758 require File::Temp;
236 1         7 require File::Basename;
237 1 50       166 my($tmpfh,$tmpfile) = File::Temp::tempfile(DIR => File::Basename::dirname($cache_file))
238             or die "Can't create temporary file: $!";
239 1         983 while (<$ifh>) {
240 9 100       822 last if /^$/;
241             }
242             {
243 1         4 local $/ = \8192;
  1         6  
244 1         5 while (<$ifh>) {
245 2795         1842779 print $tmpfh $_;
246             }
247             }
248 1 50       129 close $tmpfh
249             or die "Error while writing temporary file $tmpfile: $!";
250 1 50       467 rename $tmpfile, $cache_file
251             or die "While renaming $tmpfile to $cache_file: $!";
252             }
253 13 50       719 open my $fh, $cache_file
254             or die "Can't open $cache_file: $!";
255 13         60 $fh;
256             }
257            
258             }
259              
260             ######################################################################
261              
262             {
263              
264             package Parse::CPAN::Packages::Fast::Package;
265              
266             our $VERSION = $Parse::CPAN::Packages::Fast::VERSION;
267              
268             # Use inside-out technique for this member, to hide it in dumps etc.
269             my %obj_to_packages;
270              
271             sub new {
272 4     4   15 my($class, $package_name, $packages) = @_;
273             my $self = bless { package => $package_name,
274 4         26 version => $packages->{pkg_ver}{$package_name},
275             }, 'Parse::CPAN::Packages::Fast::Package';
276 4         32 $obj_to_packages{$self} = $packages;
277 4         15 $self;
278             }
279              
280             for my $method (qw(package version)) {
281 3     3   24 no strict 'refs';
  3         7  
  3         816  
282 6     6   1510 *{$method} = sub { shift->{$method} };
283             }
284              
285             sub distribution {
286 2     2   6 my $self = shift;
287 2         7 my $packages = $obj_to_packages{$self};
288 2         11 my $dist = $packages->{pkg_to_dist}->{$self->package};
289 2         11 Parse::CPAN::Packages::Fast::Distribution->new($dist, $packages);
290             }
291              
292             sub prefix {
293 1     1   3 my $self = shift;
294 1         4 $self->distribution->prefix;
295             }
296              
297             sub DESTROY {
298 4     4   114593 my $self = shift;
299 4         591492 delete $obj_to_packages{$self};
300             }
301             }
302              
303             ######################################################################
304              
305             {
306             package Parse::CPAN::Packages::Fast::Distribution;
307              
308             our $VERSION = $Parse::CPAN::Packages::Fast::VERSION;
309              
310 3     3   38 use base qw(CPAN::DistnameInfo);
  3         7  
  3         1339  
311              
312             # Use inside-out technique for this member, to hide it in dumps etc.
313             my %obj_to_packages;
314              
315             sub new {
316 130802     130802   330541 my($class, $pathname, $packages) = @_;
317 130802         322270 my $self = $class->SUPER::new($pathname);
318 130802         9562948 $obj_to_packages{$self} = $packages;
319 130802         316690 $self;
320             }
321            
322             sub prefix {
323 2     2   1488 my $self = shift;
324 2         32 my $prefix = $self->pathname;
325 2         14 $prefix =~ s{^authors/id/}{};
326 2         57 $prefix;
327             }
328              
329             sub contains {
330 1     1   4 my $self = shift;
331 1         4 my $packages = $obj_to_packages{$self};
332 1         3 map { Parse::CPAN::Packages::Fast::Package->new($_, $packages) } @{ $packages->{dist_to_pkgs}{$self->pathname} };
  3         16  
  1         7  
333             }
334              
335             sub DESTROY {
336 130802     130802   587130 my $self = shift;
337 130802         852107 delete $obj_to_packages{$self};
338             }
339              
340             # Methods found in original Parse::CPAN::Packages::Distribution
341             sub add_package {
342 0     0     die "NYI";
343             }
344              
345             # Would be nice to have:
346             sub is_latest_distribution {
347 0     0     die "NYI";
348             }
349             }
350              
351             ######################################################################
352              
353             1;
354              
355             __END__