Skip to content

Commit

Permalink
Better errors for pystac/satstac import issues (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
gjoseph92 authored Feb 2, 2022
1 parent 0bc305f commit ed2b9f7
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 11 deletions.
42 changes: 31 additions & 11 deletions stackstac/stac_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
cast,
)

possible_problems: list[str] = []

try:
from satstac import Item as SatstacItem
from satstac import ItemCollection as SatstacItemCollection
except ImportError:
from satstac.item import Item as SatstacItem
from satstac.itemcollection import ItemCollection as SatstacItemCollection
except ImportError as e:

class SatstacItem:
_data: ItemDict
Expand All @@ -31,11 +33,20 @@ class SatstacItemCollection:
def __iter__(self) -> Iterator[SatstacItem]:
...

if not isinstance(e, ModuleNotFoundError):
possible_problems.append(
"Your version of `satstac` is too old (or new) for stackstac. "
"`satstac.Item` and `satstac.ItemCollection` aren't supported "
f"because they could not be imported: {e!r}"
)
del e


try:
from pystac import Catalog as PystacCatalog
from pystac import Item as PystacItem
except ImportError:
from pystac import ItemCollection as PystacItemCollection
except ImportError as e:

class PystacItem:
def to_dict(self) -> ItemDict:
Expand All @@ -45,18 +56,20 @@ class PystacCatalog:
def get_all_items(self) -> Iterator[PystacItem]:
...


# pystac 1.0
try:
from pystac import ItemCollection as PystacItemCollection
except ImportError:

class PystacItemCollection:
features: List[PystacItem]

def __iter__(self) -> Iterator[PystacItem]:
...

if not isinstance(e, ModuleNotFoundError):
possible_problems.append(
"Your version of `pystac` is too old (or new) for stackstac. "
"`pystac.Item`, `pystac.ItemCollection`, and `pystac.Catalog` aren't supported "
f"because they could not be imported: {e!r}"
)
del e


class EOBand(TypedDict, total=False):
name: str
Expand Down Expand Up @@ -160,4 +173,11 @@ def items_to_plain(items: Union[ItemCollectionIsh, ItemIsh]) -> ItemSequence:
if isinstance(items, PystacItemCollection):
return [item.to_dict() for item in items]

raise TypeError(f"Unrecognized STAC collection type {type(items)}: {items!r}")
raise TypeError(
f"Unrecognized STAC collection type {type(items)}: {items!r}"
+ (
"\n".join(["\nPossible problems:"] + possible_problems)
if possible_problems
else ""
)
)
99 changes: 99 additions & 0 deletions stackstac/tests/test_stac_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import importlib
from datetime import datetime
import sys
from types import ModuleType

import pytest
import satstac
import pystac

from stackstac import stac_types


def test_normal_case():
assert stac_types.SatstacItem is satstac.item.Item
assert stac_types.SatstacItemCollection is satstac.itemcollection.ItemCollection

assert stac_types.PystacItem is pystac.Item
assert stac_types.PystacItemCollection is pystac.ItemCollection
assert stac_types.PystacCatalog is pystac.Catalog


def test_missing_satstac(monkeypatch: pytest.MonkeyPatch):
"Test importing works without satstac"
monkeypatch.setitem(sys.modules, "satstac", None) # type: ignore
monkeypatch.setitem(sys.modules, "satstac.item", None) # type: ignore
monkeypatch.setitem(sys.modules, "satstac.itemcollection", None) # type: ignore
# Type "ModuleType | None" cannot be assigned to type "ModuleType"

reloaded_stac_types = importlib.reload(stac_types)
assert "stackstac" in reloaded_stac_types.SatstacItem.__module__
assert "stackstac" in reloaded_stac_types.SatstacItemCollection.__module__

assert not reloaded_stac_types.possible_problems


def test_missing_pystac(monkeypatch: pytest.MonkeyPatch):
"Test importing works without pystac"
monkeypatch.setitem(sys.modules, "pystac", None) # type: ignore
# Type "ModuleType | None" cannot be assigned to type "ModuleType"

reloaded_stac_types = importlib.reload(stac_types)
assert "stackstac" in reloaded_stac_types.PystacItem.__module__
assert "stackstac" in reloaded_stac_types.PystacCatalog.__module__
assert "stackstac" in reloaded_stac_types.PystacItemCollection.__module__

assert not reloaded_stac_types.possible_problems


@pytest.mark.parametrize(
"module, path, inst",
[
(
satstac,
"item.Item",
satstac.item.Item(
{"id": "foo"},
),
),
(
satstac,
"itemcollection.ItemCollection",
satstac.itemcollection.ItemCollection(
[],
),
),
(pystac, "Item", pystac.Item("foo", None, None, datetime(2000, 1, 1), {})),
(pystac, "Catalog", pystac.Catalog("foo", "bar")),
(
pystac,
"ItemCollection",
pystac.ItemCollection(
[],
),
),
],
)
def test_unimportable_path(
module: ModuleType, path: str, inst: object, monkeypatch: pytest.MonkeyPatch
):
"""
Test that importing still works when a type isn't importable from pystac/satstac,
but the overall module is importable. (Simulating a breaking change/incompatible version.)
Verify that a useful error is shown when `items_to_plain` fails.
"""
parts = path.split(".")
modname = module.__name__

# Delete the `path` from `module`. We do this instead of just putting None in `sys.modules`,
# so that we get a proper `ImportError` instead of `ModuleNotFoundError`.
for p in parts:
prev = module
module = getattr(module, p)
monkeypatch.delattr(prev, p)

reloaded_stac_types = importlib.reload(stac_types)

with pytest.raises(TypeError, match=f"Your version of `{modname}` is too old"):
reloaded_stac_types.items_to_plain(inst)

0 comments on commit ed2b9f7

Please sign in to comment.