6

I currently use a block eval to test that I've set an attribute as read-only. Is there a simpler way to do this?

Example from working code:

#Test that sample_for is ready only
eval { $snp_obj->sample_for('t/sample_manifest2.txt');};
like($@, qr/read-only/xms, "'sample_for' is read-only");



UPDATE

Thanks to friedo, Ether, and Robert P for their answers and to Ether, Robert P, and jrockway for their comments.

I like how Ether's answer ensures that $is_read_only is only a true or false value (i.e. but never a coderef) by negating it with a !. Double negation also provides that. Thus, you can use $is_read_only in an is() function, without it printing out the coderef.

See Robert P's answer below for the most complete answer. Everyone has been very helpful and built on each other's answers and comments. Overall, I think he's helped me the most, hence his is now marked the accepted answer. Again, thanks to Ether, Robert P, friedo, and jrockway.



In case you might be wondering, as I did at first, here is documentation about the difference between get_attribute and find_attribute_by_name (from Class::MOP::Class):

$metaclass->get_attribute($attribute_name)

    This will return a Class::MOP::Attribute for the specified $attribute_name. If the 
    class does not have the specified attribute, it returns undef.

    NOTE that get_attribute does not search superclasses, for that you need to use
    find_attribute_by_name.
Community
  • 1
  • 1
Christopher Bottoms
  • 11,218
  • 8
  • 50
  • 99
  • That would be better written as `ok($snp_obj->meta->get_attribute('sample_for')->get_write_method(), "'sample_for' is read-only");` -- on test failure, `is()` prints the 2nd argument (which would be a coderef).. not to mention you have the 1st and 2nd arguments reversed: `is($has, $expected, $test_name)`. – Ether Apr 01 '10 at 19:19
  • If your @attribute_names array is carefully constructed, you should be fine; but if the attribute doesn't exist you'll explode :) – Robert P Apr 02 '10 at 16:55
  • +1 for noting how to locate attribute in superclass – user1027562 Jun 27 '13 at 15:16

3 Answers3

5

As documented in Class::MOP::Attribute:

my $attr = $this->meta->find_attribute_by_name($attr_name);
my $is_read_only = ! $attr->get_write_method();

$attr->get_write_method() will get the writer method (either one you created or one that was generated), or undef if there isn't one.

Ether
  • 53,118
  • 13
  • 86
  • 159
  • Thanks! Knowing that it returns `undef` allows for a one line test (I tried posting it here, but it did not look very pretty). – Christopher Bottoms Apr 01 '10 at 18:59
  • Well, actually... That tests if it has a write method. That doesn't test if it has a read method though. It doesn't have to have either, technically. It's not a very useful attribute if it doesn't, but you can have it! – Robert P Apr 01 '10 at 23:36
  • @Robert: Yes, strictly speaking it checks that the attribute is "not writeable" (not isa => 'rw'), which is not quite the same as "readonly" (isa => 'ro'). – Ether Apr 01 '10 at 23:55
  • 3
    Attributes without readers are plenty useful. Consider the case of `has foo => ( isa => 'ArrayRef', traits => ['Array'], handles => { add_foo => 'push', get_foo => 'pop' })`. No need for a reader! – jrockway Apr 02 '10 at 00:58
  • @jrockway that's an excellent example of an attribute without readers or writers. Thanks! – Robert P Apr 02 '10 at 16:25
5

Technically, an attribute does not need to have a read or a write method. Most of the time it will, but not always. An example (graciously stolen from jrockway's comment) is below:

has foo => ( 
    isa => 'ArrayRef', 
    traits => ['Array'], 
    handles => { add_foo => 'push', get_foo => 'pop' }
)

This attribute will exist, but not have standard readers and writers.

So to test in every situation that an attribute has been defined as is => 'RO', you need to check both the write and the read method. You could do it with this subroutine:

# returns the read method if it exists, or undef otherwise.
sub attribute_is_read_only {
    my ($obj, $attribute_name) = @_;
    my $attribute = $obj->meta->get_attribute($attribute_name);

    return unless defined $attribute;
    return (! $attribute->get_write_method() && $attribute->get_read_method());
}

Alternatively, you could add a double negation before the last return to boolify the return value:

return !! (! $attribute->get_write_method() && $attribute->get_read_method());
Community
  • 1
  • 1
Robert P
  • 15,707
  • 10
  • 68
  • 112
3

You should be able to get this from the object's metaclass:

unless ( $snp_obj->meta->get_attribute( 'sample_for' )->get_write_method ) { 
    # no write method, so it's read-only
}

See Class::MOP::Attribute for more.

friedo
  • 65,762
  • 16
  • 114
  • 184