Repository: mie-lab/trackintel

Issue:
Postgis should warn when no index column is provided
The index of a dataframe plays a very important role in trackintel. Therefore, we included a warning if no explicit index column is provided and the default index is assumed when csv files are read e.g., [here](https://github.com/mie-lab/trackintel/blob/563473b885a91858a8463da9db66353d5adb1089/trackintel/io/file.py#L176)
I think it would be very helpful to have the same functionality for the read postgis functions (forgetting to set the index column actually caused errors for me several times...)

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/mie-lab/trackintel/pull/502?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) Report
Patch coverage: **`100.00%`** and project coverage change: **`+0.12%`** :tada:
> Comparison is base [(`07b201a`)](https://app.codecov.io/gh/mie-lab/trackintel/commit/07b201a13399c5b900ead5d9c8a22daecf956b06?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) 92.37% compared to head [(`72ad3c5`)](https://app.codecov.io/gh/mie-lab/trackintel/pull/502?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) 92.50%.
> Report is 2 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #502      +/-   ##
==========================================
+ Coverage   92.37%   92.50%   +0.12%     
==========================================
  Files          39       40       +1     
  Lines        2019     2027       +8     
  Branches      344      356      +12     
==========================================
+ Hits         1865     1875      +10     
  Misses        133      133              
+ Partials       21       19       -2     
```


| [Files Changed](https://app.codecov.io/gh/mie-lab/trackintel/pull/502?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab) | Coverage Δ | |
|---|---|---|
| [trackintel/io/file.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/502?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9pby9maWxlLnB5) | `100.00% <100.00%> (+1.96%)` | :arrow_up: |
| [trackintel/io/postgis.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/502?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9pby9wb3N0Z2lzLnB5) | `96.07% <100.00%> (+0.28%)` | :arrow_up: |
| [trackintel/io/util.py](https://app.codecov.io/gh/mie-lab/trackintel/pull/502?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab#diff-dHJhY2tpbnRlbC9pby91dGlsLnB5) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/mie-lab/trackintel/pull/502?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mie-lab).


Patch:
diff --git a/appveyor/run_with_env.cmd b/appveyor/run_with_env.cmd
deleted file mode 100644
index 9f8ef26..0000000
--- a/appveyor/run_with_env.cmd
+++ /dev/null
@@ -1,90 +0,0 @@
-:: Adapted from https://github.com/Toblerity/Fiona/blob/master/appveyor/run_with_env.cmd
-:: 
-:: To build extensions for 64 bit Python 3, we need to configure environment
-:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of:
-:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1)
-::
-:: To build extensions for 64 bit Python 2, we need to configure environment
-:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of:
-:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0)
-::
-:: 32 bit builds, and 64-bit builds for 3.5 and beyond, do not require specific
-:: environment configurations.
-::
-:: Note: this script needs to be run with the /E:ON and /V:ON flags for the
-:: cmd interpreter, at least for (SDK v7.0)
-::
-:: More details at:
-:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows
-:: http://stackoverflow.com/a/13751649/163740
-::
-:: Author: Olivier Grisel
-:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
-::
-:: Notes about batch files for Python people:
-::
-:: Quotes in values are literally part of the values:
-::      SET FOO="bar"
-:: FOO is now five characters long: " b a r "
-:: If you don't want quotes, don't include them on the right-hand side.
-::
-:: The CALL lines at the end of this file look redundant, but if you move them
-:: outside of the IF clauses, they do not run properly in the SET_SDK_64==Y
-:: case, I don't know why.
-@ECHO OFF
-
-SET COMMAND_TO_RUN=%*
-SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows
-SET WIN_WDK=c:\Program Files (x86)\Windows Kits\10\Include\wdf
-
-:: Extract the major and minor versions, and allow for the minor version to be
-:: more than 9.  This requires the version number to have two dots in it.
-SET MAJOR_PYTHON_VERSION=%PYTHON_VERSION:~0,1%
-IF "%PYTHON_VERSION:~3,1%" == "." (
-    SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,1%
-) ELSE (
-    SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,2%
-)
-
-:: Based on the Python version, determine what SDK version to use, and whether
-:: to set the SDK for 64-bit.
-IF %MAJOR_PYTHON_VERSION% == 2 (
-    SET WINDOWS_SDK_VERSION="v7.0"
-    SET SET_SDK_64=Y
-) ELSE (
-    IF %MAJOR_PYTHON_VERSION% == 3 (
-        SET WINDOWS_SDK_VERSION="v7.1"
-        IF %MINOR_PYTHON_VERSION% LEQ 4 (
-            SET SET_SDK_64=Y
-        ) ELSE (
-            SET SET_SDK_64=N
-            IF EXIST "%WIN_WDK%" (
-                :: See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/
-                REN "%WIN_WDK%" 0wdf
-            )
-        )
-    ) ELSE (
-        ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%"
-        EXIT 1
-    )
-)
-
-IF %PYTHON_ARCH% == 64 (
-    IF %SET_SDK_64% == Y (
-        ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture
-        SET DISTUTILS_USE_SDK=1
-        SET MSSdk=1
-        "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION%
-        "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release
-        ECHO Executing: %COMMAND_TO_RUN%
-        call %COMMAND_TO_RUN% || EXIT 1
-    ) ELSE (
-        ECHO Using default MSVC build environment for 64 bit architecture
-        ECHO Executing: %COMMAND_TO_RUN%
-        call %COMMAND_TO_RUN% || EXIT 1
-    )
-) ELSE (
-    ECHO Using default MSVC build environment for 32 bit architecture
-    ECHO Executing: %COMMAND_TO_RUN%
-    call %COMMAND_TO_RUN% || EXIT 1
-)
\ No newline at end of file
diff --git a/trackintel/io/file.py b/trackintel/io/file.py
index 9e9743a..aa34913 100644
--- a/trackintel/io/file.py
+++ b/trackintel/io/file.py
@@ -1,7 +1,4 @@
 import ast
-import warnings
-from functools import wraps
-from inspect import signature
 
 import geopandas as gpd
 import pandas as pd
@@ -15,22 +12,7 @@ from trackintel.io.from_geopandas import (
     read_triplegs_gpd,
     read_trips_gpd,
 )
-
-
-def _index_warning_default_none(func):
-    """Decorator function that warns if index_col None is not set explicit."""
-
-    @wraps(func)  # copy all metadata
-    def wrapper(*args, **kwargs):
-        bound_values = signature(func).bind(*args, **kwargs)  # binds only available args and kwargs
-        if "index_col" not in bound_values.arguments:
-            warnings.warn(
-                "Assuming default index as unique identifier. "
-                "Pass 'index_col=None' as explicit argument to avoid a warning when reading csv files."
-            )
-        return func(*args, **kwargs)
-
-    return wrapper
+from trackintel.io.util import _index_warning_default_none
 
 
 @_index_warning_default_none
diff --git a/trackintel/io/postgis.py b/trackintel/io/postgis.py
index 93d98f4..dade23f 100644
--- a/trackintel/io/postgis.py
+++ b/trackintel/io/postgis.py
@@ -10,6 +10,7 @@ from sqlalchemy import create_engine
 from sqlalchemy.types import JSON
 
 import trackintel as ti
+from trackintel.io.util import _index_warning_default_none
 
 
 def _handle_con_string(func):
@@ -40,6 +41,7 @@ def _handle_con_string(func):
     return wrapper
 
 
+@_index_warning_default_none
 @_handle_con_string
 def read_positionfixes_postgis(
     sql,
@@ -140,6 +142,7 @@ def write_positionfixes_postgis(
     )
 
 
+@_index_warning_default_none
 @_handle_con_string
 def read_triplegs_postgis(
     sql,
@@ -239,6 +242,7 @@ def write_triplegs_postgis(
     )
 
 
+@_index_warning_default_none
 @_handle_con_string
 def read_staypoints_postgis(
     sql,
@@ -340,6 +344,7 @@ def write_staypoints_postgis(
     )
 
 
+@_index_warning_default_none
 @_handle_con_string
 def read_locations_postgis(
     sql,
@@ -461,6 +466,7 @@ def write_locations_postgis(
     )
 
 
+@_index_warning_default_none
 @_handle_con_string
 def read_trips_postgis(
     sql,
@@ -590,6 +596,7 @@ def write_trips_postgis(
         )
 
 
+@_index_warning_default_none
 @_handle_con_string
 def read_tours_postgis(
     sql,
diff --git a/trackintel/io/util.py b/trackintel/io/util.py
new file mode 100644
index 0000000..b359b65
--- /dev/null
+++ b/trackintel/io/util.py
@@ -0,0 +1,19 @@
+import warnings
+from functools import wraps
+from inspect import signature
+
+
+def _index_warning_default_none(func):
+    """Decorator function that warns if index_col None is not set explicit."""
+
+    @wraps(func)  # copy all metadata
+    def wrapper(*args, **kwargs):
+        bound_values = signature(func).bind(*args, **kwargs)  # binds only available args and kwargs
+        if "index_col" not in bound_values.arguments:
+            warnings.warn(
+                "Assuming default index as unique identifier. "
+                "Pass 'index_col=None' as explicit argument to avoid a warning."
+            )
+        return func(*args, **kwargs)
+
+    return wrapper
diff --git a/trackintel/model/locations.py b/trackintel/model/locations.py
index c8dd578..fbdaf90 100644
--- a/trackintel/model/locations.py
+++ b/trackintel/model/locations.py
@@ -1,14 +1,13 @@
 import pandas as pd
 import trackintel as ti
-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.model.util import _copy_docstring, _register_trackintel_accessor
 from trackintel.preprocessing.filter import spatial_filter
 from trackintel.visualization.locations import plot_locations
 
 
-@pd.api.extensions.register_dataframe_accessor("as_locations")
+@_register_trackintel_accessor("as_locations")
 class LocationsAccessor(object):
     """A pandas accessor to treat (Geo)DataFrames as collections of locations.
 
diff --git a/trackintel/model/positionfixes.py b/trackintel/model/positionfixes.py
index 45ccb4a..0b33a83 100644
--- a/trackintel/model/positionfixes.py
+++ b/trackintel/model/positionfixes.py
@@ -6,10 +6,10 @@ 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
-from trackintel.model.util import get_speed_positionfixes
+from trackintel.model.util import get_speed_positionfixes, _register_trackintel_accessor
 
 
-@pd.api.extensions.register_dataframe_accessor("as_positionfixes")
+@_register_trackintel_accessor("as_positionfixes")
 class PositionfixesAccessor(object):
     """A pandas accessor to treat (Geo)DataFrames as collections of `Positionfixes`.
 
diff --git a/trackintel/model/staypoints.py b/trackintel/model/staypoints.py
index 1d0d4a5..12a8887 100644
--- a/trackintel/model/staypoints.py
+++ b/trackintel/model/staypoints.py
@@ -4,13 +4,13 @@ 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.model.util import _copy_docstring, _register_trackintel_accessor
 from trackintel.preprocessing.filter import spatial_filter
 from trackintel.preprocessing.staypoints import generate_locations, merge_staypoints
 from trackintel.visualization.staypoints import plot_staypoints
 
 
-@pd.api.extensions.register_dataframe_accessor("as_staypoints")
+@_register_trackintel_accessor("as_staypoints")
 class StaypointsAccessor(object):
     """A pandas accessor to treat (Geo)DataFrames as collections of `Staypoints`.
 
diff --git a/trackintel/model/tours.py b/trackintel/model/tours.py
index 67a6b40..578afaf 100644
--- a/trackintel/model/tours.py
+++ b/trackintel/model/tours.py
@@ -1,8 +1,9 @@
 import pandas as pd
 import trackintel as ti
+from trackintel.model.util import _register_trackintel_accessor
 
 
-@pd.api.extensions.register_dataframe_accessor("as_tours")
+@_register_trackintel_accessor("as_tours")
 class ToursAccessor(object):
     """A pandas accessor to treat DataFrames as collections of `Tours`.
 
diff --git a/trackintel/model/triplegs.py b/trackintel/model/triplegs.py
index 8cf1e52..c3d7fd9 100644
--- a/trackintel/model/triplegs.py
+++ b/trackintel/model/triplegs.py
@@ -7,13 +7,13 @@ 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, get_speed_triplegs
+from trackintel.model.util import _copy_docstring, get_speed_triplegs, _register_trackintel_accessor
 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")
+@_register_trackintel_accessor("as_triplegs")
 class TriplegsAccessor(object):
     """A pandas accessor to treat (Geo)DataFrames as collections of `Tripleg`.
 
diff --git a/trackintel/model/trips.py b/trackintel/model/trips.py
index 0675a15..1f35c4a 100644
--- a/trackintel/model/trips.py
+++ b/trackintel/model/trips.py
@@ -1,14 +1,14 @@
 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
+from trackintel.model.util import _copy_docstring, _register_trackintel_accessor
 import pandas as pd
 import geopandas as gpd
 
 import trackintel as ti
 
 
-@pd.api.extensions.register_dataframe_accessor("as_trips")
+@_register_trackintel_accessor("as_trips")
 class TripsAccessor(object):
     """A pandas accessor to treat (Geo)DataFrames as collections of trips.
 
diff --git a/trackintel/model/util.py b/trackintel/model/util.py
index a0f0500..8396679 100644
--- a/trackintel/model/util.py
+++ b/trackintel/model/util.py
@@ -108,3 +108,34 @@ def _single_tripleg_mean_speed(positionfixes):
 def _copy_docstring(wrapped, assigned=("__doc__",), updated=[]):
     """Thin wrapper for `functools.update_wrapper` to mimic `functools.wraps` but to only copy the docstring."""
     return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
+
+
+class NonCachedAccessor:
+    def __init__(self, name: str, accessor) -> None:
+        self._name = name
+        self._accessor = accessor
+
+    def __get__(self, obj, cls):
+        if obj is None:
+            # we're accessing the attribute of the class, i.e., Dataset.geo
+            return self._accessor
+        # copied code from pandas accessor, minus the caching
+        return self._accessor(obj)
+
+
+def _register_trackintel_accessor(name: str):
+    from pandas import DataFrame
+
+    def decorator(accessor):
+        if hasattr(DataFrame, name):
+            warnings.warn(
+                f"registration of accessor {repr(accessor)} under name "
+                f"{repr(name)} for type {repr(DataFrame)} is overriding a preexisting "
+                f"attribute with the same name.",
+                UserWarning,
+            )
+        setattr(DataFrame, name, NonCachedAccessor(name, accessor))
+        DataFrame._accessors.add(name)
+        return accessor
+
+    return decorator


Test patch:
diff --git a/tests/io/test_dataset_reader.py b/tests/io/test_dataset_reader.py
index 59b3d53..7e5f4b9 100644
--- a/tests/io/test_dataset_reader.py
+++ b/tests/io/test_dataset_reader.py
@@ -100,8 +100,6 @@ class TestReadGeolife:
         captured_noprint = capsys.readouterr()
         assert captured_noprint.err == ""
 
-        assert True
-
     def test_label_reading(self):
         """Test data types of the labels returned by read_geolife."""
         _, labels = read_geolife(os.path.join("tests", "data", "geolife_modes"))
diff --git a/tests/io/test_file.py b/tests/io/test_file.py
index 46b5081..cd1e538 100644
--- a/tests/io/test_file.py
+++ b/tests/io/test_file.py
@@ -25,10 +25,10 @@ class TestPositionfixes:
         column_mapping = {"lat": "latitude", "lon": "longitude", "time": "tracked_at"}
         mod_pfs = ti.read_positionfixes_csv(mod_file, sep=";", index_col="id", columns=column_mapping)
         assert mod_pfs.equals(pfs)
-        pfs["tracked_at"] = pfs["tracked_at"].apply(lambda d: d.isoformat().replace("+00:00", "Z"))
 
+        date_format = "%Y-%m-%dT%H:%M:%SZ"
         columns = ["user_id", "tracked_at", "latitude", "longitude", "elevation", "accuracy"]
-        pfs.as_positionfixes.to_csv(tmp_file, sep=";", columns=columns)
+        pfs.as_positionfixes.to_csv(tmp_file, sep=";", columns=columns, date_format=date_format)
         assert filecmp.cmp(orig_file, tmp_file, shallow=False)
         os.remove(tmp_file)
 
@@ -49,11 +49,10 @@ class TestPositionfixes:
         pfs = ti.read_positionfixes_csv(file, sep=";", index_col="id")
         assert pd.api.types.is_datetime64tz_dtype(pfs["tracked_at"])
 
-        # check if a timezone will be set after manually deleting the timezone
-        pfs["tracked_at"] = pfs["tracked_at"].dt.tz_localize(None)
-        assert not pd.api.types.is_datetime64tz_dtype(pfs["tracked_at"])
+        # check if a timezone will be set without storing the timezone
+        date_format = "%Y-%m-%d %H:%M:%S"
         tmp_file = os.path.join("tests", "data", "positionfixes_test_2.csv")
-        pfs.as_positionfixes.to_csv(tmp_file, sep=";")
+        pfs.as_positionfixes.to_csv(tmp_file, sep=";", date_format=date_format)
         pfs = ti.read_positionfixes_csv(tmp_file, sep=";", index_col="id", tz="utc")
 
         assert pd.api.types.is_datetime64tz_dtype(pfs["tracked_at"])
@@ -94,11 +93,10 @@ class TestTriplegs:
         mod_tpls = ti.read_triplegs_csv(mod_file, sep=";", columns=column_mapping, index_col="id")
 
         assert mod_tpls.equals(tpls)
-        tpls["started_at"] = tpls["started_at"].apply(lambda d: d.isoformat().replace("+00:00", "Z"))
-        tpls["finished_at"] = tpls["finished_at"].apply(lambda d: d.isoformat().replace("+00:00", "Z"))
 
+        date_format = "%Y-%m-%dT%H:%M:%SZ"
         columns = ["user_id", "started_at", "finished_at", "geom"]
-        tpls.as_triplegs.to_csv(tmp_file, sep=";", columns=columns)
+        tpls.as_triplegs.to_csv(tmp_file, sep=";", columns=columns, date_format=date_format)
         assert filecmp.cmp(orig_file, tmp_file, shallow=False)
         os.remove(tmp_file)
 
@@ -119,11 +117,10 @@ class TestTriplegs:
         tpls = ti.read_triplegs_csv(file, sep=";", index_col="id")
         assert pd.api.types.is_datetime64tz_dtype(tpls["started_at"])
 
-        # check if a timezone will be set after manually deleting the timezone
-        tpls["started_at"] = tpls["started_at"].dt.tz_localize(None)
-        assert not pd.api.types.is_datetime64tz_dtype(tpls["started_at"])
+        # check if a timezone will be set without storing the timezone
         tmp_file = os.path.join("tests", "data", "triplegs_test_2.csv")
-        tpls.as_triplegs.to_csv(tmp_file, sep=";")
+        date_format = "%Y-%m-%d %H:%M:%S"
+        tpls.as_triplegs.to_csv(tmp_file, sep=";", date_format=date_format)
         tpls = ti.read_triplegs_csv(tmp_file, sep=";", index_col="id", tz="utc")
 
         assert pd.api.types.is_datetime64tz_dtype(tpls["started_at"])
@@ -161,11 +158,10 @@ class TestStaypoints:
         sp = ti.read_staypoints_csv(orig_file, sep=";", tz="utc", index_col="id")
         mod_sp = ti.read_staypoints_csv(mod_file, columns={"User": "user_id"}, sep=";", index_col="id")
         assert mod_sp.equals(sp)
-        sp["started_at"] = sp["started_at"].apply(lambda d: d.isoformat().replace("+00:00", "Z"))
-        sp["finished_at"] = sp["finished_at"].apply(lambda d: d.isoformat().replace("+00:00", "Z"))
 
+        date_format = "%Y-%m-%dT%H:%M:%SZ"
         columns = ["user_id", "started_at", "finished_at", "elevation", "geom"]
-        sp.as_staypoints.to_csv(tmp_file, sep=";", columns=columns)
+        sp.as_staypoints.to_csv(tmp_file, sep=";", columns=columns, date_format=date_format)
         assert filecmp.cmp(orig_file, tmp_file, shallow=False)
         os.remove(tmp_file)
 
@@ -186,11 +182,10 @@ class TestStaypoints:
         sp = ti.read_staypoints_csv(file, sep=";", index_col="id")
         assert pd.api.types.is_datetime64tz_dtype(sp["started_at"])
 
-        # check if a timezone will be set after manually deleting the timezone
-        sp["started_at"] = sp["started_at"].dt.tz_localize(None)
-        assert not pd.api.types.is_datetime64tz_dtype(sp["started_at"])
+        # check if a timezone will be without storing the timezone
         tmp_file = os.path.join("tests", "data", "staypoints_test_2.csv")
-        sp.as_staypoints.to_csv(tmp_file, sep=";")
+        date_format = "%Y-%m-%d %H:%M:%S"
+        sp.as_staypoints.to_csv(tmp_file, sep=";", date_format=date_format)
         sp = ti.read_staypoints_csv(tmp_file, sep=";", index_col="id", tz="utc")
 
         assert pd.api.types.is_datetime64tz_dtype(sp["started_at"])
@@ -299,10 +294,9 @@ class TestTrips:
         mod_trips_wo_geom = pd.DataFrame(mod_trips.drop(columns=["geom"]))
         assert mod_trips_wo_geom.equals(trips)
 
-        trips["started_at"] = trips["started_at"].apply(lambda d: d.isoformat().replace("+00:00", "Z"))
-        trips["finished_at"] = trips["finished_at"].apply(lambda d: d.isoformat().replace("+00:00", "Z"))
+        date_format = "%Y-%m-%dT%H:%M:%SZ"
         columns = ["user_id", "started_at", "finished_at", "origin_staypoint_id", "destination_staypoint_id"]
-        trips.as_trips.to_csv(tmp_file, sep=";", columns=columns)
+        trips.as_trips.to_csv(tmp_file, sep=";", columns=columns, date_format=date_format)
         assert filecmp.cmp(orig_file, tmp_file, shallow=False)
         os.remove(tmp_file)
 
@@ -313,11 +307,10 @@ class TestTrips:
         trips = ti.read_trips_csv(file, sep=";", index_col="id")
         assert pd.api.types.is_datetime64tz_dtype(trips["started_at"])
 
-        # check if a timezone will be set after manually deleting the timezone
-        trips["started_at"] = trips["started_at"].dt.tz_localize(None)
-        assert not pd.api.types.is_datetime64tz_dtype(trips["started_at"])
+        # check if a timezone will be set without storing the timezone
         tmp_file = os.path.join("tests", "data", "trips_test_2.csv")
-        trips.as_trips.to_csv(tmp_file, sep=";")
+        date_format = "%Y-%m-%d %H:%M:%S"
+        trips.as_trips.to_csv(tmp_file, sep=";", date_format=date_format)
         trips = ti.read_trips_csv(tmp_file, sep=";", index_col="id", tz="utc")
 
         assert pd.api.types.is_datetime64tz_dtype(trips["started_at"])
diff --git a/tests/io/test_from_geopandas.py b/tests/io/test_from_geopandas.py
index 55a3e9a..6cb7668 100644
--- a/tests/io/test_from_geopandas.py
+++ b/tests/io/test_from_geopandas.py
@@ -36,7 +36,7 @@ def example_positionfixes():
     ]
     pfs = gpd.GeoDataFrame(data=list_dict, geometry="geom", crs="EPSG:4326")
     pfs.index.name = "id"
-    assert pfs.as_positionfixes
+    pfs.as_positionfixes
     return pfs
 
 
@@ -206,7 +206,7 @@ def example_locations():
     extent = Polygon(coords)
     locs["extent"] = extent  # broadcasting
     locs["extent"] = gpd.GeoSeries(locs["extent"])  # dtype
-    assert locs.as_locations
+    locs.as_locations
     return locs
 
 
@@ -264,7 +264,7 @@ def example_trips():
         d["finished_at"] = d["started_at"] + h
     trips = gpd.GeoDataFrame(data=list_dict, geometry="geom", crs="EPSG:2056")
     trips.index.name = "id"
-    assert trips.as_trips
+    trips.as_trips
     return trips
 
 
diff --git a/tests/io/test_util.py b/tests/io/test_util.py
new file mode 100644
index 0000000..46b0b3f
--- /dev/null
+++ b/tests/io/test_util.py
@@ -0,0 +1,23 @@
+import pytest
+from trackintel.io.util import _index_warning_default_none
+
+
+class Test_index_warning_default_none:
+    def test_index_set(self):
+        """Test if a set index creates no warning."""
+
+        @_index_warning_default_none
+        def foo(index_col=None):
+            return
+
+        foo(index_col=None)
+
+    def test_index_default(self):
+        """Test if default index creates a warning."""
+
+        @_index_warning_default_none
+        def foo(index_col=None):
+            return
+
+        with pytest.warns(UserWarning):
+            foo()
diff --git a/tests/model/test_util.py b/tests/model/test_util.py
index b5f2e87..23c9431 100644
--- a/tests/model/test_util.py
+++ b/tests/model/test_util.py
@@ -1,16 +1,22 @@
 import os
 from functools import WRAPPER_ASSIGNMENTS
 
-from geopandas import GeoDataFrame
 import numpy as np
-from pandas import Timestamp, Timedelta
+import pandas as pd
 import pytest
+from geopandas import GeoDataFrame
 from geopandas.testing import assert_geodataframe_equal
+from pandas import Timedelta, Timestamp
 from shapely.geometry import Point
 
 import trackintel as ti
 from trackintel.io.postgis import read_trips_postgis
-from trackintel.model.util import _copy_docstring, get_speed_positionfixes
+from trackintel.model.util import (
+    NonCachedAccessor,
+    _copy_docstring,
+    _register_trackintel_accessor,
+    get_speed_positionfixes,
+)
 
 
 @pytest.fixture
@@ -207,3 +213,51 @@ class Test_copy_docstring:
                 assert attr_bar != old_docs
             else:
                 assert attr_foo != attr_bar
+
+
+class TestNonCachedAccessor:
+    """Test if NonCachedAccessor works"""
+
+    def test_accessor(self):
+        """Test accessor on class object and class instance."""
+
+        def foo(val):
+            return val
+
+        class A:
+            nca = NonCachedAccessor("nca_test", foo)
+
+        a = A()
+        assert A.nca == foo  # class object
+        assert a.nca == a  # class instance
+
+
+class Test_register_trackintel_accessor:
+    """Test if accessors are correctly registered."""
+
+    def test_register(self):
+        """Test if accessor is registered in DataFrame"""
+
+        def foo(val):
+            return val
+
+        bar = _register_trackintel_accessor("foo")(foo)
+        assert foo == bar
+        assert "foo" in pd.DataFrame._accessors
+        assert foo == pd.DataFrame.foo
+        # remove accessor again to make tests independent
+        pd.DataFrame._accesors = pd.DataFrame._accessors.remove("foo")
+        del pd.DataFrame.foo
+
+    def test_duplicate_name_warning(self):
+        """Test that duplicate name raises warning"""
+
+        def foo(val):
+            return val
+
+        _register_trackintel_accessor("foo")(foo)
+        with pytest.warns(UserWarning):
+            _register_trackintel_accessor("foo")(foo)
+        # remove accessor again to make tests independent
+        pd.DataFrame._accesors = pd.DataFrame._accessors.remove("foo")
+        del pd.DataFrame.foo