diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index f2b72c7..6e5e99e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -37,7 +37,7 @@ jobs: env: - ci/envs/37-minimal.yaml - ci/envs/38-no-optional-deps.yaml - - ci/envs/37-pd10.yaml + - ci/envs/37-pd11.yaml - ci/envs/37-latest-defaults.yaml - ci/envs/37-latest-conda-forge.yaml - ci/envs/38-latest-conda-forge.yaml diff --git a/ci/envs/37-minimal.yaml b/ci/envs/37-minimal.yaml index 5fd0714..9df7b6d 100644 --- a/ci/envs/37-minimal.yaml +++ b/ci/envs/37-minimal.yaml @@ -6,9 +6,10 @@ dependencies: - python=3.7 # required - numpy=1.18 - - pandas==0.25 - - shapely=1.6 - - fiona=1.8.13 + - pandas==1.0.5 + - shapely=1.7 + - fiona=1.8.13.post1 + - pyproj=2.6.1.post1 - packaging #- pyproj # testing @@ -19,12 +20,9 @@ dependencies: # optional - rtree - matplotlib - - matplotlib=3.1 - # - mapclassify=2.4.0 - doesn't build due to conflicts + - matplotlib=3.2 + - mapclassify=2.4.0 - geopy - SQLalchemy - libspatialite - pyarrow - - pip - - pip: - - pyproj==2.2.2 diff --git a/ci/envs/37-pd10.yaml b/ci/envs/37-pd11.yaml similarity index 90% rename from ci/envs/37-pd10.yaml rename to ci/envs/37-pd11.yaml index 83cfb24..a58c7ec 100644 --- a/ci/envs/37-pd10.yaml +++ b/ci/envs/37-pd11.yaml @@ -4,10 +4,11 @@ channels: dependencies: - python=3.7 # required - - pandas=1.0 + - pandas=1.1 - shapely - fiona - numpy=<1.19 + - pyproj=3.1.0 #- pyproj - geos - packaging @@ -24,7 +25,6 @@ dependencies: - libspatialite - pip - pip: - - pyproj==3.0.1 - geopy - mapclassify==2.4.0 - pyarrow diff --git a/doc/source/getting_started/install.rst b/doc/source/getting_started/install.rst index a9b889d..b7daaa5 100644 --- a/doc/source/getting_started/install.rst +++ b/doc/source/getting_started/install.rst @@ -138,10 +138,10 @@ Dependencies Required dependencies: - `numpy`_ -- `pandas`_ (version 0.25 or later) -- `shapely`_ (interface to `GEOS`_) -- `fiona`_ (interface to `GDAL`_) -- `pyproj`_ (interface to `PROJ`_; version 2.2.0 or later) +- `pandas`_ (version 1.0 or later) +- `shapely`_ (interface to `GEOS`_; version 1.7 or later) +- `fiona`_ (interface to `GDAL`_; version 1.8 or later) +- `pyproj`_ (interface to `PROJ`_; version 2.6.1 or later) - `packaging`_ Further, optional dependencies are: @@ -155,7 +155,7 @@ Further, optional dependencies are: For plotting, these additional packages may be used: -- `matplotlib`_ (>= 3.1.0) +- `matplotlib`_ (>= 3.2.0) - `mapclassify`_ (>= 2.4.0) diff --git a/environment-dev.yml b/environment-dev.yml index 2487d60..981008f 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -5,10 +5,10 @@ dependencies: - python # required - fiona>=1.8 - - pandas>=0.25 + - pandas>=1.0.0 - pygeos - - pyproj>=2.2.0 - - shapely>=1.6 + - pyproj>=2.6.1.post1 + - shapely>=1.7 - packaging # testing @@ -33,10 +33,10 @@ dependencies: # geocoding - geopy # geodatabase access - - psycopg2>=2.5.1 - - SQLAlchemy>=0.8.3 + - psycopg2>=2.8.0 + - SQLAlchemy>=1.3 # plotting - - matplotlib>=2.2 + - matplotlib>=3.2 - mapclassify # spatial access methods - - rtree>=0.8 + - rtree>=0.9 diff --git a/geopandas/_compat.py b/geopandas/_compat.py index b9142e6..ab6edc2 100644 --- a/geopandas/_compat.py +++ b/geopandas/_compat.py @@ -15,7 +15,6 @@ import shapely.geos # pandas compat # ----------------------------------------------------------------------------- -PANDAS_GE_10 = Version(pd.__version__) >= Version("1.0.0") PANDAS_GE_11 = Version(pd.__version__) >= Version("1.1.0") PANDAS_GE_115 = Version(pd.__version__) >= Version("1.1.5") PANDAS_GE_12 = Version(pd.__version__) >= Version("1.2.0") @@ -28,7 +27,6 @@ PANDAS_GE_14 = Version(pd.__version__) >= Version("1.4.0rc0") # ----------------------------------------------------------------------------- -SHAPELY_GE_17 = Version(shapely.__version__) >= Version("1.7.0") SHAPELY_GE_18 = Version(shapely.__version__) >= Version("1.8") SHAPELY_GE_20 = Version(shapely.__version__) >= Version("2.0") @@ -222,9 +220,11 @@ try: except ImportError: HAS_RTREE = False + # ----------------------------------------------------------------------------- # pyproj compat # ----------------------------------------------------------------------------- PYPROJ_LT_3 = Version(pyproj.__version__) < Version("3") PYPROJ_GE_31 = Version(pyproj.__version__) >= Version("3.1") +PYPROJ_GE_32 = Version(pyproj.__version__) >= Version("3.2") diff --git a/geopandas/_vectorized.py b/geopandas/_vectorized.py index 611551f..457a81f 100644 --- a/geopandas/_vectorized.py +++ b/geopandas/_vectorized.py @@ -56,7 +56,7 @@ def isna(value): return True elif isinstance(value, float) and np.isnan(value): return True - elif compat.PANDAS_GE_10 and value is pd.NA: + elif value is pd.NA: return True else: return False diff --git a/geopandas/array.py b/geopandas/array.py index 553fb69..5f3f24b 100644 --- a/geopandas/array.py +++ b/geopandas/array.py @@ -1,4 +1,3 @@ -from collections.abc import Iterable import numbers import operator import warnings @@ -359,24 +358,19 @@ class GeometryArray(ExtensionArray): if isinstance(idx, numbers.Integral): return _geom_to_shapely(self.data[idx]) # array-like, slice - if compat.PANDAS_GE_10: - # for pandas >= 1.0, validate and convert IntegerArray/BooleanArray - # to numpy array, pass-through non-array-like indexers - idx = pd.api.indexers.check_array_indexer(self, idx) - return GeometryArray(self.data[idx], crs=self.crs) - else: - if isinstance(idx, (Iterable, slice)): - return GeometryArray(self.data[idx], crs=self.crs) - else: - raise TypeError("Index type not supported", idx) + # validate and convert IntegerArray/BooleanArray + # to numpy array, pass-through non-array-like indexers + idx = pd.api.indexers.check_array_indexer(self, idx) + return GeometryArray(self.data[idx], crs=self.crs) def __setitem__(self, key, value): - if compat.PANDAS_GE_10: - # for pandas >= 1.0, validate and convert IntegerArray/BooleanArray - # keys to numpy array, pass-through non-array-like indexers - key = pd.api.indexers.check_array_indexer(self, key) + # validate and convert IntegerArray/BooleanArray + # keys to numpy array, pass-through non-array-like indexers + key = pd.api.indexers.check_array_indexer(self, key) if isinstance(value, pd.Series): value = value.values + if isinstance(value, pd.DataFrame): + value = value.values.flatten() if isinstance(value, (list, np.ndarray)): value = from_shapely(value) if isinstance(value, GeometryArray): @@ -1054,11 +1048,10 @@ class GeometryArray(ExtensionArray): dtype ): string_values = to_wkt(self) - if compat.PANDAS_GE_10: - pd_dtype = pd.api.types.pandas_dtype(dtype) - if isinstance(pd_dtype, pd.StringDtype): - # ensure to return a pandas string array instead of numpy array - return pd.array(string_values, dtype=pd_dtype) + pd_dtype = pd.api.types.pandas_dtype(dtype) + if isinstance(pd_dtype, pd.StringDtype): + # ensure to return a pandas string array instead of numpy array + return pd.array(string_values, dtype=pd_dtype) return string_values.astype(dtype, copy=False) else: return np.array(self, dtype=dtype, copy=copy) diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py index 0b8a39a..1e3c1fd 100644 --- a/geopandas/geodataframe.py +++ b/geopandas/geodataframe.py @@ -1773,11 +1773,6 @@ individually so that features may have different properties # Overridden to fix GH1870, that return type is not preserved always # (and where it was, geometry col was not) - if not compat.PANDAS_GE_10: - raise NotImplementedError( - "GeoDataFrame.convert_dtypes requires pandas >= 1.0" - ) - return GeoDataFrame( super().convert_dtypes(*args, **kwargs), geometry=self.geometry.name, @@ -2249,5 +2244,5 @@ def _dataframe_set_geometry(self, col, drop=False, inplace=False, crs=None): DataFrame.set_geometry = _dataframe_set_geometry -if compat.PANDAS_GE_10 and not compat.PANDAS_GE_11: # i.e. on pandas 1.0.x +if not compat.PANDAS_GE_11: # i.e. on pandas 1.0.x _geodataframe_constructor_with_fallback._from_axes = GeoDataFrame._from_axes diff --git a/geopandas/io/sql.py b/geopandas/io/sql.py index 485cf93..9bd0f1d 100644 --- a/geopandas/io/sql.py +++ b/geopandas/io/sql.py @@ -369,15 +369,7 @@ def _write_postgis( try: from geoalchemy2 import Geometry except ImportError: - raise ImportError("'to_postgis()' requires geoalchemy2 package. ") - - if not compat.SHAPELY_GE_17: - raise ImportError( - "'to_postgis()' requires newer version of Shapely " - "(>= '1.7.0').\nYou can update the library using " - "'pip install shapely --upgrade' or using " - "'conda update shapely' if using conda package manager." - ) + raise ImportError("'to_postgis()' requires geoalchemy2 package.") gdf = gdf.copy() geom_name = gdf.geometry.name diff --git a/geopandas/plotting.py b/geopandas/plotting.py index 603b8b8..8555dcc 100644 --- a/geopandas/plotting.py +++ b/geopandas/plotting.py @@ -71,7 +71,7 @@ def _expand_kwargs(kwargs, multiindex): from typing import Iterable mpl = Version(matplotlib.__version__) - if mpl >= Version("3.4") or (mpl > Version("3.3.2") and "+" in mpl): + if mpl >= Version("3.4"): # alpha is supported as array argument with matplotlib 3.4+ scalar_kwargs = ["marker", "path_effects"] else: diff --git a/geopandas/tests/test_api.py b/geopandas/tests/test_api.py index 5ca9dfc..62080d9 100644 --- a/geopandas/tests/test_api.py +++ b/geopandas/tests/test_api.py @@ -1,8 +1,6 @@ import subprocess import sys -from geopandas._compat import PANDAS_GE_10 - def test_no_additional_imports(): # test that 'import geopandas' does not import any of the optional or @@ -20,10 +18,8 @@ def test_no_additional_imports(): "psycopg2", "geopy", "geoalchemy2", + "matplotlib", } - if PANDAS_GE_10: - # pandas > 0.25 stopped importing matplotlib by default - blacklist.add("matplotlib") code = """ import sys diff --git a/geopandas/tests/test_array.py b/geopandas/tests/test_array.py index 2724b24..bdba171 100644 --- a/geopandas/tests/test_array.py +++ b/geopandas/tests/test_array.py @@ -144,9 +144,7 @@ def test_from_wkb(): missing_values = [None] if not compat.USE_PYGEOS: missing_values.extend([b"", np.nan]) - - if compat.PANDAS_GE_10: - missing_values.append(pd.NA) + missing_values.append(pd.NA) res = from_wkb(missing_values) np.testing.assert_array_equal(res, np.full(len(missing_values), None)) @@ -220,9 +218,7 @@ def test_from_wkt(string_type): missing_values = [None] if not compat.USE_PYGEOS: missing_values.extend([f(""), np.nan]) - - if compat.PANDAS_GE_10: - missing_values.append(pd.NA) + missing_values.append(pd.NA) res = from_wkb(missing_values) np.testing.assert_array_equal(res, np.full(len(missing_values), None)) @@ -436,10 +432,6 @@ def test_binary_geo_scalar(attr): "is_ring", marks=[ pytest.mark.filterwarnings("ignore:is_ring:FutureWarning"), - pytest.mark.skipif( - not compat.SHAPELY_GE_17, - reason="is_ring on empty Polygon doesn't work in Shapely 1.6", - ), ], ), ], @@ -482,10 +474,6 @@ def test_unary_predicates(attr): # for is_ring we raise a warning about the value for Polygon changing @pytest.mark.filterwarnings("ignore:is_ring:FutureWarning") -@pytest.mark.skipif( - not compat.SHAPELY_GE_17, - reason="is_ring on empty Polygon doesn't work in Shapely 1.6", -) def test_is_ring(): g = [ shapely.geometry.LinearRing([(0, 0), (1, 1), (1, -1)]), @@ -766,6 +754,29 @@ def test_getitem(): assert P5.equals(points[1]) +@pytest.mark.parametrize( + "item", + [ + geopandas.GeoDataFrame( + geometry=[shapely.geometry.Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])] + ), + geopandas.GeoSeries( + [shapely.geometry.Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])] + ), + np.array([shapely.geometry.Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])]), + [shapely.geometry.Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])], + shapely.geometry.Polygon([(0, 0), (2, 0), (2, 2), (0, 2)]), + ], +) +def test_setitem(item): + points = [shapely.geometry.Point(i, i) for i in range(10)] + P = from_shapely(points) + + P[[0]] = item + + assert isinstance(P[0], shapely.geometry.Polygon) + + def test_equality_ops(): with pytest.raises(ValueError): P[:5] == P[:7] @@ -886,7 +897,6 @@ def test_isna(NA): assert t1[0] is None -@pytest.mark.skipif(not compat.PANDAS_GE_10, reason="pd.NA introduced in pandas 1.0") def test_isna_pdNA(): t1 = T.copy() t1[0] = pd.NA @@ -922,7 +932,8 @@ class TestEstimateUtmCrs: self.landmarks.estimate_utm_crs() else: assert self.landmarks.estimate_utm_crs() == CRS("EPSG:32618") - assert self.landmarks.estimate_utm_crs("NAD83") == CRS("EPSG:26918") + if compat.PYPROJ_GE_32: # result is unstable in older pyproj + assert self.landmarks.estimate_utm_crs("NAD83") == CRS("EPSG:26918") @pytest.mark.skipif(compat.PYPROJ_LT_3, reason="requires pyproj 3 or higher") def test_estimate_utm_crs__projected(self): diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py index 17a5a71..89d6f98 100644 --- a/geopandas/tests/test_geodataframe.py +++ b/geopandas/tests/test_geodataframe.py @@ -7,7 +7,6 @@ from packaging.version import Version import numpy as np import pandas as pd -import pyproj from pyproj import CRS from pyproj.exceptions import CRSError from shapely.geometry import Point, Polygon @@ -24,7 +23,6 @@ from pandas.testing import assert_frame_equal, assert_index_equal, assert_series import pytest -PYPROJ_LT_3 = Version(pyproj.__version__) < Version("3") TEST_NEAREST = compat.PYGEOS_GE_010 and compat.USE_PYGEOS pandas_133 = Version(pd.__version__) == Version("1.3.3") @@ -794,12 +792,13 @@ class TestDataFrame: assert self.df.crs == unpickled.crs def test_estimate_utm_crs(self): - if PYPROJ_LT_3: + if compat.PYPROJ_LT_3: with pytest.raises(RuntimeError, match=r"pyproj 3\+ required"): self.df.estimate_utm_crs() else: assert self.df.estimate_utm_crs() == CRS("EPSG:32618") - assert self.df.estimate_utm_crs("NAD83") == CRS("EPSG:26918") + if compat.PYPROJ_GE_32: # result is unstable in older pyproj + assert self.df.estimate_utm_crs("NAD83") == CRS("EPSG:26918") def test_to_wkb(self): wkbs0 = [ diff --git a/geopandas/tests/test_geoseries.py b/geopandas/tests/test_geoseries.py index 6d785d3..9182f03 100644 --- a/geopandas/tests/test_geoseries.py +++ b/geopandas/tests/test_geoseries.py @@ -21,7 +21,7 @@ from shapely.geometry import ( from shapely.geometry.base import BaseGeometry from geopandas import GeoSeries, GeoDataFrame, read_file, datasets, clip -from geopandas._compat import PYPROJ_LT_3, ignore_shapely2_warnings +from geopandas._compat import ignore_shapely2_warnings from geopandas.array import GeometryArray, GeometryDtype from geopandas.testing import assert_geoseries_equal @@ -29,6 +29,8 @@ from geopandas.tests.util import geom_equals from pandas.testing import assert_series_equal import pytest +import geopandas._compat as compat + class TestSeries: def setup_method(self): @@ -205,27 +207,28 @@ class TestSeries: self.landmarks.to_crs(crs=None, epsg=None) def test_estimate_utm_crs__geographic(self): - if PYPROJ_LT_3: + if compat.PYPROJ_LT_3: with pytest.raises(RuntimeError, match=r"pyproj 3\+ required"): self.landmarks.estimate_utm_crs() else: assert self.landmarks.estimate_utm_crs() == CRS("EPSG:32618") - assert self.landmarks.estimate_utm_crs("NAD83") == CRS("EPSG:26918") + if compat.PYPROJ_GE_32: # result is unstable in older pyproj + assert self.landmarks.estimate_utm_crs("NAD83") == CRS("EPSG:26918") - @pytest.mark.skipif(PYPROJ_LT_3, reason="requires pyproj 3 or higher") + @pytest.mark.skipif(compat.PYPROJ_LT_3, reason="requires pyproj 3 or higher") def test_estimate_utm_crs__projected(self): assert self.landmarks.to_crs("EPSG:3857").estimate_utm_crs() == CRS( "EPSG:32618" ) - @pytest.mark.skipif(PYPROJ_LT_3, reason="requires pyproj 3 or higher") + @pytest.mark.skipif(compat.PYPROJ_LT_3, reason="requires pyproj 3 or higher") def test_estimate_utm_crs__out_of_bounds(self): with pytest.raises(RuntimeError, match="Unable to determine UTM CRS"): GeoSeries( [Polygon([(0, 90), (1, 90), (2, 90)])], crs="EPSG:4326" ).estimate_utm_crs() - @pytest.mark.skipif(PYPROJ_LT_3, reason="requires pyproj 3 or higher") + @pytest.mark.skipif(compat.PYPROJ_LT_3, reason="requires pyproj 3 or higher") def test_estimate_utm_crs__missing_crs(self): with pytest.raises(RuntimeError, match="crs must be set"): GeoSeries([Polygon([(0, 90), (1, 90), (2, 90)])]).estimate_utm_crs() diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py index 801e88a..4d2ed82 100644 --- a/geopandas/tests/test_pandas_methods.py +++ b/geopandas/tests/test_pandas_methods.py @@ -263,11 +263,6 @@ def test_astype_invalid_geodataframe(): assert res["a"].dtype == object -@pytest.mark.xfail( - not compat.PANDAS_GE_10, - reason="Convert dtypes new in pandas 1.0", - raises=NotImplementedError, -) def test_convert_dtypes(df): # https://github.com/geopandas/geopandas/issues/1870 @@ -662,7 +657,6 @@ def test_df_apply_returning_series(df): assert_series_equal(result, df["value1"].rename(None)) -@pytest.mark.skipif(not compat.PANDAS_GE_10, reason="attrs introduced in pandas 1.0") def test_preserve_attrs(df): # https://github.com/geopandas/geopandas/issues/1654 df.attrs["name"] = "my_name" diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py index d2cac73..39cf3de 100644 --- a/geopandas/tests/test_plotting.py +++ b/geopandas/tests/test_plotting.py @@ -1384,9 +1384,6 @@ class TestPlotCollections: ) def test_points(self): - # failing with matplotlib 1.4.3 (edge stays black even when specified) - pytest.importorskip("matplotlib", "1.5.0") - from geopandas.plotting import _plot_point_collection, plot_point_collection from matplotlib.collections import PathCollection @@ -1843,11 +1840,6 @@ def _get_ax(fig, label): Previously, we did `fig.axes[1]`, but in matplotlib 3.4 the order switched and the colorbar ax was first and subplot ax second. """ - if Version(matplotlib.__version__) < Version("3.0.0"): - if label == "": - return fig.axes[1] - elif label == "": - return fig.axes[0] for ax in fig.axes: if ax.get_label() == label: return ax diff --git a/requirements-dev.txt b/requirements-dev.txt index 8adc89e..dac7d96 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,19 +1,19 @@ # required fiona>=1.8 -pandas>=0.25 -pyproj>=2.2.0 -shapely>=1.6 +pandas>=1.0.0 +pyproj>=2.6.1.post1 +shapely>=1.7 packaging # geodatabase access -psycopg2>=2.5.1 -SQLAlchemy>=0.8.3 +psycopg2>=2.8.0 +SQLAlchemy>=1.3 # geocoding geopy # plotting -matplotlib>=2.2 +matplotlib>=3.2 mapclassify # testing @@ -22,7 +22,7 @@ pytest-cov codecov # spatial access methods -rtree>=0.8 +rtree>=0.9 # styling black diff --git a/setup.py b/setup.py index 2a58918..3ed6d86 100644 --- a/setup.py +++ b/setup.py @@ -30,10 +30,10 @@ if os.environ.get("READTHEDOCS", False) == "True": INSTALL_REQUIRES = [] else: INSTALL_REQUIRES = [ - "pandas >= 0.25.0", - "shapely >= 1.6", + "pandas >= 1.0.0", + "shapely >= 1.7", "fiona >= 1.8", - "pyproj >= 2.2.0", + "pyproj >= 2.6.1.post1", "packaging", ]