Repository: BlueBrain/MorphIO (language: python)

Issue:
Inexpensive number of points property for Section and Morphology
To check for the number of points of a section in python, we usually do:
```python
n_points = len(section.points)
```
However, that requires creating the points dataset on the python side so that its length is determined.

I would like to introduce an option where the number of points can be determined inexpensively using a property or the `__len__` dunder.
Possible variations:
```python
n_points = section.n_points
n_points = section.number_of_points
n_points = len(section)
```
And the same would be applied to the entire Morphology so that we don't have to copy all its points to find their number.


Discussion / hints:

Fix patch:
diff --git a/binds/python/bind_immutable.cpp b/binds/python/bind_immutable.cpp
index e0f5f4dc5..7b9933ea8 100644
--- a/binds/python/bind_immutable.cpp
+++ b/binds/python/bind_immutable.cpp
@@ -87,6 +87,12 @@ void bind_immutable_module(py::module& m) {
             "Returns a list with all points from all sections (soma points are not included)\n"
             "Note: points belonging to the n'th section are located at indices:\n"
             "[Morphology.sectionOffsets(n), Morphology.sectionOffsets(n+1)[")
+
+        .def_property_readonly(
+            "n_points",
+            [](const morphio::Morphology& obj) { return obj.points().size(); },
+            "Returns the number of points from all sections (soma points are not included)")
+
         .def_property_readonly(
             "diameters",
             [](const morphio::Morphology& morpho) {
@@ -264,6 +270,12 @@ void bind_immutable_module(py::module& m) {
             "points",
             [](morphio::Section* section) { return span_array_to_ndarray(section->points()); },
             "Returns list of section's point coordinates")
+
+        .def_property_readonly(
+            "n_points",
+            [](const morphio::Section& section) { return section.points().size(); },
+            "Returns the number of points in section")
+
         .def_property_readonly(
             "diameters",
             [](morphio::Section* section) { return span_to_ndarray(section->diameters()); },
diff --git a/binds/python/bind_vasculature.cpp b/binds/python/bind_vasculature.cpp
index 365ec91b8..53aab6dd5 100644
--- a/binds/python/bind_vasculature.cpp
+++ b/binds/python/bind_vasculature.cpp
@@ -59,6 +59,12 @@ void bind_vasculature(py::module& m) {
                                  morpho->points().data());
             },
             "Returns a list with all points from all sections")
+
+        .def_property_readonly(
+            "n_points",
+            [](const morphio::vasculature::Vasculature& obj) { return obj.points().size(); },
+            "Returns the number of points from all sections")
+
         .def_property_readonly(
             "diameters",
             [](morphio::vasculature::Vasculature* morpho) {
@@ -119,6 +125,12 @@ void bind_vasculature(py::module& m) {
                 return span_array_to_ndarray(section->points());
             },
             "Returns list of section's point coordinates")
+
+        .def_property_readonly(
+            "n_points",
+            [](const morphio::vasculature::Section& section) { return section.points().size(); },
+            "Returns the number of point in section")
+
         .def_property_readonly(
             "diameters",
             [](morphio::vasculature::Section* section) {


Test patch:
diff --git a/tests/test_0_API.py b/tests/test_0_API.py
index 5bbc35b55..8001e1305 100644
--- a/tests/test_0_API.py
+++ b/tests/test_0_API.py
@@ -29,14 +29,14 @@ def test_mut_immut_have_same_methods():
     def methods(cls):
         return set(method for method in dir(cls) if not method[:2] == '__')
 
-    only_in_immut = {'section_types', 'diameters', 'perimeters', 'points', 'section_offsets',
+    only_in_immut = {'section_types', 'diameters', 'perimeters', 'points', 'n_points', 'section_offsets',
                      'as_mutable'}
     only_in_mut = {'remove_unifurcations', 'write', 'append_root_section', 'delete_section', 'build_read_only',
                    'as_immutable'}
     assert (methods(morphio.Morphology) - only_in_immut ==
                  methods(morphio.mut.Morphology) - only_in_mut)
 
-    assert (methods(morphio.Section) ==
+    assert (methods(morphio.Section) - {'n_points'} ==
                  methods(morphio.mut.Section) - {'append_section'})
 
     assert (methods(morphio.Soma) ==
diff --git a/tests/test_10_vasculature.py b/tests/test_10_vasculature.py
index fabcb5737..838304601 100644
--- a/tests/test_10_vasculature.py
+++ b/tests/test_10_vasculature.py
@@ -41,10 +41,12 @@ def test_components_vasculature():
 
     assert len(morphology.sections) == 3080
     assert len(morphology.points) == 55807
+    assert morphology.n_points == len(morphology.points)
     assert len(morphology.diameters) == 55807
     assert_array_almost_equal(morphology.diameters[-5:],
                               np.array([0.78039801, 0.78039801, 0.78039801, 2.11725187, 2.11725187]))
     assert len(morphology.section_types) == 3080
+    assert morphology.section(0).n_points == len(morphology.section(0).points)
     assert len(morphology.section(0).predecessors) == 0
     assert len(morphology.section(0).successors) == 2
 
diff --git a/tests/test_4_immut.py b/tests/test_4_immut.py
index 6cb6f01a5..2633bf606 100644
--- a/tests/test_4_immut.py
+++ b/tests/test_4_immut.py
@@ -20,6 +20,12 @@
 })
 
 
+def test_components():
+    for cell in CELLS.values():
+        assert cell.n_points == len(cell.points)
+        assert cell.section(0).n_points == len(cell.section(0).points)
+
+
 def test_is_root():
     for _, cell in CELLS.items():
         assert all(section.is_root for section in cell.root_sections)