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

Typecheck some API interfaces #593

Merged
merged 28 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
19e2e99
Add typechecking and action config
mfisher87 Sep 4, 2024
4cc2eb9
Typecheck granules module
mfisher87 Sep 4, 2024
041c5ba
Correct invalid type annotation
mfisher87 Aug 14, 2024
a43a25e
Flesh out CMR params & EGI subset types
mfisher87 Aug 26, 2024
3fc95f1
Define generic type annotation for Parameters class
mfisher87 Aug 26, 2024
b9c77e8
Partially annotate the Query class
mfisher87 Sep 4, 2024
da851aa
Add miscellaneous annotation
mfisher87 Aug 26, 2024
2da7a0b
Add all dependencies to typecheck environment
mfisher87 Aug 27, 2024
c8ba01a
Add notes to pyright config
mfisher87 Aug 27, 2024
27f4641
Remove pyright ignores for modules without any errors
mfisher87 Sep 4, 2024
181099f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2024
e6e7fe2
Clarify use of term "EGI" in docstring
mfisher87 Sep 4, 2024
0440fa9
Type valid data product short names
mfisher87 Sep 4, 2024
df78ca2
Fixup API parameter types
mfisher87 Sep 5, 2024
ee70196
Fixup missed name change
mfisher87 Sep 5, 2024
434c28a
Use Python 3.9-compatible annotation style
mfisher87 Sep 5, 2024
a36d17c
Correct product name type name
mfisher87 Sep 12, 2024
a8996d1
Narrow argument types
mfisher87 Sep 12, 2024
dfe4c09
Set pyright's Python version to 3.9
mfisher87 Sep 12, 2024
fddf23a
Unblock upgrading sphinx extensions by replacing sphinx-panels
mfisher87 Sep 12, 2024
e155a9b
Parse and correctly generate docs from numpy-style docstrings
mfisher87 Sep 12, 2024
51b3492
Fix typo
mfisher87 Sep 13, 2024
7f60849
Autogenerate docs for types
mfisher87 Sep 13, 2024
4cb5956
Tell the typechecker to trust us on the type of reqparams
mfisher87 Sep 13, 2024
7a423a8
Tweak style of panels in index page
mfisher87 Sep 13, 2024
6a4acd9
Rename "Specific" classes to "Required"
mfisher87 Sep 13, 2024
e41aed0
Mark EGI subset shape params NotRequired
mfisher87 Sep 13, 2024
81d60dd
Merge branch 'development' into refactor-and-typecheck-api-interfaces
JessicaS11 Sep 16, 2024
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
29 changes: 29 additions & 0 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Typecheck

on:
pull_request:
push:
branches:
- main
- development


jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-python@v5
with:
python-version: "3.10"
mfisher87 marked this conversation as resolved.
Show resolved Hide resolved

- name: Install package and test dependencies
run: |
python -m pip install .[complete]
JessicaS11 marked this conversation as resolved.
Show resolved Hide resolved
python -m pip install -r requirements-dev.txt

- uses: jakebailey/pyright-action@v2
81 changes: 68 additions & 13 deletions icepyx/core/APIformatting.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Generate and format information for submitting to API (CMR and NSIDC)
"""Generate and format information for submitting to API (CMR and NSIDC)."""

import datetime as dt
from typing import Any, Generic, Literal, TypeVar, Union, overload

from icepyx.core.types import (
CMRParams,
EGIParamsSubset,
EGISpecificRequiredParams,
)

# ----------------------------------------------------------------------
# parameter-specific formatting for display
Expand Down Expand Up @@ -183,12 +190,56 @@ def to_string(params):
return "&".join(param_list)


ParameterType = Literal["CMR", "required", "subset"]
# DevGoal: When Python 3.12 is minimum supported version, migrate to PEP695 style
T = TypeVar("T", bound=ParameterType)

JessicaS11 marked this conversation as resolved.
Show resolved Hide resolved

class _FmtedKeysDescriptor:
"""Enable the Parameters class' fmted_keys property to be typechecked correctly.

See: https://github.com/microsoft/pyright/issues/3071#issuecomment-1043978070
"""

@overload
def __get__(
self,
instance: 'Parameters[Literal["CMR"]]',
owner: Any,
JessicaS11 marked this conversation as resolved.
Show resolved Hide resolved
) -> CMRParams: ...

@overload
def __get__(
self,
instance: 'Parameters[Literal["required"]]',
owner: Any,
) -> EGISpecificRequiredParams: ...

@overload
def __get__(
self,
instance: 'Parameters[Literal["subset"]]',
owner: Any,
) -> EGIParamsSubset: ...

def __get__(
self,
instance: "Parameters",
owner: Any,
) -> Union[CMRParams, EGISpecificRequiredParams, EGIParamsSubset]:
"""
Returns the dictionary of formatted keys associated with the
parameter object.
"""
return instance._fmted_keys


# ----------------------------------------------------------------------
# DevNote: Currently, this class is not tested!!
# DevGoal: this could be expanded, similar to the variables class, to provide users with valid options if need be
# DevGoal: currently this does not do much by way of checking/formatting of other subsetting options (reprojection or formats)
# it would be great to incorporate that so that people can't just feed any keywords in...
class Parameters:
class Parameters(Generic[T]):
"""
JessicaS11 marked this conversation as resolved.
Show resolved Hide resolved
Build and update the parameter lists needed to submit a data order

Expand All @@ -206,7 +257,14 @@ class Parameters:
on the type of query. Must be one of ['search','download']
"""

def __init__(self, partype, values=None, reqtype=None):
fmted_keys = _FmtedKeysDescriptor()

def __init__(
self,
partype: T,
values=None,
reqtype=None,
):
assert partype in [
"CMR",
"required",
Expand Down Expand Up @@ -242,15 +300,7 @@ def poss_keys(self):

# return self._wanted

@property
def fmted_keys(self):
"""
Returns the dictionary of formatted keys associated with the
parameter object.
"""
return self._fmted_keys

def _get_possible_keys(self):
JessicaS11 marked this conversation as resolved.
Show resolved Hide resolved
def _get_possible_keys(self) -> dict[str, list[str]]:
"""
Use the parameter type to get a list of possible parameter keys.
"""
Expand Down Expand Up @@ -347,7 +397,7 @@ def check_values(self):
else:
return False

def build_params(self, **kwargs):
def build_params(self, **kwargs) -> None:
"""
Build the parameter dictionary of formatted key:value pairs for submission to NSIDC
in the data request.
Expand Down Expand Up @@ -443,3 +493,8 @@ def build_params(self, **kwargs):
k = "Boundingshape"

self._fmted_keys.update({k: kwargs["spatial_extent"]})


CMRParameters = Parameters[Literal["CMR"]]
RequiredParameters = Parameters[Literal["required"]]
SubsetParameters = Parameters[Literal["subset"]]
2 changes: 1 addition & 1 deletion icepyx/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def __init__(self, auth=None):
self._s3login_credentials = None
self._s3_initial_ts = None # timer for 1h expiration on s3 credentials

def __str__(self):
def __str__(self) -> str:
if self.session:
repr_string = "EarthdataAuth obj with session initialized"
else:
Expand Down
36 changes: 27 additions & 9 deletions icepyx/core/granules.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
from __future__ import annotations

import datetime
import io
import json
import os
import pprint
import re
import time
from typing import Optional
from xml.etree import ElementTree as ET
import zipfile

import numpy as np
import requests
from requests.compat import unquote

import icepyx.core.APIformatting as apifmt
from icepyx.core.auth import EarthdataAuthMixin
import icepyx.core.exceptions
from icepyx.core.types import CMRParams, EGISpecificRequiredParams
from icepyx.core.urls import DOWNLOAD_BASE_URL, GRANULE_SEARCH_BASE_URL, ORDER_BASE_URL


Expand Down Expand Up @@ -170,14 +175,19 @@
# ----------------------------------------------------------------------
# Methods

def get_avail(self, CMRparams, reqparams, cloud=False):
def get_avail(
self,
CMRparams: Optional[CMRParams],
reqparams: Optional[EGISpecificRequiredParams],
mfisher87 marked this conversation as resolved.
Show resolved Hide resolved
cloud=False,
):
"""
Get a list of available granules for the query object's parameters.
Generates the `avail` attribute of the granules object.

Parameters
----------
CMRparams : dictionary
CMRparams :
JessicaS11 marked this conversation as resolved.
Show resolved Hide resolved
Dictionary of properly formatted CMR search parameters.
reqparams : dictionary
Dictionary of properly formatted parameters required for searching, ordering,
Expand Down Expand Up @@ -261,13 +271,13 @@
# DevGoal: add kwargs to allow subsetting and more control over request options.
def place_order(
self,
CMRparams,
reqparams,
CMRparams: CMRParams,
reqparams: EGISpecificRequiredParams,
subsetparams,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
subsetparams,
subsetparams: Union[EGIRequiredParamsSubset, dict[Never, Never]],

I might have the syntax wrong. In some cases we specify the type from types.py, and in others from APIformatting.py (e.g. in query.py).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dict[Never, Never] means an empty dict, and this change doesn't pass type checking. In what case would reqparams={} be passed?

Copy link
Member

@JessicaS11 JessicaS11 Sep 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was simply adding a type for subsetparams, since it wasn't specified. It sounds like it would need to be something more like Optional[EGIRequiredParamsSubset] so it doesn't otherwise require an empty dict.

In what case would reqparams={} be passed?

dict[Never, Never] is not being specified as a valid type to reqparams.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woops, I meant subsetparams :)

verbose,
subset=True,
geom_filepath=None,
): # , **kwargs):
):
"""
Place an order for the available granules for the query object.
Adds the list of zipped files (orders) to the granules data object (which is
Expand All @@ -276,11 +286,11 @@

Parameters
----------
CMRparams : dictionary
CMRparams :
Dictionary of properly formatted CMR search parameters.
reqparams : dictionary
reqparams :
Dictionary of properly formatted parameters required for searching, ordering,
or downloading from NSIDC.
or downloading from NSIDC (via their EGI system).
subsetparams : dictionary
Dictionary of properly formatted subsetting parameters. An empty dictionary
is passed as input here when subsetting is set to False in query methods.
Expand Down Expand Up @@ -359,7 +369,7 @@
request.raise_for_status()
esir_root = ET.fromstring(request.content)
if verbose is True:
print("Order request URL: ", requests.utils.unquote(request.url))
print("Order request URL: ", unquote(request.url))

Check warning on line 372 in icepyx/core/granules.py

View check run for this annotation

Codecov / codecov/patch

icepyx/core/granules.py#L372

Added line #L372 was not covered by tests
JessicaS11 marked this conversation as resolved.
Show resolved Hide resolved
print(
"Order request response XML content: ",
request.content.decode("utf-8"),
Expand Down Expand Up @@ -402,6 +412,7 @@
loop_root = ET.fromstring(loop_response.content)

# Continue loop while request is still processing
loop_root = None

Check warning on line 415 in icepyx/core/granules.py

View check run for this annotation

Codecov / codecov/patch

icepyx/core/granules.py#L415

Added line #L415 was not covered by tests
while status == "pending" or status == "processing":
print(
"Your order status is still ",
Expand All @@ -425,6 +436,13 @@
if status == "pending" or status == "processing":
continue

if not isinstance(loop_root, ET.Element):
# The typechecker determined that loop_root could be unbound at this
# point. We know for sure this shouldn't be possible, though, because
# the while loop should run once.
# See: https://github.com/microsoft/pyright/discussions/2033
raise RuntimeError("Programmer error!")

Check warning on line 444 in icepyx/core/granules.py

View check run for this annotation

Codecov / codecov/patch

icepyx/core/granules.py#L444

Added line #L444 was not covered by tests

# Order can either complete, complete_with_errors, or fail:
# Provide complete_with_errors error message:
if status == "complete_with_errors" or status == "failed":
Expand Down
21 changes: 14 additions & 7 deletions icepyx/core/query.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import pprint
from typing import Optional, Union

import geopandas as gpd
import matplotlib.pyplot as plt
from typing_extensions import Never

import icepyx.core.APIformatting as apifmt
from icepyx.core.auth import EarthdataAuthMixin
Expand All @@ -11,6 +13,7 @@
import icepyx.core.is2ref as is2ref
import icepyx.core.spatial as spat
import icepyx.core.temporal as tp
from icepyx.core.types import CMRParams, EGIParamsSubset, EGISpecificRequiredParams
import icepyx.core.validate_inputs as val
from icepyx.core.variables import Variables as Variables
from icepyx.core.visualization import Visualize
Expand Down Expand Up @@ -393,6 +396,10 @@
GenQuery
"""

_CMRparams: apifmt.CMRParameters
_reqparams: apifmt.RequiredParameters
_subsetparams: Optional[apifmt.SubsetParameters]

# ----------------------------------------------------------------------
# Constructors

Expand Down Expand Up @@ -532,7 +539,7 @@
return sorted(set(self._tracks))

@property
def CMRparams(self):
def CMRparams(self) -> CMRParams:
"""
Display the CMR key:value pairs that will be submitted.
It generates the dictionary if it does not already exist.
Expand Down Expand Up @@ -573,7 +580,7 @@
return self._CMRparams.fmted_keys

@property
def reqparams(self):
def reqparams(self) -> EGISpecificRequiredParams:
"""
Display the required key:value pairs that will be submitted.
It generates the dictionary if it does not already exist.
Expand All @@ -599,7 +606,7 @@
# @property
# DevQuestion: if I make this a property, I get a "dict" object is not callable
# when I try to give input kwargs... what approach should I be taking?
def subsetparams(self, **kwargs):
def subsetparams(self, **kwargs) -> Union[EGIParamsSubset, dict[Never, Never]]:
"""
Display the subsetting key:value pairs that will be submitted.
It generates the dictionary if it does not already exist
Expand Down Expand Up @@ -1001,7 +1008,7 @@
if "email" in self._reqparams.fmted_keys or email is False:
self._reqparams.build_params(**self._reqparams.fmted_keys)
elif email is True:
user_profile = self.auth.get_user_profile()
user_profile = self.auth.get_user_profile() # pyright: ignore[reportAttributeAccessIssue]

Check warning on line 1011 in icepyx/core/query.py

View check run for this annotation

Codecov / codecov/patch

icepyx/core/query.py#L1011

Added line #L1011 was not covered by tests
self._reqparams.build_params(
**self._reqparams.fmted_keys, email=user_profile["email_address"]
)
Expand Down Expand Up @@ -1135,14 +1142,14 @@
import geoviews as gv
from shapely.geometry import Polygon # noqa: F401

gv.extension("bokeh")
gv.extension("bokeh") # pyright: ignore[reportCallIssue]

Check warning on line 1145 in icepyx/core/query.py

View check run for this annotation

Codecov / codecov/patch

icepyx/core/query.py#L1145

Added line #L1145 was not covered by tests

bbox_poly = gv.Path(gdf["geometry"]).opts(color="red", line_color="red")
tile = gv.tile_sources.EsriImagery.opts(width=500, height=500)
return tile * bbox_poly
return tile * bbox_poly # pyright: ignore[reportOperatorIssue]

Check warning on line 1149 in icepyx/core/query.py

View check run for this annotation

Codecov / codecov/patch

icepyx/core/query.py#L1149

Added line #L1149 was not covered by tests

except ImportError:
world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) # pyright: ignore[reportAttributeAccessIssue]

Check warning on line 1152 in icepyx/core/query.py

View check run for this annotation

Codecov / codecov/patch

icepyx/core/query.py#L1152

Added line #L1152 was not covered by tests
f, ax = plt.subplots(1, figsize=(12, 6))
world.plot(ax=ax, facecolor="lightgray", edgecolor="gray")
gdf.plot(ax=ax, color="#FF8C00", alpha=0.7)
Expand Down
Loading
Loading