I'm new to MOOSE and Perl OOP, and I'm struggling understanding the execution order of the code.
I want to create a class reading files, so an attribute of the object should be the filehandle and another the filename to be read.
My problem is that the attribute 'filehandle' has a builder that requires $self->filename, but sometimes at runtime 'filename' is not (yet) available when the builder is called.
Thanks for you help
My ideal object creation:
my $file = FASTQ::Reader->new(
filename => "$Bin/test.fastq",
);
Perl module:
has filename => (
is => 'ro', isa => 'Str', required => 1,
);
has fh => (
is => 'ro', isa => 'FileHandle', builder => '_build_file_handler',
);
sub _build_file_handler {
my ($self) = #_;
say Dumper $self;
open(my $fh, "<", $self->filename) or die ("cant open " . $self->filename . "\n");
return $fh;
}
See: https://gist.github.com/telatin/a81a4097913af55c5b86f9e01a2d89ae
If a value of one attribute depends on another attribute, make it lazy.
#!/usr/bin/perl
use warnings;
use strict;
{ package My::Class;
use Moose;
has filename => (is => 'ro', isa => 'Str', required => 1);
has fh => (is => 'rw', isa => 'FileHandle', lazy => 1, builder => '_build_fh');
# ~~~~~~~~~
sub _build_fh {
my ($self) = #_;
open my $fh, '<', $self->filename or die $!;
return $fh
}
}
my $o = 'My::Class'->new(filename => __FILE__);
print while readline $o->fh;
See Laziness in Moose::Manual::Attributes:
if the default value for this attribute depends on some other attributes, then the attribute must be lazy.
Related
You may infer from the question that this is my first Moose class.
How do I set an attribute FileHandle to *STDOUT?
This doesn't work.
has 'output' => (
is => 'rw',
isa => 'FileHandle',
default => sub { openhandle(*STDOUT) }
);
The output when run is:
Attribute (output) does not pass the type constraint because: Validation failed for 'FileHandle' with value *main::STDOUT
The documentation claims:
FileHandle accepts either an IO::Handle object or a builtin perl
filehandle (see "openhandle" in Scalar::Util).
What am I missing?
Thanks.
-E
I don't know what else you may need there, but this works for starters
The WithFH.pm
package WithFH;
use feature 'say';
use Moose;
has 'fh' => (is => 'ro', isa => 'FileHandle', default => sub { \*STDOUT } );
sub say {
my $self = shift;
say { $self->{fh} } "#_";
}
__PACKAGE__->meta->make_immutable;
1;
and the main
use warnings;
use strict;
use feature 'say';
use WithFH;
my $wfh = WithFH->new;
$wfh->say("hi");
That prints hi to STDOUT.
I'm writing a script to help me get proficient in Moose. I've got the following bit of code:
package Dir;
use Moose;
use Modern::Perl;
use File;
has 'dirs' => (is => 'ro', isa => 'HashRef[Dir]' );
has 'files' => (is => 'ro', isa => 'HashRef[File]');
has 'dir_class' => (is => 'ro', isa => 'ClassName', default => 'Dir');
has 'file_class' => (is => 'ro', isa => 'ClassName', default => 'File');
sub BUILD {
my $self = shift;
my $path = $self->path;
my $name = $self->name;
my (%dirs, %files);
# populate dirs attribute with LaborData::Data::Dir objects
opendir my $dh, $path or die "Can't opendir '$path': $!";
# Get files and dirs and separate them out
my #dirs_and_files = grep { ! m{^\.$|^\.\.$} } readdir $dh;
closedir $dh or die "Can't closedir '$path': $!";
my #dir_names = grep { -d "$path/$_" } grep { !m{^\.} } #dirs_and_files;
my #file_names = grep { -f "$path/$_" } grep { !m{^\.} } #dirs_and_files;
# Create objects
map { $dirs{$_} = $self->dir_class->new ( path => $path . '/' . $_ ) } #dir_names;
map { $files{$_} = $self->file_class->new ( path => $path . '/' . $_ ) } #file_names;
# Set attributes
$self->dirs ( \%dirs );
$self->files ( \%files );
}
The code results in the following error: died: Moose::Exception::CannotAssignValueToReadOnlyAccessor (Cannot assign a value to a read-only accessor at reader Dir::dirs
To get around this error, I could either make the attributes rw or use builder methods for the dirs and files attributes. The former solution is undesirable and the latter solution will require duplication of code (for example, the directory will need to be opened twice) and so is also undesirable.
What is the best solution to this problem?
You can assign a writer to your read-only attribute and use that internally from your BUILD. Name it with an _ to indicate it's internal.
package Foo;
use Moose;
has bar => ( is => 'ro', writer => '_set_bar' );
sub BUILD {
my $self = shift;
$self->_set_bar('foobar');
}
package main;
Foo->new;
This will not throw an exception.
It's essentially the same as making it rw, but now the writer is not the same accessor as the reader. The _ indicates that it's internal, so it's less undesirable than just using rw. Remember that you cannot really protect anything in Perl anyway. If your user wants to get to the internals they will.
I found one possible solution, though it's frowned upon:
# Set attributes
$self->{dirs} = \%dirs;
$self->{files} = \%files;
myscript.pl
my $R;
my $f1 = "f1.log";
my $f2 = "f2.log";
my $f3 = "f3.log";
sub checkflags {
GetOptions('a=s' => \$f1,
'b=s' => \$f2,
'c=s' => \$f3,
);
open $R, '>', $f1 or die "Cannot open file\n"; # Line a
}
All the flags are optional.
If I call the script as
perl myscript.pl -a=filename
I need to append a .log to the filename before opening it at Line a.
For that I need to know whether GetOptions read something into $f1 or not.
How can this be done?
The simplest solution is to look for /[.]log$/ in $f1 and add it if it isn't present. Unfortunately that means that when the user passes in "foo.log" and wanted it to become "foo.log.log" it won't, but I think we can agree that user is a jerk.
A better option, that will make the jerk happy, is:
#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
GetOptions(
'a=s' => \my $f1,
'b=s' => \my $f2,
'c=s' => \my $f3,
);
if (defined $f1) {
$f1 .= ".log";
} else {
$f1 = "f1.log";
}
print "$f1\n";
If you want to define all of default names at the top, use a different variable to do that (it is probably better reading code anyway):
#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
my $default_f1 = "f1.log";
my $default_f2 = "f2.log";
my $default_f3 = "f3.log";
GetOptions(
'a=s' => \my $f1,
'b=s' => \my $f2,
'c=s' => \my $f3,
);
if (defined $f1) {
$f1 .= ".log";
} else {
$f1 = $default_f1;
}
print "$f1\n";
if (defined $f1) {
# You got a -a option
}
But personally I'd prefer to read the options into a hash and then use exists().
$f1 = "$f1.log" unless $f1 =~ m/\.log$/i;
Appends the log extension if the file name does not already have one. Since the default value ends in log, nothing happens. And it works if the user types the log on the command line.
One way to achieve this is to use Moose and MooseX::Getopt:
package MyApp;
use strict;
use warnings;
use Moose;
with 'MooseX::Getopt';
has f1 => (
is => 'ro', isa => 'Str',
cmd_aliases => 'a',
default => 'f1.log',
predicate => 'has_a',
);
has f2 => (
is => 'ro', isa => 'Str',
cmd_aliases => 'b',
default => 'f2.log',
predicate => 'has_b',
);
has f3 => (
is => 'ro', isa => 'Str',
cmd_aliases => 'c',
default => 'f3.log',
predicate => 'has_c',
);
# this is run immediately after construction
sub BUILD
{
my $this = shift;
print "a was provided\n" if $this->has_a;
print "b was provided\n" if $this->has_b;
print "c was provided\n" if $this->has_c;
}
1;
I have to read a file in the BUILD method and I want to use the load method of the MooseX::Storage package.
But this load method create a new object and so when I instatiate the object this isn’t the object read from file. In the code below I create an object $m1 with state 2 to write the file, I create $m2 with no parameter to read the file but $m2 doesn’t contain the right value.
The package:
package mia;
use Moose;
use MooseX::Storage;
with Storage(format => 'JSON', io => 'File');
has 'nome' => ( is => 'rw', isa => 'Str', default =>'',);
has 'stato' => ( is => 'rw', isa => 'Int', default =>1,);
sub BUILD {
my $self=shift;
if ($self->stato==1){
$self=mia->load("mia.dat");
}
if ($self->stato==2){
$self->stato(0);
$self->nome("prova");
$self->store("mia.dat");
}
}
sub stampa(){
my $self=shift;
print $self->nome." ".$self->stato;
}
the main program
use mia;
my $m;
$m1=mia->new(stato=>2);
$m2=mia->new();
print "\nm1 \n";
$m1->stampa();
print "\nm2 \n";
$m2->stampa();
Your code seems to be acting as if BUILD is a constructor, which it isn't -- it's more like a post-construction hook where you can perform other things like read values from a DB. You should instead either:
store the result of mia->load in an attribute, and optionally use delegated methods to access it, or
use the result of mia->load as the object, rather than constructing a separate one.
Here is an example of the first case, separating the MooseX::Storage object from the object that controls it:
package miaController;
use Moose;
use mia;
has 'nome' => ( is => 'rw', isa => 'Str', default =>'',);
has 'stato' => ( is => 'rw', isa => 'Int', default =>1,);
has 'mia' => ( is => 'rw', isa => 'mia', lazy => 1);
sub BUILD
{
my $self = shift;
if ($self->stato == 1)
{
$self->mia(mia->load("mia.dat"));
}
elsif ($self->stato == 2)
{
$self->stato(0);
$self->nome("prova");
$self->mia->store("mia.dat");
}
}
sub stampa
{
my $self = shift;
print $self->nome." ".$self->stato;
}
package mia;
use Moose;
use MooseX::Storage;
with Storage(format => 'JSON', io => 'File');
package main:
use miaController;
my $m1=miaController->new(stato=>2);
my $m2=miaController->new();
print "\nm1 \n";
$m1->stampa();
print "\nm2 \n";
$m2->stampa();
I'm having difficulty using MooseX::Declare properly when calling BUILDARGS.
I'm trying to create an object as an interface for a file. (Specifically, I want an interface to a binary file that lets me peek at the next few bytes in the file then chomp them off for further processing.)
I want to be able to create one of these objects like this
my $f = binary_file_buffer->new( $file_name );
and then use it like this
while( my $block_id = $f->peek( $id_offset, $id_length ) ) {
$block_id = unpack_block_id( $block_id );
$munge_block{ $block_id }->(
$f->pop( $block_size[ $block_id ] )
);
}
My of binary_file_buffer class definition/declaration looks like this
use MooseX::Declare;
class binary_file_buffer {
use FileHandle;
use Carp;
has _file => ( is => 'ro', isa => 'FileHandle' );
has _file_name => ( is => 'ro', isa => 'Str' );
has _buff => ( is => 'rw', isa => 'Str', default => '' );
method BUILDARGS ( Str $file_name ) {
my $file = FileHandle->new( $file_name );
carp "unable to open $file_name : $!" unless defined $file;
$file->binmode;
return (
_file_name => $file_name,
_file => $file,
);
}
# get the next n bytes from the buffer.
method pop ( Int $len ) {
# ... Make sure there is data in _buff
return substr( $self->{_buff}, 0, $len, '' );
}
# Look around inside the buffer without changing the location for pop
method peek ( Int $offset, Int $len ) {
# ... Make sure there is data in _buff
return substr( $self->{_buff}, $offset, $len );
}
}
(There is buffer loading and managing code that I didn't include here. It is fairly straight forward.)
The problem is, I use the keyword method in the BUILDARGS declaration. So, MooseX::Declare expects a binary_file_buffer object as the first argument to BUILDARGS. But BUILDARGS gets the arguments passed to new, so the first argument is the string a 'binary_file_buffer', the name of the package. As a result it fails the type checking and dies when creating an object using new, like I did in the first code snippet. (At least that's my understanding of what is happening.)
The error message I get is:
Validation failed for 'MooseX::Types::Structured::Tuple[MooseX::Types::Structured::Tuple[Object,Str,Bool],MooseX::Types::Structured::Dict[]]' failed with value [ [ "binary_file_buffer", "drap_iono_t1.log", 0 ], { } ], Internal Validation Error is: Validation failed for 'MooseX::Types::Structured::Tuple[Object,Str,Bool]' failed with value [ "binary_file_buffer", "drap_iono_t1.log", 0 ] at C:/bin/perl/site/lib/MooseX/Method/Signatures/Meta/Method.pm line 445
MooseX::Method::Signatures::Meta::Method::validate('MooseX::Method::Signatures::Meta::Method=HASH(0x2a623b4)', 'ARRAY(0x2a62764)') called at C:/bin/perl/site/lib/MooseX/Method/Signatures/Meta/Method.pm line 145
binary_file_buffer::BUILDARGS('binary_file_buffer', 'drap_iono_t1.log') called at generated method (unknown origin) line 5
binary_file_buffer::new('binary_file_buffer', 'drap_iono_t1.log') called at logshred.pl line 13
I like the type checking sugar the method keyword supplies for $file_name, but I don't know how to get it since BUILDARGS isn't technically a method.
Does MooseX::Declare have a way to skip the $self creation, or something like that?
Am I doing this the proper MooseX::Declare way? Or am I missing something?
I think you want something like method BUILDARGS (ClassName $class: Str $filename) { ... } in which you explicitly define the invocant as ClassName $class.
I think you want:
#!/use/bin/perl
use strict;
use warnings;
use MooseX::Declare;
class BinaryFile::Buffer {
use FileHandle;
use Carp;
has file => ( is => 'ro', isa => 'FileHandle');
has file_name => ( is => 'ro', isa => 'Str');
has _buff => (
is => 'rw',
isa => 'Str',
default => '',
init_arg => undef
);
sub BUILDARGS {
my ($class, $file_name) = #_;
my $file = FileHandle->new( $file_name ) or do {
carp "unable to open ", $file_name, " : $!";
return;
};
$file->binmode;
return $class->SUPER::BUILDARGS(
file_name => $file_name,
file => $file
);
}
# get the next n bytes from the buffer.
method pop(Int $len) {
# ... Make sure there is data in _buff
return substr( $self->buff, 0, $len, '' );
}
# Look around inside the buffer without changing the location for pop
method peek(Int $offset, Int $len) {
# ... Make sure there is data in _buff
return substr( $self->buff, $offset, $len );
}
}
my $f = BinaryFile::Buffer->new($0);
print $f->file_name, "\n";
also a really neat way of doing it (just an expansion of the answer before me):
use MooseX::MultiMethods;
multi method BUILDARGS (ClassName $class: Str $filename) {
#do whatever you want to do if only a strg is passed
}
that way, MooseX::MultiMethods will take care that if you do NOT call
FileHandle->new( $file_name ),
but
FileHandle->new(
_filename => $file_name
);
(which is the normal syntax),
it would still work!
Also, you could ( which is not so useful for filenames but in other cases )
add a
multi method ( ClassName $class, Int $some_number ){}
that way, new could now handle hashrefs, integers and strings...
oh the possibilities... ;)