Repository: opendatacube/odc-geo
Pull request #6 (2022-01-19 04:38:37)

More import renames
- `odc.geo.` is now fairly sparse with only classes and typedefs
  - probably worthwhile importing `GeoBox` in there too, now that it doesn't pull in `xarray`
- Add `odc.geo.xr` that hooks into `.odc` property on xarray objects when imported
- De-duplicating some common code between array/dataset extensions
- Cleaning up tests
  - removing old references to `geometry` module
  - some pylint guided fixes
- Packaging
  - Make `xarray` and `rasterio` dependencies optional


codecov[bot]: # [Codecov](https://codecov.io/gh/opendatacube/odc-geo/pull/6?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube) Report
> Merging [#6](https://codecov.io/gh/opendatacube/odc-geo/pull/6?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube) (4524cae) into [develop](https://codecov.io/gh/opendatacube/odc-geo/commit/202cd8aeb3b03a54d9e484c3514c19a8b57b1a7b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube) (202cd8a) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/opendatacube/odc-geo/pull/6/graphs/tree.svg?width=650&height=150&src=pr&token=Qc7isqXNGF&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube)](https://codecov.io/gh/opendatacube/odc-geo/pull/6?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube)

```diff
@@             Coverage Diff             @@
##           develop       #6      +/-   ##
===========================================
- Coverage    97.00%   96.98%   -0.02%     
===========================================
  Files           12       13       +1     
  Lines         1568     1560       -8     
===========================================
- Hits          1521     1513       -8     
  Misses          47       47              
```


| [Impacted Files](https://codecov.io/gh/opendatacube/odc-geo/pull/6?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube) | Coverage Δ | |
|---|---|---|
| [odc/geo/roi.py](https://codecov.io/gh/opendatacube/odc-geo/pull/6/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-b2RjL2dlby9yb2kucHk=) | `100.00% <ø> (ø)` | |
| [odc/geo/\_\_init\_\_.py](https://codecov.io/gh/opendatacube/odc-geo/pull/6/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-b2RjL2dlby9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [odc/geo/\_overlap.py](https://codecov.io/gh/opendatacube/odc-geo/pull/6/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-b2RjL2dlby9fb3ZlcmxhcC5weQ==) | `100.00% <100.00%> (ø)` | |
| [odc/geo/\_xr\_interop.py](https://codecov.io/gh/opendatacube/odc-geo/pull/6/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-b2RjL2dlby9feHJfaW50ZXJvcC5weQ==) | `100.00% <100.00%> (ø)` | |
| [odc/geo/crs.py](https://codecov.io/gh/opendatacube/odc-geo/pull/6/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-b2RjL2dlby9jcnMucHk=) | `100.00% <100.00%> (ø)` | |
| [odc/geo/geobox.py](https://codecov.io/gh/opendatacube/odc-geo/pull/6/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-b2RjL2dlby9nZW9ib3gucHk=) | `100.00% <100.00%> (ø)` | |
| [odc/geo/geom.py](https://codecov.io/gh/opendatacube/odc-geo/pull/6/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-b2RjL2dlby9nZW9tLnB5) | `100.00% <100.00%> (ø)` | |
| [odc/geo/gridspec.py](https://codecov.io/gh/opendatacube/odc-geo/pull/6/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-b2RjL2dlby9ncmlkc3BlYy5weQ==) | `100.00% <100.00%> (ø)` | |
| [odc/geo/xr.py](https://codecov.io/gh/opendatacube/odc-geo/pull/6/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube#diff-b2RjL2dlby94ci5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/opendatacube/odc-geo/pull/6?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/opendatacube/odc-geo/pull/6?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube). Last update [202cd8a...4524cae](https://codecov.io/gh/opendatacube/odc-geo/pull/6?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opendatacube).


Patch:
diff --git a/odc/geo/__init__.py b/odc/geo/__init__.py
index 01e4e58..4a80cff 100644
--- a/odc/geo/__init__.py
+++ b/odc/geo/__init__.py
@@ -10,105 +10,28 @@
 
 from ._version import __version__
 
-from ._crs import (
+from .crs import (
     CRS,
     CRSError,
     CRSMismatchError,
     MaybeCRS,
     SomeCRS,
-    crs_units_per_degree,
-    norm_crs,
-    norm_crs_or_error,
 )
 
-from ._geom import (
+from .geom import (
     BoundingBox,
     CoordList,
     Geometry,
-    bbox_intersection,
-    bbox_union,
-    box,
-    chop_along_antimeridian,
-    clip_lon180,
-    common_crs,
-    intersects,
-    line,
-    lonlat_bounds,
-    mid_longitude,
-    multigeom,
-    multiline,
-    multipoint,
-    multipolygon,
-    point,
-    polygon_from_transform,
-    polygon,
-    projected_lon,
-    sides,
-    unary_intersection,
-    unary_union,
-)
-
-from ._roi import (
-    roi_boundary,
-    roi_center,
-    roi_from_points,
-    roi_intersect,
-    roi_is_empty,
-    roi_is_full,
-    roi_normalise,
-    roi_pad,
-    roi_shape,
-    scaled_down_roi,
-    scaled_down_shape,
-    scaled_up_roi,
-    w_,
 )
 
 __all__ = [
     "__version__",
-    "bbox_intersection",
-    "bbox_union",
     "BoundingBox",
-    "box",
-    "chop_along_antimeridian",
-    "clip_lon180",
-    "common_crs",
     "CoordList",
-    "crs_units_per_degree",
     "CRS",
     "CRSError",
     "CRSMismatchError",
     "Geometry",
-    "intersects",
-    "line",
-    "lonlat_bounds",
     "MaybeCRS",
-    "mid_longitude",
-    "multigeom",
-    "multiline",
-    "multipoint",
-    "multipolygon",
-    "norm_crs",
-    "norm_crs_or_error",
-    "point",
-    "polygon_from_transform",
-    "polygon",
-    "projected_lon",
-    "roi_boundary",
-    "roi_center",
-    "roi_from_points",
-    "roi_intersect",
-    "roi_is_empty",
-    "roi_is_full",
-    "roi_normalise",
-    "roi_pad",
-    "roi_shape",
-    "scaled_down_roi",
-    "scaled_down_shape",
-    "scaled_up_roi",
-    "sides",
     "SomeCRS",
-    "unary_intersection",
-    "unary_union",
-    "w_",
 ]
diff --git a/odc/geo/_overlap.py b/odc/geo/_overlap.py
index 98c3d83..8440af7 100644
--- a/odc/geo/_overlap.py
+++ b/odc/geo/_overlap.py
@@ -10,7 +10,7 @@ import numpy as np
 from affine import Affine
 from numpy import linalg
 
-from ._roi import roi_boundary, roi_center, roi_from_points, roi_is_empty
+from .roi import roi_boundary, roi_center, roi_from_points, roi_is_empty
 from .geobox import GeoBox, gbox_boundary
 from .math import is_affine_st, maybe_int, snap_scale
 
diff --git a/odc/geo/_xr_interop.py b/odc/geo/_xr_interop.py
index 6894b2e..9c56801 100644
--- a/odc/geo/_xr_interop.py
+++ b/odc/geo/_xr_interop.py
@@ -13,7 +13,7 @@ import numpy
 import xarray
 from affine import Affine
 
-from ._crs import CRS, CRSError, SomeCRS, norm_crs_or_error
+from .crs import CRS, CRSError, SomeCRS, norm_crs_or_error
 from .geobox import Coordinate, GeoBox
 from .math import affine_from_axis
 
@@ -296,15 +296,15 @@ def _locate_geo_info(src: XarrayObject) -> GeoState:
     return GeoState(spatial_dims=sdims, transform=transform, crs=crs, geobox=geobox)
 
 
-@xarray.register_dataarray_accessor("odc")
 class ODCExtension:
     """
-    ODC extension for xarray.
+    ODC extension base class.
+
+    Common accessors for both Array/Dataset.
     """
 
-    def __init__(self, xx: xarray.DataArray):
-        self._xx = xx
-        self._state = _locate_geo_info(xx)
+    def __init__(self, state: GeoState):
+        self._state = state
 
     @property
     def spatial_dims(self) -> Optional[Tuple[str, str]]:
@@ -325,9 +325,20 @@ class ODCExtension:
     def geobox(self) -> Optional[GeoBox]:
         return self._state.geobox
 
+
+@xarray.register_dataarray_accessor("odc")
+class ODCExtensionDa(ODCExtension):
+    """
+    ODC extension for :py:class:`xarray.DataArray`.
+    """
+
+    def __init__(self, xx: xarray.DataArray):
+        ODCExtension.__init__(self, _locate_geo_info(xx))
+        self._xx = xx
+
     @property
-    def uncached(self) -> "ODCExtension":
-        return ODCExtension(self._xx)
+    def uncached(self) -> "ODCExtensionDa":
+        return ODCExtensionDa(self._xx)
 
     def assign_crs(
         self, crs: SomeCRS, crs_coord_name: str = "spatial_ref"
@@ -336,33 +347,14 @@ class ODCExtension:
 
 
 @xarray.register_dataset_accessor("odc")
-class ODCExtensionDs:
+class ODCExtensionDs(ODCExtension):
     """
-    ODC extension for xarray.
+    ODC extension for :py:class:`xarray.Dataset`.
     """
 
     def __init__(self, ds: xarray.Dataset):
+        ODCExtension.__init__(self, _locate_geo_info(ds))
         self._ds = ds
-        self._state = _locate_geo_info(ds)
-
-    @property
-    def spatial_dims(self) -> Optional[Tuple[str, str]]:
-        """Return names of spatial dimensions, or ``None``."""
-        return self._state.spatial_dims
-
-    @property
-    def transform(self) -> Optional[Affine]:
-        return self._state.transform
-
-    affine = transform
-
-    @property
-    def crs(self) -> Optional[CRS]:
-        return self._state.crs
-
-    @property
-    def geobox(self) -> Optional[GeoBox]:
-        return self._state.geobox
 
     @property
     def uncached(self) -> "ODCExtensionDs":
diff --git a/odc/geo/_crs.py b/odc/geo/crs.py
similarity index 95%
rename from odc/geo/_crs.py
rename to odc/geo/crs.py
index cedb21f..b2ce71d 100644
--- a/odc/geo/_crs.py
+++ b/odc/geo/crs.py
@@ -201,19 +201,19 @@ class CRS:
         return self._crs
 
     @property
-    def valid_region(self) -> Optional["_geom.Geometry"]:
+    def valid_region(self) -> Optional["geom.Geometry"]:
         """Return valid region of this CRS.
 
         Bounding box in Lon/Lat as a 4 point Polygon in EPSG:4326.
         None if not defined
         """
 
-        from . import _geom  # pylint: disable=import-outside-toplevel
+        from . import geom  # pylint: disable=import-outside-toplevel
 
         aou = self._crs.area_of_use
         if aou is None:
             return None
-        return _geom.box(aou.west, aou.south, aou.east, aou.north, "EPSG:4326")
+        return geom.box(aou.west, aou.south, aou.east, aou.north, "EPSG:4326")
 
     @property
     def crs_str(self) -> str:
@@ -291,7 +291,7 @@ def crs_units_per_degree(
     A floating number S such that `S*degrees -> meters`
     """
 
-    from . import _geom  # pylint: disable=import-outside-toplevel
+    from . import geom  # pylint: disable=import-outside-toplevel
 
     if isinstance(lon, tuple):
         lon, lat = lon
@@ -300,11 +300,11 @@ def crs_units_per_degree(
     if lon2 > 180:
         lon2 = lon - step
 
-    ll = _geom.line([(lon, lat), (lon2, lat)], "EPSG:4326")
+    ll = geom.line([(lon, lat), (lon2, lat)], "EPSG:4326")
     xy = ll.to_crs(crs, resolution=math.inf)
 
     return xy.length / step
 
 
 if TYPE_CHECKING:
-    from . import _geom  # pragma: no cover
+    from . import geom  # pragma: no cover
diff --git a/odc/geo/geobox.py b/odc/geo/geobox.py
index 94ec757..3d38f74 100644
--- a/odc/geo/geobox.py
+++ b/odc/geo/geobox.py
@@ -10,15 +10,15 @@ from typing import Dict, Iterable, List, Optional, Tuple, Union
 import numpy
 from affine import Affine
 
-from ._crs import CRS, MaybeCRS
-from ._geom import (
+from .crs import CRS, MaybeCRS
+from .geom import (
     BoundingBox,
     Geometry,
     bbox_intersection,
     bbox_union,
     polygon_from_transform,
 )
-from ._roi import align_up, polygon_path, roi_normalise, roi_shape
+from .roi import align_up, polygon_path, roi_normalise, roi_shape
 from .math import clamp, is_affine_st, is_almost_int
 
 # pylint: disable=invalid-name
diff --git a/odc/geo/_geom.py b/odc/geo/geom.py
similarity index 99%
rename from odc/geo/_geom.py
rename to odc/geo/geom.py
index ae42882..83945e8 100644
--- a/odc/geo/_geom.py
+++ b/odc/geo/geom.py
@@ -15,7 +15,7 @@ from affine import Affine
 from shapely import geometry, ops
 from shapely.geometry import base
 
-from ._crs import CRS, CRSMismatchError, MaybeCRS, SomeCRS, norm_crs, norm_crs_or_error
+from .crs import CRS, CRSMismatchError, MaybeCRS, SomeCRS, norm_crs, norm_crs_or_error
 
 _BoundingBox = namedtuple("_BoundingBox", ("left", "bottom", "right", "top"))
 CoordList = List[Tuple[float, float]]
diff --git a/odc/geo/gridspec.py b/odc/geo/gridspec.py
index 0332c57..7d61375 100644
--- a/odc/geo/gridspec.py
+++ b/odc/geo/gridspec.py
@@ -8,9 +8,9 @@ from typing import Iterator, Optional, Tuple
 
 from affine import Affine
 
-from ._crs import CRS
+from .crs import CRS
 from .geobox import GeoBox
-from ._geom import BoundingBox, Geometry, intersects
+from .geom import BoundingBox, Geometry, intersects
 
 
 class GridSpec:
diff --git a/odc/geo/_roi.py b/odc/geo/roi.py
similarity index 100%
rename from odc/geo/_roi.py
rename to odc/geo/roi.py
diff --git a/odc/geo/xr.py b/odc/geo/xr.py
new file mode 100644
index 0000000..1cf6528
--- /dev/null
+++ b/odc/geo/xr.py
@@ -0,0 +1,32 @@
+# This file is part of the Open Data Cube, see https://opendatacube.org for more information
+#
+# Copyright (c) 2015-2020 ODC Contributors
+# SPDX-License-Identifier: Apache-2.0
+"""
+Xarray Intergration.
+
+Importing this module enables `.odc.` extension on :py:class:`xarray.DataArray` and
+:py:class:`xarray.Dataset` objects.
+
+For more details see: :py:class:`~odc.geo.xr.ODCExtension` and
+:py:class:`~odc.geo.xr.ODCExtensionDs`.
+"""
+from ._xr_interop import (
+    ODCExtension,
+    ODCExtensionDa,
+    ODCExtensionDs,
+    assign_crs,
+    register_geobox,
+    spatial_dims,
+    xr_coords,
+)
+
+__all__ = (
+    "ODCExtension",
+    "ODCExtensionDa",
+    "ODCExtensionDs",
+    "assign_crs",
+    "register_geobox",
+    "spatial_dims",
+    "xr_coords",
+)
diff --git a/setup.cfg b/setup.cfg
index 60327bb..4d7d882 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -36,10 +36,23 @@ install_requires =
     cachetools
     numpy
     pyproj
-    rasterio
     shapely
+
+[options.extras_require]
+xr =
     xarray>=0.19
 
+warp =
+    rasterio
+
+all =
+    rasterio
+    xarray>=0.19
+
+test =
+    pytest
+
+
 [options.packages.find]
 include =
   odc*


Test patch:
diff --git a/tests/test_crs.py b/tests/test_crs.py
index 302f4ec..25ff586 100644
--- a/tests/test_crs.py
+++ b/tests/test_crs.py
@@ -10,24 +10,24 @@ import pytest
 import rasterio.crs
 from pytest import approx
 
-from odc import geo as geometry
-from odc.geo import CRS, CRSMismatchError, common_crs, crs_units_per_degree
+from odc.geo import geom
+from odc.geo.crs import CRS, CRSError, CRSMismatchError, crs_units_per_degree
+from odc.geo.geom import common_crs
 from odc.geo.testutils import epsg3577, epsg3857, epsg4326
 
+# pylint: disable=missing-class-docstring,use-implicit-booleaness-not-comparison
+# pylint: disable=comparison-with-itself,no-self-use
+
 
 def test_common_crs():
     assert common_crs([]) is None
     assert (
-        common_crs(
-            [geometry.point(0, 0, epsg4326), geometry.line([(0, 0), (1, 1)], epsg4326)]
-        )
+        common_crs([geom.point(0, 0, epsg4326), geom.line([(0, 0), (1, 1)], epsg4326)])
         is epsg4326
     )
 
     with pytest.raises(CRSMismatchError):
-        common_crs(
-            [geometry.point(0, 0, epsg4326), geometry.line([(0, 0), (1, 1)], epsg3857)]
-        )
+        common_crs([geom.point(0, 0, epsg4326), geom.line([(0, 0), (1, 1)], epsg3857)])
 
 
 class TestCRSEqualityComparisons:
@@ -41,7 +41,7 @@ class TestCRSEqualityComparisons:
         assert (a == TestCRSEqualityComparisons) is False
 
     def test_australian_albers_comparison(self):
-        a = geometry.CRS(
+        a = CRS(
             """PROJCS["GDA94_Australian_Albers",GEOGCS["GCS_GDA_1994",
                             DATUM["Geocentric_Datum_of_Australia_1994",SPHEROID["GRS_1980",6378137,298.257222101]],
                             PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],
@@ -62,8 +62,8 @@ class TestCRSEqualityComparisons:
 
 
 def test_no_epsg():
-    c = geometry.CRS("+proj=longlat +no_defs +ellps=GRS80")
-    b = geometry.CRS(
+    c = CRS("+proj=longlat +no_defs +ellps=GRS80")
+    b = CRS(
         """GEOGCS["GRS 1980(IUGG, 1980)",DATUM["unknown",SPHEROID["GRS80",6378137,298.257222101]],
                         PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]"""
     )
@@ -73,8 +73,7 @@ def test_no_epsg():
 
 
 def test_crs():
-    CRS = geometry.CRS
-    custom_crs = geometry.CRS(
+    custom_crs = CRS(
         """PROJCS["unnamed",
                            GEOGCS["Unknown datum based upon the custom spheroid",
                            DATUM["Not specified (based on custom spheroid)", SPHEROID["Custom spheroid",6371007.181,0]],
@@ -107,7 +106,7 @@ def test_crs():
     assert crs2 == crs
     assert crs.proj is crs2.proj
 
-    assert epsg4326.valid_region == geometry.box(-180, -90, 180, 90, epsg4326)
+    assert epsg4326.valid_region == geom.box(-180, -90, 180, 90, epsg4326)
     assert epsg3857.valid_region.crs == epsg4326
     xmin, _, xmax, _ = epsg3857.valid_region.boundingbox
     assert (xmin, xmax) == (-180, 180)
@@ -132,7 +131,7 @@ def test_crs():
     ]
 
     for bad in bad_crs:
-        with pytest.raises(geometry.CRSError):
+        with pytest.raises(CRSError):
             CRS(bad)
 
     with pytest.warns(DeprecationWarning):
@@ -159,7 +158,7 @@ def test_crs_compat():
 
     assert (CRS(crs_rio) == crs_rio) is True
 
-    with pytest.raises(geometry.CRSError):
+    with pytest.raises(CRSError):
         CRS(("random", "tuple"))
 
     crs = CRS("epsg:3857")
diff --git a/tests/test_gbox_ops.py b/tests/test_gbox_ops.py
index c0d465b..f97a2ba 100644
--- a/tests/test_gbox_ops.py
+++ b/tests/test_gbox_ops.py
@@ -6,10 +6,12 @@ import numpy as np
 import pytest
 from affine import Affine
 
-from odc import geo as geometry
 from odc.geo import geobox as gbx
+from odc.geo import geom as geometry
 from odc.geo.geobox import GeoBox
 
+# pylint: disable=pointless-statement,too-many-statements
+
 epsg3857 = geometry.CRS("EPSG:3857")
 
 
diff --git a/tests/test_geobox.py b/tests/test_geobox.py
index d2cf576..1dccced 100644
--- a/tests/test_geobox.py
+++ b/tests/test_geobox.py
@@ -7,8 +7,7 @@ import numpy as np
 import pytest
 from affine import Affine
 
-from odc import geo as geometry
-from odc.geo import CRS
+from odc.geo import CRS, geom
 from odc.geo.geobox import (
     GeoBox,
     bounding_box_in_pixel_domain,
@@ -18,14 +17,9 @@ from odc.geo.geobox import (
     scaled_down_geobox,
 )
 from odc.geo.math import apply_affine
-from odc.geo.testutils import (
-    epsg3577,
-    epsg3857,
-    epsg4326,
-    mkA,
-    xy_from_gbox,
-    xy_norm,
-)
+from odc.geo.testutils import epsg3577, epsg3857, epsg4326, mkA, xy_from_gbox, xy_norm
+
+# pylint: disable=pointless-statement,too-many-statements
 
 
 def test_geobox_simple():
@@ -141,7 +135,7 @@ def test_geobox():
         ],
     ]
     for points in points_list:
-        polygon = geometry.polygon(points, crs=epsg3577)
+        polygon = geom.polygon(points, crs=epsg3577)
         resolution = (-25, 25)
         geobox = GeoBox.from_geopolygon(polygon, resolution)
 
diff --git a/tests/test_geom.py b/tests/test_geom.py
index 9cf60fc..cf0d393 100644
--- a/tests/test_geom.py
+++ b/tests/test_geom.py
@@ -12,16 +12,29 @@ from affine import Affine
 from pytest import approx
 from shapely.errors import ShapelyDeprecationWarning
 
-from odc import geo as geometry
-from odc.geo import (
-    CRS,
-    BoundingBox,
-    CRSMismatchError,
+from odc.geo import CRS, BoundingBox, CRSMismatchError, geom
+from odc.geo._overlap import (
+    affine_from_pts,
+    compute_axis_overlap,
+    compute_reproject_roi,
+    decompose_rws,
+    get_scale_at_point,
+    native_pix_transform,
+)
+from odc.geo.crs import norm_crs, norm_crs_or_error
+from odc.geo.geobox import GeoBox, _align_pix, _round_to_res, scaled_down_geobox
+from odc.geo.geom import (
     bbox_union,
     chop_along_antimeridian,
     clip_lon180,
+    densify,
+    force_2d,
     multigeom,
     projected_lon,
+)
+from odc.geo.math import apply_affine, is_affine_st, split_translation
+from odc.geo.roi import (
+    polygon_path,
     roi_boundary,
     roi_center,
     roi_from_points,
@@ -36,19 +49,6 @@ from odc.geo import (
     scaled_up_roi,
     w_,
 )
-from odc.geo._crs import norm_crs, norm_crs_or_error
-from odc.geo._geom import densify, force_2d
-from odc.geo._overlap import (
-    affine_from_pts,
-    compute_axis_overlap,
-    compute_reproject_roi,
-    decompose_rws,
-    get_scale_at_point,
-    native_pix_transform,
-)
-from odc.geo._roi import polygon_path
-from odc.geo.geobox import GeoBox, _align_pix, _round_to_res, scaled_down_geobox
-from odc.geo.math import apply_affine, is_affine_st, split_translation
 from odc.geo.testutils import (
     SAMPLE_WKT_WITHOUT_AUTHORITY,
     AlbersGS,
@@ -62,9 +62,12 @@ from odc.geo.testutils import (
     xy_from_gbox,
 )
 
+# pylint: disable=protected-access, pointless-statement
+# pylint: disable=too-many-statements,too-many-locals,too-many-lines
+
 
 def test_pickleable():
-    poly = geometry.polygon([(10, 20), (20, 20), (20, 10), (10, 20)], crs=epsg4326)
+    poly = geom.polygon([(10, 20), (20, 20), (20, 10), (10, 20)], crs=epsg4326)
     pickled = pickle.dumps(poly, pickle.HIGHEST_PROTOCOL)
     unpickled = pickle.loads(pickled)
     assert poly == unpickled
@@ -73,58 +76,56 @@ def test_pickleable():
 def test_props():
     crs = epsg4326
 
-    box1 = geometry.box(10, 10, 30, 30, crs=crs)
+    box1 = geom.box(10, 10, 30, 30, crs=crs)
     assert box1
     assert box1.is_valid
     assert not box1.is_empty
     assert box1.area == 400.0
     assert box1.boundary.length == 80.0
-    assert box1.centroid == geometry.point(20, 20, crs)
+    assert box1.centroid == geom.point(20, 20, crs)
     assert isinstance(box1.wkt, str)
 
-    triangle = geometry.polygon([(10, 20), (20, 20), (20, 10), (10, 20)], crs=crs)
-    assert triangle.boundingbox == geometry.BoundingBox(10, 10, 20, 20)
+    triangle = geom.polygon([(10, 20), (20, 20), (20, 10), (10, 20)], crs=crs)
+    assert triangle.boundingbox == geom.BoundingBox(10, 10, 20, 20)
     assert triangle.envelope.contains(triangle)
 
     assert box1.length == 80.0
 
-    box1copy = geometry.box(10, 10, 30, 30, crs=crs)
+    box1copy = geom.box(10, 10, 30, 30, crs=crs)
     assert box1 == box1copy
     assert box1.convex_hull == box1copy  # NOTE: this might fail because of point order
 
-    box2 = geometry.box(20, 10, 40, 30, crs=crs)
+    box2 = geom.box(20, 10, 40, 30, crs=crs)
     assert box1 != box2
 
-    bbox = geometry.BoundingBox(1, 0, 10, 13)
+    bbox = geom.BoundingBox(1, 0, 10, 13)
     assert bbox.width == 9
     assert bbox.height == 13
     assert bbox.points == [(1, 0), (1, 13), (10, 0), (10, 13)]
 
     assert bbox.transform(Affine.identity()) == bbox
-    assert bbox.transform(Affine.translation(1, 2)) == geometry.BoundingBox(
-        2, 2, 11, 15
-    )
+    assert bbox.transform(Affine.translation(1, 2)) == geom.BoundingBox(2, 2, 11, 15)
 
-    pt = geometry.point(3, 4, crs)
+    pt = geom.point(3, 4, crs)
     assert pt.json["coordinates"] == (3.0, 4.0)
     assert "Point" in str(pt)
     assert bool(pt) is True
     assert pt.__nonzero__() is True
 
     # check "CRS as string is converted to class automatically"
-    assert isinstance(geometry.point(3, 4, "epsg:3857").crs, geometry.CRS)
+    assert isinstance(geom.point(3, 4, "epsg:3857").crs, geom.CRS)
 
     # constructor with bad input should raise ValueError
     with pytest.raises(ValueError):
-        geometry.Geometry(object())
+        geom.Geometry(object())
 
 
 def test_tests():
-    box1 = geometry.box(10, 10, 30, 30, crs=epsg4326)
-    box2 = geometry.box(20, 10, 40, 30, crs=epsg4326)
-    box3 = geometry.box(30, 10, 50, 30, crs=epsg4326)
-    box4 = geometry.box(40, 10, 60, 30, crs=epsg4326)
-    minibox = geometry.box(15, 15, 25, 25, crs=epsg4326)
+    box1 = geom.box(10, 10, 30, 30, crs=epsg4326)
+    box2 = geom.box(20, 10, 40, 30, crs=epsg4326)
+    box3 = geom.box(30, 10, 50, 30, crs=epsg4326)
+    box4 = geom.box(40, 10, 60, 30, crs=epsg4326)
+    minibox = geom.box(15, 15, 25, 25, crs=epsg4326)
 
     assert not box1.touches(box2)
     assert box1.touches(box3)
@@ -154,10 +155,10 @@ def test_tests():
 
 
 def test_ops():
-    box1 = geometry.box(10, 10, 30, 30, crs=epsg4326)
-    box2 = geometry.box(20, 10, 40, 30, crs=epsg4326)
-    box3 = geometry.box(20, 10, 40, 30, crs=epsg4326)
-    box4 = geometry.box(40, 10, 60, 30, crs=epsg4326)
+    box1 = geom.box(10, 10, 30, 30, crs=epsg4326)
+    box2 = geom.box(20, 10, 40, 30, crs=epsg4326)
+    box3 = geom.box(20, 10, 40, 30, crs=epsg4326)
+    box4 = geom.box(40, 10, 60, 30, crs=epsg4326)
     no_box = None
 
     assert box1 != box2
@@ -167,7 +168,7 @@ def test_ops():
     union1 = box1.union(box2)
     assert union1.area == 600.0
 
-    with pytest.raises(geometry.CRSMismatchError):
+    with pytest.raises(geom.CRSMismatchError):
         box1.union(box2.to_crs(epsg3857))
 
     inter1 = box1.intersection(box2)
@@ -186,12 +187,12 @@ def test_ops():
     assert symdiff1.area == 400.0
 
     # test segmented
-    line = geometry.line([(0, 0), (0, 5), (10, 5)], epsg4326)
+    line = geom.line([(0, 0), (0, 5), (10, 5)], epsg4326)
     line2 = line.segmented(2)
     assert line.crs is line2.crs
     assert line.length == line2.length
     assert len(line.coords) < len(line2.coords)
-    poly = geometry.polygon([(0, 0), (0, 5), (10, 5)], epsg4326)
+    poly = geom.polygon([(0, 0), (0, 5), (10, 5)], epsg4326)
     poly2 = poly.segmented(2)
     assert poly.crs is poly2.crs
     assert poly.length == poly2.length
@@ -218,11 +219,11 @@ def test_ops():
         assert np.array(line).shape == (3, 2)
 
     # test simplify
-    poly = geometry.polygon([(0, 0), (0, 5), (10, 5)], epsg4326)
+    poly = geom.polygon([(0, 0), (0, 5), (10, 5)], epsg4326)
     assert poly.simplify(100) == poly
 
     # test iteration
-    poly_2_parts = geometry.Geometry(
+    poly_2_parts = geom.Geometry(
         {
             "type": "MultiPolygon",
             "coordinates": [
@@ -260,56 +261,56 @@ def test_ops():
     assert all(p.crs == poly_2_parts.crs for p in pp)
 
     # test transform
-    assert geometry.point(0, 0, epsg4326).transform(
+    assert geom.point(0, 0, epsg4326).transform(
         lambda x, y: (x + 1, y + 2)
-    ) == geometry.point(1, 2, epsg4326)
+    ) == geom.point(1, 2, epsg4326)
 
     # test sides
-    box = geometry.box(1, 2, 11, 22, epsg4326)
-    lines = list(geometry.sides(box))
+    box = geom.box(1, 2, 11, 22, epsg4326)
+    lines = list(geom.sides(box))
     assert all(line.crs is epsg4326 for line in lines)
     assert len(lines) == 4
-    assert lines[0] == geometry.line([(1, 2), (1, 22)], epsg4326)
-    assert lines[1] == geometry.line([(1, 22), (11, 22)], epsg4326)
-    assert lines[2] == geometry.line([(11, 22), (11, 2)], epsg4326)
-    assert lines[3] == geometry.line([(11, 2), (1, 2)], epsg4326)
+    assert lines[0] == geom.line([(1, 2), (1, 22)], epsg4326)
+    assert lines[1] == geom.line([(1, 22), (11, 22)], epsg4326)
+    assert lines[2] == geom.line([(11, 22), (11, 2)], epsg4326)
+    assert lines[3] == geom.line([(11, 2), (1, 2)], epsg4326)
 
 
 def test_geom_split():
-    box = geometry.box(0, 0, 10, 30, epsg4326)
-    line = geometry.line([(5, 0), (5, 30)], epsg4326)
+    box = geom.box(0, 0, 10, 30, epsg4326)
+    line = geom.line([(5, 0), (5, 30)], epsg4326)
     bb = list(box.split(line))
     assert len(bb) == 2
     assert box.contains(bb[0] | bb[1])
     assert (box ^ (bb[0] | bb[1])).is_empty
 
     with pytest.raises(CRSMismatchError):
-        list(box.split(geometry.line([(5, 0), (5, 30)], epsg3857)))
+        list(box.split(geom.line([(5, 0), (5, 30)], epsg3857)))
 
 
 def test_multigeom():
     p1, p2 = (0, 0), (1, 2)
     p3, p4 = (3, 4), (5, 6)
-    b1 = geometry.box(*p1, *p2, epsg4326)
-    b2 = geometry.box(*p3, *p4, epsg4326)
+    b1 = geom.box(*p1, *p2, epsg4326)
+    b2 = geom.box(*p3, *p4, epsg4326)
     bb = multigeom([b1, b2])
     assert bb.type == "MultiPolygon"
     assert bb.crs is b1.crs
     assert len(list(bb)) == 2
 
-    assert geometry.multipolygon([b1.boundary.coords, b2.boundary.coords], b1.crs) == bb
+    assert geom.multipolygon([b1.boundary.coords, b2.boundary.coords], b1.crs) == bb
 
-    g1 = geometry.line([p1, p2], None)
-    g2 = geometry.line([p3, p4], None)
+    g1 = geom.line([p1, p2], None)
+    g2 = geom.line([p3, p4], None)
     gg = multigeom(iter([g1, g2, g1]))
     assert gg.type == "MultiLineString"
     assert gg.crs is g1.crs
     assert len(list(gg)) == 3
-    assert geometry.multiline([[p1, p2], [p3, p4], [p1, p2]], None) == gg
+    assert geom.multiline([[p1, p2], [p3, p4], [p1, p2]], None) == gg
 
-    g1 = geometry.point(*p1, epsg3857)
-    g2 = geometry.point(*p2, epsg3857)
-    g3 = geometry.point(*p3, epsg3857)
+    g1 = geom.point(*p1, epsg3857)
+    g2 = geom.point(*p2, epsg3857)
+    g3 = geom.point(*p3, epsg3857)
     gg = multigeom(iter([g1, g2, g3]))
     assert gg.type == "MultiPoint"
     assert gg.crs is g1.crs
@@ -318,17 +319,15 @@ def test_multigeom():
     assert list(gg)[1] == g2
     assert list(gg)[2] == g3
 
-    assert geometry.multipoint([p1, p2, p3], epsg3857) == gg
+    assert geom.multipoint([p1, p2, p3], epsg3857) == gg
 
     # can't mix types
     with pytest.raises(ValueError):
-        multigeom([geometry.line([p1, p2], None), geometry.point(*p1, None)])
+        multigeom([geom.line([p1, p2], None), geom.point(*p1, None)])
 
     # can't mix CRSs
     with pytest.raises(CRSMismatchError):
-        multigeom(
-            [geometry.line([p1, p2], epsg4326), geometry.line([p3, p4], epsg3857)]
-        )
+        multigeom([geom.line([p1, p2], epsg4326), geom.line([p3, p4], epsg3857)])
 
     # only some types are supported on input
     with pytest.raises(ValueError):
@@ -336,7 +335,7 @@ def test_multigeom():
 
 
 def test_shapely_wrappers():
-    poly = geometry.polygon([(0, 0), (0, 5), (10, 5)], epsg4326)
+    poly = geom.polygon([(0, 0), (0, 5), (10, 5)], epsg4326)
 
     assert isinstance(poly.svg(), str)
     assert isinstance(poly._repr_svg_(), str)
@@ -345,7 +344,7 @@ def test_shapely_wrappers():
     assert len(poly.interiors) == 0
     assert len(with_hole.interiors) == 1
     assert isinstance(with_hole.interiors, list)
-    assert isinstance(with_hole.interiors[0], geometry.Geometry)
+    assert isinstance(with_hole.interiors[0], geom.Geometry)
     assert with_hole.interiors[0].crs == with_hole.crs
     assert poly.exterior.crs == poly.crs
 
@@ -361,7 +360,7 @@ def test_shapely_wrappers():
 
 
 def test_to_crs():
-    poly = geometry.polygon([(0, 0), (0, 5), (10, 5)], epsg4326)
+    poly = geom.polygon([(0, 0), (0, 5), (10, 5)], epsg4326)
     num_points = 3
     assert poly.crs is epsg4326
     assert poly.to_crs(epsg3857).crs is epsg3857
@@ -379,14 +378,12 @@ def test_to_crs():
     assert len(poly.to_crs(epsg3857, float("+inf")).exterior.xy[0]) == num_points + 1
 
     # test the segmentation works on multi-polygons
-    mpoly = geometry.box(0, 0, 1, 3, "EPSG:4326") | geometry.box(
-        2, 4, 3, 6, "EPSG:4326"
-    )
+    mpoly = geom.box(0, 0, 1, 3, "EPSG:4326") | geom.box(2, 4, 3, 6, "EPSG:4326")
 
     assert mpoly.type == "MultiPolygon"
     assert mpoly.to_crs(epsg3857).type == "MultiPolygon"
 
-    poly = geometry.polygon([(0, 0), (0, 5), (10, 5)], None)
+    poly = geom.polygon([(0, 0), (0, 5), (10, 5)], None)
     assert poly.crs is None
     with pytest.raises(ValueError):
         poly.to_crs(epsg3857)
@@ -440,66 +437,66 @@ def test_bbox_union():
 
 
 def test_unary_union():
-    box1 = geometry.box(10, 10, 30, 30, crs=epsg4326)
-    box2 = geometry.box(20, 10, 40, 30, crs=epsg4326)
-    box3 = geometry.box(30, 10, 50, 30, crs=epsg4326)
-    box4 = geometry.box(40, 10, 60, 30, crs=epsg4326)
+    box1 = geom.box(10, 10, 30, 30, crs=epsg4326)
+    box2 = geom.box(20, 10, 40, 30, crs=epsg4326)
+    box3 = geom.box(30, 10, 50, 30, crs=epsg4326)
+    box4 = geom.box(40, 10, 60, 30, crs=epsg4326)
 
-    union0 = geometry.unary_union([box1])
+    union0 = geom.unary_union([box1])
     assert union0 == box1
 
-    union1 = geometry.unary_union([box1, box4])
+    union1 = geom.unary_union([box1, box4])
     assert union1.type == "MultiPolygon"
     assert union1.area == 2.0 * box1.area
 
-    union2 = geometry.unary_union([box1, box2])
+    union2 = geom.unary_union([box1, box2])
     assert union2.type == "Polygon"
     assert union2.area == 1.5 * box1.area
 
-    union3 = geometry.unary_union([box1, box2, box3, box4])
+    union3 = geom.unary_union([box1, box2, box3, box4])
     assert union3.type == "Polygon"
     assert union3.area == 2.5 * box1.area
 
-    union4 = geometry.unary_union([union1, box2, box3])
+    union4 = geom.unary_union([union1, box2, box3])
     assert union4.type == "Polygon"
     assert union4.area == 2.5 * box1.area
 
-    assert geometry.unary_union([]) is None
+    assert geom.unary_union([]) is None
 
     with pytest.raises(ValueError):
-        geometry.unary_union([box1, box1.to_crs(epsg3577)])
+        geom.unary_union([box1, box1.to_crs(epsg3577)])
 
 
 def test_unary_intersection():
-    box1 = geometry.box(10, 10, 30, 30, crs=epsg4326)
-    box2 = geometry.box(15, 10, 35, 30, crs=epsg4326)
-    box3 = geometry.box(20, 10, 40, 30, crs=epsg4326)
-    box4 = geometry.box(25, 10, 45, 30, crs=epsg4326)
-    box5 = geometry.box(30, 10, 50, 30, crs=epsg4326)
-    box6 = geometry.box(35, 10, 55, 30, crs=epsg4326)
-
-    inter1 = geometry.unary_intersection([box1])
+    box1 = geom.box(10, 10, 30, 30, crs=epsg4326)
+    box2 = geom.box(15, 10, 35, 30, crs=epsg4326)
+    box3 = geom.box(20, 10, 40, 30, crs=epsg4326)
+    box4 = geom.box(25, 10, 45, 30, crs=epsg4326)
+    box5 = geom.box(30, 10, 50, 30, crs=epsg4326)
+    box6 = geom.box(35, 10, 55, 30, crs=epsg4326)
+
+    inter1 = geom.unary_intersection([box1])
     assert bool(inter1)
     assert inter1 == box1
 
-    inter2 = geometry.unary_intersection([box1, box2])
+    inter2 = geom.unary_intersection([box1, box2])
     assert bool(inter2)
     assert inter2.area == 300.0
 
-    inter3 = geometry.unary_intersection([box1, box2, box3])
+    inter3 = geom.unary_intersection([box1, box2, box3])
     assert bool(inter3)
     assert inter3.area == 200.0
 
-    inter4 = geometry.unary_intersection([box1, box2, box3, box4])
+    inter4 = geom.unary_intersection([box1, box2, box3, box4])
     assert bool(inter4)
     assert inter4.area == 100.0
 
-    inter5 = geometry.unary_intersection([box1, box2, box3, box4, box5])
+    inter5 = geom.unary_intersection([box1, box2, box3, box4, box5])
     assert bool(inter5)
     assert inter5.type == "LineString"
     assert inter5.length == 20.0
 
-    inter6 = geometry.unary_intersection([box1, box2, box3, box4, box5, box6])
+    inter6 = geom.unary_intersection([box1, box2, box3, box4, box5, box6])
     assert not bool(inter6)
     assert inter6.is_empty
 
@@ -592,29 +589,29 @@ def test_projected_lon():
 
 
 def test_chop():
-    poly = geometry.box(618300, -1876800, 849000, -1642500, "EPSG:32660")
+    poly = geom.box(618300, -1876800, 849000, -1642500, "EPSG:32660")
 
     chopped = chop_along_antimeridian(poly)
     assert chopped.crs is poly.crs
     assert chopped.type == "MultiPolygon"
-    assert len([g for g in chopped]) == 2
+    assert len(list(chopped)) == 2
 
-    poly = geometry.box(0, 0, 10, 20, "EPSG:4326")._to_crs(epsg3857)
+    poly = geom.box(0, 0, 10, 20, "EPSG:4326")._to_crs(epsg3857)
     assert poly.crs is epsg3857
     assert chop_along_antimeridian(poly) is poly
 
     with pytest.raises(ValueError):
-        chop_along_antimeridian(geometry.box(0, 1, 2, 3, None))
+        chop_along_antimeridian(geom.box(0, 1, 2, 3, None))
 
 
 def test_clip_lon180():
     err = 1e-9
 
     def b(rside):
-        return geometry.box(170, 0, rside, 10, epsg4326)
+        return geom.box(170, 0, rside, 10, epsg4326)
 
     def b_neg(lside):
-        return geometry.box(lside, 0, -170, 10, epsg4326)
+        return geom.box(lside, 0, -170, 10, epsg4326)
 
     assert clip_lon180(b(180 - err)) == b(180)
     assert clip_lon180(b(-180 + err)) == b(180)
@@ -623,7 +620,7 @@ def test_clip_lon180():
     assert clip_lon180(b_neg(-180 + err)) == b_neg(-180)
 
     bb = multigeom([b(180 - err), b_neg(180 - err)])
-    bb_ = [g for g in clip_lon180(bb)]
+    bb_ = list(clip_lon180(bb))
     assert bb_[0] == b(180)
     assert bb_[1] == b_neg(-180)
 
@@ -632,7 +629,7 @@ def test_wrap_dateline():
     albers_crs = epsg3577
     geog_crs = epsg4326
 
-    wrap = geometry.polygon(
+    wrap = geom.polygon(
         [
             (3658653.1976781483, -4995675.379595791),
             (4025493.916030875, -3947239.249752495),
@@ -644,10 +641,10 @@ def test_wrap_dateline():
     )
     wrapped = wrap.to_crs(geog_crs)
     assert wrapped.type == "Polygon"
-    assert wrapped.intersects(geometry.line([(0, -90), (0, 90)], crs=geog_crs))
+    assert wrapped.intersects(geom.line([(0, -90), (0, 90)], crs=geog_crs))
     wrapped = wrap.to_crs(geog_crs, wrapdateline=True)
     assert wrapped.type == "MultiPolygon"
-    assert not wrapped.intersects(geometry.line([(0, -90), (0, 90)], crs=geog_crs))
+    assert not wrapped.intersects(geom.line([(0, -90), (0, 90)], crs=geog_crs))
 
 
 @pytest.mark.parametrize(
@@ -677,7 +674,7 @@ def test_wrap_dateline():
     ],
 )
 def test_wrap_dateline_sinusoidal(pts):
-    sinus_crs = geometry.CRS(
+    sinus_crs = geom.CRS(
         """PROJCS["unnamed",
                            GEOGCS["Unknown datum based upon the custom spheroid",
                            DATUM["Not specified (based on custom spheroid)", SPHEROID["Custom spheroid",6371007.181,0]],
@@ -689,23 +686,23 @@ def test_wrap_dateline_sinusoidal(pts):
                            UNIT["Meter",1]]"""
     )
 
-    wrap = geometry.polygon(pts, crs=sinus_crs)
+    wrap = geom.polygon(pts, crs=sinus_crs)
     wrapped = wrap.to_crs(epsg4326)
     assert wrapped.type == "Polygon"
     wrapped = wrap.to_crs(epsg4326, wrapdateline=True)
     assert wrapped.type == "MultiPolygon"
-    assert not wrapped.intersects(geometry.line([(0, -90), (0, 90)], crs=epsg4326))
+    assert not wrapped.intersects(geom.line([(0, -90), (0, 90)], crs=epsg4326))
 
 
 def test_wrap_dateline_utm():
-    poly = geometry.box(618300, -1876800, 849000, -1642500, "EPSG:32660")
+    poly = geom.box(618300, -1876800, 849000, -1642500, "EPSG:32660")
 
     wrapped = poly.to_crs(epsg4326)
     assert wrapped.type == "Polygon"
-    assert wrapped.intersects(geometry.line([(0, -90), (0, 90)], crs=epsg4326))
+    assert wrapped.intersects(geom.line([(0, -90), (0, 90)], crs=epsg4326))
     wrapped = poly.to_crs(epsg4326, wrapdateline=True)
     assert wrapped.type == "MultiPolygon"
-    assert not wrapped.intersects(geometry.line([(0, -90), (0, 90)], crs=epsg4326))
+    assert not wrapped.intersects(geom.line([(0, -90), (0, 90)], crs=epsg4326))
 
 
 def test_3d_geometry_converted_to_2d_geometry():
@@ -720,8 +717,8 @@ def test_3d_geometry_converted_to_2d_geometry():
     geom_3d = {"coordinates": [coordinates], "type": "Polygon"}
     geom_2d = {"coordinates": [[(x, y) for x, y, z in coordinates]], "type": "Polygon"}
 
-    g_2d = geometry.Geometry(geom_2d)
-    g_3d = geometry.Geometry(geom_3d)
+    g_2d = geom.Geometry(geom_2d)
+    g_3d = geom.Geometry(geom_3d)
 
     assert {2} == {len(pt) for pt in g_3d.boundary.coords}  # All coordinates are 2D
 
@@ -734,8 +731,8 @@ def test_3d_point_converted_to_2d_point():
     point_3d = {"coordinates": point, "type": "Point"}
     point_2d = {"coordinates": (point[0], point[1]), "type": "Point"}
 
-    p_2d = geometry.Geometry(point_2d)
-    p_3d = geometry.Geometry(point_3d)
+    p_2d = geom.Geometry(point_2d)
+    p_3d = geom.Geometry(point_3d)
 
     assert len(p_3d.coords[0]) == 2
 
@@ -743,14 +740,11 @@ def test_3d_point_converted_to_2d_point():
 
 
 def test_mid_longitude():
-    assert geometry.mid_longitude(geometry.point(10, 3, "epsg:4326")) == 10
-    assert geometry.mid_longitude(geometry.point(10, 3, "epsg:4326").buffer(3)) == 10
-    assert (
-        geometry.mid_longitude(
-            geometry.point(10, 3, "epsg:4326").buffer(3).to_crs("epsg:3857")
-        )
-        == 10
-    )
+    assert geom.mid_longitude(geom.point(10, 3, "epsg:4326")) == approx(10)
+    assert geom.mid_longitude(geom.point(10, 3, "epsg:4326").buffer(3)) == approx(10)
+    assert geom.mid_longitude(
+        geom.point(10, 3, "epsg:4326").buffer(3).to_crs("epsg:3857")
+    ) == approx(10)
 
 
 def test_polygon_path():
@@ -832,7 +826,7 @@ def test_apply_affine():
     assert yy_.shape == xx.shape
 
     xy_expect = [A * (x, y) for x, y in zip(xx.ravel(), yy.ravel())]
-    xy_got = [(x, y) for x, y in zip(xx_.ravel(), yy_.ravel())]
+    xy_got = list(zip(xx_.ravel(), yy_.ravel()))
 
     np.testing.assert_array_almost_equal(xy_expect, xy_got)
 
@@ -844,9 +838,7 @@ def test_point_transformer():
     pts = [(0, 0), (0, 1), (1, 2), (10, 11)]
     x, y = np.vstack(pts).astype("float64").T
 
-    pts_expect = [
-        geometry.point(*pt, epsg3857).to_crs(epsg4326).points[0] for pt in pts
-    ]
+    pts_expect = [geom.point(*pt, epsg3857).to_crs(epsg4326).points[0] for pt in pts]
 
     x_expect = [pt[0] for pt in pts_expect]
     y_expect = [pt[1] for pt in pts_expect]
@@ -967,7 +959,7 @@ def test_scale_at_point():
 def test_pix_transform():
     pt = tuple(
         int(x / 10) * 10
-        for x in geometry.point(145, -35, epsg4326).to_crs(epsg3577).coords[0]
+        for x in geom.point(145, -35, epsg4326).to_crs(epsg3577).coords[0]
     )
 
     A = mkA(scale=(20, -20), translation=pt)
@@ -1158,6 +1150,7 @@ def test_axis_overlap():
 
 def test_base_internals():
     no_epsg_crs = CRS(SAMPLE_WKT_WITHOUT_AUTHORITY)
+    assert no_epsg_crs.epsg is None
 
     gjson_bad = {"type": "a", "coordinates": [1, [2, 3, 4]]}
     assert force_2d(gjson_bad) == {"type": "a", "coordinates": [1, [2, 3]]}
@@ -1176,16 +1169,16 @@ def test_base_internals():
 
 
 def test_geom_clone():
-    b = geometry.box(0, 0, 10, 20, epsg4326)
+    b = geom.box(0, 0, 10, 20, epsg4326)
     assert b == b.clone()
     assert b.geom is not b.clone().geom
 
-    assert b == geometry.Geometry(b)
-    assert b.geom is not geometry.Geometry(b).geom
+    assert b == geom.Geometry(b)
+    assert b.geom is not geom.Geometry(b).geom
 
 
 @pytest.mark.parametrize(
-    "left, right, off, res, expect",
+    "left, right, res, off, expect",
     [
         (20, 30, 10, 0, (20, 1)),
         (20, 30.5, 10, 0, (20, 1)),
@@ -1199,26 +1192,26 @@ def test_geom_clone():
         (20, 30, -10, -3, (37, 2)),
     ],
 )
-def test_align_pix(left, right, off, res, expect):
-    assert _align_pix(left, right, off, res) == expect
+def test_align_pix(left, right, res, off, expect):
+    assert _align_pix(left, right, res, off) == expect
 
 
 def test_lonlat_bounds():
     # example from landsat scene: spans lon=180
-    poly = geometry.box(618300, -1876800, 849000, -1642500, "EPSG:32660")
+    poly = geom.box(618300, -1876800, 849000, -1642500, "EPSG:32660")
 
-    bb = geometry.lonlat_bounds(poly)
+    bb = geom.lonlat_bounds(poly)
     assert bb.left < 180 < bb.right
-    assert geometry.lonlat_bounds(poly) == geometry.lonlat_bounds(poly, resolution=1e8)
+    assert geom.lonlat_bounds(poly) == geom.lonlat_bounds(poly, resolution=1e8)
 
-    bb = geometry.lonlat_bounds(poly, mode="quick")
+    bb = geom.lonlat_bounds(poly, mode="quick")
     assert bb.right - bb.left > 180
 
-    poly = geometry.box(1, -10, 2, 20, "EPSG:4326")
-    assert geometry.lonlat_bounds(poly) == poly.boundingbox
+    poly = geom.box(1, -10, 2, 20, "EPSG:4326")
+    assert geom.lonlat_bounds(poly) == poly.boundingbox
 
     with pytest.raises(ValueError):
-        geometry.lonlat_bounds(geometry.box(0, 0, 1, 1, None))
+        geom.lonlat_bounds(geom.box(0, 0, 1, 1, None))
 
     multi = {
         "type": "MultiPolygon",
@@ -1228,11 +1221,11 @@ def test_lonlat_bounds():
         ],
     }
 
-    multi_geom = geometry.Geometry(multi, "epsg:4326")
+    multi_geom = geom.Geometry(multi, "epsg:4326")
     multi_geom_projected = multi_geom.to_crs("epsg:32659", math.inf)
 
-    ll_bounds = geometry.lonlat_bounds(multi_geom)
-    ll_bounds_projected = geometry.lonlat_bounds(multi_geom_projected)
+    ll_bounds = geom.lonlat_bounds(multi_geom)
+    ll_bounds_projected = geom.lonlat_bounds(multi_geom_projected)
 
     assert ll_bounds == approx(ll_bounds_projected)
 
@@ -1241,7 +1234,7 @@ def test_lonlat_bounds():
     True, reason="Bounds computation for large geometries in safe mode is broken"
 )
 def test_lonalt_bounds_more_than_180():
-    poly = geometry.box(-150, -30, 150, 30, epsg4326).to_crs(epsg3857, math.inf)
+    poly = geom.box(-150, -30, 150, 30, epsg4326).to_crs(epsg3857, math.inf)
 
-    assert geometry.lonlat_bounds(poly, "quick") == approx((-150, -30, 150, 30))
-    assert geometry.lonlat_bounds(poly, "safe") == approx((-150, -30, 150, 30))
+    assert geom.lonlat_bounds(poly, "quick") == approx((-150, -30, 150, 30))
+    assert geom.lonlat_bounds(poly, "safe") == approx((-150, -30, 150, 30))
diff --git a/tests/test_gridspec.py b/tests/test_gridspec.py
index 6b7accd..e12ecbe 100644
--- a/tests/test_gridspec.py
+++ b/tests/test_gridspec.py
@@ -4,9 +4,13 @@
 # SPDX-License-Identifier: Apache-2.0
 import numpy
 
-from odc.geo import CRS, BoundingBox, polygon
+from odc.geo import CRS, BoundingBox
+from odc.geo.geom import polygon
 from odc.geo.gridspec import GridSpec
 
+# pylint: disable=protected-access,use-implicit-booleaness-not-comparison
+# pylint: disable=comparison-with-itself,unnecessary-comprehension
+
 
 def test_gridspec():
     gs = GridSpec(
diff --git a/tests/test_xr_interop.py b/tests/test_xr_interop.py
index 33372ea..e91a4f4 100644
--- a/tests/test_xr_interop.py
+++ b/tests/test_xr_interop.py
@@ -2,20 +2,20 @@
 #
 # Copyright (c) 2015-2020 ODC Contributors
 # SPDX-License-Identifier: Apache-2.0
-from unittest.mock import MagicMock
-
 import pytest
 import xarray as xr
 
-import odc.geo
-from odc.geo._xr_interop import register_geobox, xr_coords
+from odc.geo import geom
 from odc.geo.geobox import GeoBox
 from odc.geo.testutils import epsg3577, mkA, purge_crs_info, xr_zeros
+from odc.geo.xr import register_geobox, xr_coords
+
+# pylint: disable=redefined-outer-name
 
 
 @pytest.fixture
 def geobox_epsg4326():
-    _box = odc.geo.box(-20, -10, 20, 10, "epsg:4326")
+    _box = geom.box(-20, -10, 20, 10, "epsg:4326")
     yield GeoBox.from_geopolygon(_box, 5)
 
 
@@ -115,10 +115,10 @@ def test_odc_extension(xx_epsg4326: xr.DataArray, geobox_epsg4326: GeoBox):
     assert _xx.odc.crs == gbox.crs
 
     # 1-d xarrays should report None for everything
-    assert _xx.XX.odc.spatial_dims == None
-    assert _xx.XX.odc.crs == None
-    assert _xx.XX.odc.transform == None
-    assert _xx.XX.odc.geobox == None
+    assert _xx.XX.odc.spatial_dims is None
+    assert _xx.XX.odc.crs is None
+    assert _xx.XX.odc.transform is None
+    assert _xx.XX.odc.geobox is None
 
 
 def test_odc_extension_ds(xx_epsg4326: xr.DataArray, geobox_epsg4326: GeoBox):
@@ -148,10 +148,10 @@ def test_odc_extension_ds(xx_epsg4326: xr.DataArray, geobox_epsg4326: GeoBox):
     assert _xx.odc.crs == gbox.crs
 
     # 1-d xarrays should report None for everything
-    assert _xx.XX.odc.spatial_dims == None
-    assert _xx.XX.odc.crs == None
-    assert _xx.XX.odc.transform == None
-    assert _xx.XX.odc.geobox == None
+    assert _xx.XX.odc.spatial_dims is None
+    assert _xx.XX.odc.crs is None
+    assert _xx.XX.odc.transform is None
+    assert _xx.XX.odc.geobox is None
 
 
 def test_assign_crs(xx_epsg4326: xr.DataArray):
@@ -205,7 +205,7 @@ def test_corrupt_inputs(xx_epsg4326: xr.DataArray):
     xx = xx.assign_coords(_crs2=xx.spatial_ref)
     xx.encoding.pop("grid_mapping")
     with pytest.warns(UserWarning):
-        xx.odc.crs == "epsg:4326"
+        assert xx.odc.crs == "epsg:4326"
 
     # attribute based search with bad CRS string
     xx = purge_crs_info(xx_epsg4326)
@@ -229,7 +229,7 @@ def test_corrupt_inputs(xx_epsg4326: xr.DataArray):
     with pytest.warns(UserWarning):
         _crs = xx.odc.crs
         assert _crs is not None
-        assert _crs == epsg3577 or _crs == "epsg:4326"
+        assert _crs in (epsg3577, "epsg:4326")
 
 
 def test_geobox_hook(xx_epsg4326: xr.DataArray):