Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Added geodataframe support #118

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nptyping/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from nptyping.ndarray import NDArray
from nptyping.package_info import __version__
from nptyping.pandas_.dataframe import DataFrame
from nptyping.geopandas_.geodataframe import GeoDataFrame
from nptyping.recarray import RecArray
from nptyping.shape import Shape
from nptyping.shape_expression import (
Expand Down Expand Up @@ -185,4 +186,5 @@
"Unicode",
"Str0",
"DataFrame",
"GeoDataFrame",
]
Empty file added nptyping/geopandas_/__init__.py
Empty file.
116 changes: 116 additions & 0 deletions nptyping/geopandas_/geodataframe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""
MIT License

Copyright (c) 2023 Ramon Hagenaars

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import inspect
from abc import ABC
from typing import Any, Tuple

import numpy as np

from nptyping import InvalidArgumentsError
from nptyping.base_meta_classes import (
FinalMeta,
ImmutableMeta,
InconstructableMeta,
MaybeCheckableMeta,
PrintableMeta,
SubscriptableMeta,
)
from nptyping.error import DependencyError
from nptyping.nptyping_type import NPTypingType
from nptyping.geopandas_.typing_ import dtype_per_name
from nptyping.pandas_.dataframe import DataFrameMeta
from nptyping.structure import Structure
from nptyping.structure_expression import check_structure

try:
import geopandas as gpd
except ImportError: # pragma: no cover
gpd = None # type: ignore[misc, assignment]


class GeoDataFrameMeta(
DataFrameMeta,
SubscriptableMeta,
InconstructableMeta,
ImmutableMeta,
FinalMeta,
MaybeCheckableMeta,
PrintableMeta,
implementation="GeoDataFrame",
):
"""
Metaclass that is coupled to nptyping.GeoDataFrame. It contains all actual logic
such as instance checking.
"""

def __instancecheck__( # pylint: disable=bad-mcs-method-argument
self, instance: Any
) -> bool:
structure = self.__args__[0]

if gpd is None:
raise DependencyError( # pragma: no cover
"GeoPandas needs to be installed for instance checking."
)

if not isinstance(instance, gpd.GeoDataFrame):
return False

if structure is Any:
return True

structured_dtype = np.dtype(
[(column, dtype.str) for column, dtype in instance.dtypes.items()]
)
return check_structure(structured_dtype, structure, dtype_per_name)

@property
def __module__(cls) -> str:
return cls._get_module(inspect.stack(), "nptyping.geopandas_.geodataframe")


class GeoDataFrame(NPTypingType, ABC, metaclass=GeoDataFrameMeta):
"""
An nptyping equivalent of geopandas GeoDataFrame.

## No arguments means a GeoDataFrame of any structure.
>>> GeoDataFrame
GeoDataFrame[Any]

## You can use Structure Expression.
>>> from nptyping import GeoDataFrame, Structure
>>> GeoDataFrame[Structure["x: Int, y: Int"]]
GeoDataFrame[Structure['[x, y]: Int']]

## Instance checking can be done and the structure is also checked.
>>> import geopandas as gpd
>>> gdf = gpd.GeoDataFrame({'x': [1, 2, 3], 'y': [4., 5., 6.]})
>>> isinstance(gdf, GeoDataFrame[Structure['x: Int, y: Float']])
True
>>> isinstance(gdf, GeoDataFrame[Structure['x: Float, y: Int']])
False

"""

__args__ = (Any,)
27 changes: 27 additions & 0 deletions nptyping/geopandas_/geodataframe.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""
MIT License

Copyright (c) 2023 Ramon Hagenaars

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

import geopandas as gpd

GeoDataFrame = gpd.GeoDataFrame
34 changes: 34 additions & 0 deletions nptyping/geopandas_/typing_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

"""
MIT License

Copyright (c) 2023 Ramon Hagenaars

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from nptyping.typing_ import Object
from nptyping.typing_ import dtype_per_name as dtype_per_name_default

dtype_per_name = {
**dtype_per_name_default, # type: ignore[arg-type]
# Override the `String` and `Str` to point to `Object`. Pandas uses Object
# for string types in Dataframes and Series.
"String": Object,
"Str": Object,
}
Empty file added tests/geopandas_/__init__.py
Empty file.
75 changes: 75 additions & 0 deletions tests/geopandas_/test_dataframe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from typing import Any
from unittest import TestCase

import geopandas as gpd
from nptyping import InvalidArgumentsError, GeoDataFrame
from nptyping import Structure as S
from nptyping.typing_ import Literal as L
from shapely.geometry import Point


class DataframeTest(TestCase):
def test_isinstance_success(self):
gdf = gpd.GeoDataFrame(
{
"x": [1, 2, 3],
"y": [2.0, 3.0, 4.0],
"z": [Point(0, 0), Point(1, 1), Point(2, 2)],
}
)

self.assertIsInstance(gdf, GeoDataFrame[S["x: Int, y: Float, z: Obj"]])

def test_isinstance_any(self):
gdf = gpd.GeoDataFrame(
{
"x": [1, 2, 3],
"y": [2.0, 3.0, 4.0],
"z": ["a", "b", "c"],
}
)

self.assertIsInstance(gdf, GeoDataFrame[Any])

def test_isinstance_fail(self):
gdf = gpd.GeoDataFrame(
{
"x": [1, 2, 3],
"y": [2.0, 3.0, 4.0],
"z": ["a", "b", "c"],
}
)

self.assertNotIsInstance(gdf, GeoDataFrame[S["x: Float, y: Int, z: Obj"]])

def test_string_is_aliased(self):
gdf = gpd.GeoDataFrame(
{
"x": ["a", "b", "c"],
"y": ["d", "e", "f"],
}
)

self.assertIsInstance(gdf, GeoDataFrame[S["x: Str, y: String"]])

def test_isinstance_fail_with_random_type(self):
self.assertNotIsInstance(42, GeoDataFrame[S["x: Float, y: Int, z: Obj"]])

def test_literal_is_allowed(self):
GeoDataFrame[L["x: Int, y: Int"]]

def test_string_is_not_allowed(self):
with self.assertRaises(InvalidArgumentsError):
GeoDataFrame["x: Int, y: Int"]

def test_repr(self):
self.assertEqual(
"GeoDataFrame[Structure['[x, y]: Int']]", repr(GeoDataFrame[S["x: Int, y: Int"]])
)
self.assertEqual("GeoDataFrame[Any]", repr(GeoDataFrame))
self.assertEqual("GeoDataFrame[Any]", repr(GeoDataFrame[Any]))

def test_str(self):
self.assertEqual("GeoDataFrame[[x, y]: Int]", str(GeoDataFrame[S["x: Int, y: Int"]]))
self.assertEqual("GeoDataFrame[Any]", str(GeoDataFrame))
self.assertEqual("GeoDataFrame[Any]", str(GeoDataFrame[Any]))