JuliaPy / PyCall.jl

Package to call Python functions from the Julia language
MIT License
1.47k stars 190 forks source link

propertynames fails for "optional" property decorators #857

Closed sethaxen closed 3 years ago

sethaxen commented 4 years ago

When a Python class has an optional property decorator (i.e. only works when some optional dependency is available), then propertynames fails. Here's a minimal example:

julia> using PyCall
[ Info: Precompiling PyCall [438e738f-606a-5dbb-bf0a-cddfbfd45ab0]

julia> py"""
       class MyClass:
           _myprop = None

           @property
           def myprop(self):
               if self._myprop is None:
                   raise(Exception("myprop is not defined"))
               return self._myprop
       """

julia> o = py"MyClass()"
PyObject <__main__.MyClass object at 0x7fb2d9011fa0>

julia> propertynames(o)
ERROR: PyError ($(Expr(:escape, :(ccall(#= /Users/saxen/.julia/packages/PyCall/BcTLp/src/pyfncall.jl:43 =# @pysym(:PyObject_Call), PyPtr, (PyPtr, PyPtr, PyPtr), o, pyargsptr, kw))))) <class 'Exception'>
Exception('myprop is not defined')
  File "/Users/saxen/.julia/conda/3/lib/python3.8/inspect.py", line 350, in getmembers
    value = getattr(object, key)
  File "/Users/saxen/.julia/packages/PyCall/BcTLp/src/pyeval.jl", line 7, in myprop
    pynamespace(m::Module) =

Stacktrace:
 [1] pyerr_check at /Users/saxen/.julia/packages/PyCall/BcTLp/src/exception.jl:62 [inlined]
 [2] pyerr_check at /Users/saxen/.julia/packages/PyCall/BcTLp/src/exception.jl:66 [inlined]
 [3] _handle_error(::String) at /Users/saxen/.julia/packages/PyCall/BcTLp/src/exception.jl:83
 [4] macro expansion at /Users/saxen/.julia/packages/PyCall/BcTLp/src/exception.jl:97 [inlined]
 [5] #110 at /Users/saxen/.julia/packages/PyCall/BcTLp/src/pyfncall.jl:43 [inlined]
 [6] disable_sigint at ./c.jl:446 [inlined]
 [7] __pycall! at /Users/saxen/.julia/packages/PyCall/BcTLp/src/pyfncall.jl:42 [inlined]
 [8] _pycall!(::PyObject, ::PyObject, ::Tuple{PyObject}, ::Int64, ::Ptr{Nothing}) at /Users/saxen/.julia/packages/PyCall/BcTLp/src/pyfncall.jl:29
 [9] _pycall! at /Users/saxen/.julia/packages/PyCall/BcTLp/src/pyfncall.jl:11 [inlined]
 [10] #pycall#115 at /Users/saxen/.julia/packages/PyCall/BcTLp/src/pyfncall.jl:80 [inlined]
 [11] pycall at /Users/saxen/.julia/packages/PyCall/BcTLp/src/pyfncall.jl:80 [inlined]
 [12] propertynames(::PyObject) at /Users/saxen/.julia/packages/PyCall/BcTLp/src/PyCall.jl:319
 [13] top-level scope at REPL[7]:1

julia> o._myprop = 1
1

julia> propertynames(o)
28-element Array{Symbol,1}:
 :__class__
 :__delattr__
 :__dict__
 :__dir__
 :__doc__
 :__eq__
 :__format__
 :__ge__
 :__getattribute__
 :__gt__
 :__hash__
 :__init__
 :__init_subclass__
 :__le__
 :__lt__
 :__module__
 :__ne__
 :__new__
 :__reduce__
 :__reduce_ex__
 :__repr__
 :__setattr__
 :__sizeof__
 :__str__
 :__subclasshook__
 :__weakref__
 :_myprop
 :myprop

This happens because propertynames calls Python's inspect.getmembers, which calls all decorator functions, but it then discards the values. Wouldn't it be safer to use the stdlib function dir for this?

julia> py"dir(MyClass())"
28-element Array{String,1}:
 "__class__"
 "__delattr__"
 "__dict__"
 "__dir__"
 "__doc__"
 "__eq__"
 "__format__"
 "__ge__"
 "__getattribute__"
 "__gt__"
 "__hash__"
 "__init__"
 "__init_subclass__"
 "__le__"
 "__lt__"
 "__module__"
 "__ne__"
 "__new__"
 "__reduce__"
 "__reduce_ex__"
 "__repr__"
 "__setattr__"
 "__sizeof__"
 "__str__"
 "__subclasshook__"
 "__weakref__"
 "_myprop"
 "myprop"