Repository: pyccel/pyccel (python)

Issue:
Add tests for `@property` decorator
### Describe the feature
The `@property` decorator is a very useful tool in classes. So much so that internally we have been using the property decorator without having added support for it in our translations. As a result it seems that this decorator now works out of the box! Currently a warning is raised claiming that we are ignoring the decorator:
```
pyccel:
 |warning [semantic]: tmp_cls.py| Decorator(s) not used (property)
```
However the code translates perfectly and clearly uses this property. We should therefore remove this warning, add tests and improve the documentation to show that this is supported.

### Test Code
The following code:
```python
class A:
    def __init__(self, x : float):
        self._x = x

    @property
    def x(self):
        return self._x

if __name__ == '__main__':
    x = 2.0
    y = A(x)
    z = y.x
```
currently raises the above warning but generates the following code:
```fortran
module tmp_cls


  use, intrinsic :: ISO_C_Binding, only : i64 => C_INT64_T , b1 => &
        C_BOOL , f64 => C_DOUBLE
  implicit none

  type, public :: A
    real(f64) :: private_x
    logical(b1), private :: is_freed

    contains
    procedure :: create => a_create
    procedure :: x => a_x
    procedure :: free => a_free
  end type A

  contains


  !........................................

  subroutine a_create(self, x)

    implicit none

    class(A), intent(inout) :: self
    real(f64), value :: x

    self%is_freed = .False._b1
    self%private_x = x

  end subroutine a_create

  !........................................


  !........................................

  function a_x(self) result(Out_0001)

    implicit none

    real(f64) :: Out_0001
    class(A), intent(inout) :: self

    Out_0001 = self%private_x
    return

  end function a_x

  !........................................


  !........................................

  subroutine a_free(self)

    implicit none

    class(A), intent(inout) :: self

    if (.not. self%is_freed) then
      ! pass
      self%is_freed = .True._b1
    end if

  end subroutine a_free

  !........................................


end module tmp_cls
```
```fortran
program prog_prog_tmp_cls

  use tmp_cls

  use, intrinsic :: ISO_C_Binding, only : i64 => C_INT64_T , b1 => &
        C_BOOL , f64 => C_DOUBLE
  implicit none

  real(f64) :: x
  type(A) :: y
  real(f64) :: z

  x = 2.0_f64
  call y % create(x)
  z = y % x()
  call y % free()

end program prog_prog_tmp_cls
```

Similarly for C:
```c
#include "tmp_cls.h"
#include <stdlib.h>
#include <stdbool.h>


/*........................................*/
void A__init__(struct A* self, double x)
{
    self->is_freed = 0;
    self->private_x = x;
}
/*........................................*/
/*........................................*/
double A__x(struct A* self)
{
    return self->private_x;
}
/*........................................*/
/*........................................*/
void A__del__(struct A* self)
{
    if (!self->is_freed)
    {
        // pass
        self->is_freed = 1;
    }
}
/*........................................*/
```
```c
#include "tmp_cls.h"
#include <stdlib.h>
#include <stdbool.h>
int main()
{
    double x;
    struct A y;
    double z;
    x = 2.0;
    A__init__(&y, x);
    z = A__x(&y);
    A__del__(&y);
    return 0;
}
```

Pull request description:
Add support for @property
The semantic handling and the printing of methods using the `@property` decorator was already working, however official support for the decorator was missing due to the missing wrapper support. This PR adds the wrapping and removes the warning about this decorator. Fixes #1834 

**Commit Summary**
- Add documentation
- Prefer assertions for errors that only affect developers
- Allow the setter of a property to be missing (it is then saved as `None`)
- Add wrapping of a class property. The arguments of such functions are slightly different to those of a basic function.
- Add `-Wno-incompatible-pointer-types` to Python flags to remove unavoidable warnings
- Add tests

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a140bac..4295340f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -56,6 +56,7 @@ All notable changes to this project will be documented in this file.
 -   #1980 : Extend The C support for min and max to more than two variables
 -   #2081 : Add support for multi operator expressions
 -   Add support for inhomogeneous tuple annotations.
+-   #1834 : Add support for `@property` decorator.
 -   \[INTERNALS\] Add abstract class `SetMethod` to handle calls to various set methods.
 -   \[INTERNALS\] Added `container_rank` property to `ast.datatypes.PyccelType` objects.
 -   \[INTERNALS\] Add a `__call__` method to `FunctionDef` to create `FunctionCall` instances.
diff --git a/docs/classes.md b/docs/classes.md
index d08c4f75..85b8b3f4 100644
--- a/docs/classes.md
+++ b/docs/classes.md
@@ -188,6 +188,233 @@ MyClass Object created!
 ==158858== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
 ```
 
+## Class properties
+
+Pyccel now supports class properties (to retrieve a constant value only).
+
+### - Python Example
+
+```python
+import numpy as np
+
+class MyClass:
+    def __init__(self, param1 : 'int', param2 : 'int'):
+        self._param1 = param1
+        self._param2 = np.ones(param2)
+        print("MyClass Object created!")
+
+    @property
+    def param1(self):
+        return self._param1
+
+    @property
+    def param2(self):
+        return self._param2
+
+if __name__ == '__main__':
+    obj = MyClass1(2, 4)
+    print(obj.param1)
+    print(obj.param2)
+```
+
+### PYTHON _OUTPUT_
+```Shell
+MyClass Object created!
+2
+[1. 1. 1. 1.]
+```
+
+### - Header File Equivalent
+
+```C
+struct MyClass {
+    int64_t private_param1;
+    t_ndarray private_param2;
+    bool is_freed;
+};
+
+void MyClass__init__(struct MyClass* self, int64_t param1, int64_t param2);
+int64_t MyClass__param1(struct MyClass* self);
+t_ndarray MyClass__param2(struct MyClass* self);
+void MyClass__del__(struct MyClass* self);
+```
+
+### - C File Equivalent
+
+```C
+/*........................................*/
+void MyClass__init__(struct MyClass* self, int64_t param1, int64_t param2)
+{
+    self->is_freed = 0;
+    self->private_param1 = param1;
+    self->private_param2 = array_create(1, (int64_t[]){param2}, nd_double, false, order_c);
+    array_fill((double)1.0, self->private_param2);
+    printf("MyClass Object created!\n");
+}
+/*........................................*/
+/*........................................*/
+int64_t MyClass__param1(struct MyClass* self)
+{
+    return self->private_param1;
+}
+/*........................................*/
+/*........................................*/
+t_ndarray MyClass__param2(struct MyClass* self)
+{
+    t_ndarray Out_0001 = {.shape = NULL};
+    alias_assign(&Out_0001, self->private_param2);
+    return Out_0001;
+}
+/*........................................*/
+/*........................................*/
+void MyClass__del__(struct MyClass* self)
+{
+    if (!self->is_freed)
+    {
+        // pass
+        free_array(&self->private_param2);
+        self->is_freed = 1;
+    }
+}
+/*........................................*/
+```
+
+### - C Program File Equivalent
+```C
+int main()
+{
+    struct MyClass obj;
+    t_ndarray Dummy_0000 = {.shape = NULL};
+    int64_t i;
+    MyClass__init__(&obj, INT64_C(2), INT64_C(4));
+    printf("%"PRId64"\n", MyClass__param1(&obj));
+    Dummy_0000 = MyClass__param2(&obj);
+    printf("[");
+    for (i = INT64_C(0); i < Dummy_0000.shape[INT64_C(0)] - INT64_C(1); i += INT64_C(1))
+    {
+        printf("%.15lf ", GET_ELEMENT(Dummy_0000, nd_double, i));
+    }
+    printf("%.15lf]\n", GET_ELEMENT(Dummy_0000, nd_double, Dummy_0000.shape[INT64_C(0)] - INT64_C(1)));
+    MyClass__del__(&obj);
+    free_pointer(&Dummy_0000);
+    return 0;
+}
+```
+
+### - Fortran File Equivalent
+
+```fortran
+  type, public :: MyClass
+    integer(i64) :: private_param1
+    real(f64), allocatable :: private_param2(:)
+    logical(b1), private :: is_freed
+
+    contains
+    procedure :: create => myclass_create
+    procedure :: param1 => myclass_param1
+    procedure :: param2 => myclass_param2
+    procedure :: free => myclass_free
+  end type MyClass
+
+  contains
+
+
+  !........................................
+
+  subroutine myclass_create(self, param1, param2)
+
+    implicit none
+
+    class(MyClass), intent(inout) :: self
+    integer(i64), value :: param1
+    integer(i64), value :: param2
+
+    self%is_freed = .False._b1
+    self%private_param1 = param1
+    allocate(self%private_param2(0:param2 - 1_i64))
+    self%private_param2 = 1.0_f64
+    write(stdout, '(A)', advance="yes") 'MyClass Object created!'
+
+  end subroutine myclass_create
+
+  !........................................
+
+
+  !........................................
+
+  function myclass_param1(self) result(Out_0001)
+
+    implicit none
+
+    integer(i64) :: Out_0001
+    class(MyClass), intent(inout) :: self
+
+    Out_0001 = self%private_param1
+    return
+
+  end function myclass_param1
+
+  !........................................
+
+
+  !........................................
+
+  subroutine myclass_param2(self, Out_0001)
+
+    implicit none
+
+    real(f64), pointer, intent(out) :: Out_0001(:)
+    class(MyClass), target, intent(inout) :: self
+
+    Out_0001(0:) => self%private_param2
+    return
+
+  end subroutine myclass_param2
+
+  !........................................
+
+
+  !........................................
+
+  subroutine myclass_free(self)
+
+    implicit none
+
+    class(MyClass), intent(inout) :: self
+
+    if (.not. self%is_freed) then
+      ! pass
+      if (allocated(self%private_param2)) then
+        deallocate(self%private_param2)
+      end if
+      self%is_freed = .True._b1
+    end if
+
+  end subroutine myclass_free
+
+  !........................................
+```
+
+### - Fortran Program File Equivalent
+
+```fortran
+  type(MyClass), target :: obj
+  real(f64), pointer :: Dummy_0000(:)
+  integer(i64) :: i
+
+  call obj % create(2_i64, 4_i64)
+  write(stdout, '(I0)', advance="yes") obj % param1()
+  call obj % param2(Out_0001 = Dummy_0000)
+  write(stdout, '(A)', advance="no") '['
+  do i = 0_i64, size(Dummy_0000, kind=i64) - 1_i64 - 1_i64
+    write(stdout, '(F0.15, A)', advance="no") Dummy_0000(i) , ' '
+  end do
+  write(stdout, '(F0.15, A)', advance="no") Dummy_0000(size(Dummy_0000, &
+        kind=i64) - 1_i64) , ']'
+  write(stdout, '()', advance="yes")
+  call obj % free()
+```
+
 ## Limitations
 
-It's important to note that Pyccel does not support class inheritance, properties, magic methods or static class variables. For our first implementation, the focus of Pyccel is primarily on core class functionality and memory management.
+It's important to note that Pyccel does not support class inheritance, magic methods or static class variables. For our first implementation, the focus of Pyccel is primarily on core class functionality and memory management.
diff --git a/pyccel/ast/bind_c.py b/pyccel/ast/bind_c.py
index dd3b6db4..dfaeaae1 100644
--- a/pyccel/ast/bind_c.py
+++ b/pyccel/ast/bind_c.py
@@ -608,10 +608,8 @@ class BindCClassProperty(PyccelAstNode):
     __slots__ = ('_getter', '_setter', '_python_name', '_docstring', '_class_type')
     _attribute_nodes = ('_getter', '_setter')
     def __init__(self, python_name, getter, setter, class_type, docstring = None):
-        if not isinstance(getter, BindCFunctionDef):
-            raise TypeError("Getter should be a BindCFunctionDef")
-        if not isinstance(setter, BindCFunctionDef):
-            raise TypeError("Setter should be a BindCFunctionDef")
+        assert isinstance(getter, BindCFunctionDef)
+        assert isinstance(setter, BindCFunctionDef) or setter is None
         self._python_name = python_name
         self._getter = getter
         self._setter = setter
diff --git a/pyccel/ast/cwrapper.py b/pyccel/ast/cwrapper.py
index 5033c766..194a5bb8 100644
--- a/pyccel/ast/cwrapper.py
+++ b/pyccel/ast/cwrapper.py
@@ -843,10 +843,8 @@ class PyGetSetDefElement(PyccelAstNode):
     _attribute_nodes = ('_getter', '_setter', '_docstring')
     __slots__ = ('_python_name', '_getter', '_setter', '_docstring')
     def __init__(self, python_name, getter, setter, docstring):
-        if not isinstance(getter, PyFunctionDef):
-            raise TypeError("Getter should be a PyFunctionDef")
-        if not isinstance(setter, PyFunctionDef):
-            raise TypeError("Setter should be a PyFunctionDef")
+        assert isinstance(getter, PyFunctionDef)
+        assert isinstance(setter, PyFunctionDef) or setter is None
         self._python_name = python_name
         self._getter = getter
         self._setter = setter
diff --git a/pyccel/codegen/printing/cwrappercode.py b/pyccel/codegen/printing/cwrappercode.py
index 6d0252e0..d37e5b7f 100644
--- a/pyccel/codegen/printing/cwrappercode.py
+++ b/pyccel/codegen/printing/cwrappercode.py
@@ -285,7 +285,7 @@ class CWrapperCodePrinter(CCodePrinter):
                     "};\n")
             sig_methods = c.methods + (c.new_func,) + tuple(f for i in c.interfaces for f in i.functions) + \
                           tuple(i.interface_func for i in c.interfaces) + \
-                          tuple(getset for p in c.properties for getset in (p.getter, p.setter))
+                          tuple(getset for p in c.properties for getset in (p.getter, p.setter) if getset)
             function_signatures += '\n'+''.join(self.function_signature(f)+';\n' for f in sig_methods)
             macro_defs += f'#define {type_name} (*(PyTypeObject*){API_var.name}[{i}])\n'
 
@@ -393,7 +393,7 @@ class CWrapperCodePrinter(CCodePrinter):
 
         original_scope = expr.original_class.scope
         getters = tuple(p.getter for p in expr.properties)
-        setters = tuple(p.setter for p in expr.properties)
+        setters = tuple(p.setter for p in expr.properties if p.setter)
         print_methods = expr.methods + (expr.new_func,) + expr.interfaces + \
                          getters + setters
         functions = '\n'.join(self._print(f) for f in print_methods)
@@ -417,13 +417,13 @@ class CWrapperCodePrinter(CCodePrinter):
                                                     if f.docstring else '""'
             funcs[py_name] = (f.name, docstring)
 
-        property_definitions = ''.join(('{\n'
-                                        f'"{p.python_name}",\n'
-                                        f'(getter) {p.getter.name},\n'
-                                        f'(setter) {p.setter.name},\n'
-                                        f'{self._print(p.docstring)},\n'
-                                        'NULL\n'
-                                        '},\n') for p in expr.properties)
+        property_definitions = ''.join(''.join(('{\n',
+                                        f'"{p.python_name}",\n',
+                                        f'(getter) {p.getter.name},\n',
+                                        f'(setter) {p.setter.name},\n' if p.setter else '(setter) NULL,\n',
+                                        f'{self._print(p.docstring)},\n',
+                                        'NULL\n',
+                                        '},\n')) for p in expr.properties)
         property_definitions += '{ NULL }\n'
 
         method_def_funcs = ''.join(('{\n'
diff --git a/pyccel/codegen/printing/fcode.py b/pyccel/codegen/printing/fcode.py
index 5a2d3330..7ac8794f 100644
--- a/pyccel/codegen/printing/fcode.py
+++ b/pyccel/codegen/printing/fcode.py
@@ -3735,7 +3735,7 @@ class FCodePrinter(CodePrinter):
 
     def _print_BindCClassDef(self, expr):
         funcs = [expr.new_func, *expr.methods, *[f for i in expr.interfaces for f in i.functions],
-                 *[a.getter for a in expr.attributes], *[a.setter for a in expr.attributes]]
+                 *[a.getter for a in expr.attributes], *[a.setter for a in expr.attributes if a.setter]]
         sep = f'\n{self._print(SeparatorComment(40))}\n'
         return '', sep.join(self._print(f) for f in funcs)
 
diff --git a/pyccel/codegen/wrapper/c_to_python_wrapper.py b/pyccel/codegen/wrapper/c_to_python_wrapper.py
index 0236a7ca..c18f883c 100644
--- a/pyccel/codegen/wrapper/c_to_python_wrapper.py
+++ b/pyccel/codegen/wrapper/c_to_python_wrapper.py
@@ -1213,7 +1213,6 @@ class CToPythonWrapper(Wrapper):
         -------
         PyModule
             The module which can be called from Python.
-
         """
         pymod = self._wrap_Module(expr)
 
@@ -1242,7 +1241,8 @@ class CToPythonWrapper(Wrapper):
                     external_funcs.append(FunctionDef(f.name, f.arguments, f.results, [], is_header = True, scope = Scope()))
             for a in c.attributes:
                 for f in (a.getter, a.setter):
-                    external_funcs.append(FunctionDef(f.name, f.arguments, f.results, [], is_header = True, scope = Scope()))
+                    if f:
+                        external_funcs.append(FunctionDef(f.name, f.arguments, f.results, [], is_header = True, scope = Scope()))
         pymod.external_funcs = external_funcs
 
         return pymod
@@ -1397,12 +1397,19 @@ class CToPythonWrapper(Wrapper):
         original_c_args = expr.arguments
 
         # Get the arguments of the PyFunctionDef
-        if in_interface:
-            func_args = [FunctionDefArgument(a) for a in self._get_python_argument_variables(python_args)]
+        if 'property' in original_func.decorators:
+            func_args = [self.get_new_PyObject('self_obj', dtype = class_dtype),
+                         func_scope.get_temporary_variable(VoidType(), memory_handling='alias')]
+            self._python_object_map[python_args[0]] = func_args[0]
+            func_args = [FunctionDefArgument(a) for a in func_args]
             body = []
         else:
-            func_args, body = self._unpack_python_args(python_args, class_dtype)
-            func_args = [FunctionDefArgument(a) for a in func_args]
+            if in_interface:
+                func_args = [FunctionDefArgument(a) for a in self._get_python_argument_variables(python_args)]
+                body = []
+            else:
+                func_args, body = self._unpack_python_args(python_args, class_dtype)
+                func_args = [FunctionDefArgument(a) for a in func_args]
 
         # Get the code required to extract the C-compatible arguments from the Python arguments
         wrapped_args = [self._wrap(a) for a in python_args]
@@ -1466,7 +1473,14 @@ class CToPythonWrapper(Wrapper):
         self.scope.functions[func_name] = function
         self._python_object_map[expr] = function
 
-        return function
+        if 'property' in original_func.decorators:
+            python_name = original_func.scope.get_python_name(original_func.name)
+            docstring = LiteralString(
+                            '\n'.join(original_func.docstring.comments)
+                            if original_func.docstring else f"The attribute {python_name}")
+            return PyGetSetDefElement(python_name, function, None, docstring)
+        else:
+            return function
 
     def _wrap_FunctionDefArgument(self, expr):
         """
@@ -1812,7 +1826,10 @@ class CToPythonWrapper(Wrapper):
         if isinstance(getter_result.dtype, CustomDataType):
             arg_code.append(Allocate(getter_result, shape=(), status='unallocated'))
 
-        wrapped_var = expr.getter.original_function
+        if isinstance(expr.getter.original_function, DottedVariable):
+            wrapped_var = expr.getter.original_function
+        else:
+            wrapped_var = expr.getter.original_function.results[0].var
         res_wrapper.extend(self._incref_return_pointer(getter_args[0], getter_result, wrapped_var))
 
         getter_body = [*setup,
@@ -1829,51 +1846,57 @@ class CToPythonWrapper(Wrapper):
         # ----------------------------------------------------------------------------------
         #                        Create setter
         # ----------------------------------------------------------------------------------
-        self._error_exit_code = PyccelUnarySub(LiteralInteger(1, dtype=CNativeInt()))
-        setter_name = self.scope.get_new_name(f'{class_type.name}_{name}_setter')
-        setter_scope = self.scope.new_child_scope(setter_name)
-        self.scope = setter_scope
-
-        original_args = expr.setter.bind_c_arguments
-        f_wrapped_args = expr.setter.arguments
-
-        self_arg = original_args[0]
-        set_val_arg = original_args[1]
-        for a in f_wrapped_args:
-            self.scope.insert_symbol(a.var.name)
-        self.scope.insert_symbol(self_arg.original_function_argument_variable.name)
-        self.scope.insert_symbol(set_val_arg.original_function_argument_variable.name)
-
-        setter_args = [self.get_new_PyObject('self_obj', dtype = class_type),
-                       self.get_new_PyObject(f'{name}_obj'),
-                       setter_scope.get_temporary_variable(VoidType(), memory_handling='alias')]
-        setter_result = [FunctionDefResult(setter_scope.get_temporary_variable(CNativeInt()))]
-
-        self._python_object_map[self_arg] = setter_args[0]
-        self._python_object_map[set_val_arg] = setter_args[1]
-
-        if isinstance(wrapped_var.class_type, FixedSizeNumericType) or wrapped_var.is_alias:
-            wrapped_args = [self._wrap(a) for a in original_args]
-            arg_code = [l for a in wrapped_args for l in a['body']]
-            func_call_args = [ca for a in wrapped_args for ca in a['args']]
+        if expr.setter:
+            self._error_exit_code = PyccelUnarySub(LiteralInteger(1, dtype=CNativeInt()))
+            setter_name = self.scope.get_new_name(f'{class_type.name}_{name}_setter')
+            setter_scope = self.scope.new_child_scope(setter_name)
+            self.scope = setter_scope
+
+            original_args = expr.setter.bind_c_arguments
+            f_wrapped_args = expr.setter.arguments
+
+            self_arg = original_args[0]
+            set_val_arg = original_args[1]
+            for a in f_wrapped_args:
+                self.scope.insert_symbol(a.var.name)
+            self.scope.insert_symbol(self_arg.original_function_argument_variable.name)
+            self.scope.insert_symbol(set_val_arg.original_function_argument_variable.name)
+
+            setter_args = [self.get_new_PyObject('self_obj', dtype = class_type),
+                           self.get_new_PyObject(f'{name}_obj'),
+                           setter_scope.get_temporary_variable(VoidType(), memory_handling='alias')]
+            setter_result = [FunctionDefResult(setter_scope.get_temporary_variable(CNativeInt()))]
+
+            self._python_object_map[self_arg] = setter_args[0]
+            self._python_object_map[set_val_arg] = setter_args[1]
+
+            if isinstance(wrapped_var.class_type, FixedSizeNumericType) or wrapped_var.is_alias:
+                wrapped_args = [self._wrap(a) for a in original_args]
+                arg_code = [l for a in wrapped_args for l in a['body']]
+                func_call_args = [ca for a in wrapped_args for ca in a['args']]
+
+                setter_body = [*arg_code,
+                               expr.setter(*func_call_args),
+                               *self._save_referenced_objects(expr.setter, setter_args),
+                               Return((LiteralInteger(0, dtype=CNativeInt()),))]
+            else:
+                setter_body = [PyErr_SetString(PyAttributeError,
+                                            LiteralString("Can't reallocate memory via Python interface.")),
+                            Return([self._error_exit_code])]
+            self.exit_scope()
 
-            setter_body = [*arg_code,
-                           expr.setter(*func_call_args),
-                           *self._save_referenced_objects(expr.setter, setter_args),
-                           Return((LiteralInteger(0, dtype=CNativeInt()),))]
+            args = [FunctionDefArgument(a) for a in setter_args]
+            setter = PyFunctionDef(setter_name, args, setter_result, setter_body,
+                                    original_function = expr, scope = setter_scope)
         else:
-            setter_body = [PyErr_SetString(PyAttributeError,
-                                        LiteralString("Can't reallocate memory via Python interface.")),
-                        Return([self._error_exit_code])]
-        self.exit_scope()
+            setter = None
 
-        args = [FunctionDefArgument(a) for a in setter_args]
-        setter = PyFunctionDef(setter_name, args, setter_result, setter_body,
-                                original_function = expr, scope = setter_scope)
         self._error_exit_code = Nil()
 
-        return PyGetSetDefElement(expr.python_name, getter, setter,
-                                LiteralString(f"The attribute {expr.python_name}"))
+        docstring = LiteralString(
+                        '\n'.join(expr.docstring.comments)
+                        if expr.docstring else f"The attribute {expr.python_name}")
+        return PyGetSetDefElement(expr.python_name, getter, setter, docstring)
 
     def _wrap_ClassDef(self, expr):
         """
@@ -1917,6 +1940,8 @@ class CToPythonWrapper(Wrapper):
                 wrapped_class.add_new_method(self._get_class_destructor(f, orig_cls_dtype, wrapped_class.scope))
             elif python_name == '__init__':
                 wrapped_class.add_new_method(self._get_class_initialiser(f, orig_cls_dtype))
+            elif 'property' in f.decorators:
+                wrapped_class.add_property(self._wrap(f))
             else:
                 wrapped_class.add_new_method(self._wrap(f))
 
diff --git a/pyccel/codegen/wrapper/fortran_to_c_wrapper.py b/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
index e8c0a5e5..d1c9e6e4 100644
--- a/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
+++ b/pyccel/codegen/wrapper/fortran_to_c_wrapper.py
@@ -637,10 +637,16 @@ class FortranToCWrapper(Wrapper):
                     severity='warning',
                     symbol=expr)
 
+        properties_getters = [BindCClassProperty(expr.scope.get_python_name(m.original_function.name),
+                                                 m, None, expr.class_type, m.original_function.docstring)
+                                for m in methods if 'property' in m.original_function.decorators]
+        methods = [m for m in methods if m not in properties_getters
+                        if 'property' not in m.original_function.decorators]
+
         # Pseudo-self variable is useful for pre-defined attributes which are not DottedVariables
         pseudo_self = Variable(expr.class_type, 'self', cls_base = expr)
         properties = [self._wrap(v if isinstance(v, DottedVariable) else v.clone(v.name, new_class = DottedVariable, lhs=pseudo_self)) \
                         for v in expr.attributes if not v.is_private and not isinstance(v.class_type, TupleType)]
         return BindCClassDef(expr, new_func = new_method, methods = methods,
-                             interfaces = interfaces, attributes = properties,
+                             interfaces = interfaces, attributes = properties_getters + properties,
                              docstring = expr.docstring, class_type = expr.class_type)
diff --git a/pyccel/compilers/default_compilers.py b/pyccel/compilers/default_compilers.py
index 166085d2..8e17841c 100644
--- a/pyccel/compilers/default_compilers.py
+++ b/pyccel/compilers/default_compilers.py
@@ -289,6 +289,8 @@ pgfortran_info.update(python_info)
 nvc_info.update(python_info)
 nvfort_info.update(python_info)
 
+gcc_info['python']['flags'].append('-Wno-incompatible-pointer-types')
+
 available_compilers = {('GNU', 'c') : gcc_info,
                        ('GNU', 'fortran') : gfort_info,
                        ('intel', 'c') : icc_info,
diff --git a/pyccel/parser/semantic.py b/pyccel/parser/semantic.py
index 13f2e3d5..0a0590bd 100644
--- a/pyccel/parser/semantic.py
+++ b/pyccel/parser/semantic.py
@@ -4094,7 +4094,7 @@ class SemanticParser(BasicParser):
         if cls_name:
             bound_class = self.scope.find(cls_name, 'classes', raise_if_missing = True)
 
-        not_used = [d for d in decorators if d not in def_decorators.__all__]
+        not_used = [d for d in decorators if d not in (*def_decorators.__all__, 'property')]
         if len(not_used) >= 1:
             errors.report(UNUSED_DECORATORS, symbol=', '.join(not_used), severity='warning')
 


Test patch:
diff --git a/tests/epyccel/classes/classes_5.py b/tests/epyccel/classes/classes_5.py
new file mode 100644
index 00000000..b050e6d6
--- /dev/null
+++ b/tests/epyccel/classes/classes_5.py
@@ -0,0 +1,21 @@
+# pylint: disable=missing-class-docstring, missing-function-docstring, missing-module-docstring
+# coding: utf-8
+
+class Point(object):
+    def __init__(self, x : 'float[:]'):
+        self._X = 10
+        self._x = x
+
+    def __del__(self):
+        pass
+
+    def translate(self, a : 'float[:]'):
+        self._x[:]   =  self._x + a
+
+    @property
+    def x(self):
+        return self._x
+
+    @property
+    def X(self):
+        return self._X
diff --git a/tests/epyccel/test_docstrings.py b/tests/epyccel/test_docstrings.py
index 8e990d7c..251e1051 100644
--- a/tests/epyccel/test_docstrings.py
+++ b/tests/epyccel/test_docstrings.py
@@ -60,3 +60,27 @@ def test_class_docstring(language):
 
     python_doc, pyccel_doc = pad_docstrings(A.__doc__, B.__doc__)
     assert python_doc == pyccel_doc
+
+def test_property_docstring(language):
+    class MyA:
+        """
+        Class containing x
+        """
+        def __init__(self : 'MyA', x : int):
+            self._x = x
+
+        @property
+        def x(self):
+            """
+            This is a property it cannot be set.
+            """
+            return self._x
+
+    B = epyccel(MyA, language=language)
+
+    print(MyA.__doc__, B.__doc__)
+
+    python_doc, pyccel_doc = pad_docstrings(MyA.__doc__, B.__doc__)
+    assert python_doc == pyccel_doc
+    python_doc, pyccel_doc = pad_docstrings(MyA.x.__doc__, B.x.__doc__)
+    assert python_doc == pyccel_doc
diff --git a/tests/epyccel/test_epyccel_classes.py b/tests/epyccel/test_epyccel_classes.py
index 39098a24..ba383659 100644
--- a/tests/epyccel/test_epyccel_classes.py
+++ b/tests/epyccel/test_epyccel_classes.py
@@ -172,6 +172,28 @@ def test_classes_4(language):
 
     p1_py = mod.Point()
 
+def test_classes_5(language):
+    import classes.classes_5 as mod
+    modnew = epyccel(mod, language = language)
+
+    x1 = np.array([0.,0.,0.])
+    x2 = np.array([0.,0.,0.])
+    a = np.array([1.,1.,1.])
+
+    p1_py = mod.Point(x1)
+    p1_l  = modnew.Point(x2)
+
+    assert np.allclose(p1_py.x, p1_l.x, rtol=RTOL, atol=ATOL)
+
+    p1_py.translate(a)
+    p1_l.translate(a)
+
+    assert np.allclose(p1_py.x, p1_l.x, rtol=RTOL, atol=ATOL)
+    assert np.allclose(x1, x2, rtol=RTOL, atol=ATOL)
+
+    with pytest.raises(AttributeError):
+        p1_l.x = 4.0
+
 def test_classes_6(language):
     import classes.classes_6 as mod
     modnew = epyccel(mod, language = language)