diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c989d30..3485e94 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - id: mixed-line-ending - id: check-case-conflict - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.3 # must match requirements-tests.txt + rev: v0.4.5 # must match requirements-tests.txt hooks: - id: ruff - id: ruff-format diff --git a/README.md b/README.md index 6b97eb5..e68f56e 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ This project is licensed under the MIT License. Complete Yes - Partially + Yes shapely diff --git a/requirements-tests.txt b/requirements-tests.txt index e3a6f84..76a5303 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,9 +1,9 @@ # Tools # ----- -ruff==0.4.3 # must match .pre-commit-config.yaml +ruff==0.4.5 # must match .pre-commit-config.yaml pytest>=8.0 mypy==1.10.0 -pyright==1.1.361 +pyright==1.1.364 # Runtime dependencies # -------------------- @@ -15,10 +15,10 @@ geopandas>=0.14.4,<1.0 # shapely pyproj>=3.6.1 # geopandas -pandas-stubs>=2.2.1.240316 +pandas-stubs>=2.2.2.240514 matplotlib>=3.8.0 -folium>=0.15.1 -rtree>=1.1.0 +folium>=0.16.0 +rtree>=1.2.0 # netfields and psqlextra django-types>=0.19.1 djangorestframework-types>=0.8.0 diff --git a/stubs/geopandas-stubs/geodataframe.pyi b/stubs/geopandas-stubs/geodataframe.pyi index 339a48b..1518fba 100644 --- a/stubs/geopandas-stubs/geodataframe.pyi +++ b/stubs/geopandas-stubs/geodataframe.pyi @@ -1,6 +1,7 @@ import os from _typeshed import Incomplete, SupportsGetItem, SupportsRead, SupportsWrite -from collections.abc import Callable, Generator, Hashable, Iterable, Mapping, Sequence +from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence +from json import JSONEncoder from typing import Any, Literal, Protocol, overload, type_check_only from typing_extensions import Self, TypeAlias, deprecated @@ -21,7 +22,8 @@ from geopandas.plotting import GeoplotAccessor from geopandas.tools.clip import _Mask as _ClipMask # XXX: cannot use pd.Series[Geometry] because of pd.Series type variable bounds -_GeometryColumn: TypeAlias = Hashable | Sequence[Geometry] | NDArray[np.object_] | pd.Series[Any] | GeometryArray | GeoSeries +_GeomSeq: TypeAlias = Sequence[Geometry] | NDArray[np.object_] | pd.Series[Any] | GeometryArray | GeoSeries +_GeomCol: TypeAlias = Hashable | _GeomSeq # name of column or column values _ConvertibleToDataFrame: TypeAlias = ( ListLikeU | pd.DataFrame | dict[Any, Any] | Iterable[ListLikeU | tuple[Hashable, ListLikeU] | dict[Any, Any]] ) @@ -44,7 +46,7 @@ class GeoDataFrame(GeoPandasBase, pd.DataFrame): # type: ignore[misc] dtype: Dtype | None = None, copy: bool | None = None, *, - geometry: _GeometryColumn | None = None, + geometry: _GeomCol | None = None, crs: _ConvertibleToCRS | None = None, ) -> Self: ... @overload @@ -56,7 +58,7 @@ class GeoDataFrame(GeoPandasBase, pd.DataFrame): # type: ignore[misc] dtype: Dtype | None = None, copy: bool | None = None, *, - geometry: _GeometryColumn | None = None, + geometry: _GeomCol | None = None, crs: _ConvertibleToCRS | None = None, ) -> Self: ... def __init__( @@ -67,25 +69,35 @@ class GeoDataFrame(GeoPandasBase, pd.DataFrame): # type: ignore[misc] dtype: Dtype | None = None, copy: bool | None = None, *, - geometry: _GeometryColumn | None = None, + geometry: _GeomCol | None = None, crs: _ConvertibleToCRS | None = None, ) -> None: ... def __setattr__(self, attr: str, val: Any) -> None: ... @property def geometry(self) -> GeoSeries: ... @geometry.setter - def geometry(self, col: _GeometryColumn) -> None: ... + def geometry(self, col: _GeomSeq) -> None: ... + @overload def set_geometry( - self, col: _GeometryColumn, drop: bool = False, inplace: bool = False, crs: _ConvertibleToCRS | None = None - ): ... - def rename_geometry(self, col: Hashable, inplace: bool = False) -> Self: ... + self, col: _GeomCol, drop: bool = False, inplace: Literal[False] = False, crs: _ConvertibleToCRS | None = None + ) -> Self: ... + @overload + def set_geometry( + self, col: _GeomCol, drop: bool = False, *, inplace: Literal[True], crs: _ConvertibleToCRS | None = None + ) -> None: ... + @overload + def set_geometry(self, col: _GeomCol, drop: bool, inplace: Literal[True], crs: _ConvertibleToCRS | None = None) -> None: ... + @overload + def rename_geometry(self, col: Hashable, inplace: Literal[False] = False) -> Self: ... + @overload + def rename_geometry(self, col: Hashable, inplace: Literal[True]) -> None: ... @property def crs(self) -> CRS | None: ... @crs.setter - def crs(self, value: _ConvertibleToCRS) -> None: ... + def crs(self, value: _ConvertibleToCRS | None) -> None: ... @classmethod def from_dict( # type: ignore[override] - cls, data: Mapping[Hashable, Any], geometry: _GeometryColumn | None = None, crs: _ConvertibleToCRS | None = None, **kwargs + cls, data: Mapping[Hashable, Any], geometry: _GeomCol | None = None, crs: _ConvertibleToCRS | None = None, **kwargs ) -> Self: ... @classmethod def from_file( @@ -97,7 +109,7 @@ class GeoDataFrame(GeoPandasBase, pd.DataFrame): # type: ignore[misc] rows: int | slice | None = None, engine: Literal["fiona", "pyogrio"] | None = None, ignore_geometry: Literal[False] = False, - **kwargs: Any, # depends on engine + **kwargs, # engine dependent ) -> Self: ... @classmethod def from_features( @@ -124,7 +136,7 @@ class GeoDataFrame(GeoPandasBase, pd.DataFrame): # type: ignore[misc] params: list[Scalar] | tuple[Scalar, ...] | Mapping[str, Scalar] | None = None, *, chunksize: int, - ) -> Generator[GeoDataFrame, None, None]: ... + ) -> Iterator[GeoDataFrame]: ... @overload @classmethod def from_postgis( @@ -140,15 +152,48 @@ class GeoDataFrame(GeoPandasBase, pd.DataFrame): # type: ignore[misc] chunksize: None = None, ) -> GeoDataFrame: ... def to_json( # type: ignore[override] - self, na: str = "null", show_bbox: bool = False, drop_id: bool = False, to_wgs84: bool = False, **kwargs + self, + na: str = "null", + show_bbox: bool = False, + drop_id: bool = False, + to_wgs84: bool = False, + *, + # json.dumps kwargs + skipkeys: bool = False, + ensure_ascii: bool = True, + check_circular: bool = True, + allow_nan: bool = True, + cls: type[JSONEncoder] | None = None, + indent: int | str | None = None, + separators: tuple[str, str] | None = None, + default: Callable[[Any], Any] | None = None, + sort_keys: bool = False, + **kwargs, ) -> str: ... @property def __geo_interface__(self) -> dict[str, Any]: ... - def iterfeatures( - self, na: str = "null", show_bbox: bool = False, drop_id: bool = False - ) -> Generator[dict[str, Any], None, None]: ... - def to_wkb(self, hex: bool = False, **kwargs) -> pd.DataFrame: ... - def to_wkt(self, **kwargs) -> pd.DataFrame: ... + def iterfeatures(self, na: str = "null", show_bbox: bool = False, drop_id: bool = False) -> Iterator[dict[str, Any]]: ... + def to_wkb( + self, + hex: bool = False, + *, + # shapely kwargs + output_dimension: int = ..., + byte_order: int = ..., + include_srid: bool = ..., + flavor: Literal["iso", "extended"] = ..., + **kwargs, + ) -> pd.DataFrame: ... + def to_wkt( + self, + *, + # shapely kwargs + rounding_precision: int = ..., + trim: bool = ..., + output_dimension: int = ..., + old_3d: bool = ..., + **kwargs, + ) -> pd.DataFrame: ... def to_parquet( # type: ignore[override] self, path: str | os.PathLike[str] | SupportsWrite[Incomplete], @@ -184,15 +229,35 @@ class GeoDataFrame(GeoPandasBase, pd.DataFrame): # type: ignore[misc] overwrite: bool | Incomplete = ..., # TODO can it be None? (accepted by fiona, not sure about pyogrio) **kwargs: Any, # engine and driver dependent ) -> None: ... + @overload def set_crs( - self, crs: _ConvertibleToCRS | None = None, epsg: int | None = None, inplace: bool = False, allow_override: bool = False + self, crs: _ConvertibleToCRS, epsg: int | None = None, inplace: bool = False, allow_override: bool = False ) -> Self: ... - def to_crs(self, crs: _ConvertibleToCRS | None = None, epsg: int | None = None, inplace: bool = False) -> Self | None: ... + @overload + def set_crs( + self, crs: _ConvertibleToCRS | None = None, *, epsg: int, inplace: bool = False, allow_override: bool = False + ) -> Self: ... + @overload + def set_crs(self, crs: _ConvertibleToCRS | None, epsg: int, inplace: bool = False, allow_override: bool = False) -> Self: ... + @overload + def to_crs(self, crs: _ConvertibleToCRS, epsg: int | None = None, inplace: Literal[False] = False) -> Self: ... + @overload + def to_crs(self, crs: _ConvertibleToCRS | None = None, *, epsg: int, inplace: Literal[False] = False) -> Self: ... + @overload + def to_crs(self, crs: _ConvertibleToCRS | None, epsg: int, inplace: Literal[False] = False) -> Self: ... + @overload + def to_crs(self, crs: _ConvertibleToCRS, epsg: int | None = None, *, inplace: Literal[True]) -> None: ... + @overload + def to_crs(self, crs: _ConvertibleToCRS, epsg: int | None, inplace: Literal[True]) -> None: ... + @overload + def to_crs(self, crs: _ConvertibleToCRS | None = None, *, epsg: int, inplace: Literal[True]) -> None: ... + @overload + def to_crs(self, crs: _ConvertibleToCRS | None, epsg: int, inplace: Literal[True]) -> None: ... def estimate_utm_crs(self, datum_name: str = "WGS 84") -> CRS: ... # def __getitem__(self, key): ... # def __setitem__(self, key, value) -> None: ... def copy(self, deep: bool = True) -> Self: ... - def merge(self, *args, **kwargs) -> GeoDataFrame | pd.DataFrame: ... + # def merge(self, *args, **kwargs) -> GeoDataFrame | pd.DataFrame: ... def apply( # type: ignore[override] self, func: Callable[..., Incomplete], @@ -245,18 +310,26 @@ class GeoDataFrame(GeoPandasBase, pd.DataFrame): # type: ignore[misc] chunksize: int | None = None, dtype: dict[Any, Incomplete] | None = None, ) -> None: ... - @deprecated("'^' operator is deprecated. Use method `symmetric_difference` instead.") + @deprecated("'^' operator is deprecated. Use the `symmetric_difference` method instead.") def __xor__(self, other: GeoSeries | Geometry) -> GeoSeries: ... # type: ignore[override] - @deprecated("'|' operator is deprecated. Use method `union` instead.") + @deprecated("'|' operator is deprecated. Use the `union` method instead.") def __or__(self, other: GeoSeries | Geometry) -> GeoSeries: ... # type: ignore[override] - @deprecated("'&' operator is deprecated. Use method `intersection` instead.") + @deprecated("'&' operator is deprecated. Use the `intersection` method instead.") def __and__(self, other: GeoSeries | Geometry) -> GeoSeries: ... # type: ignore[override] - @deprecated("'-' operator is deprecated. Use method `difference` instead.") + @deprecated("'-' operator is deprecated. Use the `difference` method instead.") def __sub__(self, other: GeoSeries | Geometry) -> GeoSeries: ... # type: ignore[override] @property def plot(self) -> GeoplotAccessor: ... explore = _explore - def sjoin(self, df: GeoDataFrame, *args, **kwargs) -> GeoDataFrame: ... + def sjoin( + self, + df: GeoDataFrame, + # *args, **kwargs passed to geopandas.sjoin + how: str = "inner", + predicate: str = "intersects", + lsuffix: str = "left", + rsuffix: str = "right", + ) -> GeoDataFrame: ... def sjoin_nearest( self, right: GeoDataFrame, diff --git a/stubs/geopandas-stubs/geoseries.pyi b/stubs/geopandas-stubs/geoseries.pyi index 2bc4736..99dc445 100644 --- a/stubs/geopandas-stubs/geoseries.pyi +++ b/stubs/geopandas-stubs/geoseries.pyi @@ -2,7 +2,7 @@ import json import os from _typeshed import Incomplete, SupportsRead, SupportsWrite, Unused from collections.abc import Callable, Hashable, Mapping, Sequence -from typing import Any, Literal, overload +from typing import Any, Literal, final, overload from typing_extensions import Self, TypeAlias, deprecated import pandas as pd @@ -25,7 +25,6 @@ _GeoListLike: TypeAlias = ArrayLike | Sequence[Geometry] | IndexOpsMixin[Incompl _ConvertibleToGeoSeries: TypeAlias = Geometry | Mapping[int, Geometry] | Mapping[str, Geometry] | _GeoListLike class GeoSeries(GeoPandasBase, pd.Series[BaseGeometry]): # type: ignore[type-var,misc] # pyright: ignore[reportInvalidTypeArguments] - crs: CRS # Override the weird annotation of Series.__new__ in pandas-stubs def __new__( self, @@ -49,6 +48,8 @@ class GeoSeries(GeoPandasBase, pd.Series[BaseGeometry]): # type: ignore[type-va copy: bool | None = None, fastpath: bool = False, ) -> None: ... + @final + def copy(self, deep: bool = True) -> Self: ... # to override pandas definition @property def values(self) -> GeometryArray: ... @deprecated("Method `Series.append()` has been removed in pandas version '2.0'.") @@ -71,7 +72,7 @@ class GeoSeries(GeoPandasBase, pd.Series[BaseGeometry]): # type: ignore[type-va rows: int | slice | None = None, engine: Literal["fiona", "pyogrio"] | None = None, ignore_geometry: Literal[False] = False, - **kwargs: Any, # depends on engine + **kwargs: Any, # engine dependent ) -> GeoSeries: ... @classmethod def from_wkb( @@ -149,10 +150,22 @@ class GeoSeries(GeoPandasBase, pd.Series[BaseGeometry]): # type: ignore[type-va plot = plot_series # type: ignore[assignment] # pyright: ignore explore = _explore_geoseries def explode(self, ignore_index: bool = False, index_parts: bool | None = None) -> GeoSeries: ... + @overload + def set_crs( + self, crs: _ConvertibleToCRS, epsg: int | None = None, inplace: bool = False, allow_override: bool = False + ) -> Self: ... + @overload def set_crs( - self, crs: _ConvertibleToCRS | None = None, epsg: int | None = None, inplace: bool = False, allow_override: bool = False + self, crs: _ConvertibleToCRS | None = None, *, epsg: int, inplace: bool = False, allow_override: bool = False ) -> Self: ... - def to_crs(self, crs: _ConvertibleToCRS | None = None, epsg: int | None = None) -> GeoSeries: ... + @overload + def set_crs(self, crs: _ConvertibleToCRS | None, epsg: int, inplace: bool = False, allow_override: bool = False) -> Self: ... + @overload + def to_crs(self, crs: _ConvertibleToCRS, epsg: int | None = None) -> GeoSeries: ... + @overload + def to_crs(self, crs: _ConvertibleToCRS | None = None, *, epsg: int) -> GeoSeries: ... + @overload + def to_crs(self, crs: _ConvertibleToCRS | None, epsg: int) -> GeoSeries: ... def estimate_utm_crs(self, datum_name: str = "WGS 84") -> CRS: ... def to_json( # type: ignore[override] self, diff --git a/stubs/geopandas-stubs/io/sql.pyi b/stubs/geopandas-stubs/io/sql.pyi index 7e0723a..30894e9 100644 --- a/stubs/geopandas-stubs/io/sql.pyi +++ b/stubs/geopandas-stubs/io/sql.pyi @@ -1,5 +1,5 @@ import sqlite3 -from collections.abc import Generator, Mapping +from collections.abc import Iterator, Mapping from contextlib import AbstractContextManager from typing import Any, Protocol, overload from typing_extensions import TypeAlias, deprecated @@ -85,7 +85,7 @@ def _read_postgis( params: list[Scalar] | tuple[Scalar, ...] | Mapping[str, Scalar] | None = None, *, chunksize: int, -) -> Generator[GeoDataFrame, None, None]: ... +) -> Iterator[GeoDataFrame]: ... @overload def _read_postgis( sql: str, @@ -109,7 +109,7 @@ def _read_postgis( parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = None, params: list[Scalar] | tuple[Scalar, ...] | Mapping[str, Scalar] | None = None, chunksize: int | None = None, -) -> GeoDataFrame | Generator[GeoDataFrame, None, None]: ... +) -> GeoDataFrame | Iterator[GeoDataFrame]: ... @deprecated("Function `geopandas.io.sql.read_postgis()` is deprecated. Use `geopandas.read_postgis()` instead.") def read_postgis( sql: str, @@ -121,4 +121,4 @@ def read_postgis( parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = None, params: list[Scalar] | tuple[Scalar, ...] | Mapping[str, Scalar] | None = None, chunksize: int | None = None, -) -> GeoDataFrame | Generator[GeoDataFrame, None, None]: ... +) -> GeoDataFrame | Iterator[GeoDataFrame]: ... diff --git a/tests/geopandas/__init__.py b/tests/geopandas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/geopandas/test_geodataframe.py b/tests/geopandas/test_geodataframe.py new file mode 100644 index 0000000..30e9291 --- /dev/null +++ b/tests/geopandas/test_geodataframe.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from types import NoneType + +import geopandas as gpd +import pytest +from geopandas import GeoDataFrame, GeoSeries +from pyproj import CRS +from shapely import LineString, Point, Polygon +from typing_extensions import assert_type + +from tests import check + +P = Point(1, 2) +LS = LineString([(0, 0), (1, 1)]) +PO: Polygon = P.buffer(1) +GDF = gpd.GeoDataFrame({"x": [1, 2], "geometry": [Point(1, 2), Point(3, 4)]}) + + +def test_geometry() -> None: + gdf = GDF.copy() + geo = gdf.geometry + + # getter + check(assert_type(gdf.geometry, GeoSeries), GeoSeries) + + # setter + gdf.geometry = geo + gdf.geometry = geo.values # type: ignore[assignment] # https://github.com/python/mypy/issues/3004 + gdf.geometry = [Point(1, 2), Point(3, 4)] # type: ignore[assignment] # https://github.com/python/mypy/issues/3004 + with pytest.raises(Exception): + gdf.geometry = "geometry" # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue] + with pytest.raises(Exception): + gdf.geometry = [1, 2] # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue] + + # set_geometry + check(assert_type(gdf.set_geometry(geo), GeoDataFrame), GeoDataFrame) + check(assert_type(gdf.set_geometry(geo, inplace=True), None), NoneType) + check(assert_type(gdf.set_geometry(geo, False, True), None), NoneType) + check(assert_type(gdf.set_geometry([Point(1, 2), Point(3, 4)]), GeoDataFrame), GeoDataFrame) + check(assert_type(gdf.set_geometry("geometry"), GeoDataFrame), GeoDataFrame) + with pytest.raises(Exception): + gdf.set_geometry([1, 2]) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] + + # rename_geometry + check(assert_type(gdf.rename_geometry("geom"), GeoDataFrame), GeoDataFrame) + check(assert_type(gdf.rename_geometry("geom", inplace=True), None), NoneType) + check(assert_type(gdf.rename_geometry("geometry", True), None), NoneType) + + +def test_crs() -> None: + gdf = GDF.copy() + crs = CRS("EPSG:4326") + + # getter + check(assert_type(gdf.crs, CRS | None), NoneType) + gdf.set_crs(crs, inplace=True) + check(assert_type(gdf.crs, CRS | None), CRS) + + # setter + gdf.crs = None + gdf.crs = crs + gdf.crs = "EPSG:4326" # type: ignore[assignment] # https://github.com/python/mypy/issues/3004 + gdf.crs = 4326 # type: ignore[assignment] # https://github.com/python/mypy/issues/3004 + with pytest.raises(Exception): + gdf.crs = 1.5 # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue] + + # set_crs + check(assert_type(gdf.set_crs(crs), GeoDataFrame), GeoDataFrame) + check(assert_type(gdf.set_crs(crs, inplace=True), GeoDataFrame), GeoDataFrame) + check(assert_type(gdf.set_crs(crs, None, True), GeoDataFrame), GeoDataFrame) + check(assert_type(gdf.set_crs("EPSG:4326"), GeoDataFrame), GeoDataFrame) + check(assert_type(gdf.set_crs(crs=4326), GeoDataFrame), GeoDataFrame) + check(assert_type(gdf.set_crs(epsg=4326), GeoDataFrame), GeoDataFrame) + with pytest.raises(Exception): + gdf.set_crs() # type: ignore[call-overload] # pyright: ignore[reportCallIssue] + with pytest.raises(Exception): + gdf.set_crs(None) # type: ignore[call-overload] # pyright: ignore[reportArgumentType] + with pytest.raises(Exception): + gdf.set_crs(None, None) # type: ignore[call-overload] # pyright: ignore[reportArgumentType,reportCallIssue] + + # to_crs + check(assert_type(gdf.to_crs(crs), GeoDataFrame), GeoDataFrame) + check(assert_type(gdf.to_crs(crs, inplace=True), None), NoneType) + check(assert_type(gdf.to_crs(crs, None, True), None), NoneType) + check(assert_type(gdf.to_crs("EPSG:4326"), GeoDataFrame), GeoDataFrame) + check(assert_type(gdf.to_crs(crs=4326), GeoDataFrame), GeoDataFrame) + check(assert_type(gdf.to_crs(epsg=4326), GeoDataFrame), GeoDataFrame) + with pytest.raises(Exception): + gdf.to_crs() # type: ignore[call-overload] # pyright: ignore[reportCallIssue] + with pytest.raises(Exception): + gdf.to_crs(None) # type: ignore[call-overload] # pyright: ignore[reportArgumentType] + with pytest.raises(Exception): + gdf.to_crs(None, None) # type: ignore[call-overload] # pyright: ignore[reportArgumentType,reportCallIssue] + with pytest.raises(Exception): + gdf.to_crs(inplace=True) # type: ignore[call-overload] # pyright: ignore[reportCallIssue] + with pytest.raises(Exception): + gdf.to_crs(None, inplace=True) # type: ignore[call-overload] # pyright: ignore[reportArgumentType,reportCallIssue] + with pytest.raises(Exception): + gdf.to_crs(None, None, inplace=True) # type: ignore[call-overload] # pyright: ignore[reportArgumentType,reportCallIssue] + with pytest.raises(Exception): + gdf.to_crs(None, None, True) # type: ignore[call-overload] # pyright: ignore[reportArgumentType,reportCallIssue] + + # estimate_utm_crs + check(assert_type(gdf.estimate_utm_crs(), CRS), CRS) + check(assert_type(gdf.estimate_utm_crs("WGS 84"), CRS), CRS) + with pytest.raises(Exception): + gdf.estimate_utm_crs(84) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] + with pytest.raises(Exception): + gdf.estimate_utm_crs(CRS) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] diff --git a/tests/geopandas/test_geoseries.py b/tests/geopandas/test_geoseries.py new file mode 100644 index 0000000..14bd722 --- /dev/null +++ b/tests/geopandas/test_geoseries.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from types import NoneType + +import geopandas as gpd +import pytest +from geopandas import GeoSeries +from pyproj import CRS +from shapely import LineString, Point, Polygon +from typing_extensions import assert_type + +from tests import check + +P = Point(1, 2) +LS = LineString([(0, 0), (1, 1)]) +PO: Polygon = P.buffer(1) +GS = gpd.GeoSeries([Point(1, 2), Point(3, 4)]) + + +def test_geometry() -> None: + gs = GS.copy() + + # getter + check(assert_type(gs.geometry, GeoSeries), GeoSeries) + + +def test_crs() -> None: + gs = GS.copy() + crs = CRS("EPSG:4326") + + # getter + check(assert_type(gs.crs, CRS | None), NoneType) + gs.set_crs(crs, inplace=True) + check(assert_type(gs.crs, CRS | None), CRS) + + # setter + gs.crs = None + gs.crs = crs + gs.crs = "EPSG:4326" # type: ignore[assignment] # https://github.com/python/mypy/issues/3004 + gs.crs = 4326 # type: ignore[assignment] # https://github.com/python/mypy/issues/3004 + assert isinstance(gs.crs, CRS) + with pytest.raises(Exception): + gs.crs = 1.5 # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue] + + # set_crs + check(assert_type(gs.set_crs(crs), GeoSeries), GeoSeries) + check(assert_type(gs.set_crs(crs, inplace=True), GeoSeries), GeoSeries) + check(assert_type(gs.set_crs(crs, None, True), GeoSeries), GeoSeries) + check(assert_type(gs.set_crs("EPSG:4326"), GeoSeries), GeoSeries) + check(assert_type(gs.set_crs(crs=4326), GeoSeries), GeoSeries) + check(assert_type(gs.set_crs(epsg=4326), GeoSeries), GeoSeries) + with pytest.raises(Exception): + gs.set_crs() # type: ignore[call-overload] # pyright: ignore[reportCallIssue] + with pytest.raises(Exception): + gs.set_crs(None) # type: ignore[call-overload] # pyright: ignore[reportArgumentType] + with pytest.raises(Exception): + gs.set_crs(None, None) # type: ignore[call-overload] # pyright: ignore[reportArgumentType,reportCallIssue] + + # to_crs + check(assert_type(gs.to_crs(crs), GeoSeries), GeoSeries) + check(assert_type(gs.to_crs("EPSG:4326"), GeoSeries), GeoSeries) + check(assert_type(gs.to_crs(crs=4326), GeoSeries), GeoSeries) + check(assert_type(gs.to_crs(epsg=4326), GeoSeries), GeoSeries) + with pytest.raises(Exception): + gs.to_crs() # type: ignore[call-overload] # pyright: ignore[reportCallIssue] + with pytest.raises(Exception): + gs.to_crs(None) # type: ignore[call-overload] # pyright: ignore[reportArgumentType] + with pytest.raises(Exception): + gs.to_crs(None, None) # type: ignore[call-overload] # pyright: ignore[reportArgumentType,reportCallIssue] + + # estimate_utm_crs + check(assert_type(gs.estimate_utm_crs(), CRS), CRS) + check(assert_type(gs.estimate_utm_crs("WGS 84"), CRS), CRS) + with pytest.raises(Exception): + gs.estimate_utm_crs(84) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] + with pytest.raises(Exception): + gs.estimate_utm_crs(CRS) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] diff --git a/tests/geopandas/test_io.py b/tests/geopandas/test_io.py new file mode 100644 index 0000000..6f6824d --- /dev/null +++ b/tests/geopandas/test_io.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from collections import OrderedDict +from pathlib import Path +from typing import TYPE_CHECKING + +import geopandas as gpd +import pandas as pd +import pytest +from geopandas.io.file import infer_schema +from shapely import LineString, Point, Polygon +from typing_extensions import assert_type + +from tests import check + +if TYPE_CHECKING: + from geopandas.io.file import _Schema +else: + _Schema = dict + +P = Point(1, 2) +LS = LineString([(0, 0), (1, 1)]) +PO: Polygon = P.buffer(1) +GDF = gpd.GeoDataFrame({"x": [1, 2], "geometry": [Point(1, 2), Point(3, 4)]}) + + +def test_read_file(tmp_path: Path) -> None: + file = tmp_path / "test.gpkg" + GDF.to_file(file, driver="GPKG", layer="test") + gdf = gpd.read_file(file) + check(assert_type(gdf, gpd.GeoDataFrame), gpd.GeoDataFrame) + df = gpd.read_file(file, ignore_geometry=True) + check(assert_type(df, pd.DataFrame), pd.DataFrame) + assert not isinstance(df, gpd.GeoDataFrame) + + with pytest.raises(Exception): + gpd.read_file(file, engine="toto") # type: ignore[call-overload] # pyright: ignore[reportArgumentType] + + +def test_infer_schema() -> None: + schema = infer_schema(GDF) + heterogeneous_schema = infer_schema(gpd.GeoDataFrame({"x": [1, 2], "geometry": [P, LS]})) + check(assert_type(schema, _Schema), _Schema, dtype=str) + check(assert_type(schema["geometry"], str | list[str]), str) + check(assert_type(heterogeneous_schema["geometry"], str | list[str]), list, dtype=str) + check(assert_type(schema["properties"], OrderedDict[str, str]), OrderedDict, dtype=str) + properties_values = list(schema["properties"].values()) + check(assert_type(properties_values, list[str]), list, dtype=str)