Skip to content
New issue

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

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

Already on GitHub? # to your account

Maintenance fixes #202

Merged
merged 9 commits into from
Feb 7, 2025
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ jobs:
if: steps.conda_cache.outputs.cache-hit != 'true'
with:
miniforge-version: latest
channels: conda-forge,defaults
activate-environment: ""

- name: Dump Conda Environment Info
Expand Down
6 changes: 3 additions & 3 deletions dev-env.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: odc-geo
channels:
- nodefaults
- conda-forge

dependencies:
Expand All @@ -23,15 +24,14 @@ dependencies:
## linting tools
- autopep8
- autoflake
- black
- black >=25.1
- isort
- mypy
- pycodestyle
- pylint
- pylint =3
- docutils

## test
- pytest
- hypothesis
- pytest-cov
- pytest-timeout
3 changes: 1 addition & 2 deletions odc/geo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
#
# Copyright (c) 2015-2020 ODC Contributors
# SPDX-License-Identifier: Apache-2.0
""" Geometric shapes and operations on them
"""
"""Geometric shapes and operations on them"""
# import order is important here
# _crs <-- _geom <-- _geobox <- other
# isort: skip_file
Expand Down
9 changes: 5 additions & 4 deletions odc/geo/_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,13 @@ def explore(
:return: A :py:mod:`folium` map containing the plotted xarray data.
"""
# pylint: disable=too-many-arguments, protected-access

if not have.folium:
raise ModuleNotFoundError(
have.check_or_error(
"folium",
msg=(
"'folium' is required but not installed. "
"Please install it before using `.explore()`."
)
),
)

from folium import LayerControl, Map

Expand Down
5 changes: 2 additions & 3 deletions odc/geo/_rgba.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
""" Helpers for dealing with RGB(A) images.
"""
"""Helpers for dealing with RGB(A) images."""

import functools
from typing import Any, List, Optional, Tuple
Expand Down Expand Up @@ -193,7 +192,7 @@ def _matplotlib_colorize(
if robust:
if x.dtype.kind != "f":
x = x.astype("float32")
_vmin, _vmax = np.nanpercentile(x, [2, 98])
_vmin, _vmax = (float(x) for x in np.nanpercentile(x, [2, 98])) # type: ignore

# do not override configured values
if vmin is None:
Expand Down
1 change: 1 addition & 0 deletions odc/geo/_xr_interop.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,7 @@ def _xr_reproject_da(
else:
dst = numpy.full(dst_shape, fill_value, dtype=dtype)

# pylint: disable=possibly-used-before-assignment
dst = rio_reproject(
src.values,
dst,
Expand Down
9 changes: 7 additions & 2 deletions odc/geo/cog/_az.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ class AzMultiPartUpload(AzureLimits, MultiPartUploadBase):
# pylint: disable=too-many-instance-attributes

def __init__(
self, account_url: str, container: str, blob: str, credential: Any = None
self,
account_url: str,
container: str,
blob: str,
credential: Any = None,
client: Any = None,
):
"""
Initialise Azure multipart upload.
Expand All @@ -56,7 +61,7 @@ def __init__(
# pylint: disable=import-outside-toplevel,import-error
from azure.storage.blob import BlobServiceClient

self.blob_service_client = BlobServiceClient(
self.blob_service_client = client or BlobServiceClient(
account_url=account_url, credential=credential
)
self.container_client = self.blob_service_client.get_container_client(container)
Expand Down
5 changes: 3 additions & 2 deletions odc/geo/cog/_rio.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def _write_cog(
else:
overview_levels = [2**i for i in range(1, 6)]

path: Path | None = None
if fname != ":mem:":
path = check_write_path(
fname, overwrite
Expand Down Expand Up @@ -226,7 +227,7 @@ def _write(pix, band, dst):

# Deal efficiently with "no overviews needed case"
if len(overview_levels) == 0:
if fname == ":mem:":
if path is None:
with rasterio.MemoryFile() as mem:
with mem.open(driver="GTiff", **rio_opts) as dst:
_write(pix, band, dst)
Expand All @@ -246,7 +247,7 @@ def _write(pix, band, dst):
_write(pix, band, tmp)
tmp.build_overviews(overview_levels, resampling)

if fname == ":mem:":
if path is None:
with rasterio.MemoryFile() as mem2:
rio_copy(
tmp,
Expand Down
2 changes: 1 addition & 1 deletion odc/geo/crs.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def _make_crs_key(crs_spec: Union[int, str, Hashable, CRSLike]) -> Hashable:

@cachetools.cached(_crs_cache, key=_make_crs_key)
def _make_crs(
crs_spec: Union[str, int, _CRS, CRSLike]
crs_spec: Union[str, int, _CRS, CRSLike],
) -> Tuple[_CRS, str, Optional[int]]:
epsg = EPSG_UNSET
if isinstance(crs_spec, str):
Expand Down
2 changes: 1 addition & 1 deletion odc/geo/data/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import json
import lzma
import threading

from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
Expand Down Expand Up @@ -84,6 +83,7 @@ class Countries(_CachedGeoDataFrame):

def frame_by_iso3(self, iso3):
df = self._instance
assert df is not None
return df[df.ISO_A3 == iso3]


Expand Down
70 changes: 45 additions & 25 deletions odc/geo/geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#
# Copyright (c) 2015-2020 ODC Contributors
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations

import array
import functools
import itertools
Expand All @@ -20,12 +22,14 @@
Sequence,
Tuple,
Union,
cast,
)

import numpy
from affine import Affine
from pyproj.aoi import AreaOfInterest
from shapely import geometry, ops
from shapely.coords import CoordinateSequence
from shapely.geometry import base

from ._interop import have
Expand Down Expand Up @@ -443,7 +447,10 @@ def to_geom(x) -> base.BaseGeometry:
return to_geom(xx)


def densify(coords: CoordList, resolution: float) -> CoordList:
def densify(
coords: Sequence[Tuple[float, float]] | CoordinateSequence,
resolution: float,
) -> CoordList:
"""
Adds points so they are at most `resolution` units apart.
"""
Expand All @@ -452,15 +459,16 @@ def densify(coords: CoordList, resolution: float) -> CoordList:
def short_enough(p1, p2):
return (p1[0] ** 2 + p2[0] ** 2) < d2

new_coords = [coords[0]]
new_coords: List[Tuple[float, float]] = [cast(Tuple[float, float], coords[0])]
for p1, p2 in zip(coords[:-1], coords[1:]):
p1, p2 = (cast(Tuple[float, float], p) for p in (p1, p2))
if not short_enough(p1, p2):
segment = geometry.LineString([p1, p2])
segment_length = segment.length
d = resolution
while d < segment_length:
(pt,) = segment.interpolate(d).coords
new_coords.append(pt)
((x0, x1),) = segment.interpolate(d).coords
new_coords.append((x0, x1))
d += resolution

new_coords.append(p2)
Expand Down Expand Up @@ -563,13 +571,17 @@ def is_ring(self) -> bool: return self.geom.is_ring
@property
def boundary(self) -> "Geometry": return Geometry(self.geom.boundary, self.crs)
@property
def exterior(self) -> "Geometry": return Geometry(self.geom.exterior, self.crs)
def exterior(self) -> "Geometry":
assert isinstance(self.geom, geometry.Polygon)
return Geometry(self.geom.exterior, self.crs)
@property
def interiors(self) -> List["Geometry"]: return [Geometry(g, self.crs) for g in self.geom.interiors]
def interiors(self) -> List["Geometry"]:
assert isinstance(self.geom, geometry.Polygon)
return [Geometry(g, self.crs) for g in self.geom.interiors]
@property
def centroid(self) -> "Geometry": return Geometry(self.geom.centroid, self.crs)
@property
def coords(self) -> CoordList: return list(self.geom.coords)
def coords(self) -> CoordList: return cast(list[tuple[float, float]], list(self.geom.coords))
@property
def points(self) -> CoordList: return self.coords
@property
Expand Down Expand Up @@ -612,15 +624,26 @@ def segmentize_shapely(geom: base.BaseGeometry) -> base.BaseGeometry:
"MultiPolygon",
"MultiLineString",
]:
return type(geom)([segmentize_shapely(g) for g in geom.geoms])
assert isinstance(
geom,
(
geometry.GeometryCollection,
geometry.MultiPolygon,
geometry.MultiLineString,
),
)
_geoms = [segmentize_shapely(g) for g in geom.geoms]
return type(geom)(_geoms) # type: ignore

if geom.geom_type in ["LineString", "LinearRing"]:
return type(geom)(densify(list(geom.coords), resolution))
assert isinstance(geom, (geometry.LineString, geometry.LinearRing))
return type(geom)(densify(geom.coords, resolution))

if geom.geom_type == "Polygon":
assert isinstance(geom, geometry.Polygon)
return geometry.Polygon(
densify(list(geom.exterior.coords), resolution),
[densify(list(i.coords), resolution) for i in geom.interiors],
densify(geom.exterior.coords, resolution),
[densify(i.coords, resolution) for i in geom.interiors],
)

raise ValueError(
Expand All @@ -637,7 +660,7 @@ def interpolate(self, distance: float) -> "Geometry":
"""
return Geometry(self.geom.interpolate(distance), self.crs)

def buffer(self, distance: float, resolution: float = 30) -> "Geometry":
def buffer(self, distance: float, resolution: int | None = None) -> "Geometry":
return Geometry(self.geom.buffer(distance, resolution=resolution), self.crs)

def simplify(self, tolerance: float, preserve_topology: bool = True) -> "Geometry":
Expand Down Expand Up @@ -676,7 +699,7 @@ def _tr(A, x, y):

def _to_crs(self, crs: CRS) -> "Geometry":
assert self.crs is not None
return Geometry(ops.transform(self.crs.transformer_to_crs(crs), self.geom), crs)
return Geometry(ops.transform(self.crs.transformer_to_crs(crs), self.geom), crs) # type: ignore

def to_crs(
self,
Expand Down Expand Up @@ -849,14 +872,8 @@ def explore(
:return: A :py:mod:`folium` map containing the plotted Geometry.
"""
# pylint: disable=import-outside-toplevel, redefined-builtin

if not have.folium:
raise ModuleNotFoundError(
"'folium' is required but not installed. "
"Please install it before using `.explore()`."
)

from folium import Map, GeoJson
have.check_or_error("folium")
from folium import GeoJson, Map

# Create folium Map if required
map_kwds = {} if map_kwds is None else map_kwds
Expand Down Expand Up @@ -993,26 +1010,29 @@ def filter(self, pred: Callable[[float, float], bool]) -> "Geometry":
return polygon(pts, self.crs, *inners)

if self.geom_type in ("LinearRing", "LineString"):
assert isinstance(self.geom, (geometry.LinearRing, geometry.LineString))
pts = [(x, y) for x, y in self.points if pred(x, y)]
if len(pts) == 1:
pts = [] # need at least 2 points for this type
_geom = type(self.geom)(pts)
return Geometry(_geom, self.crs)

if self.geom_type == "Point":
assert isinstance(self.geom, geometry.Point)
x, y = self.coords[0]
if pred(x, y):
return self
return Geometry(geometry.Point(), self.crs)

if self.geom_type == "MultiPoint":
assert isinstance(self.geom, geometry.MultiPoint)
pts = [(x, y) for x, y in [pt.points[0] for pt in self.geoms] if pred(x, y)]
return multipoint(pts, self.crs)

if self.is_multi:
_filtered = [g.filter(pred).geom for g in self.geoms]
_filtered = [g for g in _filtered if not g.is_empty]
_geom = type(self.geom)(_filtered)
_geom = type(self.geom)(_filtered) # type: ignore
return Geometry(_geom, self.crs)

raise AssertionError("Unhandled geometry type detected") # pragma: no cover
Expand Down Expand Up @@ -1260,11 +1280,11 @@ def _multigeom(geoms: List[base.BaseGeometry]) -> base.BaseGeometry:
geoms = [g for g in geoms if not g.is_empty]
src_type = src_type.pop()
if src_type == "Polygon":
return geometry.MultiPolygon(geoms)
return geometry.MultiPolygon(geoms) # type: ignore
if src_type == "Point":
return geometry.MultiPoint(geoms)
return geometry.MultiPoint(geoms) # type: ignore
if src_type == "LineString":
return geometry.MultiLineString(geoms)
return geometry.MultiLineString(geoms) # type: ignore
return geometry.GeometryCollection(geoms)


Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ profile = "black"
[tool.pylint.messages_control]

max-line-length = 120
max-args = 7
max-args = 15
max-positional-arguments = 12

disable = [
"missing-function-docstring",
Expand All @@ -36,4 +37,5 @@ disable = [
"ungrouped-imports",
"wrong-import-position",
"too-few-public-methods",
"unsubscriptable-object",
]
11 changes: 11 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,19 @@ all =

test =
pytest
pytest-cov
pytest-timeout
geopandas
%(warp)s

test-all =
%(test)s
%(tiff)s
%(s3)s
%(az)s
folium
ipyleaflet
matplotlib


[options.packages.find]
Expand Down
6 changes: 6 additions & 0 deletions tests/.pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[MESSAGES CONTROL]
disable =
all

enable = E,F
max-line-length = 120
2 changes: 0 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
from odc.geo.geobox import GeoBox
from odc.geo.xr import rasterize

# pylint: disable=protected-access,import-outside-toplevel,redefined-outer-name


@pytest.fixture(scope="session")
def data_dir():
Expand Down
Loading
Loading