diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5814deeee..0a59eb527 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ on: jobs: lint: - name: Lint + name: Lint & Mypy runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -27,6 +27,8 @@ jobs: uses: actions/setup-python@v4 with: python-version: "3.10" + - name: mypy + run: make mypy - name: lint run: make lint - name: fmtcheck diff --git a/Makefile b/Makefile index dd1ebcecd..85317cb1d 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ $(VENV_NAME)/bin/activate: setup.py requirements.txt ${VENV_NAME}/bin/python -m pip install -r requirements.txt @touch $(VENV_NAME)/bin/activate -test: venv pyright lint +test: venv pyright lint mypy @${VENV_NAME}/bin/tox -p auto -e $(DEFAULT_TEST_ENV) $(TOX_ARGS) test-nomock: venv @@ -25,6 +25,9 @@ coveralls: venv pyright: venv @${VENV_NAME}/bin/tox -e pyright $(PYRIGHT_ARGS) +mypy: venv + @${VENV_NAME}/bin/tox -e mypy $(MYPY_ARGS) + fmt: venv @${VENV_NAME}/bin/tox -e fmt @@ -43,4 +46,4 @@ update-version: codegen-format: fmt -.PHONY: ci-test clean codegen-format coveralls fmt fmtcheck lint test test-nomock test-travis update-version venv pyright +.PHONY: ci-test clean codegen-format coveralls fmt fmtcheck lint test test-nomock test-travis update-version venv pyright mypy diff --git a/pyproject.toml b/pyproject.toml index 7c651f1f0..2aa2b6c2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,10 +23,24 @@ exclude = ''' ) ''' [tool.pyright] -include = ["stripe", "tests/test_generated_examples.py"] +include = [ + "stripe", + "tests/test_generated_examples.py", + "tests/test_exports.py", +] exclude = ["build", "**/__pycache__"] reportMissingTypeArgument = true reportUnnecessaryCast = true reportUnnecessaryComparison = true reportUnnecessaryContains = true reportUnnecessaryIsInstance = true +reportPrivateImportUsage = true +reportUnnecessaryTypeIgnoreComment = true + +[tool.mypy] +follow_imports = "silent" +python_version = "3.10" +files = ["tests/test_exports.py"] +disallow_untyped_calls = true +disallow_untyped_defs = true +warn_unused_ignores = true diff --git a/requirements.txt b/requirements.txt index 2dfa00516..4078f2661 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,6 @@ virtualenv<20.22.0 pyright == 1.1.336 black == 22.8.0 flake8 +mypy == 1.7.0 -r test-requirements.txt diff --git a/stripe/__init__.py b/stripe/__init__.py index 1302dfc6b..a9feb66e7 100644 --- a/stripe/__init__.py +++ b/stripe/__init__.py @@ -38,9 +38,9 @@ log: Optional[Literal["debug", "info"]] = None # API resources -from stripe.api_resources import * # pyright: ignore # noqa +from stripe.api_resources import * # noqa -from stripe.api_resources import abstract # pyright: ignore # noqa +from stripe.api_resources import abstract # noqa # OAuth from stripe.oauth import OAuth # noqa diff --git a/stripe/api_requestor.py b/stripe/api_requestor.py index aa721a382..773a8c363 100644 --- a/stripe/api_requestor.py +++ b/stripe/api_requestor.py @@ -101,7 +101,7 @@ def __init__( self._client = client elif stripe.default_http_client: self._client = stripe.default_http_client - if proxy != self._default_proxy: # type: ignore + if proxy != self._default_proxy: warnings.warn( "stripe.proxy was updated after sending a " "request - this is a no-op. To use a different proxy, " diff --git a/stripe/api_resources/abstract/api_resource.py b/stripe/api_resources/abstract/api_resource.py index cd9f4c01d..ecfdb5fa4 100644 --- a/stripe/api_resources/abstract/api_resource.py +++ b/stripe/api_resources/abstract/api_resource.py @@ -147,7 +147,7 @@ def _static_request( if idempotency_key is not None: headers = {} if headers is None else headers.copy() - headers.update(util.populate_headers(idempotency_key)) # type: ignore + headers.update(util.populate_headers(idempotency_key)) response, api_key = requestor.request(method_, url_, params, headers) return util.convert_to_stripe_object( @@ -186,7 +186,7 @@ def _static_request_stream( if idempotency_key is not None: headers = {} if headers is None else headers.copy() - headers.update(util.populate_headers(idempotency_key)) # type: ignore + headers.update(util.populate_headers(idempotency_key)) response, _ = requestor.request_stream(method_, url_, params, headers) return response diff --git a/stripe/api_resources/abstract/test_helpers.py b/stripe/api_resources/abstract/test_helpers.py index 47197f08f..728c260fb 100644 --- a/stripe/api_resources/abstract/test_helpers.py +++ b/stripe/api_resources/abstract/test_helpers.py @@ -42,7 +42,7 @@ def class_url(cls): ) # Namespaces are separated in object names with periods (.) and in URLs # with forward slashes (/), so replace the former with the latter. - base = cls._resource_cls.OBJECT_NAME.replace(".", "/") # type: ignore + base = cls._resource_cls.OBJECT_NAME.replace(".", "/") return "/v1/test_helpers/%ss" % (base,) def instance_url(self): diff --git a/stripe/api_resources/list_object.py b/stripe/api_resources/list_object.py index 406658fc5..42cb313a6 100644 --- a/stripe/api_resources/list_object.py +++ b/stripe/api_resources/list_object.py @@ -1,4 +1,6 @@ -# pyright: strict +# pyright: strict, reportUnnecessaryTypeIgnoreComment=false +# reportUnnecessaryTypeIgnoreComment is set to false because some type ignores are required in some +# python versions but not the others from typing_extensions import Self from typing import ( @@ -115,7 +117,9 @@ def __getitem__(self, k: str) -> T: # Pyright doesn't like this because ListObject inherits from StripeObject inherits from Dict[str, Any] # and so it wants the type of __iter__ to agree with __iter__ from Dict[str, Any] # But we are iterating through "data", which is a List[T]. - def __iter__(self) -> Iterator[T]: # pyright: ignore + def __iter__( # pyright: ignore + self, + ) -> Iterator[T]: return getattr(self, "data", []).__iter__() def __len__(self) -> int: @@ -132,11 +136,11 @@ def auto_paging_iter(self) -> Iterator[T]: "ending_before" in self._retrieve_params and "starting_after" not in self._retrieve_params ): - for item in reversed(page): # type: ignore + for item in reversed(page): yield item page = page.previous_page() else: - for item in page: # type: ignore + for item in page: yield item page = page.next_page() diff --git a/stripe/api_resources/quote.py b/stripe/api_resources/quote.py index 05741199e..d5bacda66 100644 --- a/stripe/api_resources/quote.py +++ b/stripe/api_resources/quote.py @@ -1567,7 +1567,7 @@ def pdf( ... @util.class_method_variant("_cls_pdf") - def pdf( # type: ignore + def pdf( # pyright: ignore self, api_key=None, api_version=None, diff --git a/stripe/error.py b/stripe/error.py index 34a72474b..adda1b2bc 100644 --- a/stripe/error.py +++ b/stripe/error.py @@ -111,7 +111,7 @@ def __repr__(self): % ( self.__class__.__name__, self._message, - self.param, # type: ignore + self.param, # pyright: ignore self.code, self.http_status, self.request_id, diff --git a/stripe/http_client.py b/stripe/http_client.py index cbc4873e3..9c4b7101f 100644 --- a/stripe/http_client.py +++ b/stripe/http_client.py @@ -31,7 +31,7 @@ pycurl = None try: - import requests # pyright: ignore + import requests except ImportError: requests = None else: @@ -61,7 +61,7 @@ requests = None try: - from google.appengine.api import urlfetch # type: ignore + from google.appengine.api import urlfetch # pyright: ignore except ImportError: urlfetch = None diff --git a/stripe/multipart_data_generator.py b/stripe/multipart_data_generator.py index 9731cd343..510d8afe4 100644 --- a/stripe/multipart_data_generator.py +++ b/stripe/multipart_data_generator.py @@ -18,7 +18,10 @@ def __init__(self, chunk_size: int = 1028): def add_params(self, params): # Flatten parameters first - params = dict(stripe.api_requestor._api_encode(params)) # type: ignore + + params = dict( + stripe.api_requestor._api_encode(params) # pyright: ignore + ) for key, value in params.items(): if value is None: diff --git a/stripe/oauth_error.py b/stripe/oauth_error.py index d0c95ec26..7c25aef9f 100644 --- a/stripe/oauth_error.py +++ b/stripe/oauth_error.py @@ -20,7 +20,7 @@ def construct_error_object(self): if self.json_body is None: return None - return stripe.api_resources.error_object.OAuthErrorObject.construct_from( # type: ignore + return stripe.api_resources.error_object.OAuthErrorObject.construct_from( # pyright: ignore self.json_body, stripe.api_key ) diff --git a/stripe/stripe_object.py b/stripe/stripe_object.py index 1e2fd7ab2..a23bed187 100644 --- a/stripe/stripe_object.py +++ b/stripe/stripe_object.py @@ -115,7 +115,9 @@ def last_response(self) -> Optional[StripeResponse]: # StripeObject inherits from `dict` which has an update method, and this doesn't quite match # the full signature of the update method in MutableMapping. But we ignore. - def update(self, update_dict: Mapping[str, Any]) -> None: # type: ignore[override] + def update( # pyright: ignore + self, update_dict: Mapping[str, Any] + ) -> None: for k in update_dict: self._unsaved_values.add(k) diff --git a/tests/test_exports.py b/tests/test_exports.py new file mode 100644 index 000000000..5b1541125 --- /dev/null +++ b/tests/test_exports.py @@ -0,0 +1,98 @@ +# pyright: strict +import stripe + + +def test_can_import_stripe_object() -> None: + from stripe.stripe_object import ( + StripeObject as StripeObjectFromStripeStripeObject, + ) + + assert ( + stripe.stripe_object.StripeObject is StripeObjectFromStripeStripeObject + ) + + +def test_can_import_webhook_members() -> None: + from stripe import ( + Webhook, + WebhookSignature, + ) + + assert Webhook is not None + assert WebhookSignature is not None + + +def test_can_import_abstract() -> None: + from stripe.api_resources.abstract import ( + APIResource as APIResourceFromApiResourcesAbstract, + ) + from stripe.stripe_object import ( + StripeObject, + ) + + assert ( + APIResourceFromApiResourcesAbstract[StripeObject] + == stripe.abstract.APIResource[StripeObject] + ) + + +def test_can_import_app_info() -> None: + from stripe.app_info import AppInfo as AppInfoFromStripeAppInfo + from stripe import AppInfo as AppInfoFromStripe + + assert AppInfoFromStripeAppInfo is AppInfoFromStripe + assert AppInfoFromStripeAppInfo is stripe.AppInfo + + +def test_can_import_stripe_response() -> None: + from stripe.stripe_response import ( + StripeResponse as StripeResponseFromStripeResponse, + ) + + assert ( + StripeResponseFromStripeResponse + is stripe.stripe_response.StripeResponse + ) + + +def test_can_import_oauth_members() -> None: + from stripe import ( + OAuth, + ) + + assert OAuth is not None + + +def test_can_import_errors() -> None: + from stripe.error import ( + StripeError as StripeErrorFromStripeError, + ) + + assert StripeErrorFromStripeError is not None + + +def test_can_import_top_level_resource() -> None: + from stripe import Account as AccountFromStripe + from stripe.api_resources import Account as AccountFromStripeResources + from stripe.api_resources.account import ( + Account as AccountFromStripeResourcesAccount, + ) + + assert stripe.Account == AccountFromStripe + assert AccountFromStripe == AccountFromStripeResources + assert AccountFromStripeResourcesAccount == AccountFromStripeResources + + +def test_can_import_namespaced_resource() -> None: + from stripe import tax as TaxPackage + from stripe.api_resources.tax import ( + Calculation as CalculationFromResources, + ) + from stripe.api_resources.tax.calculation import ( + Calculation as CalculationFromResourcesCalculation, + ) + + assert stripe.tax is TaxPackage + assert stripe.tax.Calculation is TaxPackage.Calculation + assert stripe.tax.Calculation is CalculationFromResources + assert CalculationFromResources is CalculationFromResourcesCalculation diff --git a/tox.ini b/tox.ini index 87461395d..7d157e410 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ envlist = fmt lint pyright + mypy py{312,311,310,39,38,37,36,py3} ignore_base_python_conflict = false @@ -32,13 +33,14 @@ commands = pytest --cov {posargs:-n auto} --ignore stripe # CFLAGS="-I$(brew --prefix openssl@1.1)/include" passenv = LDFLAGS,CFLAGS -[testenv:{lint,fmt,pyright}] +[testenv:{lint,fmt,pyright,mypy}] basepython = python3.10 skip_install = true commands = pyright: pyright {posargs} lint: python -m flake8 --show-source stripe tests setup.py fmt: black . {posargs} + mypy: mypy {posargs} deps = -r requirements.txt