From e20ee370933b26a9cee229f88ff652ba3832a19d Mon Sep 17 00:00:00 2001 From: David Barnett Date: Sun, 25 Aug 2024 06:11:43 -0600 Subject: [PATCH] tooling: generate some type stubs Use mypy stubgen tool (https://mypy.readthedocs.io/en/stable/stubgen.html) to generate a few stubs for types used in the project in place of "# type: ignore" overrides. General approach: 1. run `stubgen -o stubs -m some.module` 2. check for errors by running `tox -e type` 3. curate pyi files to fix errors (delete conflicting types, etc) --- gcalcli/gcal.py | 2 +- gcalcli/utils.py | 2 +- pyproject.toml | 8 +- stubs/google/auth/credentials.pyi | 49 ++++++++++ stubs/google/auth/exceptions.pyi | 4 + stubs/google/auth/transport/__init__.pyi | 20 ++++ stubs/google/auth/transport/requests.pyi | 21 +++++ stubs/google/oauth2/credentials.pyi | 24 +++++ stubs/google_auth_oauthlib/flow.pyi | 36 ++++++++ stubs/googleapiclient/discovery.pyi | 26 ++++++ .../parsedatetime/parsedatetime/__init__.pyi | 92 +++++++++++++++++++ tox.ini | 1 + 12 files changed, 280 insertions(+), 5 deletions(-) create mode 100644 stubs/google/auth/credentials.pyi create mode 100644 stubs/google/auth/exceptions.pyi create mode 100644 stubs/google/auth/transport/__init__.pyi create mode 100644 stubs/google/auth/transport/requests.pyi create mode 100644 stubs/google/oauth2/credentials.pyi create mode 100644 stubs/google_auth_oauthlib/flow.pyi create mode 100644 stubs/googleapiclient/discovery.pyi create mode 100644 stubs/parsedatetime/parsedatetime/__init__.pyi diff --git a/gcalcli/gcal.py b/gcalcli/gcal.py index b7ebb50a..4deddd48 100644 --- a/gcalcli/gcal.py +++ b/gcalcli/gcal.py @@ -17,7 +17,7 @@ from dateutil.relativedelta import relativedelta from dateutil.tz import tzlocal from google_auth_oauthlib.flow import InstalledAppFlow # type: ignore -from googleapiclient.discovery import build # type: ignore +from googleapiclient.discovery import build from googleapiclient.errors import HttpError # type: ignore from google.auth.transport.requests import Request # type: ignore diff --git a/gcalcli/utils.py b/gcalcli/utils.py index 75d84c46..8d87500d 100644 --- a/gcalcli/utils.py +++ b/gcalcli/utils.py @@ -6,7 +6,7 @@ from dateutil.parser import parse as dateutil_parse from dateutil.tz import tzlocal -from parsedatetime.parsedatetime import Calendar # type: ignore +from parsedatetime.parsedatetime import Calendar locale.setlocale(locale.LC_ALL, '') fuzzy_date_parse = Calendar().parse diff --git a/pyproject.toml b/pyproject.toml index c7888154..c7b031d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,9 @@ Repository = "https://github.com/insanum/gcalcli" [project.optional-dependencies] vobject = ["vobject"] +[tool.setuptools] +packages = ["gcalcli"] + [tool.setuptools.data-files] "share/man/man1" = ["docs/man1/gcalcli.1"] @@ -59,6 +62,5 @@ float_to_top = true combine_star = true py_version = 3 -[[tool.mypy.overrides]] -module = ["gcalcli"] -ignore_missing_imports = true +[tool.mypy] +mypy_path = "gcalcli:stubs:tests" diff --git a/stubs/google/auth/credentials.pyi b/stubs/google/auth/credentials.pyi new file mode 100644 index 00000000..4be09710 --- /dev/null +++ b/stubs/google/auth/credentials.pyi @@ -0,0 +1,49 @@ +import abc +from _typeshed import Incomplete + +class Credentials(metaclass=abc.ABCMeta): + token: Incomplete + expiry: Incomplete + def __init__(self) -> None: ... + @property + def expired(self): ... + @property + def valid(self): ... + @abc.abstractmethod + def refresh(self, request): ... + def apply(self, headers, token: Incomplete | None = None) -> None: ... + def before_request(self, request, method, url, headers) -> None: ... + +class AnonymousCredentials(Credentials): + @property + def expired(self): ... + @property + def valid(self): ... + def refresh(self, request) -> None: ... + def apply(self, headers, token: Incomplete | None = None) -> None: ... + def before_request(self, request, method, url, headers) -> None: ... + +class ReadOnlyScoped(metaclass=abc.ABCMeta): + def __init__(self) -> None: ... + @property + def scopes(self): ... + @property + @abc.abstractmethod + def requires_scopes(self): ... + def has_scopes(self, scopes): ... + +class Scoped(ReadOnlyScoped, metaclass=abc.ABCMeta): + @abc.abstractmethod + def with_scopes(self, scopes): ... + +def with_scopes_if_required(credentials, scopes): ... + +class Signing(metaclass=abc.ABCMeta): + @abc.abstractmethod + def sign_bytes(self, message): ... + @property + @abc.abstractmethod + def signer_email(self): ... + @property + @abc.abstractmethod + def signer(self): ... diff --git a/stubs/google/auth/exceptions.pyi b/stubs/google/auth/exceptions.pyi new file mode 100644 index 00000000..13ac7232 --- /dev/null +++ b/stubs/google/auth/exceptions.pyi @@ -0,0 +1,4 @@ +class GoogleAuthError(Exception): ... +class TransportError(GoogleAuthError): ... +class RefreshError(GoogleAuthError): ... +class DefaultCredentialsError(GoogleAuthError): ... diff --git a/stubs/google/auth/transport/__init__.pyi b/stubs/google/auth/transport/__init__.pyi new file mode 100644 index 00000000..8a3a69c9 --- /dev/null +++ b/stubs/google/auth/transport/__init__.pyi @@ -0,0 +1,20 @@ +import abc +from _typeshed import Incomplete + +DEFAULT_REFRESH_STATUS_CODES: Incomplete +DEFAULT_MAX_REFRESH_ATTEMPTS: int + +class Response(metaclass=abc.ABCMeta): + @property + @abc.abstractmethod + def status(self): ... + @property + @abc.abstractmethod + def headers(self): ... + @property + @abc.abstractmethod + def data(self): ... + +class Request(metaclass=abc.ABCMeta): + @abc.abstractmethod + def __call__(self, url, method: str = 'GET', body: Incomplete | None = None, headers: Incomplete | None = None, timeout: Incomplete | None = None, **kwargs): ... diff --git a/stubs/google/auth/transport/requests.pyi b/stubs/google/auth/transport/requests.pyi new file mode 100644 index 00000000..a4aaa701 --- /dev/null +++ b/stubs/google/auth/transport/requests.pyi @@ -0,0 +1,21 @@ +import requests +from _typeshed import Incomplete +from google.auth import exceptions as exceptions, transport as transport + +class _Response(transport.Response): + def __init__(self, response) -> None: ... + @property + def status(self): ... + @property + def headers(self): ... + @property + def data(self): ... + +class Request(transport.Request): + session: Incomplete + def __init__(self, session: Incomplete | None = None) -> None: ... + def __call__(self, url, method: str = 'GET', body: Incomplete | None = None, headers: Incomplete | None = None, timeout: Incomplete | None = None, **kwargs): ... + +class AuthorizedSession(requests.Session): + credentials: Incomplete + def __init__(self, credentials, refresh_status_codes=..., max_refresh_attempts=..., refresh_timeout: Incomplete | None = None, **kwargs) -> None: ... diff --git a/stubs/google/oauth2/credentials.pyi b/stubs/google/oauth2/credentials.pyi new file mode 100644 index 00000000..f86a08fb --- /dev/null +++ b/stubs/google/oauth2/credentials.pyi @@ -0,0 +1,24 @@ +from _typeshed import Incomplete +from google.auth import credentials as credentials, exceptions as exceptions + +class Credentials(credentials.ReadOnlyScoped, credentials.Credentials): + token: Incomplete + def __init__(self, token, refresh_token: Incomplete | None = None, id_token: Incomplete | None = None, token_uri: Incomplete | None = None, client_id: Incomplete | None = None, client_secret: Incomplete | None = None, scopes: Incomplete | None = None) -> None: ... + @property + def refresh_token(self): ... + @property + def token_uri(self): ... + @property + def id_token(self): ... + @property + def client_id(self): ... + @property + def client_secret(self): ... + @property + def requires_scopes(self): ... + expiry: Incomplete + def refresh(self, request) -> None: ... + @classmethod + def from_authorized_user_info(cls, info, scopes: Incomplete | None = None): ... + @classmethod + def from_authorized_user_file(cls, filename, scopes: Incomplete | None = None): ... diff --git a/stubs/google_auth_oauthlib/flow.pyi b/stubs/google_auth_oauthlib/flow.pyi new file mode 100644 index 00000000..bd76e7ba --- /dev/null +++ b/stubs/google_auth_oauthlib/flow.pyi @@ -0,0 +1,36 @@ +import wsgiref.simple_server +from google.oauth2.credentials import Credentials +from _typeshed import Incomplete + +class Flow: + client_type: Incomplete + client_config: Incomplete + oauth2session: Incomplete + code_verifier: Incomplete + autogenerate_code_verifier: Incomplete + def __init__(self, oauth2session, client_type, client_config, redirect_uri: Incomplete | None = None, code_verifier: Incomplete | None = None, autogenerate_code_verifier: bool = True) -> None: ... + @classmethod + def from_client_config(cls, client_config, scopes, **kwargs): ... + @classmethod + def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs): ... + @property + def redirect_uri(self): ... + @redirect_uri.setter + def redirect_uri(self, value) -> None: ... + def authorization_url(self, **kwargs): ... + def fetch_token(self, **kwargs): ... + @property + def credentials(self): ... + def authorized_session(self): ... + +class InstalledAppFlow(Flow): + redirect_uri: Incomplete + def run_local_server(self, host: str = 'localhost', bind_addr: Incomplete | None = None, port: int = 8080, authorization_prompt_message=..., success_message=..., open_browser: bool = True, redirect_uri_trailing_slash: bool = True, timeout_seconds: Incomplete | None = None, token_audience: Incomplete | None = None, browser: Incomplete | None = None, **kwargs) -> Credentials: ... + +class _WSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler): + def log_message(self, format, *args) -> None: ... + +class _RedirectWSGIApp: + last_request_uri: Incomplete + def __init__(self, success_message) -> None: ... + def __call__(self, environ, start_response): ... diff --git a/stubs/googleapiclient/discovery.pyi b/stubs/googleapiclient/discovery.pyi new file mode 100644 index 00000000..ad346f59 --- /dev/null +++ b/stubs/googleapiclient/discovery.pyi @@ -0,0 +1,26 @@ +from _typeshed import Incomplete +from email.generator import Generator as BytesGenerator + +__all__ = ['build', 'build_from_document', 'fix_method_name', 'key2param'] + +class _BytesGenerator(BytesGenerator): ... + +def fix_method_name(name): ... +def key2param(key): ... +def build(serviceName, version, http: Incomplete | None = None, discoveryServiceUrl=..., developerKey: Incomplete | None = None, model: Incomplete | None = None, requestBuilder=..., credentials: Incomplete | None = None, cache_discovery: bool = True, cache: Incomplete | None = None): ... +def build_from_document(service, base: Incomplete | None = None, future: Incomplete | None = None, http: Incomplete | None = None, developerKey: Incomplete | None = None, model: Incomplete | None = None, requestBuilder=..., credentials: Incomplete | None = None): ... + +class ResourceMethodParameters: + argmap: Incomplete + required_params: Incomplete + repeated_params: Incomplete + pattern_params: Incomplete + query_params: Incomplete + path_params: Incomplete + param_types: Incomplete + enum_params: Incomplete + def __init__(self, method_desc) -> None: ... + def set_parameters(self, method_desc) -> None: ... + +class Resource: + def __init__(self, http, baseUrl, model, requestBuilder, developerKey, resourceDesc, rootDesc, schema) -> None: ... diff --git a/stubs/parsedatetime/parsedatetime/__init__.pyi b/stubs/parsedatetime/parsedatetime/__init__.pyi new file mode 100644 index 00000000..5c5e3651 --- /dev/null +++ b/stubs/parsedatetime/parsedatetime/__init__.pyi @@ -0,0 +1,92 @@ +import datetime +import logging +import time +from _typeshed import Incomplete +from collections.abc import Generator +from typing import Tuple + +__version__: str +__url__: str +__download_url__: str +__description__: str + +class NullHandler(logging.Handler): + def emit(self, record) -> None: ... + +log: Incomplete +debug: bool +pdtLocales: Incomplete +VERSION_FLAG_STYLE: int +VERSION_CONTEXT_STYLE: int + +class Calendar: + ptc: Incomplete + version: Incomplete + def __init__(self, constants: Incomplete | None = None, version=...) -> None: ... + def context(self) -> Generator[Incomplete, None, None]: ... + @property + def currentContext(self): ... + def parseDate(self, dateString, sourceTime: Incomplete | None = None): ... + def parseDateText(self, dateString, sourceTime: Incomplete | None = None): ... + def evalRanges(self, datetimeString, sourceTime: Incomplete | None = None): ... + def parseDT(self, datetimeString, sourceTime: Incomplete | None = None, tzinfo: Incomplete | None = None, version: Incomplete | None = None) -> Tuple[datetime.datetime, Incomplete]: ... + def parse(self, datetimeString, sourceTime: Incomplete | None = None, version: Incomplete | None = None) -> Tuple[time.struct_time, Incomplete]: ... + def inc(self, source, month: Incomplete | None = None, year: Incomplete | None = None): ... + def nlp(self, inputString, sourceTime: Incomplete | None = None, version: Incomplete | None = None): ... + +class Constants: + localeID: Incomplete + fallbackLocales: Incomplete + locale: Incomplete + usePyICU: Incomplete + Second: int + Minute: int + Hour: int + Day: int + Week: int + Month: int + Year: int + rangeSep: str + BirthdayEpoch: int + StartTimeFromSourceTime: bool + StartHour: int + YearParseStyle: int + DOWParseStyle: int + CurrentDOWParseStyle: bool + RE_DATE4: Incomplete + RE_DATE3: Incomplete + RE_MONTH: Incomplete + RE_WEEKDAY: Incomplete + RE_NUMBER: Incomplete + RE_SPECIAL: Incomplete + RE_UNITS_ONLY: Incomplete + RE_UNITS: Incomplete + RE_QUNITS: Incomplete + RE_MODIFIER: Incomplete + RE_TIMEHMS: Incomplete + RE_TIMEHMS2: Incomplete + RE_NLP_PREFIX: str + RE_DATE: Incomplete + RE_DATE2: Incomplete + RE_DAY: Incomplete + RE_DAY2: Incomplete + RE_TIME: Incomplete + RE_REMAINING: str + RE_RTIMEHMS: Incomplete + RE_RTIMEHMS2: Incomplete + RE_RDATE: Incomplete + RE_RDATE3: Incomplete + DATERNG1: Incomplete + DATERNG2: Incomplete + DATERNG3: Incomplete + TIMERNG1: Incomplete + TIMERNG2: Incomplete + TIMERNG3: Incomplete + TIMERNG4: Incomplete + re_option: Incomplete + cre_source: Incomplete + cre_keys: Incomplete + def __init__(self, localeID: Incomplete | None = None, usePyICU: bool = True, fallbackLocales=['en_US']) -> None: ... + def __getattr__(self, name): ... + def daysInMonth(self, month, year): ... + def getSource(self, sourceKey, sourceTime: Incomplete | None = None): ... diff --git a/tox.ini b/tox.ini index d69edbfd..34b2e983 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,7 @@ skip_install = true deps = mypy >= 1.0 types-python-dateutil + types-requests types-vobject commands = mypy {posargs:gcalcli}