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

Type fixes #249

Merged
merged 8 commits into from
Jul 18, 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
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ repos:
hooks:
- id: mypy
files: ".*\\.py$"
exclude: ^tests/.*$
additional_dependencies:
- pystac
- types-requests
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased] - TBD

None
### Fixed

- Fix type annotation of `Client._stac_io` and avoid implicit re-exports in `pystac_client.__init__.py` [#249](https://github.com/stac-utils/pystac-client/pull/249)

## [v0.4.0] - 2022-06-08

Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
sys.path.insert(0, str(Path(__file__).parent.parent.parent.resolve()))
from pystac_client import __version__ # type: ignore # noqa: E402
from pystac_client import __version__ # noqa: E402

git_branch = (
subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"])
Expand Down
7 changes: 7 additions & 0 deletions pystac_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
# flake8: noqa
__all__ = [
"Client",
"CollectionClient",
"ConformanceClasses",
"ItemSearch",
"__version__",
]

from pystac_client.client import Client
from pystac_client.collection_client import CollectionClient
Expand Down
6 changes: 4 additions & 2 deletions pystac_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class Client(pystac.Catalog):
such as searching items (e.g., /search endpoint).
"""

_stac_io: Optional[StacApiIO]

def __repr__(self) -> str:
return "<Client id={}>".format(self.id)

Expand Down Expand Up @@ -76,7 +78,7 @@ def open(
and len(search_link.href) > 0
)
):
client._stac_io.set_conformance(None) # type: ignore
client._stac_io.set_conformance(None)

return client

Expand Down Expand Up @@ -252,7 +254,7 @@ def search(self, **kwargs: Any) -> ItemSearch:

return ItemSearch(
url=search_href,
stac_io=self._stac_io, # type: ignore
stac_io=self._stac_io,
client=self,
**kwargs,
)
Expand Down
2 changes: 2 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ requests-mock~=1.9.3
Sphinx~=3.5.1

mypy~=0.961
types-requests~=2.27.31
types-python-dateutil~=2.8.18
flake8~=4.0.1
black~=22.3.0
codespell~=2.1.0
Expand Down
2 changes: 1 addition & 1 deletion scripts/lint
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
pre-commit run codespell --all-files
pre-commit run doc8 --all-files
pre-commit run flake8 --all-files
# pre-commit run mypy --all-files
pre-commit run mypy --all-files
fi
fi
3 changes: 2 additions & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from pathlib import Path
from typing import Any

TEST_DATA = Path(__file__).parent / "data"

Expand All @@ -10,7 +11,7 @@
}


def read_data_file(file_name: str, mode="r", parse_json=False):
def read_data_file(file_name: str, mode: str = "r", parse_json: bool = False) -> Any:
file_path = TEST_DATA / file_name
with file_path.open(mode=mode) as src:
if parse_json:
Expand Down
6 changes: 3 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@


class TestCLI:
@pytest.mark.vcr
def test_item_search(self, script_runner: ScriptRunner):
@pytest.mark.vcr # type: ignore[misc]
def test_item_search(self, script_runner: ScriptRunner) -> None:
args = [
"stac-client",
"search",
Expand All @@ -19,7 +19,7 @@ def test_item_search(self, script_runner: ScriptRunner):
result = script_runner.run(*args, print_result=False)
assert result.success

def test_no_arguments(self, script_runner: ScriptRunner):
def test_no_arguments(self, script_runner: ScriptRunner) -> None:
args = ["stac-client"]
result = script_runner.run(*args, print_result=False)
assert not result.success
Expand Down
69 changes: 43 additions & 26 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pytest
from dateutil.tz import tzutc
from pystac import MediaType
from requests_mock import Mocker

from pystac_client import Client
from pystac_client.conformance import ConformanceClasses
Expand All @@ -17,17 +18,17 @@


class TestAPI:
@pytest.mark.vcr
def test_instance(self):
@pytest.mark.vcr # type: ignore[misc]
def test_instance(self) -> None:
api = Client.open(STAC_URLS["PLANETARY-COMPUTER"])

# An API instance is also a Catalog instance
assert isinstance(api, pystac.Catalog)

assert str(api) == "<Client id=microsoft-pc>"

@pytest.mark.vcr
def test_links(self):
@pytest.mark.vcr # type: ignore[misc]
def test_links(self) -> None:
api = Client.open(STAC_URLS["PLANETARY-COMPUTER"])

# Should be able to get collections via links as with a typical PySTAC Catalog
Expand All @@ -37,58 +38,63 @@ def test_links(self):
collections = list(api.get_collections())
assert len(collection_links) == len(collections)

first_collection = (
api.get_single_link("child").resolve_stac_object(root=api).target
)
first_child_link = api.get_single_link("child")
assert first_child_link is not None
first_collection = first_child_link.resolve_stac_object(root=api).target
assert isinstance(first_collection, pystac.Collection)

def test_spec_conformance(self):
def test_spec_conformance(self) -> None:
"""Testing conformance against a ConformanceClass should allow APIs using legacy
URIs to pass."""
client = Client.from_file(str(TEST_DATA / "planetary-computer-root.json"))
assert client._stac_io is not None

# Set conformsTo URIs to conform with STAC API - Core using official URI
client._stac_io._conformance = ["https://api.stacspec.org/v1.0.0-beta.1/core"]

assert client._stac_io.conforms_to(ConformanceClasses.CORE)

@pytest.mark.vcr
def test_no_conformance(self):
@pytest.mark.vcr # type: ignore[misc]
def test_no_conformance(self) -> None:
"""Should raise a NotImplementedError if no conformance info can be found.
Luckily, the test API doesn't publish a "conformance" link so we can just
remove the "conformsTo" attribute to test this."""
client = Client.from_file(str(TEST_DATA / "planetary-computer-root.json"))
assert client._stac_io is not None
client._stac_io._conformance = []
assert client._stac_io is not None

with pytest.raises(NotImplementedError):
client._stac_io.assert_conforms_to(ConformanceClasses.CORE)

with pytest.raises(NotImplementedError):
client._stac_io.assert_conforms_to(ConformanceClasses.ITEM_SEARCH)

@pytest.mark.vcr
def test_no_stac_core_conformance(self):
@pytest.mark.vcr # type: ignore[misc]
def test_no_stac_core_conformance(self) -> None:
"""Should raise a NotImplementedError if the API does not conform to the
STAC API - Core spec."""
client = Client.from_file(str(TEST_DATA / "planetary-computer-root.json"))
assert client._stac_io is not None
assert client._stac_io._conformance is not None
client._stac_io._conformance = client._stac_io._conformance[1:]

with pytest.raises(NotImplementedError):
client._stac_io.assert_conforms_to(ConformanceClasses.CORE)

assert client._stac_io.conforms_to(ConformanceClasses.ITEM_SEARCH)

@pytest.mark.vcr
def test_from_file(self):
@pytest.mark.vcr # type: ignore[misc]
def test_from_file(self) -> None:
api = Client.from_file(STAC_URLS["PLANETARY-COMPUTER"])

assert api.title == "Microsoft Planetary Computer STAC API"

def test_invalid_url(self):
def test_invalid_url(self) -> None:
with pytest.raises(TypeError):
Client.open()
Client.open() # type: ignore[call-arg]

def test_get_collections_with_conformance(self, requests_mock):
def test_get_collections_with_conformance(self, requests_mock: Mocker) -> None:
"""Checks that the "data" endpoint is used if the API published the
STAC API Collections conformance class."""
pc_root_text = read_data_file("planetary-computer-root.json")
Expand All @@ -101,11 +107,13 @@ def test_get_collections_with_conformance(self, requests_mock):
STAC_URLS["PLANETARY-COMPUTER"], status_code=200, text=pc_root_text
)
api = Client.open(STAC_URLS["PLANETARY-COMPUTER"])
assert api._stac_io is not None

assert api._stac_io.conforms_to(ConformanceClasses.COLLECTIONS)

# Get & mock the collections (rel type "data") link
collections_link = api.get_single_link("data")
assert collections_link is not None
requests_mock.get(
collections_link.href,
status_code=200,
Expand All @@ -117,7 +125,7 @@ def test_get_collections_with_conformance(self, requests_mock):
assert len(history) == 2
assert history[1].url == collections_link.href

def test_custom_request_parameters(self, requests_mock):
def test_custom_request_parameters(self, requests_mock: Mocker) -> None:
pc_root_text = read_data_file("planetary-computer-root.json")
pc_collection_dict = read_data_file(
"planetary-computer-collection.json", parse_json=True
Expand All @@ -133,13 +141,15 @@ def test_custom_request_parameters(self, requests_mock):
api = Client.open(
STAC_URLS["PLANETARY-COMPUTER"], parameters={init_qp_name: init_qp_value}
)
assert api._stac_io is not None

# Ensure that the Client will use the /collections endpoint and not fall back
# to traversing child links.
assert api._stac_io.conforms_to(ConformanceClasses.COLLECTIONS)

# Get the /collections endpoint
collections_link = api.get_single_link("data")
assert collections_link is not None

# Mock the request
requests_mock.get(
Expand All @@ -163,7 +173,7 @@ def test_custom_request_parameters(self, requests_mock):
assert actual_qp[init_qp_name][0] == init_qp_value

def test_custom_query_params_get_collections_propagation(
self, requests_mock
self, requests_mock: Mocker
) -> None:
"""Checks that query params passed to the init method are added to requests for
CollectionClients fetched from
Expand All @@ -186,6 +196,7 @@ def test_custom_query_params_get_collections_propagation(

# Get the /collections endpoint
collections_link = client.get_single_link("data")
assert collections_link is not None

# Mock the request
requests_mock.get(
Expand Down Expand Up @@ -226,14 +237,15 @@ def test_custom_query_params_get_collections_propagation(
assert actual_qp[init_qp_name][0] == init_qp_value

def test_custom_query_params_get_collection_propagation(
self, requests_mock
self, requests_mock: Mocker
) -> None:
"""Checks that query params passed to the init method are added to
requests for CollectionClients fetched from the /collections endpoint."""
pc_root_text = read_data_file("planetary-computer-root.json")
pc_collection_dict = read_data_file(
"planetary-computer-collection.json", parse_json=True
)
assert isinstance(pc_collection_dict, dict)
pc_collection_id = pc_collection_dict["id"]

requests_mock.get(
Expand All @@ -249,13 +261,15 @@ def test_custom_query_params_get_collection_propagation(

# Get the /collections endpoint
collections_link = client.get_single_link("data")
assert collections_link is not None
collection_href = collections_link.href + "/" + pc_collection_id

# Mock the request
requests_mock.get(collection_href, status_code=200, json=pc_collection_dict)

# Make the collections request
collection = client.get_collection(pc_collection_id)
assert collection is not None

# Mock the items endpoint
items_link = collection.get_single_link("items")
Expand Down Expand Up @@ -285,7 +299,7 @@ def test_custom_query_params_get_collection_propagation(
assert len(actual_qp[init_qp_name]) == 1
assert actual_qp[init_qp_name][0] == init_qp_value

def test_get_collections_without_conformance(self, requests_mock):
def test_get_collections_without_conformance(self, requests_mock: Mocker) -> None:
"""Checks that the "data" endpoint is used if the API published
the Collections conformance class."""
pc_root_dict = read_data_file("planetary-computer-root.json", parse_json=True)
Expand Down Expand Up @@ -315,6 +329,7 @@ def test_get_collections_without_conformance(self, requests_mock):
STAC_URLS["PLANETARY-COMPUTER"], status_code=200, json=pc_root_dict
)
api = Client.open(STAC_URLS["PLANETARY-COMPUTER"])
assert api._stac_io is not None

assert not api._stac_io.conforms_to(ConformanceClasses.COLLECTIONS)

Expand All @@ -334,22 +349,24 @@ def test_opening_a_collection(self) -> None:


class TestAPISearch:
@pytest.fixture(scope="function")
def api(self):
@pytest.fixture(scope="function") # type: ignore[misc]
def api(self) -> Client:
return Client.from_file(str(TEST_DATA / "planetary-computer-root.json"))

def test_search_conformance_error(self, api):
def test_search_conformance_error(self, api: Client) -> None:
"""Should raise a NotImplementedError if the API doesn't conform
to the Item Search spec. Message should
include information about the spec that was not conformed to."""
# Set the conformance to only STAC API - Core
assert api._stac_io is not None
assert api._stac_io._conformance is not None
api._stac_io._conformance = [api._stac_io._conformance[0]]

with pytest.raises(NotImplementedError) as excinfo:
api.search(limit=10, max_items=10, collections="mr-peebles")
assert str(ConformanceClasses.ITEM_SEARCH) in str(excinfo.value)

def test_no_search_link(self, api):
def test_no_search_link(self, api: Client) -> None:
# Remove the search link
api.remove_links("search")

Expand All @@ -373,7 +390,7 @@ def test_no_conforms_to(self) -> None:
api.search(limit=10, max_items=10, collections="naip")
assert "does not support search" in str(excinfo.value)

def test_search(self, api):
def test_search(self, api: Client) -> None:
results = api.search(
bbox=[-73.21, 43.99, -73.12, 44.05],
collections="naip",
Expand Down
Loading