Repository: smarie/python-pyfields

Issue:
Unable to apply @property decorator to a method on a class with @autofields
A class defined as:

```
@autofields()
class Frog:
    another: int = 1
    @property
    def balh(self):
        print('asd')
```

will raise an exception when creating...
```
TypeError                                 Traceback (most recent call last)
<ipython-input-13-a88b723ac722> in <module>
----> 1 Frog()

~/.pyenv/versions/3.6.5/envs/pd/lib/python3.6/site-packages/pyfields/init_makers.py in __init__(self, another, balh)

~/.pyenv/versions/3.6.5/envs/pd/lib/python3.6/site-packages/pyfields/init_makers.py in init_fun(*args, **kwargs)
    409                 else:
    410                     # init the field with its factory, by just getting it
--> 411                     getattr(self, field_name)
    412 
    413         return init_fun

~/.pyenv/versions/3.6.5/envs/pd/lib/python3.6/site-packages/pyfields/core.py in __get__(self, obj, obj_type)
    833             # optional: get default
    834             if self.is_default_factory:
--> 835                 value = self.default(obj)
    836             else:
    837                 value = self.default

~/.pyenv/versions/3.6.5/envs/pd/lib/python3.6/site-packages/pyfields/helpers.py in create_default(obj)
    197     if deep:
    198         def create_default(obj):
--> 199             return deepcopy(val)
    200     else:
    201         def create_default(obj):

~/.pyenv/versions/3.6.5/lib/python3.6/copy.py in deepcopy(x, memo, _nil)
    167                     reductor = getattr(x, "__reduce_ex__", None)
    168                     if reductor:
--> 169                         rv = reductor(4)
    170                     else:
    171                         reductor = getattr(x, "__reduce__", None)

TypeError: can't pickle property objects

```

A class without @autofields is able to be created without any exceptions. The call to getattr in init_makers.py will always return the value of the property rather than the property itself whereas inspecting `__dict__` on the class will return the property which should be skipped when setting up fields, eg. 

```
In [28]: Frog.__dict__.get('balh')                                                                                                                                                                      
Out[28]: <property at 0x11d3b53b8>
```

Discussion:

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 10c818e..5bf4311 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,9 @@
 # Changelog
 
+### 1.1.5 - bugfix
+
+ - `@autofields` now correctly skips `@property` and descriptor members. Fixes [#67](https://github.com/smarie/python-pyfields/issues/67)
+
 ### 1.1.4 - better python 2 packaging
 
  - packaging improvements: set the "universal wheel" flag to 1, and cleaned up the `setup.py`. In particular removed dependency to `six`. Fixes [#66](https://github.com/smarie/python-pyfields/issues/66)
diff --git a/pyfields/autofields_.py b/pyfields/autofields_.py
index 94ed099..321b8fc 100644
--- a/pyfields/autofields_.py
+++ b/pyfields/autofields_.py
@@ -2,6 +2,7 @@
 #
 #  Copyright (c) Schneider Electric Industries, 2019. All right reserved.
 import sys
+from inspect import isdatadescriptor, ismethoddescriptor
 
 try:
     from typing import Union, Callable, Type, Any, TypeVar
@@ -160,6 +161,10 @@ def autofields(check_types=False,     # type: Union[bool, DecoratedClass]
             elif isinstance(default_value, Field):
                 # already a field, no need to create
                 continue
+            elif isinstance(default_value, property) or isdatadescriptor(default_value) \
+                    or ismethoddescriptor(default_value):
+                # a property or a data or non-data descriptor > exclude
+                continue
             elif (isinstance(default_value, type) or callable(default_value)) \
                     and getattr(default_value, '__name__', None) == member_name:
                 # a function/class defined in the class > exclude
diff --git a/pyfields/helpers.py b/pyfields/helpers.py
index 4b3bdc0..d418aea 100644
--- a/pyfields/helpers.py
+++ b/pyfields/helpers.py
@@ -36,6 +36,9 @@ def get_field(cls, name):
     except ClassFieldAccessError as e:
         # we know it is a field :)
         return e.field
+    except Exception:
+        # any other exception that can happen with a descriptor for example
+        raise NotAFieldError(cls, name)
     else:
         # it is a field if is it an instance of Field
         if isinstance(member, Field):


Test patch:
diff --git a/pyfields/tests/issues/_test_py36.py b/pyfields/tests/issues/_test_py36.py
index a86e31a..23a3ae6 100644
--- a/pyfields/tests/issues/_test_py36.py
+++ b/pyfields/tests/issues/_test_py36.py
@@ -1,7 +1,7 @@
 #  Authors: Sylvain Marie <sylvain.marie@se.com>
 #
 #  Copyright (c) Schneider Electric Industries, 2019. All right reserved.
-from pyfields import field, init_fields
+from pyfields import field, init_fields, autofields
 
 
 def test_issue_51():
@@ -13,3 +13,15 @@ def test_issue_51():
             pass
 
     return A
+
+
+def test_issue_67():
+    @autofields
+    class Frog:
+        another: int = 1
+
+        @property
+        def balh(self):
+            print('asd')
+
+    return Frog
diff --git a/pyfields/tests/issues/test_issue_67.py b/pyfields/tests/issues/test_issue_67.py
new file mode 100644
index 0000000..1d9d9c7
--- /dev/null
+++ b/pyfields/tests/issues/test_issue_67.py
@@ -0,0 +1,17 @@
+#  Authors: Sylvain Marie <sylvain.marie@se.com>
+#
+#  Copyright (c) Schneider Electric Industries, 2020. All right reserved.
+import sys
+
+import pytest
+
+from pyfields import get_fields
+
+
+@pytest.mark.skipif(sys.version_info < (3, 6), reason="member type hints not supported in python < 3.6")
+def test_issue_67():
+    from ._test_py36 import test_issue_67
+    Frog = test_issue_67()
+
+    assert len(get_fields(Frog)) == 1
+    Frog()
diff --git a/pyfields/tests/test_autofields.py b/pyfields/tests/test_autofields.py
index 7fe1c72..83c6f42 100644
--- a/pyfields/tests/test_autofields.py
+++ b/pyfields/tests/test_autofields.py
@@ -5,7 +5,7 @@ import sys
 
 import pytest
 
-from pyfields import autofields, field, FieldTypeError, Field
+from pyfields import autofields, field, FieldTypeError, Field, get_fields
 from pyfields.core import NativeField
 
 
@@ -69,3 +69,31 @@ def test_autofields_basic(with_type_hints, type_check):
         assert f.fct() == 1
         assert f.barfunc(1) == 2
         assert f.barcls == str
+
+
+def test_autofields_property_descriptors():
+    """Checks that properties and descriptors are correctly ignored by autofields"""
+
+    @autofields
+    class Foo(object):
+        foo = 1
+        @property
+        def bar(self):
+            return 2
+
+        class MyDesc():
+            def __get__(self):
+                return 1
+
+        class MyDesc2():
+            def __get__(self):
+                return 0
+            def __set__(self, instance, value):
+                return
+
+        m = MyDesc()
+        p = MyDesc2()
+
+    fields = get_fields(Foo)
+    assert len(fields) == 1
+    assert fields[0].name == 'foo'