Repository: mie-lab/trackintel (python)

Issue:
Add documentation to accessor methods.
The accessor functions with the pattern `as_triplegs.xyz` do not have a complete documentation, but rather refer to the wrapped functions in a different modules. This is inconvenient for working in an editor, as one cannot look up the functionality of the various parameters in the docstring. Instead one has to look up the referenced docstring seperatly.

Two ways to change that would be:
1. Just copy the docstring over at every change. 
2. Use a decorator to copy the docstring directly.

The first way is rather error-prone as one has to remember to change both docstrings at any change. Here it would be good to write a test that checks the equality of the `__doc__` attribute of the functions. The second way can be unclear in the documentation as it replaces silently the existing docstring. Also other problem arises in terms of circular imports etc. .

Pull request description:
ENH: Copy docstring for accessor methods.
closes #209 

Added the function `copy_docstring` decorator that uses `functools.update_wrapper` to copy the docstring from the method to the accessor.
To copy the docstring from another function, the other function must be defined at creation time, so I added many imports for all functions. It needs to be verified that it works on other machines before we merge it, because I'm not sure if relative or absolute imports are used on my machine (and if that would matter).

Patch:
diff --git a/docs/modules/model.rst b/docs/modules/model.rst
index 9638bff..0b779b9 100644
--- a/docs/modules/model.rst
+++ b/docs/modules/model.rst
@@ -57,24 +57,45 @@ Available Accessors
 
 The following accessors are available within *trackintel*.
 
+UsersAccessor
+-------------
+
 .. autoclass:: trackintel.model.users.UsersAccessor
 	:members:
 
+PositionfixesAccessor
+---------------------
+
 .. autoclass:: trackintel.model.positionfixes.PositionfixesAccessor
 	:members:
 
+StaypointsAccessor
+------------------
+
 .. autoclass:: trackintel.model.staypoints.StaypointsAccessor
 	:members:
 
+TriplegsAccessor
+----------------
+
 .. autoclass:: trackintel.model.triplegs.TriplegsAccessor
 	:members:
 
+LocationsAccessor
+-----------------
+
 .. autoclass:: trackintel.model.locations.LocationsAccessor
 	:members:
 
+TripsAccessor
+-------------
+
 .. autoclass:: trackintel.model.trips.TripsAccessor
 	:members:
 
+ToursAccessor
+-------------
+
 .. autoclass:: trackintel.model.tours.ToursAccessor
 	:members:
 
@@ -87,7 +108,7 @@ Data Model (SQL)
 
 For a general description of the data model, please refer to the 
 :doc:`/modules/model`. You can download the 
-complete SQL script `here <https://github.com/mie-lab/trackintel/blob/master/sql/create_tables_pg.sql>`_ 
+complete SQL script `here <https://github.com/mie-lab/trackintel/blob/master/sql/create_tables_pg.sql>`__
 in case you want to quickly set up a database. Also take a look at the `example on github 
 <https://github.com/mie-lab/trackintel/blob/master/examples/setup_example_database.py>`_.
 
diff --git a/trackintel/model/locations.py b/trackintel/model/locations.py
index a62969e..74daac8 100644
--- a/trackintel/model/locations.py
+++ b/trackintel/model/locations.py
@@ -1,9 +1,11 @@
 import pandas as pd
-
 import trackintel as ti
-import trackintel.preprocessing.filter
-import trackintel.visualization.locations
-import trackintel.visualization.staypoints
+import trackintel.io
+from trackintel.io.file import write_locations_csv
+from trackintel.io.postgis import write_locations_postgis
+from trackintel.model.util import copy_docstring
+from trackintel.preprocessing.filter import spatial_filter
+from trackintel.visualization.locations import plot_locations
 
 
 @pd.api.extensions.register_dataframe_accessor("as_locations")
@@ -48,6 +50,7 @@ class LocationsAccessor(object):
             # One for extend and one for the center
             raise AttributeError("The center geometry must be a Point (only first checked).")
 
+    @copy_docstring(plot_locations)
     def plot(self, *args, **kwargs):
         """
         Plot this collection of locations.
@@ -56,6 +59,7 @@ class LocationsAccessor(object):
         """
         ti.visualization.locations.plot_locations(self._obj, *args, **kwargs)
 
+    @copy_docstring(write_locations_csv)
     def to_csv(self, filename, *args, **kwargs):
         """
         Store this collection of locations as a CSV file.
@@ -64,6 +68,7 @@ class LocationsAccessor(object):
         """
         ti.io.file.write_locations_csv(self._obj, filename, *args, **kwargs)
 
+    @copy_docstring(write_locations_postgis)
     def to_postgis(
         self, name, con, schema=None, if_exists="fail", index=True, index_label=None, chunksize=None, dtype=None
     ):
@@ -76,6 +81,7 @@ class LocationsAccessor(object):
             self._obj, name, con, schema, if_exists, index, index_label, chunksize, dtype
         )
 
+    @copy_docstring(spatial_filter)
     def spatial_filter(self, *args, **kwargs):
         """
         Filter locations with a geo extent.
diff --git a/trackintel/model/positionfixes.py b/trackintel/model/positionfixes.py
index eb1e098..db048c8 100644
--- a/trackintel/model/positionfixes.py
+++ b/trackintel/model/positionfixes.py
@@ -1,6 +1,11 @@
 import pandas as pd
-
 import trackintel as ti
+from trackintel.geogr.distances import calculate_distance_matrix
+from trackintel.io.file import write_positionfixes_csv
+from trackintel.io.postgis import write_positionfixes_postgis
+from trackintel.model.util import copy_docstring
+from trackintel.preprocessing.positionfixes import generate_staypoints, generate_triplegs
+from trackintel.visualization.positionfixes import plot_positionfixes
 
 
 @pd.api.extensions.register_dataframe_accessor("as_positionfixes")
@@ -68,6 +73,7 @@ class PositionfixesAccessor(object):
         lon = self._obj.geometry.x
         return (float(lon.mean()), float(lat.mean()))
 
+    @copy_docstring(generate_staypoints)
     def generate_staypoints(self, *args, **kwargs):
         """
         Generate staypoints from this collection of positionfixes.
@@ -76,6 +82,7 @@ class PositionfixesAccessor(object):
         """
         return ti.preprocessing.positionfixes.generate_staypoints(self._obj, *args, **kwargs)
 
+    @copy_docstring(generate_triplegs)
     def generate_triplegs(self, stps_input=None, *args, **kwargs):
         """
         Generate triplegs from this collection of positionfixes.
@@ -84,6 +91,7 @@ class PositionfixesAccessor(object):
         """
         return ti.preprocessing.positionfixes.generate_triplegs(self._obj, stps_input, *args, **kwargs)
 
+    @copy_docstring(plot_positionfixes)
     def plot(self, *args, **kwargs):
         """
         Plot this collection of positionfixes.
@@ -92,6 +100,7 @@ class PositionfixesAccessor(object):
         """
         ti.visualization.positionfixes.plot_positionfixes(self._obj, *args, **kwargs)
 
+    @copy_docstring(write_positionfixes_csv)
     def to_csv(self, filename, *args, **kwargs):
         """
         Store this collection of trackpoints as a CSV file.
@@ -100,6 +109,7 @@ class PositionfixesAccessor(object):
         """
         ti.io.file.write_positionfixes_csv(self._obj, filename, *args, **kwargs)
 
+    @copy_docstring(write_positionfixes_postgis)
     def to_postgis(
         self, name, con, schema=None, if_exists="fail", index=True, index_label=None, chunksize=None, dtype=None
     ):
@@ -112,6 +122,7 @@ class PositionfixesAccessor(object):
             self._obj, name, con, schema, if_exists, index, index_label, chunksize, dtype
         )
 
+    @copy_docstring(calculate_distance_matrix)
     def calculate_distance_matrix(self, *args, **kwargs):
         """
         Calculate pair-wise distance among positionfixes or to other positionfixes.
diff --git a/trackintel/model/staypoints.py b/trackintel/model/staypoints.py
index 272636d..1a7ebb1 100644
--- a/trackintel/model/staypoints.py
+++ b/trackintel/model/staypoints.py
@@ -1,6 +1,13 @@
 import pandas as pd
-
 import trackintel as ti
+from trackintel.analysis.labelling import create_activity_flag
+from trackintel.analysis.tracking_quality import temporal_tracking_quality
+from trackintel.io.file import write_staypoints_csv
+from trackintel.io.postgis import write_staypoints_postgis
+from trackintel.model.util import copy_docstring
+from trackintel.preprocessing.filter import spatial_filter
+from trackintel.preprocessing.staypoints import generate_locations
+from trackintel.visualization.staypoints import plot_staypoints
 
 
 @pd.api.extensions.register_dataframe_accessor("as_staypoints")
@@ -71,6 +78,7 @@ class StaypointsAccessor(object):
         lon = self._obj.geometry.x
         return (float(lon.mean()), float(lat.mean()))
 
+    @copy_docstring(generate_locations)
     def generate_locations(self, *args, **kwargs):
         """
         Generate locations from this collection of staypoints.
@@ -79,6 +87,7 @@ class StaypointsAccessor(object):
         """
         return ti.preprocessing.staypoints.generate_locations(self._obj, *args, **kwargs)
 
+    @copy_docstring(create_activity_flag)
     def create_activity_flag(self, *args, **kwargs):
         """
         Set a flag if a staypoint is also an activity.
@@ -87,6 +96,7 @@ class StaypointsAccessor(object):
         """
         return ti.analysis.labelling.create_activity_flag(self._obj, *args, **kwargs)
 
+    @copy_docstring(spatial_filter)
     def spatial_filter(self, *args, **kwargs):
         """
         Filter staypoints with a geo extent.
@@ -95,6 +105,7 @@ class StaypointsAccessor(object):
         """
         return ti.preprocessing.filter.spatial_filter(self._obj, *args, **kwargs)
 
+    @copy_docstring(plot_staypoints)
     def plot(self, *args, **kwargs):
         """
         Plot this collection of staypoints.
@@ -103,6 +114,7 @@ class StaypointsAccessor(object):
         """
         ti.visualization.staypoints.plot_staypoints(self._obj, *args, **kwargs)
 
+    @copy_docstring(write_staypoints_csv)
     def to_csv(self, filename, *args, **kwargs):
         """
         Store this collection of staypoints as a CSV file.
@@ -111,6 +123,7 @@ class StaypointsAccessor(object):
         """
         ti.io.file.write_staypoints_csv(self._obj, filename, *args, **kwargs)
 
+    @copy_docstring(write_staypoints_postgis)
     def to_postgis(
         self, name, con, schema=None, if_exists="fail", index=True, index_label=None, chunksize=None, dtype=None
     ):
@@ -123,6 +136,7 @@ class StaypointsAccessor(object):
             self._obj, name, con, schema, if_exists, index, index_label, chunksize, dtype
         )
 
+    @copy_docstring(temporal_tracking_quality)
     def temporal_tracking_quality(self, *args, **kwargs):
         """
         Calculate per-user temporal tracking quality (temporal coverage).
diff --git a/trackintel/model/triplegs.py b/trackintel/model/triplegs.py
index 1d4ce62..dbfb855 100644
--- a/trackintel/model/triplegs.py
+++ b/trackintel/model/triplegs.py
@@ -1,8 +1,15 @@
 import pandas as pd
-
 import trackintel as ti
-import trackintel.preprocessing.filter
-import trackintel.visualization.triplegs
+from trackintel.analysis.labelling import predict_transport_mode
+from trackintel.analysis.modal_split import calculate_modal_split
+from trackintel.analysis.tracking_quality import temporal_tracking_quality
+from trackintel.geogr.distances import calculate_distance_matrix
+from trackintel.io.file import write_triplegs_csv
+from trackintel.io.postgis import write_triplegs_postgis
+from trackintel.model.util import copy_docstring
+from trackintel.preprocessing.filter import spatial_filter
+from trackintel.preprocessing.triplegs import generate_trips
+from trackintel.visualization.triplegs import plot_triplegs
 
 
 @pd.api.extensions.register_dataframe_accessor("as_triplegs")
@@ -63,6 +70,7 @@ class TriplegsAccessor(object):
             obj["finished_at"]
         ), "dtype of finished_at is {} but has to be datetime64 and timezone aware".format(obj["finished_at"].dtype)
 
+    @copy_docstring(plot_triplegs)
     def plot(self, *args, **kwargs):
         """
         Plot this collection of triplegs.
@@ -71,6 +79,7 @@ class TriplegsAccessor(object):
         """
         ti.visualization.triplegs.plot_triplegs(self._obj, *args, **kwargs)
 
+    @copy_docstring(write_triplegs_csv)
     def to_csv(self, filename, *args, **kwargs):
         """
         Store this collection of triplegs as a CSV file.
@@ -79,6 +88,7 @@ class TriplegsAccessor(object):
         """
         ti.io.file.write_triplegs_csv(self._obj, filename, *args, **kwargs)
 
+    @copy_docstring(write_triplegs_postgis)
     def to_postgis(
         self, name, con, schema=None, if_exists="fail", index=True, index_label=None, chunksize=None, dtype=None
     ):
@@ -91,6 +101,7 @@ class TriplegsAccessor(object):
             self._obj, name, con, schema, if_exists, index, index_label, chunksize, dtype
         )
 
+    @copy_docstring(calculate_distance_matrix)
     def calculate_distance_matrix(self, *args, **kwargs):
         """
         Calculate pair-wise distance among triplegs or to other triplegs.
@@ -99,6 +110,7 @@ class TriplegsAccessor(object):
         """
         return ti.geogr.distances.calculate_distance_matrix(self._obj, *args, **kwargs)
 
+    @copy_docstring(spatial_filter)
     def spatial_filter(self, *args, **kwargs):
         """
         Filter triplegs with a geo extent.
@@ -107,6 +119,7 @@ class TriplegsAccessor(object):
         """
         return ti.preprocessing.filter.spatial_filter(self._obj, *args, **kwargs)
 
+    @copy_docstring(generate_trips)
     def generate_trips(self, *args, **kwargs):
         """
         Generate trips based on staypoints and triplegs.
@@ -124,6 +137,7 @@ class TriplegsAccessor(object):
             )
             return ti.preprocessing.triplegs.generate_trips(stps_input=args[0], tpls_input=self._obj, **kwargs)
 
+    @copy_docstring(predict_transport_mode)
     def predict_transport_mode(self, *args, **kwargs):
         """
         Predict/impute the transport mode with which each tripleg was likely covered.
@@ -132,6 +146,7 @@ class TriplegsAccessor(object):
         """
         return ti.analysis.labelling.predict_transport_mode(self._obj, *args, **kwargs)
 
+    @copy_docstring(calculate_modal_split)
     def calculate_modal_split(self, *args, **kwargs):
         """
         Calculate the modal split of the triplegs.
@@ -140,6 +155,7 @@ class TriplegsAccessor(object):
         """
         return ti.analysis.modal_split.calculate_modal_split(self._obj, *args, **kwargs)
 
+    @copy_docstring(temporal_tracking_quality)
     def temporal_tracking_quality(self, *args, **kwargs):
         """
         Calculate per-user temporal tracking quality (temporal coverage).
diff --git a/trackintel/model/trips.py b/trackintel/model/trips.py
index a29441d..73abb1c 100644
--- a/trackintel/model/trips.py
+++ b/trackintel/model/trips.py
@@ -1,3 +1,7 @@
+from trackintel.analysis.tracking_quality import temporal_tracking_quality
+from trackintel.io.postgis import write_trips_postgis
+from trackintel.io.file import write_trips_csv
+from trackintel.model.util import copy_docstring
 import pandas as pd
 
 import trackintel as ti
@@ -69,6 +73,7 @@ class TripsAccessor(object):
         """
         raise NotImplementedError
 
+    @copy_docstring(write_trips_csv)
     def to_csv(self, filename, *args, **kwargs):
         """
         Store this collection of trips as a CSV file.
@@ -77,6 +82,7 @@ class TripsAccessor(object):
         """
         ti.io.file.write_trips_csv(self._obj, filename, *args, **kwargs)
 
+    @copy_docstring(write_trips_postgis)
     def to_postgis(
         self, name, con, schema=None, if_exists="fail", index=True, index_label=None, chunksize=None, dtype=None
     ):
@@ -87,6 +93,7 @@ class TripsAccessor(object):
         """
         ti.io.postgis.write_trips_postgis(self._obj, name, con, schema, if_exists, index, index_label, chunksize, dtype)
 
+    @copy_docstring(temporal_tracking_quality)
     def temporal_tracking_quality(self, *args, **kwargs):
         """
         Calculate per-user temporal tracking quality (temporal coverage).
diff --git a/trackintel/model/util.py b/trackintel/model/util.py
new file mode 100644
index 0000000..1dc9cd1
--- /dev/null
+++ b/trackintel/model/util.py
@@ -0,0 +1,5 @@
+from functools import partial, update_wrapper
+
+
+def copy_docstring(wrapped, assigned=("__doc__",), updated=[]):
+    return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)


Test patch:
diff --git a/tests/model/test_util.py b/tests/model/test_util.py
new file mode 100644
index 0000000..bb4db52
--- /dev/null
+++ b/tests/model/test_util.py
@@ -0,0 +1,23 @@
+from trackintel.model.util import copy_docstring
+from functools import WRAPPER_ASSIGNMENTS
+from trackintel.io.postgis import read_trips_postgis
+
+
+class TestCopy_Docstring:
+    def test_default(self):
+        @copy_docstring(read_trips_postgis)
+        def bar(b: int) -> int:
+            """Old docstring."""
+            pass
+
+        old_docs = """Old docstring."""
+        print(type(old_docs))
+
+        for wa in WRAPPER_ASSIGNMENTS:
+            attr_foo = getattr(read_trips_postgis, wa)
+            attr_bar = getattr(bar, wa)
+            if wa == "__doc__":
+                assert attr_foo == attr_bar
+                assert attr_bar != old_docs
+            else:
+                assert attr_foo != attr_bar