From 89fb2b0ca8ee898b5e25c93563ed739b759ba801 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 24 Jun 2022 11:16:20 -0400 Subject: [PATCH 1/8] Add typing dependencies and run mypy with linters --- requirements-dev.txt | 2 ++ scripts/lint | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 87ec01bc..60202cb2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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 diff --git a/scripts/lint b/scripts/lint index 45f49093..186109cd 100755 --- a/scripts/lint +++ b/scripts/lint @@ -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 \ No newline at end of file From e865c415f9f44b7e6fbc9533147dc5ec6ad34d80 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 24 Jun 2022 11:21:48 -0400 Subject: [PATCH 2/8] Include test files in type checking --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d019ecc..e6f3135d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,6 @@ repos: hooks: - id: mypy files: ".*\\.py$" - exclude: ^tests/.*$ additional_dependencies: - pystac - types-requests From dd59746e76ae622e17c4fc08181ccbff8515cf77 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 24 Jun 2022 11:22:04 -0400 Subject: [PATCH 3/8] Avoid implicit re-exports --- pystac_client/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pystac_client/__init__.py b/pystac_client/__init__.py index f137af73..9615932a 100644 --- a/pystac_client/__init__.py +++ b/pystac_client/__init__.py @@ -1,4 +1,7 @@ # flake8: noqa +__all__ = [ + "Client", "CollectionClient", "ConformanceClasses", "ItemSearch", "__version__" +] from pystac_client.client import Client from pystac_client.collection_client import CollectionClient From 7c9ad9a4bfc129dc988de0ac7038cf53f36e6e76 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 24 Jun 2022 12:00:35 -0400 Subject: [PATCH 4/8] Format --- pystac_client/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pystac_client/__init__.py b/pystac_client/__init__.py index 9615932a..9e37dfcb 100644 --- a/pystac_client/__init__.py +++ b/pystac_client/__init__.py @@ -1,6 +1,10 @@ # flake8: noqa __all__ = [ - "Client", "CollectionClient", "ConformanceClasses", "ItemSearch", "__version__" + "Client", + "CollectionClient", + "ConformanceClasses", + "ItemSearch", + "__version__", ] from pystac_client.client import Client From 6a6f51cfed012611d8ed74cf5329e8b2f7346fa6 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 24 Jun 2022 12:00:42 -0400 Subject: [PATCH 5/8] Fix lots of type errors --- docs/conf.py | 2 +- tests/helpers.py | 3 +- tests/test_cli.py | 6 +-- tests/test_client.py | 67 ++++++++++++++++++++------------- tests/test_collection_client.py | 14 ++++--- tests/test_item_search.py | 56 ++++++++++++++------------- tests/test_stac_api_io.py | 11 +++--- 7 files changed, 90 insertions(+), 69 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9549ebb5..ae148395 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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"]) diff --git a/tests/helpers.py b/tests/helpers.py index ac250511..223b3a68 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,5 +1,6 @@ import json from pathlib import Path +from typing import Any TEST_DATA = Path(__file__).parent / "data" @@ -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: diff --git a/tests/test_cli.py b/tests/test_cli.py index 699f3366..e7b5c95a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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", @@ -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 diff --git a/tests/test_client.py b/tests/test_client.py index c28e2062..1827730e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -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 @@ -17,8 +18,8 @@ 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 @@ -26,8 +27,8 @@ def test_instance(self): assert str(api) == "" - @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 @@ -37,28 +38,31 @@ 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) @@ -66,11 +70,12 @@ def test_no_conformance(self): 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 client._stac_io._conformance = client._stac_io._conformance[1:] with pytest.raises(NotImplementedError): @@ -78,17 +83,17 @@ def test_no_stac_core_conformance(self): 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") @@ -101,11 +106,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, @@ -117,7 +124,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 @@ -133,6 +140,7 @@ 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. @@ -140,6 +148,7 @@ def test_custom_request_parameters(self, requests_mock): # Get the /collections endpoint collections_link = api.get_single_link("data") + assert collections_link is not None # Mock the request requests_mock.get( @@ -163,7 +172,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 @@ -186,6 +195,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( @@ -226,7 +236,7 @@ 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.""" @@ -234,6 +244,7 @@ def test_custom_query_params_get_collection_propagation( 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( @@ -249,6 +260,7 @@ 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 @@ -256,6 +268,7 @@ def test_custom_query_params_get_collection_propagation( # 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") @@ -285,7 +298,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) @@ -315,6 +328,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) @@ -334,22 +348,23 @@ 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 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") @@ -373,7 +388,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", diff --git a/tests/test_collection_client.py b/tests/test_collection_client.py index 8fdefa25..e8d1701d 100644 --- a/tests/test_collection_client.py +++ b/tests/test_collection_client.py @@ -7,26 +7,28 @@ class TestCollectionClient: - @pytest.mark.vcr - def test_instance(self): + @pytest.mark.vcr # type: ignore[misc] + def test_instance(self) -> None: client = Client.open(STAC_URLS["PLANETARY-COMPUTER"]) collection = client.get_collection("aster-l1t") assert isinstance(collection, CollectionClient) assert str(collection) == "" - @pytest.mark.vcr - def test_get_items(self): + @pytest.mark.vcr # type: ignore[misc] + def test_get_items(self) -> None: client = Client.open(STAC_URLS["PLANETARY-COMPUTER"]) collection = client.get_collection("aster-l1t") + assert collection is not None for item in collection.get_items(): assert item.collection_id == collection.id return - @pytest.mark.vcr - def test_get_item(self): + @pytest.mark.vcr # type: ignore[misc] + def test_get_item(self) -> None: client = Client.open(STAC_URLS["PLANETARY-COMPUTER"]) collection = client.get_collection("aster-l1t") + assert collection is not None item = collection.get_item("AST_L1T_00312272006020322_20150518201805") assert item assert item.id == "AST_L1T_00312272006020322_20150518201805" diff --git a/tests/test_item_search.py b/tests/test_item_search.py index d6f6fd7e..53a28c43 100644 --- a/tests/test_item_search.py +++ b/tests/test_item_search.py @@ -1,10 +1,12 @@ import json from datetime import datetime, timedelta +from typing import Any, Dict, Iterator import pystac import pytest import requests from dateutil.tz import gettz, tzutc +from pytest_benchmark.fixture import BenchmarkFixture from pystac_client import Client from pystac_client.item_search import ItemSearch @@ -29,25 +31,25 @@ class TestItemPerformance: - @pytest.fixture(scope="function") - def single_href(self) -> None: + @pytest.fixture(scope="function") # type: ignore[misc] + def single_href(self) -> str: item_href = "https://planetarycomputer.microsoft.com/api/stac/v1/collections/{collections}/items/{ids}".format( collections=ITEM_EXAMPLE["collections"], ids=ITEM_EXAMPLE["ids"] ) return item_href - def test_requests(self, benchmark, single_href): + def test_requests(self, benchmark: BenchmarkFixture, single_href: str) -> None: response = benchmark(requests.get, single_href) assert response.status_code == 200 assert response.json()["id"] == ITEM_EXAMPLE["ids"] - def test_single_item(self, benchmark, single_href): + def test_single_item(self, benchmark: BenchmarkFixture, single_href: str) -> None: item = benchmark(pystac.Item.from_file, single_href) assert item.id == ITEM_EXAMPLE["ids"] - def test_single_item_search(self, benchmark, single_href): + def test_single_item_search(self, benchmark: BenchmarkFixture, single_href: str) -> None: search = ItemSearch(url=SEARCH_URL, **ITEM_EXAMPLE) item_collection = benchmark(search.get_all_items) @@ -57,8 +59,8 @@ def test_single_item_search(self, benchmark, single_href): class TestItemSearchParams: - @pytest.fixture(scope="function") - def sample_client(self) -> None: + @pytest.fixture(scope="function") # type: ignore[misc] + def sample_client(self) -> Client: api_content = read_data_file("planetary-computer-root.json", parse_json=True) return Client.from_dict(api_content) @@ -79,7 +81,7 @@ def test_string_bbox(self) -> None: def test_generator_bbox(self) -> None: # Generator Input - def bboxer(): + def bboxer() -> Iterator[float]: yield from [-104.5, 44.0, -104.0, 45.0] search = ItemSearch(url=SEARCH_URL, bbox=bboxer()) @@ -257,7 +259,7 @@ def test_list_of_collection_strings(self) -> None: def test_generator_of_collection_strings(self) -> None: # Generator of ID strings - def collectioner(): + def collectioner() -> Iterator[str]: yield from ["naip", "landsat8_l1tp"] search = ItemSearch(url=SEARCH_URL, collections=collectioner()) @@ -297,7 +299,7 @@ def test_list_of_id_strings(self) -> None: def test_generator_of_id_string(self) -> None: # Generator of IDs - def ids(): + def ids() -> Iterator[str]: yield from [ "m_3510836_se_12_060_20180508_20190331", "m_3510840_se_12_060_20180504_20190331", @@ -416,7 +418,7 @@ def test_sortby(self) -> None: with pytest.raises(Exception): ItemSearch(url=SEARCH_URL, sortby=[1]) - def test_fields(self): + def test_fields(self) -> None: with pytest.raises(Exception): ItemSearch(url=SEARCH_URL, fields=1) @@ -464,8 +466,8 @@ def test_fields(self): class TestItemSearch: - @pytest.fixture(scope="function") - def astraea_api(self) -> None: + @pytest.fixture(scope="function") # type: ignore[misc] + def astraea_api(self) -> Client: api_content = read_data_file("astraea_api.json", parse_json=True) return Client.from_dict(api_content) @@ -499,7 +501,7 @@ def test_method_params(self) -> None: assert all(key in params for key in params_in) assert all(isinstance(params[key], str) for key in params_in) - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_results(self) -> None: search = ItemSearch( url=SEARCH_URL, @@ -511,7 +513,7 @@ def test_results(self) -> None: assert all(isinstance(item, pystac.Item) for item in results) - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_ids_results(self) -> None: ids = [ "S2B_MSIL2A_20210610T115639_N0212_R066_T33XXG_20210613T185024.SAFE", @@ -526,7 +528,7 @@ def test_ids_results(self) -> None: assert len(results) == 1 assert all(item.id in ids for item in results) - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_datetime_results(self) -> None: # Datetime range string datetime_ = "2019-01-01T00:00:01Z/2019-01-01T00:00:10Z" @@ -537,13 +539,13 @@ def test_datetime_results(self) -> None: min_datetime = datetime(2019, 1, 1, 0, 0, 1, tzinfo=tzutc()) max_datetime = datetime(2019, 1, 1, 0, 0, 10, tzinfo=tzutc()) search = ItemSearch(url=SEARCH_URL, datetime=(min_datetime, max_datetime)) - results = search.items() + new_results = search.items() assert all( min_datetime <= item.datetime <= (max_datetime + timedelta(seconds=1)) - for item in results + for item in new_results ) - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_intersects_results(self) -> None: # GeoJSON-like dict intersects_dict = { @@ -567,17 +569,17 @@ def test_intersects_results(self) -> None: # Geo-interface object class MockGeoObject: @property - def __geo_interface__(self) -> None: + def __geo_interface__(self) -> Dict[str, Any]: return intersects_dict intersects_obj = MockGeoObject() search = ItemSearch( url=SEARCH_URL, intersects=intersects_obj, collections="naip" ) - results = search.items() - assert all(isinstance(item, pystac.Item) for item in results) + new_results = search.items() + assert all(isinstance(item, pystac.Item) for item in new_results) - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_result_paging(self) -> None: search = ItemSearch( url=SEARCH_URL, @@ -593,7 +595,7 @@ def test_result_paging(self) -> None: assert pages[1] != pages[2] assert pages[1].items != pages[2].items - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_get_all_items(self) -> None: search = ItemSearch( url=SEARCH_URL, @@ -605,7 +607,7 @@ def test_get_all_items(self) -> None: item_collection = search.get_all_items() assert len(item_collection.items) == 20 - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_items_as_dicts(self) -> None: search = ItemSearch( url=SEARCH_URL, @@ -618,7 +620,7 @@ def test_items_as_dicts(self) -> None: class TestItemSearchQuery: - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_query_shortcut_syntax(self) -> None: search = ItemSearch( url=SEARCH_URL, @@ -640,7 +642,7 @@ def test_query_shortcut_syntax(self) -> None: assert len(items2) == 1 assert items1[0].id == items2[0].id - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_query_json_syntax(self) -> None: # with a list of json strs (format of CLI argument to ItemSearch) diff --git a/tests/test_stac_api_io.py b/tests/test_stac_api_io.py index c2988676..0c1ce4ee 100644 --- a/tests/test_stac_api_io.py +++ b/tests/test_stac_api_io.py @@ -2,6 +2,7 @@ from urllib.parse import parse_qs, urlsplit import pytest +from requests_mock.mocker import Mocker from pystac_client.conformance import ConformanceClasses from pystac_client.exceptions import APIError @@ -11,20 +12,20 @@ class TestSTAC_IOOverride: - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_request_input(self) -> None: stac_api_io = StacApiIO() response = stac_api_io.read_text(STAC_URLS["PLANETARY-COMPUTER"]) assert isinstance(response, str) - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_str_input(self) -> None: stac_api_io = StacApiIO() response = stac_api_io.read_text(STAC_URLS["PLANETARY-COMPUTER"]) assert isinstance(response, str) - @pytest.mark.vcr + @pytest.mark.vcr # type: ignore[misc] def test_http_error(self) -> None: stac_api_io = StacApiIO() # Attempt to access an authenticated endpoint @@ -68,7 +69,7 @@ def test_conforms_to(self) -> None: # Check that this does not raise an exception assert conformant_io.conforms_to(ConformanceClasses.CORE) - def test_custom_headers(self, requests_mock): + def test_custom_headers(self, requests_mock: Mocker) -> None: """Checks that headers passed to the init method are added to requests.""" header_name = "x-my-header" header_value = "Some Value" @@ -84,7 +85,7 @@ def test_custom_headers(self, requests_mock): assert header_name in history[0].headers assert history[0].headers[header_name] == header_value - def test_custom_query_params(self, requests_mock): + def test_custom_query_params(self, requests_mock: Mocker) -> None: """Checks that query params passed to the init method are added to requests.""" init_qp_name = "my-param" init_qp_value = "something" From af62720411eaa62c9c80eb1eb508db77a331f317 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 24 Jun 2022 12:03:14 -0400 Subject: [PATCH 6/8] Fix typing of Client._stac_io --- pystac_client/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pystac_client/client.py b/pystac_client/client.py index 53385d47..89077fee 100644 --- a/pystac_client/client.py +++ b/pystac_client/client.py @@ -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 "".format(self.id) @@ -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 @@ -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, ) From 7e733037c662d3a3a2272223aefa8a504ed0759c Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 24 Jun 2022 12:35:42 -0400 Subject: [PATCH 7/8] Fix remaining type errors --- tests/test_client.py | 2 ++ tests/test_item_search.py | 26 +++++++++++++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 1827730e..aecb6cd9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -76,6 +76,7 @@ def test_no_stac_core_conformance(self) -> None: 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): @@ -358,6 +359,7 @@ def test_search_conformance_error(self, api: Client) -> None: 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: diff --git a/tests/test_item_search.py b/tests/test_item_search.py index 53a28c43..4ed4407c 100644 --- a/tests/test_item_search.py +++ b/tests/test_item_search.py @@ -27,7 +27,7 @@ ], } -ITEM_EXAMPLE = {"collections": "io-lulc", "ids": "60U-2020"} +ITEM_EXAMPLE: Dict[str, Any] = {"collections": "io-lulc", "ids": "60U-2020"} class TestItemPerformance: @@ -49,7 +49,9 @@ def test_single_item(self, benchmark: BenchmarkFixture, single_href: str) -> Non assert item.id == ITEM_EXAMPLE["ids"] - def test_single_item_search(self, benchmark: BenchmarkFixture, single_href: str) -> None: + def test_single_item_search( + self, benchmark: BenchmarkFixture, single_href: str + ) -> None: search = ItemSearch(url=SEARCH_URL, **ITEM_EXAMPLE) item_collection = benchmark(search.get_all_items) @@ -413,18 +415,18 @@ def test_sortby(self) -> None: ) with pytest.raises(Exception): - ItemSearch(url=SEARCH_URL, sortby=1) + ItemSearch(url=SEARCH_URL, sortby=1) # type: ignore[arg-type] with pytest.raises(Exception): - ItemSearch(url=SEARCH_URL, sortby=[1]) + ItemSearch(url=SEARCH_URL, sortby=[1]) # type: ignore[arg-type] def test_fields(self) -> None: with pytest.raises(Exception): - ItemSearch(url=SEARCH_URL, fields=1) + ItemSearch(url=SEARCH_URL, fields=1) # type: ignore[arg-type] with pytest.raises(Exception): - ItemSearch(url=SEARCH_URL, fields=[1]) + ItemSearch(url=SEARCH_URL, fields=[1]) # type: ignore[list-item] search = ItemSearch(url=SEARCH_URL, fields="id,collection,+foo,-bar") assert search.get_parameters()["fields"] == { @@ -481,7 +483,7 @@ def test_method(self) -> None: assert search.method == "GET" def test_method_params(self) -> None: - params_in = { + params_in: Dict[str, Any] = { "bbox": (-72, 41, -71, 42), "ids": ( "idone", @@ -540,10 +542,12 @@ def test_datetime_results(self) -> None: max_datetime = datetime(2019, 1, 1, 0, 0, 10, tzinfo=tzutc()) search = ItemSearch(url=SEARCH_URL, datetime=(min_datetime, max_datetime)) new_results = search.items() - assert all( - min_datetime <= item.datetime <= (max_datetime + timedelta(seconds=1)) - for item in new_results - ) + + for item in new_results: + assert item.datetime is not None + assert ( + min_datetime <= item.datetime <= (max_datetime + timedelta(seconds=1)) + ) @pytest.mark.vcr # type: ignore[misc] def test_intersects_results(self) -> None: From 67c09d552798a4bd9fbb6e104b262bbd6eec6d52 Mon Sep 17 00:00:00 2001 From: Jon Duckworth Date: Fri, 24 Jun 2022 12:44:08 -0400 Subject: [PATCH 8/8] Add CHANGELOG entry for #249 --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 164b2b83..94f22c9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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