Browse Source

restrict pandas version after relevant breaking change in v1.2.0; migrate from travis to github actions

Fabian Peter Hammerle 3 years ago
parent
commit
88e19fc900
5 changed files with 132 additions and 113 deletions
  1. 88 0
      .github/workflows/python.yml
  2. 0 78
      .travis.yml
  3. 36 30
      freesurfer_stats/__init__.py
  4. 5 2
      setup.py
  5. 3 3
      tests/test_cortical_parcellation_stats.py

+ 88 - 0
.github/workflows/python.yml

@@ -0,0 +1,88 @@
+# sync with https://github.com/fphammerle/ical2vdir/blob/master/.github/workflows/python.yml
+
+# https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions
+
+# shown in badge
+# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/configuring-a-workflow#adding-a-workflow-status-badge-to-your-repository
+name: tests
+
+on:
+  push:
+  pull_request:
+  schedule:
+  - cron: '0 20 * * 5'
+
+jobs:
+  tests:
+    runs-on: ubuntu-18.04
+    strategy:
+      matrix:
+        python-version:
+        - 3.5
+        - 3.6
+        - 3.7
+        - 3.8
+        pandas-version:
+        - '' # locked version
+        - 0.21.*
+        - 0.22.*
+        - 0.23.*
+        - 0.24.*
+        - 0.25.*
+        - 1.1.*
+        exclude:
+          # https://travis-ci.org/github/fphammerle/freesurfer-stats/jobs/683777317#L208
+          # https://github.com/pandas-dev/pandas/commit/18efcb27361478daa3118079ecb166c733691ecb#diff-2eeaed663bd0d25b7e608891384b7298R814
+        - python-version: 3.5
+          pandas-version: 1.1.*
+        - python-version: 3.7
+          pandas-version: 0.21.*
+        - python-version: 3.7
+          pandas-version: 0.22.*
+        # > /tmp/pip-install-g4jx0np4/numpy/_configtest.c:6: undefined reference to `exp'
+        # https://travis-ci.org/github/fphammerle/freesurfer-stats/jobs/683704331#L437
+        - python-version: 3.8
+          pandas-version: 0.21.*
+        # https://travis-ci.org/github/fphammerle/freesurfer-stats/jobs/683704330#L437
+        - python-version: 3.8
+          pandas-version: 0.22.*
+        # no python-version3.8 wheels for pandas v0.24.2 & v0.23.4 available
+        # https://travis-ci.org/github/fphammerle/freesurfer-stats/builds/701952350
+        # build takes longer than 10min
+        # https://travis-ci.org/github/fphammerle/freesurfer-stats/jobs/702077404#L199
+        - python-version: 3.8
+          pandas-version: 0.23.*
+        - python-version: 3.8
+          pandas-version: 0.24.*
+      fail-fast: false
+    steps:
+    - uses: actions/checkout@v1
+    - uses: actions/setup-python@v1
+      with:
+        python-version: ${{ matrix.python-version }}
+    - run: pip install --upgrade pipenv==2020.8.13
+    - run: pipenv install --python "$PYTHON_VERSION" --deploy --dev
+      env:
+        PYTHON_VERSION: ${{ matrix.python-version }}
+    # `pipenv install --selective-upgrade "pandas==$PANDAS_VERSION"` was not effective
+    - run: '[ -z "$PANDAS_VERSION" ] || pipenv run pip install "pandas==$PANDAS_VERSION"'
+      env:
+        PANDAS_VERSION: ${{ matrix.pandas-version }}
+    - run: pipenv graph
+    - run: pipenv run pytest --cov="$(cat *.egg-info/top_level.txt)" --cov-report=term-missing --cov-fail-under=100
+    - run: pipenv run pylint --load-plugins=pylint_import_requirements "$(cat *.egg-info/top_level.txt)"
+    # https://github.com/PyCQA/pylint/issues/352
+    # disable parse-error due to:
+    # > tests/resources/__init__.py:1:0: F0010: error while code parsing: Unable to load file tests/resources/__init__.py:
+    # > [Errno 2] No such file or directory: 'tests/resources/__init__.py' (parse-error)
+    - run: pipenv run pylint --disable=parse-error tests/*
+    # >=1.9.0 to detect branch name
+    # https://github.com/coveralls-clients/coveralls-python/pull/207
+    # https://github.com/coverallsapp/github-action/issues/4#issuecomment-547036866
+    # 1.11.0 https://github.com/coveralls-clients/coveralls-python/issues/219
+    - run: pip install 'coveralls>=1.9.0,<2,!=1.11.0'
+    # https://github.com/coverallsapp/github-action/issues/30
+    # https://github.com/coverallsapp/github-action/issues/4#issuecomment-529399410
+    - run: coveralls
+      env:
+        COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}

+ 0 - 78
.travis.yml

@@ -1,78 +0,0 @@
-language: python
-
-python:
-- 3.5
-- 3.6
-- 3.7
-- 3.7-dev
-- 3.8
-- 3.8-dev
-
-# required for python >= 3.7
-dist: xenial
-
-env:
-# https://pypi.org/project/pandas/#history
-- PANDAS_VERSION=
-- PANDAS_VERSION=1.*
-- PANDAS_VERSION=0.25.*
-- PANDAS_VERSION=0.24.*
-- PANDAS_VERSION=0.23.*
-- PANDAS_VERSION=0.22.*
-- PANDAS_VERSION=0.21.*
-
-# https://travis-ci.org/fphammerle/freesurfer-volume-reader/builds/525556257
-matrix:
-  exclude:
-  # https://travis-ci.org/github/fphammerle/freesurfer-stats/jobs/683777317#L208
-  # https://github.com/pandas-dev/pandas/commit/18efcb27361478daa3118079ecb166c733691ecb#diff-2eeaed663bd0d25b7e608891384b7298R814
-  - python: 3.5
-    env: PANDAS_VERSION=1.*
-  - python: 3.7
-    env: PANDAS_VERSION=0.21.*
-  - python: 3.7
-    env: PANDAS_VERSION=0.22.*
-  - python: 3.7-dev
-    env: PANDAS_VERSION=0.21.*
-  - python: 3.7-dev
-    env: PANDAS_VERSION=0.22.*
-  # >/tmp/pip-install-g4jx0np4/numpy/_configtest.c:6: undefined reference to `exp'
-  # https://travis-ci.org/github/fphammerle/freesurfer-stats/jobs/683704331#L437
-  - python: 3.8
-    env: PANDAS_VERSION=0.21.*
-  # https://travis-ci.org/github/fphammerle/freesurfer-stats/jobs/683704330#L437
-  - python: 3.8
-    env: PANDAS_VERSION=0.22.*
-  - python: 3.8-dev
-    env: PANDAS_VERSION=0.21.*
-  - python: 3.8-dev
-    env: PANDAS_VERSION=0.22.*
-  # no python3.8 wheels for pandas v0.24.2 & v0.23.4 available
-  # https://travis-ci.org/github/fphammerle/freesurfer-stats/builds/701952350
-  # build takes longer than 10min
-  # https://travis-ci.org/github/fphammerle/freesurfer-stats/jobs/702077404#L199
-  - python: 3.8
-    env: PANDAS_VERSION=0.23.*
-  - python: 3.8
-    env: PANDAS_VERSION=0.24.*
-  - python: 3.8-dev
-    env: PANDAS_VERSION=0.23.*
-  - python: 3.8-dev
-    env: PANDAS_VERSION=0.24.*
-
-install:
-- pip install pipenv
-- pipenv sync --dev
-- if [ ! -z "$PANDAS_VERSION" ]; then
-    pipenv install --selective-upgrade "pandas==$PANDAS_VERSION";
-  fi
-- pipenv graph
-
-script:
-- pipenv run pylint --load-plugins=pylint_import_requirements freesurfer_stats
-- pipenv run pylint tests/*
-- pipenv run pytest --cov=freesurfer_stats --cov-report=term-missing --cov-fail-under=100
-
-after_success:
-- pip install coveralls
-- coveralls

+ 36 - 30
freesurfer_stats/__init__.py

@@ -58,6 +58,26 @@ import pandas
 from freesurfer_stats.version import __version__
 
 
+def _get_filepath_or_buffer(
+    path: typing.Union[str, pathlib.Path]
+) -> typing.Tuple[typing.Any, bool]:  # (pandas._typing.FileOrBuffer, bool)
+    # path_or_buffer: typing.Union[str, pathlib.Path, typing.IO[typing.AnyStr],
+    #                              s3fs.S3File, gcsfs.GCSFile]
+    # https://github.com/pandas-dev/pandas/blob/v0.25.3/pandas/io/parsers.py#L436
+    # https://github.com/pandas-dev/pandas/blob/v0.25.3/pandas/_typing.py#L30
+    (path_or_buffer, _, _, *instructions) = pandas.io.common.get_filepath_or_buffer(
+        path
+    )
+    if instructions:  # pragma: no cover
+        # https://github.com/pandas-dev/pandas/blob/v0.25.3/pandas/io/common.py#L171
+        assert len(instructions) == 1, instructions
+        should_close = instructions[0]
+    else:  # pragma: no cover
+        # https://github.com/pandas-dev/pandas/blob/v0.21.0/pandas/io/common.py#L171
+        should_close = hasattr(path_or_buffer, "close")
+    return path_or_buffer, should_close
+
+
 class CorticalParcellationStats:
 
     _HEMISPHERE_PREFIX_TO_SIDE = {"lh": "left", "rh": "right"}
@@ -87,7 +107,7 @@ class CorticalParcellationStats:
 
     @classmethod
     def _read_column_header_line(
-        cls, stream: typing.TextIO,
+        cls, stream: typing.TextIO
     ) -> typing.Tuple[int, str, str]:
         line = cls._read_header_line(stream)
         assert line.startswith("TableCol"), line
@@ -108,7 +128,7 @@ class CorticalParcellationStats:
                     attr_value = attr_value.strip("$").rstrip()
                 if attr_name == "CreationTime":
                     attr_dt = datetime.datetime.strptime(
-                        attr_value, "%Y/%m/%d-%H:%M:%S-%Z",
+                        attr_value, "%Y/%m/%d-%H:%M:%S-%Z"
                     )
                     if attr_dt.tzinfo is None:
                         assert attr_value.endswith("-GMT")
@@ -116,7 +136,7 @@ class CorticalParcellationStats:
                     attr_value = attr_dt
                 if attr_name == "AnnotationFileTimeStamp":
                     attr_value = datetime.datetime.strptime(
-                        attr_value, "%Y/%m/%d %H:%M:%S",
+                        attr_value, "%Y/%m/%d %H:%M:%S"
                     )
                 self.headers[attr_name] = attr_value
 
@@ -129,7 +149,7 @@ class CorticalParcellationStats:
 
     @classmethod
     def _parse_whole_brain_measurements_line(
-        cls, line: str,
+        cls, line: str
     ) -> typing.Tuple[str, numpy.ndarray]:
         match = cls._GENERAL_MEASUREMENTS_REGEX.match(line)
         if not match:
@@ -145,7 +165,7 @@ class CorticalParcellationStats:
 
     @classmethod
     def _read_column_attributes(
-        cls, num: int, stream: typing.TextIO,
+        cls, num: int, stream: typing.TextIO
     ) -> typing.List[typing.Dict[str, str]]:
         columns = []
         for column_index in range(1, int(num) + 1):
@@ -193,31 +213,17 @@ class CorticalParcellationStats:
 
     @classmethod
     def read(cls, path: typing.Union[str, pathlib.Path]) -> "CorticalParcellationStats":
-        # path_or_buffer: typing.Union[str, pathlib.Path, typing.IO[typing.AnyStr],
-        #                              s3fs.S3File, gcsfs.GCSFile]
-        # https://github.com/pandas-dev/pandas/blob/v0.25.3/pandas/io/parsers.py#L436
-        # https://github.com/pandas-dev/pandas/blob/v0.25.3/pandas/_typing.py#L30
-        (
-            path_or_buffer,
-            _,
-            _,
-            *instructions,
-        ) = pandas.io.common.get_filepath_or_buffer(path)
-        # https://github.com/pandas-dev/pandas/blob/v0.25.3/pandas/io/common.py#L171
-        # https://github.com/pandas-dev/pandas/blob/v0.21.0/pandas/io/common.py#L171
-        if instructions:  # pragma: no cover
-            assert len(instructions) == 1, instructions
-            should_close = instructions[0]
-        else:  # pragma: no cover
-            should_close = hasattr(path_or_buffer, "close")
+        path_or_buffer, should_close = _get_filepath_or_buffer(path)
         stats = cls()
-        if hasattr(path_or_buffer, "readline"):
-            # pylint: disable=protected-access
-            stats._read(io.TextIOWrapper(path_or_buffer))
-        else:
-            with open(path_or_buffer, "r") as stream:
+        try:
+            if hasattr(path_or_buffer, "readline"):
                 # pylint: disable=protected-access
-                stats._read(stream)
-        if should_close:
-            path_or_buffer.close()
+                stats._read(io.TextIOWrapper(path_or_buffer))
+            else:
+                with open(path_or_buffer, "r") as stream:
+                    # pylint: disable=protected-access
+                    stats._read(stream)
+        finally:
+            if should_close:
+                path_or_buffer.close()
         return stats

+ 5 - 2
setup.py

@@ -68,9 +68,12 @@ setuptools.setup(
     packages=setuptools.find_packages(),
     install_requires=[
         "numpy<2",
-        # hoping pandas maintainers use semantic versioning
+        # pandas v1.2.0 made `get_filepath_or_buffer` private without releasing major version.
+        # semver?!? not even mentioned in changelog
+        # https://pandas.pydata.org/pandas-docs/stable/whatsnew/v1.2.0.html
+        # https://github.com/pandas-dev/pandas/commit/6d1541e1782a7b94797d5432922e64a97934cfa4#diff-934d8564d648e7521db673c6399dcac98e45adfd5230ba47d3aabfcc21979febL247
         # TODO verify lower version constraint
-        "pandas>=0.21,<2",
+        "pandas>=0.21,<1.2",
     ],
     setup_requires=["setuptools_scm"],
     tests_require=["pytest<5"],

+ 3 - 3
tests/test_cortical_parcellation_stats.py

@@ -346,7 +346,7 @@ def test__parse_whole_brain_measurements_line(
 ):
     # pylint: disable=protected-access
     column_name, value = CorticalParcellationStats._parse_whole_brain_measurements_line(
-        line,
+        line
     )
     assert column_name == expected_column_name
     assert numpy.allclose(value, [expected_value])
@@ -354,7 +354,7 @@ def test__parse_whole_brain_measurements_line(
 
 @pytest.mark.parametrize(
     "line",
-    ["Measure Cortex, CortexVol Total cortical gray matter volume, 553998.311189",],
+    ["Measure Cortex, CortexVol Total cortical gray matter volume, 553998.311189"],
 )
 def test__parse_whole_brain_measurements_line_parse_error(line):
     # pylint: disable=protected-access
@@ -364,7 +364,7 @@ def test__parse_whole_brain_measurements_line_parse_error(line):
 
 @pytest.mark.parametrize(
     "path_str",
-    [os.path.join(SUBJECTS_DIR, "fabian", "stats", "lh.aparc.DKTatlas.stats.short"),],
+    [os.path.join(SUBJECTS_DIR, "fabian", "stats", "lh.aparc.DKTatlas.stats.short")],
 )
 def test_read_pathlib(path_str: str):
     stats_str = CorticalParcellationStats.read(path_str)