Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision c311f0f2

Von Moritz Bunkus vor fast 14 Jahren hinzugefügt

  • ID c311f0f2b26213afd814d8ff2e954da54b09fcf0
  • Vorgänger f416a998
  • Nachfolger 27ffa16a

Exception::Lite als neues Standard-Exception-Modul in Fallback-Module aufgenommen

Unterschiede anzeigen:

modules/fallback/Exception/Lite.pm
# Copyright (c) 2010 Elizabeth Grace Frank-Backman.
# All rights reserved.
# Liscenced under the "Artistic Liscence"
# (see http://dev.perl.org/licenses/artistic.html)
use 5.8.8;
use strict;
use warnings;
use overload;
package Exception::Lite;
our @ISA = qw(Exporter);
our @EXPORT_OK=qw(declareExceptionClass isException isChainable
onDie onWarn);
our %EXPORT_TAGS
=( common => [qw(declareExceptionClass isException isChainable)]
, all => [@EXPORT_OK]
);
my $CLASS='Exception::Lite';
#------------------------------------------------------------------
our $STRINGIFY=3;
our $FILTER=1;
our $UNDEF='<undef>';
our $TAB=3;
our $LINE_LENGTH=120;
# provide command line control over amount and layout of debugging
# information, e.g. perl -mException::Lite=STRINGIFY=4
sub import {
Exception::Lite->export_to_level(1, grep {
if (/^(\w+)=(.*)$/) {
my $k = $1;
my $v = $2;
if ($k eq 'STRINGIFY') { $STRINGIFY=$v;
} elsif ($k eq 'FILTER') { $FILTER=$v;
} elsif ($k eq 'LINE_LENGTH') { $LINE_LENGTH=$v;
} elsif ($k eq 'TAB') { $TAB=$v;
}
0;
} else {
1;
}
} @_);
}
#------------------------------------------------------------------
# Note to source code divers: DO NOT USE THIS. This is intended for
# internal use but must be declared with "our" because we need to
# localize it. This is an implementation detail and cannot be relied
# on for future releases.
our $STACK_OFFSET=0;
#------------------------------------------------------------------
use Scalar::Util ();
use constant EVAL => '(eval)';
#==================================================================
# EXPORTABLE FUNCTIONS
#==================================================================
sub declareExceptionClass {
my ($sClass, $sSuperClass, $xFormatRule, $bCustomizeSubclass) = @_;
my $sPath = $sClass; $sPath =~ s/::/\//g; $sPath .= '.pm';
if ($INC{$sPath}) {
# we want to start with the caller's frame, not ours
local $STACK_OFFSET = $STACK_OFFSET + 1;
die 'Exception::Lite::Any'->new("declareExceptionClass failed: "
. "$sClass is already defined!");
return undef;
}
my $sRef=ref($sSuperClass);
if ($sRef) {
$bCustomizeSubclass = $xFormatRule;
$xFormatRule = $sSuperClass;
$sSuperClass=undef;
} else {
$sRef = ref($xFormatRule);
if (!$sRef && defined($xFormatRule)) {
$bCustomizeSubclass = $xFormatRule;
$xFormatRule = undef;
}
}
# set up things dependent on whether or not the class has a
# format string or expects a message for each instance
my ($sLeadingParams, $sAddOrOmit, $sRethrowMsg, $sMakeMsg);
my $sReplaceMsg='';
if ($sRef) {
$sLeadingParams='my $e; $e=shift if ref($_[0]);';
$sAddOrOmit='added an unnecessary message or format';
$sRethrowMsg='';
#generate format rule
$xFormatRule=$xFormatRule->($sClass) if ($sRef eq 'CODE');
my $sFormat= 'q{' . $xFormatRule->[0] . '}';
if (scalar($xFormatRule) == 1) {
$sMakeMsg='my $msg='.$sFormat;
} else {
my $sSprintf = 'Exception::Lite::_sprintf(' . $sFormat
. ', map {defined($_)?$_:\''. $UNDEF .'\'} @$h{qw('
. join(' ', @$xFormatRule[1..$#$xFormatRule]) . ')});';
$sMakeMsg='my $msg='.$sSprintf;
$sReplaceMsg='$_[0]->[0]='.$sSprintf;
}
} else {
$sLeadingParams = 'my $e=shift; my $msg;'.
'if(ref($e)) { $msg=shift; $msg=$e->[0] if !defined($msg);}'.
'else { $msg=$e;$e=undef; }';
$sAddOrOmit='omitted a required message';
$sRethrowMsg='my $msg=shift; $_[0]->[0]=$msg if defined($msg);';
$sMakeMsg='';
}
# put this in an eval so that it doesn't cause parse errors at
# compile time in no-threads versions of Perl
my $sTid = eval q{defined(&threads::tid)?'threads->tid':'undef'};
my $sDeclare = "package $sClass;".
'sub new { my $cl=shift;'. $sLeadingParams .
'my $st=Exception::Lite::_cacheStackTrace($e);'.
'my $h= Exception::Lite::_shiftProperties($cl' .
',$st,"'.$sAddOrOmit.'",@_);' . $sMakeMsg .
'my $self=bless([$msg,$h,$st,$$,'.$sTid.',$e,[]],$cl);';
# the remainder depends on the type of subclassing
if ($bCustomizeSubclass) {
$sDeclare .= '$self->[7]={}; $self->_new(); return $self; }'
. 'sub _p_getSubclassData { $_[0]->[7]; }';
} else {
$sDeclare .= 'return $self;}'.
'sub replaceProperties {'.
'my $h={%{$_[0]->[1]},%{$_[1]}}; $_[0]->[1]=$h;'.$sReplaceMsg.
'}'.
'sub rethrow {' .
'my $self=shift;' . $sRethrowMsg .
'Exception::Lite::_rethrow($self,"'.$sAddOrOmit.'",@_)' .
'}';
unless (isExceptionClass($sSuperClass)) {
$sDeclare .=
'sub _getInterface { \'Exception::Lite\' }' .
'sub getMessage { $_[0]->[0] };' .
'sub getProperty { $_[0]->[1]->{$_[1]} }' .
'sub isProperty { exists($_[0]->[1]->{$_[1]})?1:0 }' .
'sub getStackTrace { $_[0]->[2] }' .
'sub getFrameCount { scalar(@{$_[0]->[2]}); }' .
'sub getFile { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[0] };' .
'sub getLine { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[1] };' .
'sub getSubroutine { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[2] };' .
'sub getArgs { $_[0]->[2]->[ $_[1]?$_[1]:0 ]->[3] };' .
'sub getPackage {$_[0]->[2]->[-1]->[2] =~ /(\w+)>$/;$1}'.
'sub getPid { $_[0]->[3] }' .
'sub getTid { $_[0]->[4] }' .
'sub getChained { $_[0]->[5] }' .
'sub getPropagation { $_[0]->[6]; }' .
'use overload '.
'q{""} => \&Exception::Lite::_dumpMessage ' .
', q{0+} => \&Exception::Lite::_refaddr, fallback=>1;' .
'sub PROPAGATE { push @{$_[0]->[6]},[$_[1],$_[2]]; $_[0]}';
}
}
$sDeclare .= 'return 1;';
local $SIG{__WARN__} = sub {
my ($p,$f,$l) = caller(2);
my $s=$_[0]; $s =~ s/at \(eval \d+\)\s+line\s+\d+\.//m;
print STDERR "$s in declareExceptionClass($sClass,...) "
."in file $f, line $l\n";
};
eval $sDeclare or do {
my ($p,$f,$l) = caller(1);
print STDERR "Can't create class $sClass at file $f, line $l\n";
if ($sClass =~ /\w:\w/) {
print STDERR "Bad class name: "
."At least one ':' is not doubled\n";
} elsif ($sClass !~ /^\w+(?:::\w+)*$/) {
print STDERR "Bad class name: $sClass\n";
} else {
$sDeclare=~s/(sub |use )/\n$1/g; print STDERR "$sDeclare\n";
}
};
# this needs to be separate from the eval, otherwise it never
# ends up in @INC or @ISA, at least in Perl 5.8.8
$INC{$sPath} = __FILE__;
eval "\@${sClass}::ISA=qw($sSuperClass);" if $sSuperClass;
return $sClass;
}
#------------------------------------------------------------------
sub isChainable { return ref($_[0])?1:0; }
#------------------------------------------------------------------
sub isException {
my ($e, $sClass) = @_;
my $sRef=ref($e);
return !defined($sClass)
? ($sRef ? isExceptionClass($sRef) : 0)
: $sClass eq ''
? ($sRef eq '' ? 1 : 0)
: ($sRef eq '')
? 0
: $sRef->isa($sClass)
?1:0;
}
#------------------------------------------------------------------
sub isExceptionClass {
return defined($_[0]) && $_[0]->can('_getInterface')
&& ($_[0]->_getInterface() eq __PACKAGE__) ? 1 : 0;
}
#------------------------------------------------------------------
sub onDie {
my $iStringify = $_[0];
$SIG{__DIE__} = sub {
$Exception::Lite::STRINGIFY=$iStringify;
warn 'Exception::Lite::Any'->new('Unexpected death:'.$_[0])
unless $^S || isException($_[0]);
};
}
#------------------------------------------------------------------
sub onWarn {
my $iStringify = $_[0];
$SIG{__WARN__} = sub {
$Exception::Lite::STRINGIFY=$iStringify;
print STDERR 'Exception::Lite::Any'->new("Warning: $_[0]");
};
}
#==================================================================
# PRIVATE SUBROUTINES
#==================================================================
#------------------------------------------------------------------
sub _cacheCall {
my $iFrame = $_[0];
my @aCaller;
my $aArgs;
# caller populates @DB::args if called within DB package
eval {
# this 2 line wierdness is needed to prevent Module::Build from finding
# this and adding it to the provides list.
package
DB;
#get rid of eval and call to _cacheCall
@aCaller = caller($iFrame+2);
# mark leading undefined elements as maybe shifted away
my $iDefined;
if ($#aCaller < 0) {
@DB::args=@ARGV;
}
$aArgs = [ map {
defined($_)
? do {$iDefined=1;
"'$_'" . (overload::Method($_,'""')
? ' ('.overload::StrVal($_).')':'')}
: 'undef' . (defined($iDefined)
? '':' (maybe shifted away?)')
} @DB::args];
};
return $#aCaller < 0 ? \$aArgs : [ @aCaller[0..3], $aArgs ];
}
#------------------------------------------------------------------
sub _cacheStackTrace {
my $e=$_[0]; my $st=[];
# set up initial frame
my $iFrame= $STACK_OFFSET + 1; # call to new
my $aCall = _cacheCall($iFrame++);
my ($sPackage, $iFile, $iLine, $sSub, $sArgs) = @$aCall;
my $iLineFrame=$iFrame;
$aCall = _cacheCall($iFrame++); #context of call to new
while (ref($aCall) ne 'REF') {
$sSub = $aCall->[3]; # subroutine containing file,line
$sArgs = $aCall->[4]; # args used to call $sSub
#print STDERR "debug-2: package=$sPackage file=$iFile line=$iLine"
# ." sub=$sSub, args=@$sArgs\n";
# in evals we want the line number within the eval, but the
# name of the sub in which the eval was located. To get this
# we wait to push on the stack until we get an actual sub name
# and we avoid overwriting the location information, hence 'ne'
if (!$FILTER || ($sSub ne EVAL)) {
my $aFrame=[ $iFile, $iLine, $sSub, $sArgs ];
($sPackage, $iFile, $iLine) = @$aCall;
$iLineFrame=$iFrame;
my $sRef=ref($FILTER);
if ($sRef eq 'CODE') {
my $x = $FILTER->(@$aFrame, $iFrame, $iLineFrame);
if (ref($x) eq 'ARRAY') {
$aFrame=$x;
} elsif (!$x) {
$aFrame=undef;
}
} elsif (($sRef eq 'ARRAY') && ! _isIgnored($sSub, $FILTER)) {
$aFrame=undef;
} elsif (($sRef eq 'Regexp') && !_isIgnored($sSub, [$FILTER])) {
$aFrame=undef;
}
push(@$st, $aFrame) if $aFrame;
}
$aCall = _cacheCall($iFrame++);
}
push @$st, [ $iFile, $iLine, "<package: $sPackage>", $$aCall ];
if ($e) { my $n=$#{$e->[2]}-$#$st;$e->[2]=[@{$e->[2]}[0..$n]]};
return $st;
}
#-----------------------------
sub _isIgnored {
my ($sSub, $aIgnore) = @_;
foreach my $re (@$aIgnore) { return 1 if $sSub =~ $re; }
return 0;
}
#------------------------------------------------------------------
sub _dumpMessage {
my ($e, $iDepth) = @_;
my $sMsg = $e->getMessage();
return $sMsg unless $STRINGIFY;
if (ref($STRINGIFY) eq 'CODE') {
return $STRINGIFY->($sMsg);
}
$iDepth = 0 unless defined($iDepth);
my $sIndent = ' ' x ($TAB*$iDepth);
$sMsg = "\n${sIndent}Exception! $sMsg";
return $sMsg if $STRINGIFY == 0;
my ($sThrow, $sReach);
my $sTab = ' ' x $TAB;
$sIndent.= $sTab;
if ($STRINGIFY > 2) {
my $aPropagation = $e->getPropagation();
for (my $i=$#$aPropagation; $i >= 0; $i--) {
my ($f,$l) = @{$aPropagation->[$i]};
$sMsg .= "\n${sIndent}rethrown at file $f, line $l";
}
$sMsg .= "\n";
$sThrow='thrown ';
$sReach='reached ';
} else {
$sThrow='';
$sReach='';
}
my $st=$e->getStackTrace();
my $iTop = scalar @$st;
for (my $iFrame=0; $iFrame<$iTop; $iFrame++) {
my ($f,$l,$s,$aArgs) = @{$st->[$iFrame]};
if ($iFrame) {
#2nd and following stack frame
my $sVia="${sIndent}${sReach}via file $f, line $l";
my $sLine="$sVia in $s";
$sMsg .= (length($sLine)>$LINE_LENGTH
? "\n$sVia\n$sIndent${sTab}in $s" : "\n$sLine");
} else {
# first stack frame
my $tid=$e->getTid();
my $sAt="${sIndent}${sThrow}at file $f, line $l";
my $sLine="$sAt in $s";
$sMsg .= (length($sLine)>$LINE_LENGTH
? "\n$sAt\n$sIndent${sTab}in $s" : "\n$sLine")
. ", pid=" . $e->getPid() . (defined($tid)?", tid=$tid":'');
return "$sMsg\n" if $STRINGIFY == 1;
}
if ($STRINGIFY > 3) {
my $bTop = ($iFrame+1) == $iTop;
my $sVar= ($bTop && !$iDepth) ? '@ARGV' : '@_';
my $bMaybeEatenByGetOpt = $bTop && !scalar(@$aArgs)
&& exists($INC{'Getopt/Long.pm'});
my $sVarIndent = "\n${sIndent}" . (' ' x $TAB);
my $sArgPrefix = "${sVarIndent}".(' ' x length($sVar)).' ';
if ($bMaybeEatenByGetOpt) {
$sMsg .= $sArgPrefix . $sVar
. '() # maybe eaten by Getopt::Long?';
} else {
my $sArgs = join($sArgPrefix.',', @$aArgs);
$sMsg .= "${sVarIndent}$sVar=($sArgs";
$sMsg .= $sArgs ? "$sArgPrefix)" : ')';
}
}
}
$sMsg.="\n";
return $sMsg if $STRINGIFY == 2;
my $eChained = $e->getChained();
if (defined($eChained)) {
my $sTrigger = isException($eChained)
? _dumpMessage($eChained, $iDepth+1)
: "\n${sIndent}$eChained\n";
$sMsg .= "\n${sIndent}Triggered by...$sTrigger";
}
return $sMsg;
}
#------------------------------------------------------------------
# refaddr has a prototype($) so we can't use it directly as an
# overload operator: it complains about being passed 3 parameters
# instead of 1.
sub _refaddr { Scalar::Util::refaddr($_[0]) };
#------------------------------------------------------------------
sub _rethrow {
my $self = shift; my $sAddOrOmit = shift;
my ($p,$f,$l)=caller(1);
$self->PROPAGATE($f,$l);
if (@_%2) {
warn sprintf('bad parameter list to %s->rethrow(...)'
.'at file %d, line %d: odd number of elements in property-value '
.'list, property value has no property name and will be '
."discarded (common causes: you have %s string)\n"
,$f, $l, $sAddOrOmit);
shift @_;
}
$self->replaceProperties({@_}) if (@_);
return $self;
}
#------------------------------------------------------------------
# Traps warnings and reworks them so that they tell the user how
# to fix the problem rather than obscurely complain about an
# invisible sprintf with uninitialized values that seem to come from
# no where (and make Exception::Lite look like it is broken)
sub _sprintf {
my $sMsg;
my $sWarn;
{
local $SIG{__WARN__} = sub { $sWarn=$_[0] if !defined($sWarn) };
# sprintf has prototype ($@)
my $sFormat = shift;
$sMsg = sprintf($sFormat, @_);
}
if (defined($sWarn)) {
my $sReason='';
my ($f, $l, $s) = (caller(1))[1,2,3];
$s =~ s/::(\w+)\z/->$1/;
$sWarn =~ s/sprintf/$s/;
$sWarn =~ s/\s+at\s+[\w\/\.]+\s+line\s+\d+\.\s+\z//;
if ($sWarn
=~ m{^Use of uninitialized value in|^Missing argument}) {
my $p=$s; $p =~ s/->\w+\z//;
$sReason ="\n Most likely cause: "
. "Either you are missing property-value pairs needed to"
. "build the message or your exception class's format"
. "definition mistakenly has too many placeholders "
. "(e.g. %s,%d,etc)\n";
}
warn "$sWarn called at file $f, line $l$sReason\n";
}
return $sMsg;
}
#------------------------------------------------------------------
sub _shiftProperties {
my $cl= shift; my $st=shift; my $sAddOrOmit = shift;
if (@_%2) {
$"='|';
warn sprintf('bad parameter list to %s->new(...) at '
.'file %s, line %d: odd number of elements in property-value '
.'list, property value has no property name and will be '
.'discarded (common causes: you have %s string -or- you are '
."using a string as a chained exception)\n"
,$cl,$st->[0]->[0],$st->[0]->[1], $sAddOrOmit);
shift @_;
}
return {@_};
}
#==================================================================
# MODULE INITIALIZATION
#==================================================================
declareExceptionClass(__PACKAGE__ .'::Any');
1;
modules/fallback/Exception/Lite.pod
=head1 NAME
Exception::Lite - light weight exception handling class with smart
stack tracing, chaining, and localization support.
=head1 SYNOPSIS
# --------------------------------------------------------
# making this module available to your code
# --------------------------------------------------------
#Note: there are NO automatic exports
use Exception::Lite qw(declareExceptionClass
isException
isChainable
onDie
onWarn);
# imports only: declareExceptionClass isException isChainable
use Exception::Lite qw(:common);
# imports all exportable methods listed above
use Exception::Lite qw(:all);
# --------------------------------------------------------
# declare an exception class
# --------------------------------------------------------
# no format rule
declareExceptionClass($sClass);
declareExceptionClass($sClass, $sSuperClass);
# with format rule
declareExceptionClass($sClass, $aFormatRule);
declareExceptionClass($sClass, $sSuperClass, $aFormatRule);
# with customized subclass
declareExceptionClass($sClass, $sSuperClass, 1);
declareExceptionClass($sClass, $aFormatRule, 1);
declareExceptionClass($sClass, $sSuperClass, $aFormatRule, 1);
# --------------------------------------------------------
# throw an exception
# --------------------------------------------------------
die $sClass->new($sMsg, $prop1 => $val1, ...); #no format rule
die $sClass->new($prop1 => $val1, ...); #has format rule
#-or-
$e = $sClass->new($sMsg, $prop1 => $val1, ...); #no format rule
$e = $sClass->new($prop1 => $val1, ...); #has format rule
die $e;
# --------------------------------------------------------
# catch and test an exception
# --------------------------------------------------------
# Note: for an explanation of why we don't use if ($@)... here,
# see Catching and Rethrowing exceptions below
eval {
.... some code that may die here ...
return 1;
} or do {
my $e=$@;
if (isException($e, 'Class1')) {
... do something ...
} elsif (isExcption($e, 'Class2')) {
... do something else ...
}
};
isException($e); # does $e have the above exception methods?
isException($e,$sClass) # does $e belong to $sClass or a subclass?
# --------------------------------------------------------
# getting information about an exception object
# --------------------------------------------------------
$e->getMessage();
$e->getProperty($sName);
$e->isProperty($sName);
$e->replaceProperties($hOverride);
$e->getPid();
$e->getPackage();
$e->getTid();
$e->getStackTrace();
$e->getFrameCount();
$e->getFile($i);
$e->getLine($i);
$e->getSubroutine($i);
$e->getArgs($i);
$e->getPropagation();
$e->getChained();
# --------------------------------------------------------
# rethrowing exceptions
# --------------------------------------------------------
# using original properties and message
$@=$e; die; # pure Perl way (reset $@ in case wiped out)
die $e->rethrow(); # same thing, but a little less cryptic
# overriding original message/properties
die $e->rethrow(path=>$altpath, user=>$nameReplacingId);
# --------------------------------------------------------
# creation of chained exceptions (one triggered by another)
# (new exception with "memory" of what caused it and stack
# trace from point of cause to point of capture)
# --------------------------------------------------------
isChainable($e); # can $e be used as a chained exception?
die $sClass->new($e, $sMsg, $prop1 => $val1, ...);#no format rule
die $sClass->new($e, $prop1 => $val1, ...); #has format rule
# --------------------------------------------------------
# print out full message from an exception
# --------------------------------------------------------
print $e # print works
warn $e # warn works
print "$e\n"; # double quotes work
my $sMsg=$e."\n"; print $sMsg; # . operator works
# --------------------------------------------------------
# global control variables (maybe set on the command line)
# --------------------------------------------------------
$Exception::Lite::STRINGIFY #set rule for stringifying messages
= 1; # message and file/line where it occured
= 2; # 1 + what called what (simplified stack trace)
= 3; # 2 + plus any chained exceptions and where message
# was caught, if propagated and rethrown
= 4; # 3 + arguments given to each call in stack trace
= coderef # custom formatting routine
$Exception::Lite::TAB # set indentation for stringified
# messages, particularly indentation for
# call parameters and chained exceptions
$Exception::Lite::FILTER
= 0 # see stack exactly as Perl does
= 1 # remove frames added by eval blocks
= coderef # custom filter - see getStackTrace for details
# --------------------------------------------------------
# controlling the stack trace from the command line
# --------------------------------------------------------
perl -mException::Lite=STRINGIFY=1,FILTER=0,TAB=4
perl -m'Exception::Lite qw(STRINGIFY=1 FILTER=0 TAB=4)'
# --------------------------------------------------------
# built in exception classes
# --------------------------------------------------------
# generic wrapper for converting exception strings and other
# non-Exception::Lite exceptions into exception objects
Exception::Class::Any->new($sMessageText);
To assist in debugging and testing, this package also includes
two methods that set handlers for die and warn. These methods
should I<only> be used temporarily during active debugging. They
should not be used in production software, least they interfere
with the way other programmers using your module wish to do their
debugging and testing.
# --------------------------------------------------------
# force all exceptions/warnings to use Exception::Lite to
# print out messages and stack traces
# --------------------------------------------------------
# $stringify is the value for EXCEPTION::Lite::STRINGIFY
# that you want to use locally to print out messages. It
# will have no effect outside of the die handler
Exception::Lite::onDie($stringify);
Exception::Lite::onWarn($stringify);
=head1 DESCRIPTION
The C<Exception::Lite> class provides an easy and very light weight
way to generate context aware exceptions. It was developed because
the exception modules on CPAN as of December,2010 were heavy on
features I didn't care for and did not have the features I most
needed to test and debug code efficiently.
=head2 Features
This module provides a light weight but powerful exception class
that
=over
=item *
provides an uncluttered stack trace that clearly shows what
called what and what exception triggered what other exception.
It significantly improves on the readability of the stack trace
dumps provided by C<carp> and other exception modules on
CPAN (as of 12/2010). For further discussion and a sample, see
L</More intelligent stack trace>.
=item *
gives the user full control over the amount of debugging
information displayed when exceptions are thrown.
=item *
permits global changes to the amount of debugging information
displayed via the command line.
=item *
closely integrates exception classes, messages, and properties
so that they never get out of sync with one another. This in
turn eliminates redundant coding and helps reduce the cost of
writing,validating and maintaining a set of exceptions.
=item *
is easy to retrofit with native language support, even if this
need appears late in the development process.This makes it
suitable for use with agile development strategies.
=item *
act like strings in string context but are in fact objects with
a class hierarchy and properties.They can be thrown and rethrown
with standard Perl syntax. Like any object, they can be uniquely
identified in numeric context where they equal their reference
address (the value returned by C<Scalar::Util::refaddr()>.
=item *
does not interfere with signal handlers or the normal Perl syntax
and the assumptions of Perl operators.
=item *
can be easily extended and subclassed
=back
=head2 Lightweight how?
Despite these features C<Exception::Lite> maintains its "lite"
status by
=over
=item *
using only core modules
=item *
generating tiny exception classes (30-45LOC per class).
=item *
eliminating excess baggage by customizing generated classes to
reflect the actual needs of exception message generation. For
instance an exception wrapped around a fixed string message would
omit code for message/property integration and would be little
more than a string tied to a stack trace and property hash.
=item *
storing only the minimum amount of stack trace data needed to
generate exception messages and avoiding holding onto references
from dead stack frames. (Note: some CPAN modules hold onto
actual variables from each frame, possibly interfering with
garbage collection).
=item *
doing all its work, including class generation and utilities in
a single file that is less than half the size of the next smallest
similarly featured all-core exception class on CPAN (support for
both properties and a class heirarchy). C<Exception::Lite>
contains about 400 lines when developer comments are excluded). The
next smallest all core module is L<Exception::Base|Exception::Base>
which clocks in at just over 1000 lines after pod and developer
comments are excluded).
=item *
avoiding a heavy-weight base class. Code shared by
C<Exception::Lite> classes are stored in function calls that total
230 or so lines of code relying on nothing but core modules. This
is significantly less code than is needed by the two CPAN packages
with comparable features. The all core
L<Exception::Base|Exception::Base> class contains 700+ lines of
code. The base class of L<Exception::Class|Exception::Class> has
200 lines of its own but drags in two rather large non-core
modules as dependencies: L<Devel::StackTrace|Devel::StackTrace>
L<Class::Data::Inheritable|Class::Data::Inheritable>.
=back
C<Exception::Lite> has more features (chaining, message/property
integration) but less code due to the following factors:
=over
=item *
working with Perl syntax rather than trying to replace it.
=item *
using a light approach to OOP - exception classes have just enough
and no more OO features than are needed to be categorized by a
class, participate in a class heirarchy and to have properties.
=item *
respecting separation of concerns. C<Exception::Lite> focuses
on the core responsibility of an exception and leaves the bulk of
syntax creation (e.g. Try/Catch) to specialist modules like
L<Try::Tiny|Try::Tiny>. Other modules try to double as
comprehensive providers of exception related syntactic sugar.
=item *
not trying to be the only kind of exception that an application
uses.
=back
=head1 USAGE
=head2 Defining Exception Classes
C<Exception::Lite> provides two different ways to define messages.
The first way, without a format rule, lets you compose a freeform
message for each exception. The second way, with a format rule,
lets you closely integrate messages and properties and facilitates
localization of messages for any packages using your software.
=head3 Defining freeform messages
If you want to compose a free form message for each and every
exception, the class declaration is very simple:
declareExceptionClass($sClass);
declareExceptionClass($sClass, $sSuperClass);
# with customized subclass
declareExceptionClass($sClass, $sSuperClass, 1);
C<$sClass> is the name of the exception class.
C<$sSuperClass> is the name of the superclass, if there is one.
The superclass can be any class created by C<Exception::Lite>. It
can also be any role class, i.e. a class that has methods but no
object data of its own.
The downside of this simple exception class is that there is
absolutely no integration of your messages and any properties that
you assign to the exception. If you would like to see your property
values included in the message string,consider using a formatted
message instead.
=head3 Defining formatted messages
If you wish to include property values in your messages, you need
to declare a formatted message class. To do this, you define a
format rule and pass it to the constructor:
$aFormatRule = ['Cannot copy %s to %s', qw(from to) ];
declareExceptionClass($sClass, $aFormatRule);
declareExceptionClass($sClass, $sSuperClass, $aFormatRule);
# with customized subclass
declareExceptionClass($sClass, $aFormatRule, 1);
declareExceptionClass($sClass, $sSuperClass, $aFormatRule, 1);
Format rules are nothing more than a sprintf message string
followed by a list of properties in the same order as the
placeholders in the message string. Later on when an exception
is generated, the values of the properties will replace the
property names. Some more examples of format rules:
$aFormatRule = ['Illegal argument <%s>: %s', qw(arg reason)];
declareExceptionClass('BadArg', $aFormatRule);
$aFormatRule = ['Cannot open file <%s>> %s', qw(file reason)];
declareExceptionClass('OpenFailed', $aFormatRule);
$sFormatRule = ['Too few %s, must be at least %s', qw(item min)];
declareExceptionClass('TooFewWidgets', $aFormatRule);
Later on when you throw an exception you can forget about the message
and set the properties, the class will do the rest of the work:
die BadArg->new(arg=>$sPassword, reason=>'Too few characters');
open(my $fh, '>', $sFile)
or die OpenFailed->new(file=>$sFile, reason=>$!);
And still later when you catch the exception, you have two kinds
of information for the price of one:
# if you catch BadArg
$e->getProperty('arg') # mine
$e->getProperty('reason') # too few characters
$e->getMessage() # Illegal argument <mine>: too few characters
# if you catch OpenFailed
$e->getProperty('file') # foo.txt
$e->getProperty('reason') # path not found
$e->getMessage() # Cannot open <foo.txt>: path not found
=head2 Creating and throwing exceptions
When it comes times to create an exception, you create and
throw it like this (C<$sClass> is a placeholder for the name of
your exception class);
die $sClass->new($sMsg, prop1 => $val1, ...); #no format rule
die $sClass->new(prop1 => $val1, ...); #has format rule
#-or-
$e = $sClass->new($sMsg, prop1 => $val1, ...); #no format rule
$e = $sClass->new(prop1 => $val1, ...); #has format rule
die $e;
For example:
# Freeform exceptions (caller composes message, has message
# parameter ($sMsg) before the list of properties)
close $fh or die UnexpectedException
->new("Couldn't close file handle (huh?): $!");
die PropertySettingError("Couldn't set property"
, prop=>foo, value=>bar);
# Formatted exceptions (no $sMsg parameter)
if (length($sPassword) < 8) {
die BadArg->new(arg=>$sPassword, reason=>'Too few characters');
}
open(my $fh, '>', $sFile)
or die OpenFailed->new(file=>$sFile, reason=>$!);
In the above examples the order of the properties does not matter.
C<Exception::Lite> is using the property names, not the order of
the properties to find the right value to plug into the message
format string.
=head2 Catching and testing exceptions
In Perl there are two basic ways to work with exceptions:
* native Perl syntax
* Java like syntax (requires non-core modules)
=head3 Catching exceptions the Java way
Java uses the following idiom to catch exceptions:
try {
.... some code here ...
} catch (SomeExceptionClass e) {
... error handling code here ...
} catch (SomeOtherExceptionClass e) {
... error handling code here ...
} finally {
... cleanup code here ...
}
There are several CPAN modules that provide some sort of syntactic
sugar so that you can emulate java syntax. The one recommended
for C<Exception::Lite> users is L<Try::Tiny|Try::Tiny>.
L<Try::Tiny|Try::Tiny> is an elegant class that concerns itself
only with making it possible to use java-like syntax. It can be
used with any sort of exception.
Some of the other CPAN modules that provide java syntax also
require that you use their exception classes because the java like
syntax is part of the class definition rather than a pure
manipulation of Perl syntax.
=head3 Catching exceptions the Perl way
The most reliable and fastest way to catch an exception is to use
C< eval/do >:
eval {
...
return 1;
} or do {
# save $@ before using it - it can easily be clobbered
my $e=$@;
... do something with the exception ...
warn $e; #use $e as a string
warn $e->getMessage(); # use $e as an object
}
The C<eval> block ends with C<return 1;> to insure that successful
completion of the eval block never results in an undefined value.
In certain cases C<undef> is a valid return value for a statement,
We don't want to enter the C<do> block for any reason other than
a thrown exception.
C< eval/do > is both faster and more reliable than the C< eval/if>
which is commonly promoted in Perl programming tutorials:
# eval ... if
eval {...};
if ($@) {....}
It is faster because the C<do> block is executed if and only
if the eval fails. By contrast the C<if> must be evaluated both
in cases of succes and failure.
C< eval/do > is more reliable because the C<do> block is guaranteed
to be triggered by any die, even one that accidentally throws undef
or '' as the "exception". If an exception is thrown within the C<eval>
block, it will always evaluate to C<undef> therefore triggering the
C<do> block.
On the other hand we can't guarentee that C<$@> will be defined
even if an exception is thrown. If C<$@> is C<0>, C<undef>, or an
empty string, the C<if> block will never be entered. This happens
more often then many programmers realize. When eval exits the
C< eval > block, it calls destructors of any C<my> variables. If
any of those has an C< eval > statement, then the value of C<$@> is
wiped clean or reset to the exception generated by the destructor.
Within the C<do> block, it is a good idea to save C<$@> immediately
into a variable before doing any additional work. Any subroutine
you call might also clobber it. Even built-in commands that don't
normally set C<$@> can because Perl lets a programmer override
built-ins with user defined routines and those user define routines
might set C<$@> even if the built-in does not.
=head3 Testing exceptions
Often when we catch an exception we want to ignore some, rethrow
others, and in still other cases, fix the problem. Thus we need a
way to tell what kind of exception we've caught. C<Exception::Lite>
provides the C<isException> method for this purpose. It can be
passed any exception, including scalar exceptions:
# true if this exception was generated by Exception::Line
isException($e);
# true if this exception belongs to $sClass. It may be a member
# of the class or a subclass. C<$sClass> may be any class, not
# just an Exception::Lite generated class. You can even use this
# method to test for string (scalar) exceptions:
isException($e,$sClass);
isException($e,'Excption::Class');
isException($e, 'BadArg');
isException($e, '');
And here is an example in action. It converts an exception to a
warning and determines how to do it by checing the class.
eval {
...
return 1;
} or do {
my $e=$@;
if (Exception::Lite::isException($e)) {
# get message w/o stack trace, "$e" would produce trace
warn $e->getMessage();
} elsif (Exception::Lite::isException('Exception::Class') {
# get message w/o stack trace, "$e" would produce trace
warn $e->message();
} elsif (Exception::Lite::isException($e,'')) {
warn $e;
}
}
=head2 Rethrowing exceptions
Perl doesn't have a C<rethrow> statement. To reliably rethrow an
exception, you must set C<$@> to the original exception (in case it
has been clobbered during the error handling process) and then call
C<die> without any arguments.
eval {
...
return 1;
} or do {
my $e=$@;
# do some stuff
# rethrow $e
$@=$e; die;
}
The above code will cause the exception's C<PROPAGATE> method to
record the file and line number where the exception is rethrown.
See C<getLine>, C<getFile>, and C<getPropagation> in the class
reference below for more information.
As this Perl syntax is not exactly screaming "I'm a rethrow",
C<Exception::Lite> provides an alternative and hopefully more
intuitive way of propagating an exception. There is no magic here,
it just does what perl would do had you used the normal syntax,
i.e. call the exception's C<PROPAGATE> method.
eval {
...
return 1;
} or do {
my $e=$@;
# rethrow $e
die $e->rethrow();
}
=head2 Chaining Messages
As an exception moves up the stack, its meaning may change. For
example, suppose a subroutine throws the message "File not open".
The immediate caller might be able to use that to try and open
a different file. On the other hand, if the message gets thrown
up the stack, the fact that a file failed to open might not
have any meaning at all. That higher level code only cares that
the data it needed wasn't available. When it notifies the user,
it isn't going to say "File not found", but "Can't run market
report: missing data feed.".
When the meaning of the exception changes, it is normal to throw
a new exception with a class and message that captures the new
meaning. However, if this is all we do, we lose the original
source of the problem.
Enter chaining. Chaining is the process of making one exception
"know" what other exception caused it. You can create a new
exception without losing track of the original source of the
problem.
To chain exceptions is simple: just create a new exception and
pass the caught exception as the first parameter to C<new>. So
long as the exception is a non-scalar, it will be interpreted
as a chained exception and not a property name or message text
(the normal first parameter of C<new>).
Chaining is efficient, especially if the chained exception is
another C<Exception::Lite> exception. It does not replicate
the stack trace. Rather the original stack trace is shorted to
include only the those fromes frome the time it was created to
the time it was chained.
Any non-scalar exception can be chained. To test whether or not
a caught exception is chainable, you can use the method
C<isChainable>. This method is really nothing more than
a check to see if the exception is a non-scalar, but it helps
to make your code more self documenting if you use that method
rather than C<if (ref($e))>.
If an exception isn't chainable, and you still want to chain
it, you can wrap the exception in an exception class. You
can use the built-in C<Exception::Class::Any> or any class of
your own choosing.
#-----------------------------------------------------
# define some classes
#-----------------------------------------------------
# no format rule
declareExceptionClass('HouseholdDisaster');
# format rule
declareExceptionClass('ProjectDelay'
, ['The project was delayed % days', qw(days)]);
#-----------------------------------------------------
# chain some exceptins
#-----------------------------------------------------
eval {
.... some code here ...
return 1;
} or do {
my $e=$@;
if (Exception::Lite::isChainable($e)) {
if (Exception::Lite::isException($e, 'FooErr') {
die 'SomeNoFormatException'->new($e, "Caught a foo");
} else {
die 'SomeFormattedException'->new($e, when => 'today');
}
} elsif ($e =~ /fire/) {
die 'Exception::Lite::Any'->new($e);
die 'SomeFormattedException'->new($e, when => 'today');
} else {
# rethrow it since we can't chain it
$@=$e; die;
}
}
=head2 Reading Stack Traces
At its fullest level of detail, a stack trace looks something
like this:
Exception! Mayhem! and then ...
thrown at file Exception/Lite.t, line 307
in main::weKnowBetterThanYou, pid=24986, tid=1
@_=('ARRAY(0x83a8a90)'
,'rot, rot, rot'
,'Wikerson brothers'
,'triculous tripe'
,'There will be no more talking to hoos who are not!'
,'black bottom birdie'
,'from the three billionth flower'
,'Mrs Tucanella returns with uncles and cousins'
,'sound off! sound off! come make yourself known!'
,'Apartment 12J'
,'Jo Jo the young lad'
,'the whole world was saved by the smallest of all'
)
reached via file Exception/Lite.t, line 281
in main::notAWhatButAWho
@_=()
reached via file Exception/Lite.t, line 334 in main::__ANON__
@_=()
reached via file Exception/Lite.t, line 335 in <package: main>
@ARGV=()
Triggered by...
Exception! Horton hears a hoo!
rethrown at file Exception/Lite.t, line 315
thrown at file Exception/Lite.t, line 316
in main::horton, pid=24986, tid=1
@_=('15th of May'
,'Jungle of Nool'
,'a small speck of dust on a small clover'
,'a person's a person no matter how small'
)
reached via file Exception/Lite.t, line 310 in main::hoo
@_=('Dr Hoovey'
,'hoo-hoo scope'
,'Mrs Tucanella'
,'Uncle Nate'
)
reached via file Exception/Lite.t, line 303
in main::weKnowBetterThanYou
@_=('ARRAY(0x83a8a90)'
,'rot, rot, rot'
,'Wikerson brothers'
,'triculous tripe'
,'There will be no more talking to hoos who are not!'
,'black bottom birdie'
,'from the three billionth flower'
,'Mrs Tucanella returns with uncles and cousins'
,'sound off! sound off! come make yourself known!'
,'Apartment 12J'
,'Jo Jo the young lad'
,'the whole world was saved by the smallest of all'
)
=over
=item *
lines begining with "thrown" indicate a line where a new exception
was thrown. If an exception was chained, there might be multiple
such lines.
=item *
lines beginning with "reached via" indicate the path travelled
I<down> to the point where the exception was thrown. This is the
code that was excuted before the exception was triggered.
=item *
lines beginning with "rethrown at" indicate the path travelled
I<up> the stack by the exception I<after> it was geenerated. Each
line indicates a place where the exception was caught and rethrown.
=item *
lines introduced with "Triggered by" are exceptions that were
chained together. The original exception is the last of the
triggered exceptions. The original line is the "thrown" line
for the original exception.
=item *
C<@_> and <C@ARGV> below a line indicates what is left of the
parameters passed to a method, function or entry point routine.
In ideal circumstances they are the parameters passed to the
subroutine mentioned in the line immediately above C<@_>. In
reality, they can be overwritten or shifted away between the
point when the subroutine started and the line was reached.
Note: if you use L<Getopt::Long> to process C<@ARGV>, C<@ARGV>
will be empty reduced to an empty array. If this bothers you, you
can localize <@ARGV> before calling C<GetOptions>, like this:
my %hARGV;
{
local @ARGV = @ARGV;
GetOptions(\%hARGV,...);
}
=item *
pid is the process id where the code was running
=item *
tid is the thread id where the code was running
=back
=head1 SPECIAL TOPICS
=head2 Localization of error messages
Rather than treat the error message and properties as entirely
separate entities, it gives you the option to define a format string
that will take your property values and insert them automatically
into your message. Thus when you generate an exception, you can
specify only the properties and have your message automatically
generated without any need to repeat the property values in messy
C<sprintf>'s that clutter up your program.
One can localize from the very beginning when one declares the
class or later on after the fact if you are dealing with legacy
software or developing on an agile module and only implementing
what you need now.
To localize from the get-go:
# myLookupSub returns the arguments to declareException
# e.g. ('CopyError', [ 'On ne peut pas copier de %s a %s'
, qw(from to)])
declareExceptionClass( myLookupSub('CopyError', $ENV{LANG}) );
# .... later on, exception generation code doesn't need to
# know or care about the language. it just sets the properties
# error message depends on locale:
# en_US: 'Cannot copy A.txt to B.txt'
# fr_FR: 'On ne peut pas copier de A.txt a B.txt'
# de_DE: 'Kann nicht kopieren von A.txt nach B.txt'
die 'CopyError'->new(from => 'A.txt', to => 'B.txt');
Another alternative if you wish to localize from the get-go is
to pass a code reference instead of a format rule array. In this
case, C<Exception::Lite> will automatically pass the class name
to the subroutine and retrieve the value returned.
# anothherLookupSub has parameters ($sClass) and returns
# a format array, for example:
#
# %LOCALE_FORMAT_HASH = (
# CopyError => {
# en_US => ['Cannot copy %s to %s', qw(from to)]
# ,fr_FR => ['On ne peut pas copier de %s a %s', qw(from to)]
# ,de_DE => ['Kann nicht kopieren von %s nach %s''
# , qw(from to)]
#
# AddError => ...
# );
#
# sub anotherLookupSub {
# my ($sClass) = @_;
# my $sLocale = $ENV{LANG}
# return $LOCALE_FORMAT_HASH{$sClass}{$sLocale};
# }
#
declareExceptionClass('CopyError', &anotherLookupSub);
declareExceptionClass('AddError', &anotherLookupSub);
# error message depends on locale:
# en_US: 'Cannot copy A.txt to B.txt'
# fr_FR: 'On ne peut pas copier de A.txt a B.txt'
# de_DE: 'Kann nicht kopieren von A.txt nach B.txt'
die CopyError->new(from => 'A.txt', to => 'B.txt');
die AddError->new(path => 'C.txt');
If you need to put in localization after the fact, perhaps for a
new user interface you are developing, the design pattern might
look like this:
# in the code module you are retrofitting would be an exception
# that lived in a single language world.
declareExceptionClass('CopyError'
['Cannot copy %s to %s', [qw(from to)]);
... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet.

Auch abrufbar als: Unified diff