Skip Menu |

This queue is for tickets about the XML-Twig CPAN distribution.

Report information
The Basics
Id: 39977
Status: open
Priority: 0/
Queue: XML-Twig

People
Owner: Nobody in particular
Requestors: AZED [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: Important
Broken in: 3.32
Fixed in: (no value)



Subject: XML::Twig::Elt->set_atts() destroys any existing attributes; set_att() fails silently on hashref
From the documentation, one would think that the only difference between set_att() and set_atts() is that the latter accepts hash references and the former doesn't (and in fact, you'd think that set_atts() wouldn't accept a list, even though it does). I saw no indication of any time one might want to use set_att() instead of set_atts()... but a subtle difference just bit me: set_atts() apparently destroys all existing attributes on the element before adding the new ones. This led to a sanity-questioning moment for me, since I found this by inspecting output from my own project by eye, but the unit test covering the code that generated the missing attribute passed. Fortunately, the project is still relatively small and well-organized, so it didn't take me too long to find the completely unrelated method that was causing the damage. While I was writing the Test::More entry that catches the issue, I discovered a secondary issue with set_att(): namely that it fails in absolute silence if handed a hashref. It doesn't throw an exception, it doesn't spit out a warning, it just silently fails to change anything at all. The attached file catches both issues. These two interfaces are close enough in name that one would expect them to either behave in the same way or fail noisily if they were switched -- it's just too easy to add or a remove an 's' when typing. I would ask that set_att() please either be updated to accept hashrefs, or croak if fed one.
Subject: test_set_atts.t
# # Check for XML::Twig::Elt->set_atts destroying existing attributes # use warnings; use strict; use Test::More tests => 9; BEGIN { use_ok('XML::Twig') }; my $twig = XML::Twig->new( output_encoding => 'utf-8', pretty_print => 'record' ); my $sky; my $xml = <<END; <root> <sky sun="yellow" cloud="white" /> </root> END my %atthash = ('color' => 'purple'); $twig->parse($xml); $sky = $twig->root->first_child('sky'); $sky->set_att('color' => 'blue'); is($sky->att('sun'),'yellow', 'set_att(key => value) preserves previous attributes'); is($sky->att('color'),'blue','set_att(key => value) sets new attribute'); $twig->parse($xml); $sky = $twig->root->first_child('sky'); $sky->set_att(\%atthash); is($sky->att('sun'),'yellow', 'set_att(\%hash) preserves previous attributes'); is($sky->att('color'),'blue','set_att(\%hash) sets new attribute'); $twig->parse($xml); $sky = $twig->root->first_child('sky'); $sky->set_atts('color' => 'cerulean'); is($sky->att('sun'),'yellow', 'set_atts(key => value) preserves previous attributes'); is($sky->att('color'),'cerulean','set_atts(key => value) sets new attribute'); $twig->parse($xml); $sky = $twig->root->first_child('sky'); $sky->set_atts(\%atthash); is($sky->att('sun'),'yellow','set_atts(\%hash) preserves previous attributes'); is($sky->att('color'),'purple','set_atts(\%hash) sets new attribute'); __DATA__
Subject: Re: [rt.cpan.org #39977] XML::Twig::Elt->set_atts() destroys any existing attributes; set_att() fails silently on hashref
Date: Mon, 13 Oct 2008 14:02:00 +0200
To: bug-XML-Twig [...] rt.cpan.org
From: mirod <xmltwig [...] gmail.com>
Zed Pobre via RT wrote: Show quoted text
> Sat Oct 11 19:56:02 2008: Request 39977 was acted upon. > Transaction: Ticket created by AZED > Queue: XML-Twig > Subject: XML::Twig::Elt->set_atts() destroys any existing attributes; > set_att() fails silently on hashref > Broken in: 3.32 > Severity: Important > Owner: Nobody > Requestors: AZED@cpan.org > Status: new > Ticket <URL: http://rt.cpan.org/Ticket/Display.html?id=39977 > > > > From the documentation, one would think that the only difference between > set_att() and set_atts() is that the latter accepts hash references and > the former doesn't (and in fact, you'd think that set_atts() wouldn't > accept a list, even though it does). I saw no indication of any time > one might want to use set_att() instead of set_atts()... but a subtle > difference just bit me: set_atts() apparently destroys all existing > attributes on the element before adding the new ones. > > This led to a sanity-questioning moment for me, since I found this by > inspecting output from my own project by eye, but the unit test covering > the code that generated the missing attribute passed. Fortunately, the > project is still relatively small and well-organized, so it didn't take > me too long to find the completely unrelated method that was causing the > damage. > > While I was writing the Test::More entry that catches the issue, I > discovered a secondary issue with set_att(): namely that it fails in > absolute silence if handed a hashref. It doesn't throw an exception, it > doesn't spit out a warning, it just silently fails to change anything at > all. The attached file catches both issues. > > These two interfaces are close enough in name that one would expect them > to either behave in the same way or fail noisily if they were switched > -- it's just too easy to add or a remove an 's' when typing. I would > ask that set_att() please either be updated to accept hashrefs, or croak > if fed one.
It makes sense. I have added a test that will make set_att croak if passed a hashref. The interface is still a bit messy, and you could still end-up thinking you're doing a set_att (no s) with a list of attribute name / values and because of a typo doing a set_atts (with s). This would be accepted but would delete all pre-existing attribute. In hindsight, set_atts should not accept a list of attribute name / values, but it's too late to change that. set_att used to only accept 1 attribute / value pair, but I found myself chaining calls often enough that I allowed passing several pairs in a single call. The updated version is at the usual place (http://xmltwig.com/xmltwig ). Let me know if you have any question or comment. Thanks -- mirod
Well, set_att() is now croaking well, which I like, but the first thing I noticed is that the croak message is a little odd: improper call to set_att, usage is XML::Twig::Elt=HASH(0x86630c0)->set_att( att1 => 'val1', att2 => 'val2',...) XML::Twig::Elt=HASH? You maybe meant to stick the GI in there? I'm afraid I haven't poked at your code to see what you changed yet. I also see that you've updated the twig devel documentation for set_atts(), which is better than no warning at all, but honestly, this interface scares me. I only caught my error by Mark 1 Eyeball inspection, because it was a subtle 'action-at-a-distance' type of bug that I would not ordinarily have thought to test for. The thought of trying to find the extra 's' in a very large project is quite daunting, especially considering how natural it is to type the plural form just because you are altering multiple attributes at once. I really would have much preferred to see set_att() and set_atts() become aliases of each other with identical interface and output, and create a clobbering version called set_all_atts() or set_atts_exactly() or some such. It was the attribute deletion that prompted me to write the original bug report; the discovery of the set_att() issue was really just a side-effect of my testing. I take it that you are concerned about breaking someone else's code that was relying on this previously undocumented feature; I would strongly urge you to reconsider, on the grounds that a clean break now, well-documented in the README, is very easy to repair (it's just a search-and-replace from 'set_atts' to 'set_all_atts' or some such for those people that really were relying on attribute deletion by set_atts(), and quickly findable by grep), but code broken by an extra (or missing) letter in a method whose name does not imply the extra functionality could be very difficult to repair later. This would pretty much have to be done now, though -- people relying on an undocumented feature when a documented method would have done the same thing safely in two lines have very little room to gripe, but once you've documented it in a stable release, users could be expecting to rely on it staying that way. Again, I strongly urge you to present a clearer interface now, before you officially sanction one that I feel is extremely dangerous. Thanks for your time.
Subject: Re: [rt.cpan.org #39977] XML::Twig::Elt->set_atts() destroys any existing attributes; set_att() fails silently on hashref
Date: Mon, 13 Oct 2008 20:21:19 +0200
To: bug-XML-Twig [...] rt.cpan.org
From: mirod <xmltwig [...] gmail.com>
Zed Pobre via RT wrote: Show quoted text
> Queue: XML-Twig > Ticket <URL: http://rt.cpan.org/Ticket/Display.html?id=39977 > > > Well, set_att() is now croaking well, which I like, but the first thing > I noticed is that the croak message is a little odd: > > improper call to set_att, usage is > XML::Twig::Elt=HASH(0x86630c0)->set_att( att1 => 'val1', att2 => 'val2',...) > > XML::Twig::Elt=HASH? You maybe meant to stick the GI in there? I'm > afraid I haven't poked at your code to see what you changed yet.
Oops! The test on the error message passed because it matched just the beginning of the error message, so I did not see the message. It's fixed now, I has $elt->set_att interpolated in the string. Duh! As for changing the methods, sorry, but I won't do that. As a general policy I try really really hard not to break backward compatibility. Pretty much anything that has been written using XML::Twig since version 3.01 in January 2002 can still run just the same in the current version. I do not want to change that. I have suffered enough from that kind of behavior to be very reluctant to do it to others. If that's really a problem for you, several options are available: - you can subclass the module and rewrite one of the 2 methods, - if your project uses its own modules, you could monkeypatch XML::Twig to fix (or remove) one of the methods, - I could add an option to XML::Twig's new method that would help you, either by making the methods identical as you suggest, or by issuing a warning if set_att is used with more than 1 pair. But in any case the default behaviour will stay as is. -- mirod
Actually, an option to new() to fix this would be fine by me. While considering the mechanic to propose to you, however, I discovered a similar issue with del_att() and del_atts(), so this response will cover that as well. Let me propose the following: 1. Let set_att() accept a hashref the same way set_atts() does. This makes set_att() the safe method to use in all circumstances. 2. create set_all_atts() and del_all_atts() to do the same thing that set_atts() and del_atts() do now, unaffected by the safe_atts option described below. Have the documentation entries for set_atts() and del_atts() recommend that the user use set_all_atts() and del_all_atts() for clarity instead. 3. del_all_atts() and del_atts() should both croak if fed any argument at all. Right now, del_atts('att1',att2') will silently delete everything it can find and not even warn you that you almost certainly did something wrong. 4. new(safe_atts => 1) a. causes set_atts() to not clobber existing attributes b. causes set_atts() to always carp a warning that set_atts() is ambiguous, use set_att() or set_all_atts() instead c. causes del_att() to croak if it is fed no arguments at all (this is placed here instead of with #3 because I don't know any way to distinguish between passing no arguments and passing an empty list) d. causes del_atts() to always carp a warning that del_atts() is easily confused with del_att(), use del_all_atts() instead. I'm torn about the warnings in 4b and 4d, simply because set_atts() rolls off of the fingers so nicely when thinking about multiple attributes, but there's no getting around the fact that set_att() is the only safe and consistent interface, even assuming the existence of safe_atts. If I (using safe_atts in my new at the top of my code) wrote a sub using set_atts() relying on them not to clobber attributes, and someone cuts-and-pastes that sub but doesn't use safe_atts, havoc may ensue, so as much as I hate asking for the warnings, I am. Does that meet with your approval?