Skip to content

Commit ccf5c55

Browse files
authored
Add support for _len__ (pyccel#2129)
Add support for `__len__` magic method. Fixes pyccel#2106 **Commit Summary** - Add a `_build_PythonLen` function to handle class variables as arguments - Group all magic methods together in `PyClassDef`
1 parent eb79fa0 commit ccf5c55

File tree

12 files changed

+133
-57
lines changed

12 files changed

+133
-57
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ All notable changes to this project will be documented in this file.
5656
- #738 : Add support for homogeneous tuples with scalar elements as arguments.
5757
- Add a warning about containers in lists.
5858
- #2016 : Add support for translating arithmetic magic methods.
59+
- #2106 : Add support for `__len__` magic method.
5960
- #1980 : Extend The C support for min and max to more than two variables
6061
- #2081 : Add support for multi operator expressions
6162
- #2061 : Add C support for string declarations.

docs/classes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ Pyccel supports a subset of magic methods that are listed here:
446446
- `__irshift__`
447447
- `__iand__`
448448
- `__ior__`
449+
- `__len__`
449450
450451
Additionally the following methods are supported in the translation but are lacking the wrapper support that would allow them to be called from Python code:
451452

pyccel/ast/builtins.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .datatypes import HomogeneousTupleType, InhomogeneousTupleType
2222
from .datatypes import HomogeneousListType, HomogeneousContainerType
2323
from .datatypes import FixedSizeNumericType, HomogeneousSetType, SymbolicType
24-
from .datatypes import DictType, VoidType, TypeAlias
24+
from .datatypes import DictType, VoidType, TypeAlias, StringType
2525
from .internals import PyccelFunction, Slice, PyccelArrayShapeElement, Iterable
2626
from .literals import LiteralInteger, LiteralFloat, LiteralComplex, Nil
2727
from .literals import Literal, LiteralImaginaryUnit, convert_to_literal
@@ -725,9 +725,14 @@ class PythonLen(PyccelFunction):
725725
def __new__(cls, arg):
726726
if isinstance(arg, LiteralString):
727727
return LiteralInteger(len(arg.python_value))
728+
elif isinstance(arg.class_type, StringType):
729+
return super().__new__(cls)
728730
else:
729731
return arg.shape[0]
730732

733+
def __init__(self, arg):
734+
super().__init__(arg)
735+
731736
#==============================================================================
732737
class PythonList(TypedAstNode):
733738
"""

pyccel/ast/cwrapper.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -718,8 +718,8 @@ class definition.
718718
wrapped.
719719
"""
720720
__slots__ = ('_original_class', '_struct_name', '_type_name', '_type_object',
721-
'_new_func', '_properties', '_number_magic_methods')
722-
_attribute_nodes = ClassDef._attribute_nodes + ('_number_magic_methods',)
721+
'_new_func', '_properties', '_magic_methods')
722+
_attribute_nodes = ClassDef._attribute_nodes + ('_magic_methods',)
723723

724724
def __init__(self, original_class, struct_name, type_name, scope, **kwargs):
725725
self._original_class = original_class
@@ -728,7 +728,7 @@ def __init__(self, original_class, struct_name, type_name, scope, **kwargs):
728728
self._type_object = Variable(PyccelPyClassType(), type_name)
729729
self._new_func = None
730730
self._properties = ()
731-
self._number_magic_methods = ()
731+
self._magic_methods = ()
732732
variables = [Variable(VoidType(), 'instance', memory_handling='alias'),
733733
Variable(PyccelPyObject(), 'referenced_objects', memory_handling='alias'),
734734
Variable(PythonNativeBool(), 'is_alias')]
@@ -821,12 +821,11 @@ def properties(self):
821821
"""
822822
return self._properties
823823

824-
def add_new_magic_number_method(self, method):
824+
def add_new_magic_method(self, method):
825825
"""
826-
Add a new magic number method to the current class.
826+
Add a new magic method to the current class.
827827
828-
Add a new magic method to the current ClassDef describing
829-
a number method.
828+
Add a new magic method to the current ClassDef.
830829
831830
Parameters
832831
----------
@@ -837,16 +836,16 @@ def add_new_magic_number_method(self, method):
837836
if not isinstance(method, PyFunctionDef):
838837
raise TypeError("Method must be FunctionDef")
839838
method.set_current_user_node(self)
840-
self._number_magic_methods += (method,)
839+
self._magic_methods += (method,)
841840

842841
@property
843-
def number_magic_methods(self):
842+
def magic_methods(self):
844843
"""
845-
Get the magic methods describing number methods.
844+
Get the magic methods describing methods.
846845
847-
Get the magic methods describing number methods such as __add__.
846+
Get the magic methods describing methods such as __add__.
848847
"""
849-
return self._number_magic_methods
848+
return self._magic_methods
850849

851850
#-------------------------------------------------------------------
852851

pyccel/ast/itertoolsext.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"""
66
This module represent a call to the itertools functions for code generation.
77
"""
8-
from .builtins import PythonLen, PythonRange
98
from .core import PyccelFunctionDef, Module
109
from .internals import Iterable
1110

pyccel/codegen/printing/cwrappercode.py

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ def _print_ModuleHeader(self, expr):
286286
sig_methods = c.methods + (c.new_func,) + tuple(f for i in c.interfaces for f in i.functions) + \
287287
tuple(i.interface_func for i in c.interfaces) + \
288288
tuple(getset for p in c.properties for getset in (p.getter, p.setter) if getset) + \
289-
c.number_magic_methods
289+
c.magic_methods
290290
function_signatures += '\n'+''.join(self.function_signature(f)+';\n' for f in sig_methods)
291291
macro_defs += f'#define {type_name} (*(PyTypeObject*){API_var.name}[{i}])\n'
292292

@@ -396,7 +396,7 @@ def _print_PyClassDef(self, expr):
396396
getters = tuple(p.getter for p in expr.properties)
397397
setters = tuple(p.setter for p in expr.properties if p.setter)
398398
print_methods = expr.methods + (expr.new_func,) + expr.interfaces + \
399-
expr.number_magic_methods + getters + setters
399+
expr.magic_methods + getters + setters
400400
functions = '\n'.join(self._print(f) for f in print_methods)
401401
init_string = ''
402402
del_string = ''
@@ -435,44 +435,58 @@ def _print_PyClassDef(self, expr):
435435
'},\n')
436436
for name, (wrapper_name, doc_string) in funcs.items())
437437

438+
magic_methods = {self.get_python_name(original_scope, f.original_function): f for f in expr.magic_methods}
439+
438440
number_magic_method_name = self.scope.get_new_name(f'{expr.name}_number_methods')
439-
number_magic_methods = {self.get_python_name(original_scope, f.original_function): f for f in expr.number_magic_methods}
440441

441442
number_magic_methods_def = f"static PyNumberMethods {number_magic_method_name} = {{\n"
442-
if '__add__' in number_magic_methods:
443-
number_magic_methods_def += f" .nb_add = (binaryfunc){number_magic_methods['__add__'].name},\n"
444-
if '__sub__' in number_magic_methods:
445-
number_magic_methods_def += f" .nb_subtract = (binaryfunc){number_magic_methods['__sub__'].name},\n"
446-
if '__mul__' in number_magic_methods:
447-
number_magic_methods_def += f" .nb_multiply = (binaryfunc){number_magic_methods['__mul__'].name},\n"
448-
if '__truediv__' in number_magic_methods:
449-
number_magic_methods_def += f" .nb_true_divide = (binaryfunc){number_magic_methods['__truediv__'].name},\n"
450-
if '__lshift__' in number_magic_methods:
451-
number_magic_methods_def += f" .nb_lshift = (binaryfunc){number_magic_methods['__lshift__'].name},\n"
452-
if '__rshift__' in number_magic_methods:
453-
number_magic_methods_def += f" .nb_rshift = (binaryfunc){number_magic_methods['__rshift__'].name},\n"
454-
if '__and__' in number_magic_methods:
455-
number_magic_methods_def += f" .nb_and = (binaryfunc){number_magic_methods['__and__'].name},\n"
456-
if '__or__' in number_magic_methods:
457-
number_magic_methods_def += f" .nb_or = (binaryfunc){number_magic_methods['__or__'].name},\n"
458-
if '__iadd__' in number_magic_methods:
459-
number_magic_methods_def += f" .nb_inplace_add = (binaryfunc){number_magic_methods['__iadd__'].name},\n"
460-
if '__isub__' in number_magic_methods:
461-
number_magic_methods_def += f" .nb_inplace_subtract = (binaryfunc){number_magic_methods['__isub__'].name},\n"
462-
if '__imul__' in number_magic_methods:
463-
number_magic_methods_def += f" .nb_inplace_multiply = (binaryfunc){number_magic_methods['__imul__'].name},\n"
464-
if '__itruediv__' in number_magic_methods:
465-
number_magic_methods_def += f" .nb_inplace_true_divide = (binaryfunc){number_magic_methods['__itruediv__'].name},\n"
466-
if '__ilshift__' in number_magic_methods:
467-
number_magic_methods_def += f" .nb_inplace_lshift = (binaryfunc){number_magic_methods['__ilshift__'].name},\n"
468-
if '__irshift__' in number_magic_methods:
469-
number_magic_methods_def += f" .nb_inplace_rshift = (binaryfunc){number_magic_methods['__irshift__'].name},\n"
470-
if '__iand__' in number_magic_methods:
471-
number_magic_methods_def += f" .nb_inplace_and = (binaryfunc){number_magic_methods['__iand__'].name},\n"
472-
if '__ior__' in number_magic_methods:
473-
number_magic_methods_def += f" .nb_inplace_or = (binaryfunc){number_magic_methods['__ior__'].name},\n"
443+
if '__add__' in magic_methods:
444+
number_magic_methods_def += f" .nb_add = (binaryfunc){magic_methods['__add__'].name},\n"
445+
if '__sub__' in magic_methods:
446+
number_magic_methods_def += f" .nb_subtract = (binaryfunc){magic_methods['__sub__'].name},\n"
447+
if '__mul__' in magic_methods:
448+
number_magic_methods_def += f" .nb_multiply = (binaryfunc){magic_methods['__mul__'].name},\n"
449+
if '__truediv__' in magic_methods:
450+
number_magic_methods_def += f" .nb_true_divide = (binaryfunc){magic_methods['__truediv__'].name},\n"
451+
if '__lshift__' in magic_methods:
452+
number_magic_methods_def += f" .nb_lshift = (binaryfunc){magic_methods['__lshift__'].name},\n"
453+
if '__rshift__' in magic_methods:
454+
number_magic_methods_def += f" .nb_rshift = (binaryfunc){magic_methods['__rshift__'].name},\n"
455+
if '__and__' in magic_methods:
456+
number_magic_methods_def += f" .nb_and = (binaryfunc){magic_methods['__and__'].name},\n"
457+
if '__or__' in magic_methods:
458+
number_magic_methods_def += f" .nb_or = (binaryfunc){magic_methods['__or__'].name},\n"
459+
if '__iadd__' in magic_methods:
460+
number_magic_methods_def += f" .nb_inplace_add = (binaryfunc){magic_methods['__iadd__'].name},\n"
461+
if '__isub__' in magic_methods:
462+
number_magic_methods_def += f" .nb_inplace_subtract = (binaryfunc){magic_methods['__isub__'].name},\n"
463+
if '__imul__' in magic_methods:
464+
number_magic_methods_def += f" .nb_inplace_multiply = (binaryfunc){magic_methods['__imul__'].name},\n"
465+
if '__itruediv__' in magic_methods:
466+
number_magic_methods_def += f" .nb_inplace_true_divide = (binaryfunc){magic_methods['__itruediv__'].name},\n"
467+
if '__ilshift__' in magic_methods:
468+
number_magic_methods_def += f" .nb_inplace_lshift = (binaryfunc){magic_methods['__ilshift__'].name},\n"
469+
if '__irshift__' in magic_methods:
470+
number_magic_methods_def += f" .nb_inplace_rshift = (binaryfunc){magic_methods['__irshift__'].name},\n"
471+
if '__iand__' in magic_methods:
472+
number_magic_methods_def += f" .nb_inplace_and = (binaryfunc){magic_methods['__iand__'].name},\n"
473+
if '__ior__' in magic_methods:
474+
number_magic_methods_def += f" .nb_inplace_or = (binaryfunc){magic_methods['__ior__'].name},\n"
474475
number_magic_methods_def += '};\n'
475476

477+
seq_magic_method_name = self.scope.get_new_name(f'{expr.name}_sequence_methods')
478+
479+
seq_magic_methods_def = f"static PySequenceMethods {seq_magic_method_name} = {{\n"
480+
if '__len__' in magic_methods:
481+
seq_magic_methods_def += f" .sq_length = {magic_methods['__len__'].name},\n"
482+
seq_magic_methods_def += '};\n'
483+
484+
map_magic_method_name = self.scope.get_new_name(f'{expr.name}_mapping_methods')
485+
map_magic_methods_def = f"static PyMappingMethods {map_magic_method_name} = {{\n"
486+
if '__len__' in magic_methods:
487+
map_magic_methods_def += f" .mp_length = {magic_methods['__len__'].name},\n"
488+
map_magic_methods_def += '};\n'
489+
476490
method_def_name = self.scope.get_new_name(f'{expr.name}_methods')
477491
method_def = (f'static PyMethodDef {method_def_name}[] = {{\n'
478492
f'{method_def_funcs}'
@@ -488,6 +502,8 @@ def _print_PyClassDef(self, expr):
488502
" PyVarObject_HEAD_INIT(NULL, 0)\n"
489503
f" .tp_name = \"{self._module_name}.{name}\",\n"
490504
f" .tp_as_number = &{number_magic_method_name},\n"
505+
f" .tp_as_sequence = &{seq_magic_method_name},\n"
506+
f" .tp_as_mapping = &{map_magic_method_name},\n"
491507
f" .tp_doc = PyDoc_STR({docstring}),\n"
492508
f" .tp_basicsize = sizeof(struct {struct_name}),\n"
493509
" .tp_itemsize = 0,\n"
@@ -498,7 +514,8 @@ def _print_PyClassDef(self, expr):
498514
f" .tp_getset = {property_def_name},\n"
499515
"};\n")
500516

501-
return '\n'.join((method_def, number_magic_methods_def, property_def, type_code, functions))
517+
return '\n'.join((method_def, number_magic_methods_def, seq_magic_methods_def,
518+
map_magic_methods_def, property_def, type_code, functions))
502519

503520
def _print_PyModInitFunc(self, expr):
504521
decs = ''.join(self._print(d) for d in expr.declarations)

pyccel/codegen/wrapper/c_to_python_wrapper.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,7 +1498,7 @@ def _wrap_FunctionDef(self, expr):
14981498
func_args = [FunctionDefArgument(a) for a in func_args]
14991499
body = []
15001500
else:
1501-
if in_interface or original_func_name in magic_binary_funcs:
1501+
if in_interface or original_func_name in magic_binary_funcs or original_func_name == '__len__':
15021502
func_args = [FunctionDefArgument(a) for a in self._get_python_argument_variables(python_args)]
15031503
body = []
15041504
else:
@@ -1549,7 +1549,12 @@ def _wrap_FunctionDef(self, expr):
15491549
body.append(If( IfSection(PyccelIsNot(v, Nil()), [Deallocate(v)]) ))
15501550
else:
15511551
body.append(Deallocate(v))
1552-
body.extend(wrapped_results['body'])
1552+
1553+
if original_func_name == '__len__':
1554+
self.scope.remove_variable(python_result_variable)
1555+
python_result_variable = c_results[0]
1556+
else:
1557+
body.extend(wrapped_results['body'])
15531558
body.extend(ai for arg in wrapped_args for ai in arg['clean_up'])
15541559

15551560
# Pack the Python compatible results of the function into one argument.
@@ -2040,8 +2045,8 @@ def _wrap_ClassDef(self, expr):
20402045
wrapped_class.add_new_method(self._get_class_destructor(f, orig_cls_dtype, wrapped_class.scope))
20412046
elif python_name == '__init__':
20422047
wrapped_class.add_new_method(self._get_class_initialiser(f, orig_cls_dtype))
2043-
elif python_name in magic_binary_funcs:
2044-
wrapped_class.add_new_magic_number_method(self._wrap(f))
2048+
elif python_name in (*magic_binary_funcs, '__len__'):
2049+
wrapped_class.add_new_magic_method(self._wrap(f))
20452050
elif 'property' in f.decorators:
20462051
wrapped_class.add_property(self._wrap(f))
20472052
else:

pyccel/parser/semantic.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
from pyccel.ast.builtins import PythonComplex, PythonDict, PythonDictFunction, PythonListFunction
3131
from pyccel.ast.builtins import builtin_functions_dict, PythonImag, PythonReal
3232
from pyccel.ast.builtins import PythonList, PythonConjugate , PythonSet, VariableIterator
33-
from pyccel.ast.builtins import (PythonRange, PythonZip, PythonEnumerate,
34-
PythonTuple, Lambda, PythonMap)
33+
from pyccel.ast.builtins import PythonRange, PythonZip, PythonEnumerate, PythonTuple
34+
from pyccel.ast.builtins import Lambda, PythonMap
3535

3636
from pyccel.ast.builtin_methods.list_methods import ListMethod, ListAppend
3737
from pyccel.ast.builtin_methods.set_methods import SetAdd, SetUnion, SetCopy, SetIntersectionUpdate
@@ -5508,3 +5508,43 @@ def _build_SetIntersection(self, expr, function_call_args):
55085508
self._additional_exprs[-1].extend(body)
55095509
return lhs
55105510

5511+
def _build_PythonLen(self, expr, function_call_args):
5512+
"""
5513+
Method to visit a PythonLen node.
5514+
5515+
The purpose of this `_build` method is to construct a node representing
5516+
a call to the PythonLen function. This function returns the first element
5517+
of the shape of a variable, or a call to a method which calculates the
5518+
length (e.g. the `__len__` function).
5519+
5520+
Parameters
5521+
----------
5522+
expr : DottedName
5523+
The syntactic node that represent the call to `len()`.
5524+
5525+
function_call_args : iterable[FunctionCallArgument]
5526+
The semantic arguments passed to the function.
5527+
5528+
Returns
5529+
-------
5530+
TypedAstNode
5531+
The node representing an object which allows the result of the
5532+
PythonLen function to be obtained.
5533+
"""
5534+
arg = function_call_args[0].value
5535+
class_type = arg.class_type
5536+
if isinstance(arg, LiteralString):
5537+
return LiteralInteger(len(arg.python_value))
5538+
elif isinstance(arg.class_type, CustomDataType):
5539+
class_base = self.scope.find(str(class_type), 'classes') or get_cls_base(class_type)
5540+
magic_method = class_base.get_method('__len__', False)
5541+
if magic_method:
5542+
return self._handle_function(expr, magic_method, function_call_args)
5543+
else:
5544+
raise errors.report(f"__len__ not implemented for type {class_type}",
5545+
severity='fatal', symbol=expr)
5546+
elif arg.rank > 0:
5547+
return arg.shape[0]
5548+
else:
5549+
raise errors.report(f"__len__ not implemented for type {class_type}",
5550+
severity='fatal', symbol=expr)

tests/epyccel/classes/class_magic.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,6 @@ def __iand__(self, other : int):
5959
def __ior__(self, other : int):
6060
self.x |= other
6161
return self
62+
63+
def __len__(self):
64+
return 1

tests/epyccel/test_epyccel_classes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,4 @@ def test_class_magic(language):
444444

445445
assert a_py.x == a_l.x
446446

447+
assert len(a_py) == len(a_l)

0 commit comments

Comments
 (0)