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

Better errors for pystac/satstac import issues #125

Merged
merged 1 commit into from
Feb 2, 2022
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
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)