File Coverage

blib/lib/Parse/StackTrace/Type/Python/Frame.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Parse::StackTrace::Type::Python::Frame;
2 1     1   2135 use Moose;
  0            
  0            
3             use Parse::StackTrace::Exceptions;
4             use Data::Dumper;
5              
6             extends 'Parse::StackTrace::Frame';
7              
8             has 'error_location' => (is => 'ro', isa => 'Int');
9              
10             our $FUNCTIONLESS_FRAME = qr/
11             ^\s*File\s*"(.+?)", # 1 file
12             \s*(?:line)?\s*(\d+) # 2 line
13             /x;
14              
15             our $FULL_FRAME = qr/
16             $FUNCTIONLESS_FRAME,
17             \s*in\s*(\S+) # 3 function
18             /x;
19            
20             our $DJANGO_FRAME = qr/
21             ^\s*File\s*"(.+?)" # 1 file
22             \s*in\s+(\S+) # 2 function
23             \s*(\d+)\. # 3 line
24             \s+(.+) # 4 code
25             /x;
26              
27             use constant SYNTAX_ERROR_CARET => qr/^\s*\^\s*$/;
28              
29             sub parse {
30             my ($class, %params) = @_;
31             my $lines = $params{'lines'};
32             my $debug = $params{'debug'};
33            
34             my ($parsed, $remaining_lines) = $class->_run_regexes($lines, $debug, [
35             { regex => $FULL_FRAME, fields => [qw(file line function)] },
36             { regex => $FUNCTIONLESS_FRAME, fields => [qw(file line)] },
37             { regex => $DJANGO_FRAME, fields => [qw(file function line code)]},
38             ]);
39            
40             if ($parsed and !exists $parsed->{function}) {
41             $parsed->{function} = '';
42             }
43            
44             if (!$parsed) {
45             my $text = join("\n", @$lines);
46             Parse::StackTrace::Exception::NotAFrame->throw(
47             "Not a valid Python stack frame: $text"
48             );
49             }
50              
51             if (!$parsed->{code}) {
52             my $code_line = shift @$remaining_lines;
53             foreach my $line (@$remaining_lines) {
54             if ($line =~ SYNTAX_ERROR_CARET) {
55             my $caret_pos = index($line, '^');
56             # Account for leading space on the code line
57             if ($code_line =~ /^(\s+)/) {
58             $caret_pos -= length($1);
59             }
60             $parsed->{error_location} = $caret_pos;
61             print "Error Location: $caret_pos" if $debug;
62             last;
63             }
64             $code_line .= " $line";
65             }
66            
67             $code_line = trim($code_line);
68             if ($code_line) {
69             $parsed->{code} = $code_line;
70             }
71             }
72            
73             print STDERR "Parsed As: " . Dumper($parsed) if $debug;
74             return $class->new(%$parsed);
75             }
76              
77             sub _run_regexes {
78             my ($class, $lines, $debug, $tests) = @_;
79            
80             my (@remaining_lines, $parsed);
81             foreach my $test (@$tests) {
82             @remaining_lines = @$lines;
83             $parsed = _check_lines_against_regex(\@remaining_lines, $test->{regex},
84             $test->{fields}, $debug);
85             return ($parsed, \@remaining_lines) if $parsed;
86             }
87             return ();
88             }
89              
90             sub _check_lines_against_regex {
91             my ($lines, $regex, $fields, $debug) = @_;
92              
93             my $text = '';
94             while (my $line = shift @$lines) {
95             $text .= " $line";
96             if ($text =~ $regex) {
97             my @matches = ($1, $2, $3, $4);
98             my %parsed;
99             for (my $i = 0; $i < scalar(@$fields); $i++) {
100             $parsed{$fields->[$i]} = $matches[$i];
101             }
102             return \%parsed;
103             }
104             }
105            
106             print STDERR "Failed Match Against $regex: [$text]\n" if $debug;
107            
108             return undef;
109              
110             }
111              
112             sub trim {
113             my $str = shift;
114             return undef if !defined $str;
115             $str =~ s/^\s*//;
116             $str =~ s/\s*$//;
117             return $str;
118             }
119              
120             __PACKAGE__->meta->make_immutable;
121              
122             1;
123              
124             __END__
125              
126             =head1 NAME
127              
128             Parse::StackTrace::Type::Python::Frame - A frame from a Python stack trace
129              
130             =head1 DESCRIPTION
131              
132             This is an implementation of L<Parse::StackTrace::Frame>.
133              
134             Python frames always have a C<file> and C<line> specified.
135              
136             Most frames also have a C<function>. If they don't, the C<function> will
137             be an empty string.
138              
139             Every frame should have C<code> specified, though there's always a chance
140             that we're parsing an incomplete traceback, in which case C<code> will be
141             C<undef>.
142              
143             There is also an extra accessor for Python frames called C<error_location>.
144             If this trace is because of a SyntaxError, then this is an integer
145             indicating what character (starting from 0) in the C<code> Python thinks
146             the syntax error starts at.