First, I apologize for the poor post. Your corrections were exactly correct: This is only relevant in the context of properties/descriptors, and the property swallows the error message and it isn't printed to screen.  I should not be typing without testing.

So... what precisely should be passed to __getattr__, and what exactly are you going to do with it?
I'd say the error instance that caused __getattr__ to be raised should be passed into __getattr__, but I don't have a strong opinion on that. I'll assume that's true for the rest of this post, however.

To clarify my mistakes in my first post, your example illustrates what I wanted to show:
class A(object):
    eggs = "text"
    def __getattr__(self, name):
        if name == 'cheese':
            return "cheddar"
        raise AttributeError('%s missing' % name)
    @property
    def spam(self):
        return self.eggs.uper() # Oops.
a = A()
a.spam

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __getattr__
AttributeError: spam missing

This swallows the AttributeError from `eggs.uper()` and it isn't available.

Even if it were available, I see your point that it may not be especially useful. In one iteration of my code I was looking through the stack trace using the traceback module to find the error I wanted, but I quickly decided that was a bad idea because I couldn't reliably find the error. However, with the full error, it would (I think) be trivial to find the relevant error in the stack trace. With the full stack trace, I would hope that you could properly do any error handling you wanted.

However, if the error was available in __getattr__, we could at least `raise from` so that the error isn't completely swallowed. I.e. your example would be slightly modified like this:
class A(object):
    eggs = "text"
    def __getattr__(self, name, error):
        if name == 'cheese':
            return "cheddar"
        raise AttributeError('%s missing' % name) from error
...

which I think is useful.


That's a fairly long and heavy process, and will be quite annoying to
> those writing cross-version code using __getattr__, but it can be done.
This is another thing I hadn't considered, and I have no problem saying this non-backwards-compatible-change just isn't worth it. Maybe if there are multiple small updates to error handling it could be worth it (which I believe I read was something the devs care quite a bit about atm), but I don't think that this change is a huge deal.


One good solution to this is a "guard point" around your property functions.
I am using a very similar decorator (modified from the one you gave me a month or so ago) in my code now, and functionally it works great.  There is something that feels a bit off / like a hack to me though, and maybe it's that I am simply "renaming" the error from one I can't handle (due to name conflicts) to one I can. But the modification is just name change -- if I can handle the RuntimeError correctly, I feel like I should have just been able to handle the original AttributeError correctly (because in practice they should be raising an error in response to the exact same problem).   That said, your decorator works great and gives me the functionality I needed.


> > It's a feature.  It's why Enum classes can have members named 'value' and
> > 'name'.
> Can you explain further? What's special about value and name?
I'm also very curious about this. I've read the Enum code a few times but it hasn't clicked yet.


> > Perhaps a better approach is to prevent descriptors from leaking
> > AttributeError in the first place? Change the protocol so that if
> > descriptor.__get__ raises AttributeError, it is caught and re-raised as
> > RuntimeError, similar to StopIteration and generators.
> This can't be done globally, because that's how a descriptor can be
> made conditional (it raises AttributeError to say "this attribute does
> not, in fact, exist"). But it's easy enough - and safe enough - to do
> it just for your own module, where you know in advance that any
> AttributeError is a leak.
I have a pretty large codebase with a few different functionalities, so I'm hesitant to say "any AttributeError is a leak" in an object that might affect / be affected by other packages/functionalities.  One other thing I really like about the `noleak` decorator is that I can change the AttributeError to a custom MyCustomError, which allows me to handle MyCustomError precisely where it should be handled.  Also, maybe I'm just not fully understanding the locality of re-raising another error from descriptor.__get__ because I haven't completely wrapped my head around it yet.

On Mon, Jun 19, 2017 at 10:10 PM, Chris Angelico <rosuav@gmail.com> wrote:
On Tue, Jun 20, 2017 at 12:26 PM, Steven D'Aprano <steve@pearwood.info> wrote:
> On Tue, Jun 20, 2017 at 11:31:34AM +1000, Chris Angelico wrote:
>
>> Why not just write cross-version-compatible code as
>>
>> def __getattr__(self, name, error=None):
>>
>> ? Is there something special about getattr?
>
> You've still got to write it in the first place. That's a pain,
> especially since (1) it doesn't do you any good before 3.7 if not later,
> and (2) even if this error parameter is useful (which is yet to be
> established), it's a pretty specialised use. Most of the time, you
> already know the name that failed (its the one being looked up).

Gotcha, yep. I was just confused by your two-parter that made it look
like it would be hard (or impossible) to write code that would work on
both 3.6 and the new protocol.

> Perhaps a better approach is to prevent descriptors from leaking
> AttributeError in the first place? Change the protocol so that if
> descriptor.__get__ raises AttributeError, it is caught and re-raised as
> RuntimeError, similar to StopIteration and generators.

This can't be done globally, because that's how a descriptor can be
made conditional (it raises AttributeError to say "this attribute does
not, in fact, exist"). But it's easy enough - and safe enough - to do
it just for your own module, where you know in advance that any
AttributeError is a leak. The way generators and StopIteration
interact was more easily fixed, because generators have two legitimate
ways to emit data (yield and return), but there's no easy way for a
magic method to say "I don't have anything to return" other than an
exception.

Well, that's not strictly true. In JavaScript, they don't raise
StopIteration from iterators - they always return a pair of values
("done" and the actual value, where "done" is either false for a yield
or true for a StopIteration). That complicates the normal case but it
does make the unusual case a bit easier. Also, it's utterly and
fundamentally incompatible with the current system, so it'd have to be
a brand new competing protocol.

ChrisA
_______________________________________________
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/