Repository: JuliaPy/PyCall.jl (julia)

Issue:
Wrong error reported when accessing an attribute
Hi! I have a class that does some checking before setting an attribute value, like this (pure Python):
```python
class A:
    def __setattr__(self, name, value):
        if value == 0:
            raise ValueError(value)
        # set value...

a = A()
a.a = 5  # OK
a.a = 0  # correct error message:
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-7600552ca637> in <module>()
----> 1 a.a = 0

<ipython-input-6-585623315894> in __setattr__(self, name, value)
      2     def __setattr__(self, name, value):
      3         if value == 0:
----> 4             raise ValueError(value)
      5         # set value...
      6

ValueError: 0
```

However, running the same code with PyCall.jl gives a very confusing error message:
```julia
py"""
class A:
    def __setattr__(self, name, value):
        if value == 0:
            raise ValueError(value)
        # set value...
"""
a = py"A"()
a.a = 5  # OK
a.a = 0  # completely wrong exception:
ERROR: KeyError: key :a not found
Stacktrace:
 [1] setproperty!(::PyObject, ::Symbol, ::Int64) at /users/aplavin/.julia/packages/PyCall/ttONZ/src/PyCall.jl:329
 [2] top-level scope at none:0
```

Pull request description:
this throws KeyError as before when the attribute isn't there (AttributeError in python)
when the attribute is present, but its computation throws a python exception, this PR rethrows it in julia

fixes https://github.com/JuliaPy/PyCall.jl/issues/696

Patch:
diff --git a/src/PyCall.jl b/src/PyCall.jl
index c09cc204..7f0317ec 100644
--- a/src/PyCall.jl
+++ b/src/PyCall.jl
@@ -298,7 +298,13 @@ end
 function _getproperty(o::PyObject, s::Union{AbstractString,Symbol})
     ispynull(o) && throw(ArgumentError("ref of NULL PyObject"))
     p = ccall((@pysym :PyObject_GetAttrString), PyPtr, (PyPtr, Cstring), o, s)
-    p == C_NULL && pyerr_clear()
+    if p == C_NULL && pyerr_occurred()
+        e = pyerror("PyObject_GetAttrString")
+        if PyPtr(e.T) != @pyglobalobjptr(:PyExc_AttributeError)
+            throw(e)
+        end
+        pyerr_clear()
+    end
     return p
 end
 
@@ -327,15 +333,17 @@ setproperty!(o::PyObject, s::Symbol, v) = _setproperty!(o,s,v)
 setproperty!(o::PyObject, s::AbstractString, v) = _setproperty!(o,s,v)
 
 function _setproperty!(o::PyObject, s::Union{Symbol,AbstractString}, v)
-    if ispynull(o)
-        throw(ArgumentError("assign of NULL PyObject"))
-    end
-    if -1 == ccall((@pysym :PyObject_SetAttrString), Cint,
-                   (PyPtr, Cstring, PyPtr), o, s, PyObject(v))
+    ispynull(o) && throw(ArgumentError("assign of NULL PyObject"))
+    p = ccall((@pysym :PyObject_SetAttrString), Cint, (PyPtr, Cstring, PyPtr), o, s, PyObject(v))
+    if p == -1 && pyerr_occurred()
+        e = pyerror("PyObject_SetAttrString")
+        if PyPtr(e.T) != @pyglobalobjptr(:PyExc_AttributeError)
+            throw(e)
+        end
         pyerr_clear()
         throw(KeyError(s))
     end
-    o
+    return o
 end
 
 function getindex(o::PyObject, s::T) where T<:Union{Symbol, AbstractString}


Test patch:
diff --git a/test/runtests.jl b/test/runtests.jl
index 4594cc40..e16b31ae 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -829,3 +829,26 @@ end
         @test py"bar2" == 2
     end
 end
+
+@testset "proper exception raised" begin
+    py"""
+    class A:
+        def __getattr__(self, name):
+            if name == "a":
+                raise ValueError(name)
+            else:
+                raise AttributeError()
+
+        def __setattr__(self, name, value):
+            if value == 0:
+                raise ValueError(value)
+            else:
+                raise AttributeError()
+    """
+    a = py"A"()
+    @test_throws PyCall.PyError a.a
+    @test_throws KeyError a.b
+
+    @test_throws PyCall.PyError a.a = 0
+    @test_throws KeyError a.a = 1
+end