diff --git a/.gitignore b/.gitignore index a85ffe983e..3d55dc9b54 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ venv .hypothesis semaphore pip-wheel-metadata +.mypy_cache diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000..004d74788f --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +allow_redefinition = True diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index f1872d16e9..6444a27ce4 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -22,12 +22,25 @@ except Exception as e: sentry_sdk.capture_exception(e) """ +from sentry_sdk.hub import Hub, init +from sentry_sdk.scope import Scope +from sentry_sdk.transport import Transport, HttpTransport +from sentry_sdk.client import Client + from sentry_sdk.api import * # noqa -from sentry_sdk.api import __all__ # noqa +from sentry_sdk.api import __all__ as api_all + from sentry_sdk.consts import VERSION # noqa -# modules we consider public -__all__.append("integrations") +__all__ = api_all + [ # noqa + "Hub", + "Scope", + "Client", + "Transport", + "HttpTransport", + "init", + "integrations", +] # Initialize the debug support after everything is loaded from sentry_sdk.debug import init_debug_support diff --git a/sentry_sdk/_compat.py b/sentry_sdk/_compat.py index e57b3efb19..871995e8c1 100644 --- a/sentry_sdk/_compat.py +++ b/sentry_sdk/_compat.py @@ -1,5 +1,11 @@ import sys +if False: + from typing import Optional + from typing import Tuple + from typing import Any + from typing import Type + PY2 = sys.version_info[0] == 2 @@ -27,8 +33,8 @@ def implements_str(cls): import queue # noqa text_type = str - string_types = (text_type,) - number_types = (int, float) + string_types = (text_type,) # type: Tuple[type] + number_types = (int, float) # type: Tuple[type, type] int_types = (int,) # noqa iteritems = lambda x: x.items() @@ -39,6 +45,8 @@ def implements_str(x): return x def reraise(tp, value, tb=None): + # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[Any]) -> None + assert value is not None if value.__traceback__ is not tb: raise value.with_traceback(tb) raise value @@ -53,8 +61,9 @@ def __new__(cls, name, this_bases, d): def check_thread_support(): + # type: () -> None try: - from uwsgi import opt + from uwsgi import opt # type: ignore except ImportError: return diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index b6fc9ffc8e..bd92d73c6d 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -1,16 +1,23 @@ import inspect from contextlib import contextmanager -from sentry_sdk.hub import Hub, init +from sentry_sdk.hub import Hub from sentry_sdk.scope import Scope -from sentry_sdk.transport import Transport, HttpTransport -from sentry_sdk.client import Client -__all__ = ["Hub", "Scope", "Client", "Transport", "HttpTransport", "init"] +if False: + from typing import Any + from typing import Optional + from typing import overload + from typing import Callable + from contextlib import AbstractContextManager +else: + def overload(x): + return x -_initial_client = None + +__all__ = [] def public(f): @@ -35,16 +42,20 @@ def capture_event(event, hint=None): @hubmethod def capture_message(message, level=None): + # type: (str, Optional[Any]) -> Optional[str] hub = Hub.current if hub is not None: return hub.capture_message(message, level) + return None @hubmethod def capture_exception(error=None): + # type: (ValueError) -> Optional[str] hub = Hub.current if hub is not None: return hub.capture_exception(error) + return None @hubmethod @@ -54,7 +65,19 @@ def add_breadcrumb(*args, **kwargs): return hub.add_breadcrumb(*args, **kwargs) -@hubmethod +@overload # noqa +def configure_scope(): + # type: () -> AbstractContextManager + pass + + +@overload # noqa +def configure_scope(callback): + # type: (Callable) -> None + pass + + +@hubmethod # noqa def configure_scope(callback=None): hub = Hub.current if hub is not None: @@ -66,9 +89,24 @@ def inner(): yield Scope() return inner() + else: + # returned if user provided callback + return None -@hubmethod +@overload # noqa +def push_scope(): + # type: () -> AbstractContextManager + pass + + +@overload # noqa +def push_scope(callback): + # type: (Callable) -> None + pass + + +@hubmethod # noqa def push_scope(callback=None): hub = Hub.current if hub is not None: @@ -80,6 +118,9 @@ def inner(): yield Scope() return inner() + else: + # returned if user provided callback + return None @hubmethod @@ -91,6 +132,8 @@ def flush(timeout=None, callback=None): @hubmethod def last_event_id(): + # type: () -> Optional[str] hub = Hub.current if hub is not None: return hub.last_event_id() + return None diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 4b8dbfbf54..c1cc9d7d9d 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -19,31 +19,39 @@ from sentry_sdk.integrations import setup_integrations from sentry_sdk.utils import ContextVar +if False: + from sentry_sdk.consts import ClientOptions + from sentry_sdk.scope import Scope + from typing import Any + from typing import Dict + from typing import Optional + _client_init_debug = ContextVar("client_init_debug") def get_options(*args, **kwargs): + # type: (*str, **ClientOptions) -> ClientOptions if args and (isinstance(args[0], string_types) or args[0] is None): - dsn = args[0] + dsn = args[0] # type: Optional[str] args = args[1:] else: dsn = None rv = dict(DEFAULT_OPTIONS) - options = dict(*args, **kwargs) + options = dict(*args, **kwargs) # type: ignore if dsn is not None and options.get("dsn") is None: - options["dsn"] = dsn + options["dsn"] = dsn # type: ignore for key, value in options.items(): if key not in rv: raise TypeError("Unknown option %r" % (key,)) - rv[key] = value + rv[key] = value # type: ignore if rv["dsn"] is None: rv["dsn"] = os.environ.get("SENTRY_DSN") - return rv + return rv # type: ignore class Client(object): @@ -54,6 +62,7 @@ class Client(object): """ def __init__(self, *args, **kwargs): + # type: (*str, **ClientOptions) -> None old_debug = _client_init_debug.get(False) try: self.options = options = get_options(*args, **kwargs) @@ -79,7 +88,13 @@ def dsn(self): """Returns the configured DSN as string.""" return self.options["dsn"] - def _prepare_event(self, event, hint, scope): + def _prepare_event( + self, + event, # type: Dict[str, Any] + hint, # type: Optional[Dict[str, Any]] + scope, # type: Optional[Scope] + ): + # type: (...) -> Optional[Dict[str, Any]] if event.get("timestamp") is None: event["timestamp"] = datetime.utcnow() @@ -104,8 +119,8 @@ def _prepare_event(self, event, hint, scope): ] for key in "release", "environment", "server_name", "dist": - if event.get(key) is None and self.options[key] is not None: - event[key] = text_type(self.options[key]).strip() + if event.get(key) is None and self.options[key] is not None: # type: ignore + event[key] = text_type(self.options[key]).strip() # type: ignore if event.get("sdk") is None: sdk_info = dict(SDK_INFO) sdk_info["integrations"] = sorted(self.integrations.keys()) @@ -132,11 +147,12 @@ def _prepare_event(self, event, hint, scope): new_event = before_send(event, hint) if new_event is None: logger.info("before send dropped event (%s)", event) - event = new_event + event = new_event # type: ignore return event - def _is_ignored_error(self, event, hint=None): + def _is_ignored_error(self, event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> bool exc_info = hint.get("exc_info") if exc_info is None: return False @@ -156,7 +172,13 @@ def _is_ignored_error(self, event, hint=None): return False - def _should_capture(self, event, hint, scope=None): + def _should_capture( + self, + event, # type: Dict[str, Any] + hint, # type: Dict[str, Any] + scope=None, # type: Scope + ): + # type: (...) -> bool if scope is not None and not scope._should_capture: return False @@ -172,6 +194,7 @@ def _should_capture(self, event, hint, scope=None): return True def capture_event(self, event, hint=None, scope=None): + # type: (Dict[str, Any], Any, Scope) -> Optional[str] """Captures an event. This takes the ready made event and an optoinal hint and scope. The @@ -183,17 +206,17 @@ def capture_event(self, event, hint=None, scope=None): value of this function will be the ID of the captured event. """ if self.transport is None: - return + return None if hint is None: hint = {} rv = event.get("event_id") if rv is None: event["event_id"] = rv = uuid.uuid4().hex if not self._should_capture(event, hint, scope): - return - event = self._prepare_event(event, hint, scope) + return None + event = self._prepare_event(event, hint, scope) # type: ignore if event is None: - return + return None self.transport.capture_event(event) return rv diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 03a12ca943..f4987e77d5 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1,5 +1,45 @@ import socket +if False: + from mypy_extensions import TypedDict + from typing import Optional + from typing import Callable + from typing import Union + from typing import List + + from sentry_sdk.transport import Transport + from sentry_sdk.integrations import Integration + + ClientOptions = TypedDict( + "ClientOptions", + { + "dsn": Optional[str], + "with_locals": bool, + "max_breadcrumbs": int, + "release": Optional[str], + "environment": Optional[str], + "server_name": Optional[str], + "shutdown_timeout": int, + "integrations": List[Integration], + "in_app_include": List[str], + "in_app_exclude": List[str], + "default_integrations": bool, + "dist": Optional[str], + "transport": Optional[Union[Transport, type, Callable]], + "sample_rate": int, + "send_default_pii": bool, + "http_proxy": Optional[str], + "https_proxy": Optional[str], + "ignore_errors": List[type], + "request_bodies": str, + "before_send": Optional[Callable], + "before_breadcrumb": Optional[Callable], + "debug": bool, + "attach_stacktrace": bool, + "ca_certs": Optional[str], + }, + ) + VERSION = "0.7.3" DEFAULT_SERVER_NAME = socket.gethostname() if hasattr(socket, "gethostname") else None diff --git a/sentry_sdk/debug.py b/sentry_sdk/debug.py index 5922a50c51..7ceb822675 100644 --- a/sentry_sdk/debug.py +++ b/sentry_sdk/debug.py @@ -5,10 +5,12 @@ from sentry_sdk.hub import Hub from sentry_sdk.utils import logger from sentry_sdk.client import _client_init_debug +from logging import LogRecord class _HubBasedClientFilter(logging.Filter): def filter(self, record): + # type: (LogRecord) -> bool if _client_init_debug.get(False): return True hub = Hub.current diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index ee0b673b5a..1a812b1302 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -5,7 +5,7 @@ from contextlib import contextmanager from warnings import warn -from sentry_sdk._compat import with_metaclass, string_types +from sentry_sdk._compat import with_metaclass from sentry_sdk.scope import Scope from sentry_sdk.client import Client from sentry_sdk.utils import ( @@ -16,11 +16,29 @@ ) +if False: + from typing import Union + from typing import Any + from typing import Optional + from typing import Dict + from typing import Tuple + from typing import List + from typing import Callable + from typing import overload + from contextlib import AbstractContextManager + from sentry_sdk.integrations import Integration +else: + + def overload(x): + return x + + _local = ContextVar("sentry_current_hub") _initial_client = None def _should_send_default_pii(): + # type: () -> bool client = Hub.current.client if not client: return False @@ -57,6 +75,7 @@ def init(*args, **kwargs): class HubMeta(type): @property def current(self): + # type: () -> Hub """Returns the current instance of the hub.""" rv = _local.get(None) if rv is None: @@ -86,6 +105,7 @@ def __init__(self, hub): self._layer = hub._stack[-1] def __enter__(self): + # type: () -> Scope scope = self._layer[1] assert scope is not None return scope @@ -124,7 +144,7 @@ def __exit__(self, exc_type, exc_value, tb): logger.warning(warning) -class Hub(with_metaclass(HubMeta)): +class Hub(with_metaclass(HubMeta)): # type: ignore """The hub wraps the concurrency management of the SDK. Each thread has its own hub but the hub might transfer with the flow of execution if context vars are available. @@ -132,7 +152,10 @@ class Hub(with_metaclass(HubMeta)): If the hub is used with a with statement it's temporarily activated. """ + _stack = None # type: List[Tuple[Optional[Client], Scope]] + def __init__(self, client_or_hub=None, scope=None): + # type: (Union[Hub, Client], Optional[Any]) -> None if isinstance(client_or_hub, Hub): hub = client_or_hub client, other_scope = hub._stack[-1] @@ -142,16 +165,24 @@ def __init__(self, client_or_hub=None, scope=None): client = client_or_hub if scope is None: scope = Scope() + self._stack = [(client, scope)] - self._last_event_id = None - self._old_hubs = [] + self._last_event_id = None # type: Optional[str] + self._old_hubs = [] # type: List[Hub] def __enter__(self): + # type: () -> Hub self._old_hubs.append(Hub.current) _local.set(self) return self - def __exit__(self, exc_type, exc_value, tb): + def __exit__( + self, + exc_type, # type: Optional[type] + exc_value, # type: Optional[BaseException] + tb, # type: Optional[Any] + ): + # type: (...) -> None old = self._old_hubs.pop() _local.set(old) @@ -163,6 +194,7 @@ def run(self, callback): return callback() def get_integration(self, name_or_class): + # type: (Union[str, Integration]) -> Any """Returns the integration for this hub by name or class. If there is no client bound or the client does not have that integration then `None` is returned. @@ -170,11 +202,16 @@ def get_integration(self, name_or_class): If the return value is not `None` the hub is guaranteed to have a client attached. """ - if not isinstance(name_or_class, string_types): - name_or_class = name_or_class.identifier + if isinstance(name_or_class, str): + integration_name = name_or_class + elif name_or_class.identifier is not None: + integration_name = name_or_class.identifier + else: + raise ValueError("Integration has no name") + client = self._stack[-1][0] if client is not None: - rv = client.integrations.get(name_or_class) + rv = client.integrations.get(integration_name) if rv is not None: return rv @@ -199,6 +236,7 @@ def get_integration(self, name_or_class): @property def client(self): + # type: () -> Optional[Client] """Returns the current client on the hub.""" return self._stack[-1][0] @@ -212,6 +250,7 @@ def bind_client(self, new): self._stack[-1] = (new, top[1]) def capture_event(self, event, hint=None): + # type: (Dict[str, Any], Dict[str, Any]) -> Optional[str] """Captures an event. The return value is the ID of the event. The event is a dictionary following the Sentry v7/v8 protocol @@ -225,13 +264,15 @@ def capture_event(self, event, hint=None): if rv is not None: self._last_event_id = rv return rv + return None def capture_message(self, message, level=None): + # type: (str, Optional[Any]) -> Optional[str] """Captures a message. The message is just a string. If no level is provided the default level is `info`. """ if self.client is None: - return + return None if level is None: level = "info" return self.capture_event({"message": message, "level": level}) @@ -263,6 +304,7 @@ def _capture_internal_exception(self, exc_info): logger.error("Internal error in sentry_sdk", exc_info=exc_info) def add_breadcrumb(self, crumb=None, hint=None, **kwargs): + # type: (Dict[str, Any], Dict[str, Any], **Any) -> None """Adds a breadcrumb. The breadcrumbs are a dictionary with the data as the sentry v7/v8 protocol expects. `hint` is an optional value that can be used by `before_breadcrumb` to customize the @@ -273,7 +315,7 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): logger.info("Dropped breadcrumb because no client bound") return - crumb = dict(crumb or ()) + crumb = dict(crumb or ()) # type: Dict[str, Any] crumb.update(kwargs) if not crumb: return @@ -293,18 +335,31 @@ def add_breadcrumb(self, crumb=None, hint=None, **kwargs): scope._breadcrumbs.append(crumb) else: logger.info("before breadcrumb dropped breadcrumb (%s)", original_crumb) - while len(scope._breadcrumbs) > client.options["max_breadcrumbs"]: + + max_breadcrumbs = client.options["max_breadcrumbs"] # type: int + while len(scope._breadcrumbs) > max_breadcrumbs: scope._breadcrumbs.popleft() - def push_scope(self, callback=None): + @overload # noqa + def push_scope(self): + # type: () -> AbstractContextManager + pass + + @overload # noqa + def push_scope(self, callback): + # type: (Callable) -> None + pass + + def push_scope(self, callback=None): # noqa """Pushes a new layer on the scope stack. Returns a context manager that should be used to pop the scope again. Alternatively a callback can be provided that is executed in the context of the scope. """ + if callback is not None: with self.push_scope() as scope: callback(scope) - return + return None client, scope = self._stack[-1] new_layer = (client, copy.copy(scope)) @@ -321,13 +376,25 @@ def pop_scope_unsafe(self): assert self._stack, "stack must have at least one layer" return rv - def configure_scope(self, callback=None): + @overload # noqa + def configure_scope(self): + # type: () -> AbstractContextManager + pass + + @overload # noqa + def configure_scope(self, callback): + # type: (Callable) -> None + pass + + def configure_scope(self, callback=None): # noqa """Reconfigures the scope.""" + client, scope = self._stack[-1] if callback is not None: if client is not None: callback(scope) - return + + return None @contextmanager def inner(): diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 37e1c34485..67b0d3a8af 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -6,13 +6,23 @@ from sentry_sdk._compat import iteritems from sentry_sdk.utils import logger +if False: + from typing import Iterator + from typing import Dict + from typing import List + from typing import Set + from typing import Type + from typing import Callable + _installer_lock = Lock() -_installed_integrations = set() +_installed_integrations = set() # type: Set[str] def _generate_default_integrations_iterator(*import_strings): + # type: (*str) -> Callable[[], Iterator[Type[Integration]]] def iter_default_integrations(): + # type: () -> Iterator[Type[Integration]] """Returns an iterator of the default integration classes: """ from importlib import import_module @@ -22,7 +32,9 @@ def iter_default_integrations(): yield getattr(import_module(module), cls) for import_string in import_strings: - iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) + iter_default_integrations.__doc__ += "\n- `{}`".format( # type: ignore + import_string + ) return iter_default_integrations @@ -42,6 +54,7 @@ def iter_default_integrations(): def setup_integrations(integrations, with_defaults=True): + # type: (List[Integration], bool) -> Dict[str, Integration] """Given a list of integration instances this installs them all. When `with_defaults` is set to `True` then all default integrations are added unless they were already provided before. @@ -94,6 +107,9 @@ class Integration(object): install = None """Legacy method, do not implement.""" + identifier = None # type: str + """String unique ID of integration type""" + @staticmethod def setup_once(): """ diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 8b0c6b9e51..0a8075b2e6 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -4,16 +4,26 @@ from sentry_sdk.utils import AnnotatedValue from sentry_sdk._compat import text_type +if False: + from typing import Any + from typing import Dict + from typing import Optional + from typing import Union + class RequestExtractor(object): def __init__(self, request): + # type: (Any) -> None self.request = request def extract_into_event(self, event): + # type: (Dict[str, Any]) -> None client = Hub.current.client if client is None: return + data = None # type: Optional[Union[AnnotatedValue, Dict[str, Any]]] + content_length = self.content_length() request_info = event.setdefault("request", {}) @@ -45,6 +55,7 @@ def extract_into_event(self, event): request_info["data"] = data def content_length(self): + # type: () -> int try: return int(self.env().get("CONTENT_LENGTH", 0)) except ValueError: @@ -60,6 +71,7 @@ def form(self): raise NotImplementedError() def parsed_body(self): + # type: () -> Optional[Dict[str, Any]] form = self.form() files = self.files() if form or files: @@ -75,9 +87,11 @@ def parsed_body(self): return self.json() def is_json(self): + # type: () -> bool return _is_json_content_type(self.env().get("CONTENT_TYPE")) def json(self): + # type: () -> Optional[Any] try: if self.is_json(): raw_data = self.raw_data() @@ -87,6 +101,8 @@ def json(self): except ValueError: pass + return None + def files(self): raise NotImplementedError() @@ -98,6 +114,7 @@ def env(self): def _is_json_content_type(ct): + # type: (str) -> bool mt = (ct or "").split(";", 1)[0] return ( mt == "application/json" @@ -107,6 +124,7 @@ def _is_json_content_type(ct): def _filter_headers(headers): + # type: (Dict[str, str]) -> Dict[str, str] if _should_send_default_pii(): return headers diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 3224f2e0eb..5d0afc0869 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -9,7 +9,16 @@ from sentry_sdk.utils import capture_internal_exceptions, event_from_exception import asyncio -from aiohttp.web import Application, HTTPException +from aiohttp.web import Application, HTTPException # type: ignore + +if False: + from aiohttp.web_request import Request # type: ignore + from typing import Any + from typing import Dict + from typing import Tuple + from typing import Callable + + from sentry_sdk.utils import ExcInfo class AioHttpIntegration(Integration): @@ -17,6 +26,7 @@ class AioHttpIntegration(Integration): @staticmethod def setup_once(): + # type: () -> None if sys.version_info < (3, 7): # We better have contextvars or we're going to leak state between # requests. @@ -29,7 +39,9 @@ def setup_once(): old_handle = Application._handle async def sentry_app_handle(self, request, *args, **kwargs): + # type: (Any, Request, *Any, **Any) -> Any async def inner(): + # type: () -> Any hub = Hub.current if hub.get_integration(AioHttpIntegration) is None: return await old_handle(self, request, *args, **kwargs) @@ -55,7 +67,12 @@ async def inner(): def _make_request_processor(weak_request): - def aiohttp_processor(event, hint): + # type: (Callable[[], Request]) -> Callable + def aiohttp_processor( + event, # type: Dict[str, Any] + hint, # type: Dict[str, Tuple[type, BaseException, Any]] + ): + # type: (...) -> Dict[str, Any] request = weak_request() if request is None: return event @@ -83,10 +100,11 @@ def aiohttp_processor(event, hint): def _capture_exception(hub): + # type: (Hub) -> ExcInfo exc_info = sys.exc_info() event, hint = event_from_exception( exc_info, - client_options=hub.client.options, + client_options=hub.client.options, # type: ignore mechanism={"type": "aiohttp", "handled": False}, ) hub.capture_event(event, hint=hint) diff --git a/sentry_sdk/integrations/argv.py b/sentry_sdk/integrations/argv.py index 622a95e6b4..f730d0ca09 100644 --- a/sentry_sdk/integrations/argv.py +++ b/sentry_sdk/integrations/argv.py @@ -6,14 +6,20 @@ from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor +if False: + from typing import Any + from typing import Dict + class ArgvIntegration(Integration): identifier = "argv" @staticmethod def setup_once(): + # type: () -> None @add_global_event_processor def processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] if Hub.current.get_integration(ArgvIntegration) is not None: extra = event.setdefault("extra", {}) # If some event processor decided to set extra to e.g. an diff --git a/sentry_sdk/integrations/atexit.py b/sentry_sdk/integrations/atexit.py index 0ffdced2f6..a9e13be052 100644 --- a/sentry_sdk/integrations/atexit.py +++ b/sentry_sdk/integrations/atexit.py @@ -8,6 +8,10 @@ from sentry_sdk.utils import logger from sentry_sdk.integrations import Integration +if False: + from typing import Any + from typing import Optional + def default_callback(pending, timeout): """This is the default shutdown callback that is set on the options. @@ -28,12 +32,14 @@ class AtexitIntegration(Integration): identifier = "atexit" def __init__(self, callback=None): + # type: (Optional[Any]) -> None if callback is None: callback = default_callback self.callback = callback @staticmethod def setup_once(): + # type: () -> None @atexit.register def _shutdown(): logger.debug("atexit: got shutdown signal") diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index 816483e4bc..af70d15dd3 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -54,13 +54,13 @@ class AwsLambdaIntegration(Integration): @staticmethod def setup_once(): - import __main__ as lambda_bootstrap + import __main__ as lambda_bootstrap # type: ignore pre_37 = True # Python 3.6 or 2.7 if not hasattr(lambda_bootstrap, "handle_http_request"): try: - import bootstrap as lambda_bootstrap + import bootstrap as lambda_bootstrap # type: ignore pre_37 = False # Python 3.7 except ImportError: @@ -103,7 +103,7 @@ def sentry_to_json(*args, **kwargs): else: old_handle_event_request = lambda_bootstrap.handle_event_request - def sentry_handle_event_request( + def sentry_handle_event_request( # type: ignore lambda_runtime_client, request_handler, *args, **kwargs ): request_handler = _wrap_handler(request_handler) diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 9db17a40fb..c6b3479182 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -2,7 +2,7 @@ import sys -from celery.exceptions import SoftTimeLimitExceeded, Retry +from celery.exceptions import SoftTimeLimitExceeded, Retry # type: ignore from sentry_sdk.hub import Hub from sentry_sdk.utils import capture_internal_exceptions, event_from_exception @@ -16,7 +16,7 @@ class CeleryIntegration(Integration): @staticmethod def setup_once(): - import celery.app.trace as trace + import celery.app.trace as trace # type: ignore old_build_tracer = trace.build_tracer diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index 05f033eaa4..07d55d17af 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -3,23 +3,31 @@ from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor +if False: + from typing import Any + from typing import Dict + from typing import Optional + class DedupeIntegration(Integration): identifier = "dedupe" def __init__(self): + # type: () -> None self._last_seen = ContextVar("last-seen") @staticmethod def setup_once(): + # type: () -> None @add_global_event_processor def processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Optional[Dict[str, Any]] integration = Hub.current.get_integration(DedupeIntegration) if integration is not None: exc_info = hint.get("exc_info", None) if exc_info is not None: exc = exc_info[1] if integration._last_seen.get(None) is exc: - return + return None integration._last_seen.set(exc) return event diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index eebc8dc13a..b42bf648b2 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -4,14 +4,28 @@ import sys import weakref -from django import VERSION as DJANGO_VERSION -from django.db.models.query import QuerySet -from django.core import signals +from django import VERSION as DJANGO_VERSION # type: ignore +from django.db.models.query import QuerySet # type: ignore +from django.core import signals # type: ignore + +if False: + from typing import Any + from typing import Dict + from typing import Tuple + from typing import Union + from sentry_sdk.integrations.wsgi import _ScopedResponse + from typing import Callable + from django.core.handlers.wsgi import WSGIRequest # type: ignore + from django.http.response import HttpResponse # type: ignore + from django.http.request import QueryDict # type: ignore + from django.utils.datastructures import MultiValueDict # type: ignore + from typing import List try: - import psycopg2.sql + import psycopg2.sql # type: ignore def sql_to_string(sql): + # type: (Any) -> str if isinstance(sql, psycopg2.sql.SQL): return sql.string return sql @@ -20,13 +34,14 @@ def sql_to_string(sql): except ImportError: def sql_to_string(sql): + # type: (Any) -> str return sql try: - from django.urls import resolve + from django.urls import resolve # type: ignore except ImportError: - from django.core.urlresolvers import resolve + from django.core.urlresolvers import resolve # type: ignore from sentry_sdk import Hub from sentry_sdk.hub import _should_send_default_pii @@ -51,12 +66,14 @@ def sql_to_string(sql): if DJANGO_VERSION < (1, 10): def is_authenticated(request_user): + # type: (Any) -> bool return request_user.is_authenticated() else: def is_authenticated(request_user): + # type: (Any) -> bool return request_user.is_authenticated @@ -66,6 +83,7 @@ class DjangoIntegration(Integration): transaction_style = None def __init__(self, transaction_style="url"): + # type: (str) -> None TRANSACTION_STYLE_VALUES = ("function_name", "url") if transaction_style not in TRANSACTION_STYLE_VALUES: raise ValueError( @@ -76,6 +94,7 @@ def __init__(self, transaction_style="url"): @staticmethod def setup_once(): + # type: () -> None install_sql_hook() # Patch in our custom middleware. @@ -88,6 +107,7 @@ def setup_once(): old_app = WSGIHandler.__call__ def sentry_patched_wsgi_handler(self, environ, start_response): + # type: (Any, Dict[str, str], Callable) -> _ScopedResponse if Hub.current.get_integration(DjangoIntegration) is None: return old_app(self, environ, start_response) @@ -99,11 +119,12 @@ def sentry_patched_wsgi_handler(self, environ, start_response): # patch get_response, because at that point we have the Django request # object - from django.core.handlers.base import BaseHandler + from django.core.handlers.base import BaseHandler # type: ignore old_get_response = BaseHandler.get_response def sentry_patched_get_response(self, request): + # type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException] hub = Hub.current integration = hub.get_integration(DjangoIntegration) if integration is not None: @@ -119,6 +140,7 @@ def sentry_patched_get_response(self, request): @add_global_event_processor def process_django_templates(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] if hint.get("exc_info", None) is None: return event @@ -160,7 +182,9 @@ def _django_queryset_repr(value, hint): def _make_event_processor(weak_request, integration): + # type: (Callable[[], WSGIRequest], DjangoIntegration) -> Callable def event_processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] # if the request is gone we are fine not logging the data from # it. This might happen if the processor is pushed away to # another thread. @@ -191,6 +215,7 @@ def event_processor(event, hint): def _got_request_exception(request=None, **kwargs): + # type: (WSGIRequest, **Any) -> None hub = Hub.current integration = hub.get_integration(DjangoIntegration) if integration is not None: @@ -204,18 +229,23 @@ def _got_request_exception(request=None, **kwargs): class DjangoRequestExtractor(RequestExtractor): def env(self): + # type: () -> Dict[str, str] return self.request.META def cookies(self): + # type: () -> Dict[str, str] return self.request.COOKIES def raw_data(self): + # type: () -> bytes return self.request.body def form(self): + # type: () -> QueryDict return self.request.POST def files(self): + # type: () -> MultiValueDict return self.request.FILES def size_of_file(self, file): @@ -223,6 +253,7 @@ def size_of_file(self, file): def _set_user_info(request, event): + # type: (WSGIRequest, Dict[str, Any]) -> None user_info = event.setdefault("user", {}) user = getattr(request, "user", None) @@ -248,15 +279,19 @@ def _set_user_info(request, event): class _FormatConverter(object): def __init__(self, param_mapping): + # type: (Dict[str, int]) -> None + self.param_mapping = param_mapping - self.params = [] + self.params = [] # type: List[Any] def __getitem__(self, val): + # type: (str) -> str self.params.append(self.param_mapping.get(val)) return "%s" def format_sql(sql, params): + # type: (Any, Any) -> Tuple[str, List[str]] rv = [] if isinstance(params, dict): @@ -279,6 +314,7 @@ def format_sql(sql, params): def record_sql(sql, params): + # type: (Any, Any) -> None hub = Hub.current if hub.get_integration(DjangoIntegration) is None: return @@ -294,11 +330,12 @@ def record_sql(sql, params): def install_sql_hook(): + # type: () -> None """If installed this causes Django's queries to be captured.""" try: - from django.db.backends.utils import CursorWrapper + from django.db.backends.utils import CursorWrapper # type: ignore except ImportError: - from django.db.backends.util import CursorWrapper + from django.db.backends.util import CursorWrapper # type: ignore try: real_execute = CursorWrapper.execute diff --git a/sentry_sdk/integrations/django/templates.py b/sentry_sdk/integrations/django/templates.py index f058861bcd..467051e124 100644 --- a/sentry_sdk/integrations/django/templates.py +++ b/sentry_sdk/integrations/django/templates.py @@ -1,31 +1,43 @@ -from django.template import TemplateSyntaxError +from django.template import TemplateSyntaxError # type: ignore + +if False: + from typing import Any + from typing import Dict + from typing import Optional try: # support Django 1.9 - from django.template.base import Origin + from django.template.base import Origin # type: ignore except ImportError: # backward compatibility - from django.template.loader import LoaderOrigin as Origin + from django.template.loader import LoaderOrigin as Origin # type: ignore def get_template_frame_from_exception(exc_value): + # type: (Optional[BaseException]) -> Optional[Dict[str, Any]] + # As of Django 1.9 or so the new template debug thing showed up. if hasattr(exc_value, "template_debug"): - return _get_template_frame_from_debug(exc_value.template_debug) + return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore # As of r16833 (Django) all exceptions may contain a # ``django_template_source`` attribute (rather than the legacy # ``TemplateSyntaxError.source`` check) if hasattr(exc_value, "django_template_source"): - return _get_template_frame_from_source(exc_value.django_template_source) + return _get_template_frame_from_source( + exc_value.django_template_source # type: ignore + ) if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"): source = exc_value.source if isinstance(source, (tuple, list)) and isinstance(source[0], Origin): return _get_template_frame_from_source(source) + return None + def _get_template_frame_from_debug(debug): + # type: (Dict[str, Any]) -> Dict[str, Any] if debug is None: return None diff --git a/sentry_sdk/integrations/django/transactions.py b/sentry_sdk/integrations/django/transactions.py index 3664cda02d..7b66976919 100644 --- a/sentry_sdk/integrations/django/transactions.py +++ b/sentry_sdk/integrations/django/transactions.py @@ -7,13 +7,24 @@ import re +if False: + from django.urls.resolvers import URLResolver # type: ignore + from typing import Dict + from typing import List + from typing import Optional + from django.urls.resolvers import URLPattern # type: ignore + from typing import Tuple + from typing import Union + from re import Pattern # type: ignore + try: - from django.urls import get_resolver + from django.urls import get_resolver # type: ignore except ImportError: - from django.core.urlresolvers import get_resolver + from django.core.urlresolvers import get_resolver # type: ignore def get_regex(resolver_or_pattern): + # type: (Union[URLPattern, URLResolver]) -> Pattern """Utility method for django's deprecated resolver.regex""" try: regex = resolver_or_pattern.regex @@ -30,9 +41,10 @@ class RavenResolver(object): _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]") _camel_re = re.compile(r"([A-Z]+)([a-z])") - _cache = {} + _cache = {} # type: Dict[URLPattern, str] def _simplify(self, pattern): + # type: (str) -> str r""" Clean up urlpattern regexes into something readable by humans: @@ -69,11 +81,12 @@ def _simplify(self, pattern): return result def _resolve(self, resolver, path, parents=None): + # type: (URLResolver, str, Optional[List[URLResolver]]) -> Optional[str] match = get_regex(resolver).search(path) # Django < 2.0 if not match: - return + return None if parents is None: parents = [resolver] @@ -103,7 +116,14 @@ def _resolve(self, resolver, path, parents=None): self._cache[pattern] = result return result - def resolve(self, path, urlconf=None): + return None + + def resolve( + self, + path, # type: str + urlconf=None, # type: Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]] + ): + # type: (...) -> str resolver = get_resolver(urlconf) match = self._resolve(resolver, path) return match or path diff --git a/sentry_sdk/integrations/excepthook.py b/sentry_sdk/integrations/excepthook.py index 4afb9994ee..6949fb20eb 100644 --- a/sentry_sdk/integrations/excepthook.py +++ b/sentry_sdk/integrations/excepthook.py @@ -4,16 +4,21 @@ from sentry_sdk.utils import capture_internal_exceptions, event_from_exception from sentry_sdk.integrations import Integration +if False: + from typing import Callable + class ExcepthookIntegration(Integration): identifier = "excepthook" @staticmethod def setup_once(): + # type: () -> None sys.excepthook = _make_excepthook(sys.excepthook) def _make_excepthook(old_excepthook): + # type: (Callable) -> Callable def sentry_sdk_excepthook(exctype, value, traceback): hub = Hub.current integration = hub.get_integration(ExcepthookIntegration) diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index fe5bafb3a7..ef2c52c616 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -8,12 +8,22 @@ from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.integrations._wsgi_common import RequestExtractor +if False: + from sentry_sdk.integrations.wsgi import _ScopedResponse + from typing import Any + from typing import Dict + from werkzeug.datastructures import ImmutableTypeConversionDict + from werkzeug.datastructures import ImmutableMultiDict + from werkzeug.datastructures import FileStorage + from typing import Union + from typing import Callable + try: - import flask_login + import flask_login # type: ignore except ImportError: flask_login = None -from flask import Flask, _request_ctx_stack, _app_ctx_stack +from flask import Request, Flask, _request_ctx_stack, _app_ctx_stack # type: ignore from flask.signals import ( appcontext_pushed, appcontext_tearing_down, @@ -28,6 +38,7 @@ class FlaskIntegration(Integration): transaction_style = None def __init__(self, transaction_style="endpoint"): + # type: (str) -> None TRANSACTION_STYLE_VALUES = ("endpoint", "url") if transaction_style not in TRANSACTION_STYLE_VALUES: raise ValueError( @@ -38,6 +49,7 @@ def __init__(self, transaction_style="endpoint"): @staticmethod def setup_once(): + # type: () -> None appcontext_pushed.connect(_push_appctx) appcontext_tearing_down.connect(_pop_appctx) request_started.connect(_request_started) @@ -46,6 +58,7 @@ def setup_once(): old_app = Flask.__call__ def sentry_patched_wsgi_app(self, environ, start_response): + # type: (Any, Dict[str, str], Callable) -> _ScopedResponse if Hub.current.get_integration(FlaskIntegration) is None: return old_app(self, environ, start_response) @@ -53,10 +66,11 @@ def sentry_patched_wsgi_app(self, environ, start_response): environ, start_response ) - Flask.__call__ = sentry_patched_wsgi_app + Flask.__call__ = sentry_patched_wsgi_app # type: ignore def _push_appctx(*args, **kwargs): + # type: (*Flask, **Any) -> None hub = Hub.current if hub.get_integration(FlaskIntegration) is not None: # always want to push scope regardless of whether WSGI app might already @@ -69,12 +83,14 @@ def _push_appctx(*args, **kwargs): def _pop_appctx(*args, **kwargs): + # type: (*Flask, **Any) -> None scope_manager = getattr(_app_ctx_stack.top, "sentry_sdk_scope_manager", None) if scope_manager is not None: scope_manager.__exit__(None, None, None) def _request_started(sender, **kwargs): + # type: (Flask, **Any) -> None hub = Hub.current integration = hub.get_integration(FlaskIntegration) if integration is None: @@ -84,32 +100,42 @@ def _request_started(sender, **kwargs): app = _app_ctx_stack.top.app with hub.configure_scope() as scope: scope.add_event_processor( - _make_request_event_processor(app, weak_request, integration) + _make_request_event_processor( # type: ignore + app, weak_request, integration + ) ) class FlaskRequestExtractor(RequestExtractor): def env(self): + # type: () -> Dict[str, str] return self.request.environ def cookies(self): + # type: () -> ImmutableTypeConversionDict return self.request.cookies def raw_data(self): + # type: () -> bytes return self.request.data def form(self): + # type: () -> ImmutableMultiDict return self.request.form def files(self): + # type: () -> ImmutableMultiDict return self.request.files def size_of_file(self, file): + # type: (FileStorage) -> int return file.content_length def _make_request_event_processor(app, weak_request, integration): + # type: (Flask, Callable[[], Request], FlaskIntegration) -> Callable def inner(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] request = weak_request() # if the request is gone we are fine not logging the data from @@ -120,9 +146,9 @@ def inner(event, hint): try: if integration.transaction_style == "endpoint": - event["transaction"] = request.url_rule.endpoint + event["transaction"] = request.url_rule.endpoint # type: ignore elif integration.transaction_style == "url": - event["transaction"] = request.url_rule.rule + event["transaction"] = request.url_rule.rule # type: ignore except Exception: pass @@ -139,6 +165,7 @@ def inner(event, hint): def _capture_exception(sender, exception, **kwargs): + # type: (Flask, Union[ValueError, BaseException], **Any) -> None hub = Hub.current if hub.get_integration(FlaskIntegration) is None: return @@ -152,6 +179,7 @@ def _capture_exception(sender, exception, **kwargs): def _add_user_to_event(event): + # type: (Dict[str, Any]) -> None if flask_login is None: return diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index 60b77c217b..aac4be7178 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -12,6 +12,12 @@ ) from sentry_sdk.integrations import Integration +if False: + from logging import LogRecord + from typing import Any + from typing import Dict + from typing import Optional + DEFAULT_LEVEL = logging.INFO DEFAULT_EVENT_LEVEL = logging.ERROR @@ -19,6 +25,7 @@ def ignore_logger(name): + # type: (str) -> None """This disables the breadcrumb integration for a logger of a specific name. This primary use is for some integrations to disable breadcrumbs of this integration. @@ -30,6 +37,7 @@ class LoggingIntegration(Integration): identifier = "logging" def __init__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL): + # type: (int, int) -> None self._handler = None self._breadcrumb_handler = None @@ -40,6 +48,7 @@ def __init__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL): self._handler = EventHandler(level=event_level) def _handle_record(self, record): + # type: (LogRecord) -> None if self._handler is not None and record.levelno >= self._handler.level: self._handler.handle(record) @@ -51,9 +60,11 @@ def _handle_record(self, record): @staticmethod def setup_once(): - old_callhandlers = logging.Logger.callHandlers + # type: () -> None + old_callhandlers = logging.Logger.callHandlers # type: ignore def sentry_patched_callhandlers(self, record): + # type: (Any, LogRecord) -> Any try: return old_callhandlers(self, record) finally: @@ -66,14 +77,16 @@ def sentry_patched_callhandlers(self, record): if integration is not None: integration._handle_record(record) - logging.Logger.callHandlers = sentry_patched_callhandlers + logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore def _can_record(record): + # type: (LogRecord) -> bool return record.name not in _IGNORED_LOGGERS def _breadcrumb_from_record(record): + # type: (LogRecord) -> Dict[str, Any] return { "ty": "log", "level": _logging_to_event_level(record.levelname), @@ -85,6 +98,7 @@ def _breadcrumb_from_record(record): def _logging_to_event_level(levelname): + # type: (str) -> str return {"critical": "fatal"}.get(levelname.lower(), levelname.lower()) @@ -119,6 +133,7 @@ def _logging_to_event_level(levelname): def _extra_from_record(record): + # type: (LogRecord) -> Dict[str, None] return { k: v for k, v in vars(record).items() @@ -128,11 +143,13 @@ def _extra_from_record(record): class EventHandler(logging.Handler, object): def emit(self, record): + # type: (LogRecord) -> Any with capture_internal_exceptions(): self.format(record) return self._emit(record) def _emit(self, record): + # type: (LogRecord) -> None if not _can_record(record): return @@ -140,6 +157,8 @@ def _emit(self, record): if hub.client is None: return + hint = None # type: Optional[Dict[str, Any]] + # exc_info might be None or (None, None, None) if record.exc_info is not None and record.exc_info[0] is not None: event, hint = event_from_exception( @@ -160,7 +179,6 @@ def _emit(self, record): ] else: event = {} - hint = None event["level"] = _logging_to_event_level(record.levelname) event["logger"] = record.name @@ -176,11 +194,13 @@ def _emit(self, record): class BreadcrumbHandler(logging.Handler, object): def emit(self, record): + # type: (LogRecord) -> Any with capture_internal_exceptions(): self.format(record) return self._emit(record) def _emit(self, record): + # type: (LogRecord) -> None if not _can_record(record): return diff --git a/sentry_sdk/integrations/modules.py b/sentry_sdk/integrations/modules.py index 2611d626ba..8bc0e6a66f 100644 --- a/sentry_sdk/integrations/modules.py +++ b/sentry_sdk/integrations/modules.py @@ -4,10 +4,17 @@ from sentry_sdk.integrations import Integration from sentry_sdk.scope import add_global_event_processor +if False: + from typing import Any + from typing import Dict + from typing import Tuple + from typing import Iterator + _installed_modules = None def _generate_installed_modules(): + # type: () -> Iterator[Tuple[str, str]] try: import pkg_resources except ImportError: @@ -18,6 +25,7 @@ def _generate_installed_modules(): def _get_installed_modules(): + # type: () -> Dict[str, str] global _installed_modules if _installed_modules is None: _installed_modules = dict(_generate_installed_modules()) @@ -29,8 +37,10 @@ class ModulesIntegration(Integration): @staticmethod def setup_once(): + # type: () -> None @add_global_event_processor def processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] if Hub.current.get_integration(ModulesIntegration) is not None: event["modules"] = dict(_get_installed_modules()) return event diff --git a/sentry_sdk/integrations/pyramid.py b/sentry_sdk/integrations/pyramid.py index 785b6f7690..a062bfb460 100644 --- a/sentry_sdk/integrations/pyramid.py +++ b/sentry_sdk/integrations/pyramid.py @@ -4,8 +4,8 @@ import sys import weakref -from pyramid.httpexceptions import HTTPException -from pyramid.request import Request +from pyramid.httpexceptions import HTTPException # type: ignore +from pyramid.request import Request # type: ignore from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import capture_internal_exceptions, event_from_exception @@ -15,16 +15,29 @@ from sentry_sdk.integrations._wsgi_common import RequestExtractor from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +if False: + from pyramid.response import Response # type: ignore + from typing import Any + from sentry_sdk.integrations.wsgi import _ScopedResponse + from typing import Callable + from typing import Dict + from typing import Optional + from webob.cookies import RequestCookies # type: ignore + from webob.compat import cgi_FieldStorage # type: ignore + + from sentry_sdk.utils import ExcInfo + if getattr(Request, "authenticated_userid", None): def authenticated_userid(request): + # type: (Request) -> Optional[Any] return request.authenticated_userid else: # bw-compat for pyramid < 1.5 - from pyramid.security import authenticated_userid + from pyramid.security import authenticated_userid # type: ignore class PyramidIntegration(Integration): @@ -33,6 +46,7 @@ class PyramidIntegration(Integration): transaction_style = None def __init__(self, transaction_style="route_name"): + # type: (str) -> None TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern") if transaction_style not in TRANSACTION_STYLE_VALUES: raise ValueError( @@ -43,11 +57,13 @@ def __init__(self, transaction_style="route_name"): @staticmethod def setup_once(): - from pyramid.router import Router + # type: () -> None + from pyramid.router import Router # type: ignore old_handle_request = Router.handle_request def sentry_patched_handle_request(self, request, *args, **kwargs): + # type: (Any, Request, *Any, **Any) -> Response hub = Hub.current integration = hub.get_integration(PyramidIntegration) if integration is None: @@ -70,6 +86,7 @@ def sentry_patched_handle_request(self, request, *args, **kwargs): old_wsgi_call = Router.__call__ def sentry_patched_wsgi_call(self, environ, start_response): + # type: (Any, Dict[str, str], Callable) -> _ScopedResponse hub = Hub.current integration = hub.get_integration(PyramidIntegration) if integration is None: @@ -83,7 +100,8 @@ def sentry_patched_wsgi_call(self, environ, start_response): def _capture_exception(exc_info, **kwargs): - if issubclass(exc_info[0], HTTPException): + # type: (ExcInfo, **Any) -> None + if exc_info[0] is None or issubclass(exc_info[0], HTTPException): return hub = Hub.current if hub.get_integration(PyramidIntegration) is None: @@ -102,15 +120,19 @@ def url(self): return self.request.path_url def env(self): + # type: () -> Dict[str, str] return self.request.environ def cookies(self): + # type: () -> RequestCookies return self.request.cookies def raw_data(self): + # type: () -> str return self.request.text def form(self): + # type: () -> Dict[str, str] return { key: value for key, value in self.request.POST.items() @@ -118,6 +140,7 @@ def form(self): } def files(self): + # type: () -> Dict[str, cgi_FieldStorage] return { key: value for key, value in self.request.POST.items() @@ -125,6 +148,7 @@ def files(self): } def size_of_file(self, postdata): + # type: (cgi_FieldStorage) -> int file = postdata.file try: return os.fstat(file.fileno()).st_size @@ -133,7 +157,9 @@ def size_of_file(self, postdata): def _make_event_processor(weak_request, integration): + # type: (Callable[[], Request], PyramidIntegration) -> Callable def event_processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] request = weak_request() if request is None: return event diff --git a/sentry_sdk/integrations/rq.py b/sentry_sdk/integrations/rq.py index 5f5b0ee31d..d227e5c910 100644 --- a/sentry_sdk/integrations/rq.py +++ b/sentry_sdk/integrations/rq.py @@ -6,8 +6,18 @@ from sentry_sdk.integrations import Integration from sentry_sdk.utils import capture_internal_exceptions, event_from_exception -from rq.timeouts import JobTimeoutException -from rq.worker import Worker +from rq.timeouts import JobTimeoutException # type: ignore +from rq.worker import Worker # type: ignore + +if False: + from typing import Any + from typing import Dict + from typing import Callable + + from rq.job import Job # type: ignore + from rq.queue import Queue # type: ignore + + from sentry_sdk.utils import ExcInfo class RqIntegration(Integration): @@ -15,10 +25,12 @@ class RqIntegration(Integration): @staticmethod def setup_once(): + # type: () -> None old_perform_job = Worker.perform_job def sentry_patched_perform_job(self, job, *args, **kwargs): + # type: (Any, Job, *Queue, **Any) -> bool hub = Hub.current integration = hub.get_integration(RqIntegration) @@ -49,7 +61,9 @@ def sentry_patched_handle_exception(self, job, *exc_info, **kwargs): def _make_event_processor(weak_job): + # type: (Callable[[], Job]) -> Callable def event_processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] job = weak_job() if job is not None: with capture_internal_exceptions(): @@ -76,6 +90,7 @@ def event_processor(event, hint): def _capture_exception(exc_info, **kwargs): + # type: (ExcInfo, **Any) -> None hub = Hub.current if hub.get_integration(RqIntegration) is None: return diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 300226f531..e0f14b5576 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -9,10 +9,23 @@ from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers from sentry_sdk.integrations.logging import ignore_logger -from sanic import Sanic -from sanic.exceptions import SanicException -from sanic.router import Router -from sanic.handlers import ErrorHandler +from sanic import Sanic # type: ignore +from sanic.exceptions import SanicException # type: ignore +from sanic.router import Router # type: ignore +from sanic.handlers import ErrorHandler # type: ignore + +if False: + from sanic.request import Request # type: ignore + + from typing import Any + from typing import Callable + from typing import Dict + from typing import List + from typing import Tuple + from sanic.exceptions import InvalidUsage + from typing import Optional + from typing import Union + from sanic.request import RequestParameters class SanicIntegration(Integration): @@ -20,6 +33,7 @@ class SanicIntegration(Integration): @staticmethod def setup_once(): + # type: () -> None if sys.version_info < (3, 7): # Sanic is async. We better have contextvars or we're going to leak # state between requests. @@ -35,6 +49,7 @@ def setup_once(): old_handle_request = Sanic.handle_request async def sentry_handle_request(self, request, *args, **kwargs): + # type: (Any, Request, *Any, **Any) -> Any hub = Hub.current if hub.get_integration(SanicIntegration) is None: return old_handle_request(self, request, *args, **kwargs) @@ -56,6 +71,7 @@ async def sentry_handle_request(self, request, *args, **kwargs): old_router_get = Router.get def sentry_router_get(self, request): + # type: (Any, Request) -> Tuple[Callable, List, Dict[str, str], str] rv = old_router_get(self, request) hub = Hub.current if hub.get_integration(SanicIntegration) is not None: @@ -69,6 +85,7 @@ def sentry_router_get(self, request): old_error_handler_lookup = ErrorHandler.lookup def sentry_error_handler_lookup(self, exception): + # type: (Any, Union[ValueError, InvalidUsage]) -> Optional[Callable] _capture_exception(exception) old_error_handler = old_error_handler_lookup(self, exception) @@ -79,6 +96,7 @@ def sentry_error_handler_lookup(self, exception): return old_error_handler async def sentry_wrapped_error_handler(request, exception): + # type: (Request, ValueError) -> Any try: response = old_error_handler(request, exception) if isawaitable(response): @@ -94,7 +112,10 @@ async def sentry_wrapped_error_handler(request, exception): ErrorHandler.lookup = sentry_error_handler_lookup -def _capture_exception(exception): +def _capture_exception( + exception # type: Union[Tuple[type, BaseException, Any], ValueError, InvalidUsage] +): + # type: (...) -> None if isinstance(exception, SanicException): return @@ -113,7 +134,9 @@ def _capture_exception(exception): def _make_request_processor(weak_request): + # type: (Callable[[], Request]) -> Callable def sanic_processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] request = weak_request() if request is None: return event @@ -143,6 +166,7 @@ def sanic_processor(event, hint): class SanicRequestExtractor(RequestExtractor): def content_length(self): + # type: () -> int if self.request.body is None: return 0 return len(self.request.body) @@ -151,18 +175,22 @@ def cookies(self): return dict(self.request.cookies) def raw_data(self): + # type: () -> bytes return self.request.body def form(self): + # type: () -> RequestParameters return self.request.form def is_json(self): raise NotImplementedError() def json(self): + # type: () -> Optional[Any] return self.request.json def files(self): + # type: () -> RequestParameters return self.request.files def size_of_file(self, file): diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py index b5c3fc2090..0d0b20cbf8 100644 --- a/sentry_sdk/integrations/stdlib.py +++ b/sentry_sdk/integrations/stdlib.py @@ -3,7 +3,7 @@ try: - from httplib import HTTPConnection + from httplib import HTTPConnection # type: ignore except ImportError: from http.client import HTTPConnection @@ -13,10 +13,12 @@ class StdlibIntegration(Integration): @staticmethod def setup_once(): + # type: () -> None install_httplib() def install_httplib(): + # type: () -> None real_putrequest = HTTPConnection.putrequest real_getresponse = HTTPConnection.getresponse diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 241e8b4388..5050ba4d6b 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -16,8 +16,15 @@ ) from sentry_sdk.integrations.logging import ignore_logger -from tornado.web import RequestHandler, HTTPError -from tornado.gen import coroutine +from tornado.web import RequestHandler, HTTPError # type: ignore +from tornado.gen import coroutine # type: ignore + +if False: + from typing import Any + from typing import List + from typing import Optional + from typing import Dict + from typing import Callable class TornadoIntegration(Integration): @@ -25,7 +32,8 @@ class TornadoIntegration(Integration): @staticmethod def setup_once(): - import tornado + # type: () -> None + import tornado # type: ignore tornado_version = getattr(tornado, "version_info", None) if tornado_version is None or tornado_version < (5, 0): @@ -49,6 +57,7 @@ def setup_once(): # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await) # In that case our method should be a coroutine function too async def sentry_execute_request_handler(self, *args, **kwargs): + # type: (Any, *List, **Any) -> Any hub = Hub.current integration = hub.get_integration(TornadoIntegration) if integration is None: @@ -63,7 +72,7 @@ async def sentry_execute_request_handler(self, *args, **kwargs): else: - @coroutine + @coroutine # type: ignore def sentry_execute_request_handler(self, *args, **kwargs): hub = Hub.current integration = hub.get_integration(TornadoIntegration) @@ -83,6 +92,7 @@ def sentry_execute_request_handler(self, *args, **kwargs): old_log_exception = RequestHandler.log_exception def sentry_log_exception(self, ty, value, tb, *args, **kwargs): + # type: (Any, type, BaseException, Any, *Any, **Any) -> Optional[Any] _capture_exception(ty, value, tb) return old_log_exception(self, ty, value, tb, *args, **kwargs) @@ -90,6 +100,7 @@ def sentry_log_exception(self, ty, value, tb, *args, **kwargs): def _capture_exception(ty, value, tb): + # type: (type, BaseException, Any) -> None hub = Hub.current if hub.get_integration(TornadoIntegration) is None: return @@ -106,7 +117,9 @@ def _capture_exception(ty, value, tb): def _make_event_processor(weak_handler): + # type: (Callable[[], RequestHandler]) -> Callable def tornado_processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] handler = weak_handler() if handler is None: return event @@ -145,24 +158,30 @@ def tornado_processor(event, hint): class TornadoRequestExtractor(RequestExtractor): def content_length(self): + # type: () -> int if self.request.body is None: return 0 return len(self.request.body) def cookies(self): + # type: () -> Dict return dict(self.request.cookies) def raw_data(self): + # type: () -> bytes return self.request.body def form(self): + # type: () -> Optional[Any] # TODO: Where to get formdata and nothing else? return None def is_json(self): + # type: () -> bool return _is_json_content_type(self.request.headers.get("content-type")) def files(self): + # type: () -> Dict return {k: v[0] for k, v in self.request.files.items() if v} def size_of_file(self, file): diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 30c2633a7f..79bf351d89 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -5,20 +5,34 @@ from sentry_sdk._compat import PY2, reraise from sentry_sdk.integrations._wsgi_common import _filter_headers +if False: + from typing import Callable + from typing import Dict + from typing import List + from typing import Iterator + from typing import Any + from typing import Tuple + from typing import Optional + + from sentry_sdk.utils import ExcInfo + if PY2: def wsgi_decoding_dance(s, charset="utf-8", errors="replace"): + # type: (str, str, str) -> str return s.decode(charset, errors) else: def wsgi_decoding_dance(s, charset="utf-8", errors="replace"): + # type: (str, str, str) -> str return s.encode("latin1").decode(charset, errors) def get_host(environ): + # type: (Dict[str, str]) -> str """Return the host for the given WSGI environment. Yanked from Werkzeug.""" if environ.get("HTTP_HOST"): rv = environ["HTTP_HOST"] @@ -41,6 +55,7 @@ def get_host(environ): def get_request_url(environ): + # type: (Dict[str, str]) -> str """Return the absolute URL without query string for the given WSGI environment.""" return "%s://%s/%s" % ( @@ -54,9 +69,11 @@ class SentryWsgiMiddleware(object): __slots__ = ("app",) def __init__(self, app): + # type: (Callable) -> None self.app = app def __call__(self, environ, start_response): + # type: (Dict[str, str], Callable) -> _ScopedResponse hub = Hub(Hub.current) with hub: @@ -74,12 +91,13 @@ def __call__(self, environ, start_response): def _get_environ(environ): + # type: (Dict[str, str]) -> Iterator[Tuple[str, str]] """ Returns our whitelisted environment variables. """ keys = ("SERVER_NAME", "SERVER_PORT") if _should_send_default_pii(): - keys += ("REMOTE_ADDR",) + keys += ("REMOTE_ADDR",) # type: ignore for key in keys: if key in environ: @@ -91,6 +109,7 @@ def _get_environ(environ): # We need this function because Django does not give us a "pure" http header # dict. So we might as well use it for all WSGI integrations. def _get_headers(environ): + # type: (Dict[str, str]) -> Iterator[Tuple[str, str]] """ Returns only proper HTTP headers. @@ -107,6 +126,7 @@ def _get_headers(environ): def get_client_ip(environ): + # type: (Dict[str, str]) -> Optional[Any] """ Naively yank the first IP address in an X-Forwarded-For header and assume this is correct. @@ -121,13 +141,16 @@ def get_client_ip(environ): def _capture_exception(hub): - exc_info = sys.exc_info() - event, hint = event_from_exception( - exc_info, - client_options=hub.client.options, - mechanism={"type": "wsgi", "handled": False}, - ) - hub.capture_event(event, hint=hint) + # type: (Hub) -> ExcInfo + # Check client here as it might have been unset while streaming response + if hub.client is not None: + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=hub.client.options, + mechanism={"type": "wsgi", "handled": False}, + ) + hub.capture_event(event, hint=hint) return exc_info @@ -135,10 +158,12 @@ class _ScopedResponse(object): __slots__ = ("_response", "_hub") def __init__(self, hub, response): + # type: (Hub, List[bytes]) -> None self._hub = hub self._response = response def __iter__(self): + # type: () -> Iterator[bytes] iterator = iter(self._response) while True: @@ -163,6 +188,7 @@ def close(self): def _make_wsgi_event_processor(environ): + # type: (Dict[str, str]) -> Callable # It's a bit unfortunate that we have to extract and parse the request data # from the environ so eagerly, but there are a few good reasons for this. # @@ -183,6 +209,7 @@ def _make_wsgi_event_processor(environ): headers = _filter_headers(dict(_get_headers(environ))) def event_processor(event, hint): + # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any] with capture_internal_exceptions(): # if the code below fails halfway through we at least have some data request_info = event.setdefault("request", {}) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 25b1268705..8f19de699f 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -5,11 +5,20 @@ from sentry_sdk.utils import logger, capture_internal_exceptions, object_to_json +if False: + from typing import Any + from typing import Callable + from typing import Dict + from typing import Optional + from typing import Deque + from typing import List + global_event_processors = [] def add_global_event_processor(processor): + # type: (Callable) -> None global_event_processors.append(processor) @@ -20,6 +29,7 @@ def _attr_setter(fn): def _disable_capture(fn): @wraps(fn) def wrapper(self, *args, **kwargs): + # type: (Any, *Dict[str, Any], **Any) -> Any if not self._should_capture: return try: @@ -52,8 +62,8 @@ class Scope(object): ) def __init__(self): - self._event_processors = [] - self._error_processors = [] + self._event_processors = [] # type: List[Callable] + self._error_processors = [] # type: List[Callable] self._name = None self.clear() @@ -103,21 +113,23 @@ def remove_extra(self, key): self._extras.pop(key, None) def clear(self): + # type: () -> None """Clears the entire scope.""" self._level = None self._fingerprint = None self._transaction = None self._user = None - self._tags = {} - self._contexts = {} - self._extras = {} + self._tags = {} # type: Dict[str, Any] + self._contexts = {} # type: Dict[str, Dict] + self._extras = {} # type: Dict[str, Any] - self._breadcrumbs = deque() + self._breadcrumbs = deque() # type: Deque[Dict] self._should_capture = True def add_event_processor(self, func): + # type: (Callable) -> None """"Register a scope local event processor on the scope. This function behaves like `before_send.` @@ -125,6 +137,7 @@ def add_event_processor(self, func): self._event_processors.append(func) def add_error_processor(self, func, cls=None): + # type: (Callable, Optional[type]) -> None """"Register a scope local error processor on the scope. The error processor works similar to an event processor but is @@ -146,10 +159,13 @@ def func(event, exc_info): @_disable_capture def apply_to_event(self, event, hint=None): + # type: (Dict[str, Any], Dict[str, Any]) -> Optional[Dict[str, Any]] """Applies the information contained on the scope to the given event.""" def _drop(event, cause, ty): + # type: (Dict[str, Any], Callable, str) -> Optional[Any] logger.info("%s (%s) dropped event (%s)", ty, cause, event) + return None if self._level is not None: event["level"] = self._level @@ -192,6 +208,7 @@ def _drop(event, cause, ty): return event def __copy__(self): + # type: () -> Scope rv = object.__new__(self.__class__) rv._level = self._level diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 05cf6bb45a..d4a711a35b 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -2,7 +2,7 @@ import json import io -import urllib3 +import urllib3 # type: ignore import certifi import gzip @@ -12,10 +12,21 @@ from sentry_sdk.utils import Dsn, logger, capture_internal_exceptions from sentry_sdk.worker import BackgroundWorker +if False: + from sentry_sdk.consts import ClientOptions + from typing import Type + from typing import Any + from typing import Optional + from typing import Dict + from typing import Union + from typing import Callable + from urllib3.poolmanager import PoolManager # type: ignore + from urllib3.poolmanager import ProxyManager # type: ignore + try: from urllib.request import getproxies except ImportError: - from urllib import getproxies + from urllib import getproxies # type: ignore class Transport(object): @@ -24,12 +35,15 @@ class Transport(object): A transport is used to send an event to sentry. """ + parsed_dsn = None # type: Dsn + def __init__(self, options=None): + # type: (Optional[ClientOptions]) -> None self.options = options if options and options["dsn"]: self.parsed_dsn = Dsn(options["dsn"]) else: - self.parsed_dsn = None + self.parsed_dsn = None # type: ignore def capture_event(self, event): """This gets invoked with the event dictionary when an event should @@ -42,10 +56,12 @@ def flush(self, timeout, callback=None): pass def kill(self): + # type: () -> None """Forcefully kills the transport.""" pass def __del__(self): + # type: () -> None try: self.kill() except Exception: @@ -56,10 +72,11 @@ class HttpTransport(Transport): """The default HTTP transport.""" def __init__(self, options): + # type: (ClientOptions) -> None Transport.__init__(self, options) self._worker = BackgroundWorker() self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION) - self._disabled_until = None + self._disabled_until = None # type: Optional[datetime] self._retry = urllib3.util.Retry() self.options = options @@ -75,6 +92,7 @@ def __init__(self, options): self.hub_cls = Hub def _send_event(self, event): + # type: (Dict[str, Any]) -> None if self._disabled_until is not None: if datetime.utcnow() < self._disabled_until: return @@ -119,13 +137,21 @@ def _send_event(self, event): self._disabled_until = None def _get_pool_options(self, ca_certs): + # type: (Optional[Any]) -> Dict[str, Any] return { "num_pools": 2, "cert_reqs": "CERT_REQUIRED", "ca_certs": ca_certs or certifi.where(), } - def _make_pool(self, parsed_dsn, http_proxy, https_proxy, ca_certs): + def _make_pool( + self, + parsed_dsn, # type: Dsn + http_proxy, # type: Optional[str] + https_proxy, # type: Optional[str] + ca_certs, # type: Optional[Any] + ): + # type: (...) -> Union[PoolManager, ProxyManager] # Use http_proxy if scheme is https and https_proxy is not set proxy = parsed_dsn.scheme == "https" and https_proxy or http_proxy if not proxy: @@ -139,9 +165,11 @@ def _make_pool(self, parsed_dsn, http_proxy, https_proxy, ca_certs): return urllib3.PoolManager(**opts) def capture_event(self, event): + # type: (Dict[str, Any]) -> None hub = self.hub_cls.current def send_event_wrapper(): + # type: () -> None with hub: with capture_internal_exceptions(): self._send_event(event) @@ -149,33 +177,39 @@ def send_event_wrapper(): self._worker.submit(send_event_wrapper) def flush(self, timeout, callback=None): + # type: (float, Optional[Any]) -> None logger.debug("Flushing HTTP transport") if timeout > 0: self._worker.flush(timeout, callback) def kill(self): + # type: () -> None logger.debug("Killing HTTP transport") self._worker.kill() class _FunctionTransport(Transport): def __init__(self, func): + # type: (Callable[[Dict[str, Any]], None]) -> None Transport.__init__(self) self._func = func def capture_event(self, event): + # type: (Dict[str, Any]) -> None self._func(event) + return None def make_transport(options): + # type: (ClientOptions) -> Optional[Transport] ref_transport = options["transport"] # If no transport is given, we use the http transport class if ref_transport is None: - transport_cls = HttpTransport + transport_cls = HttpTransport # type: Type[Transport] else: try: - issubclass(ref_transport, type) + issubclass(ref_transport, type) # type: ignore except TypeError: # if we are not a class but we are a callable, assume a # function that acts as capture_event @@ -183,9 +217,11 @@ def make_transport(options): return _FunctionTransport(ref_transport) # otherwise assume an object fulfilling the transport contract return ref_transport - transport_cls = ref_transport + transport_cls = ref_transport # type: ignore # if a transport class is given only instanciate it if the dsn is not # empty or None if options["dsn"]: return transport_cls(options) + + return None diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 24fc50fe9e..bccade6400 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -16,6 +16,23 @@ PY2, ) +if False: + from typing import Any + from typing import Dict + from typing import Union + from typing import Iterator + from typing import Tuple + from typing import Optional + from typing import List + from typing import Set + from typing import Type + + from sentry_sdk.consts import ClientOptions + + ExcInfo = Tuple[ + Optional[Type[BaseException]], Optional[BaseException], Optional[Any] + ] + if PY2: # Importing ABCs from collections is deprecated, and will stop working in 3.8 # https://github.com/python/cpython/blob/master/Lib/collections/__init__.py#L49 @@ -48,6 +65,7 @@ def _get_debug_hub(): @contextmanager def capture_internal_exceptions(): + # type: () -> Iterator try: yield except Exception: @@ -61,6 +79,7 @@ def to_timestamp(value): def event_hint_with_exc_info(exc_info=None): + # type: (ExcInfo) -> Dict[str, Optional[ExcInfo]] """Creates a hint with the exc info filled in.""" if exc_info is None: exc_info = sys.exc_info() @@ -182,16 +201,20 @@ def to_header(self, timestamp=None): def get_type_name(cls): + # type: (Any) -> str return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None) def get_type_module(cls): + # type: (Any) -> Optional[Any] mod = getattr(cls, "__module__", None) if mod not in (None, "builtins", "__builtins__"): return mod + return None def should_hide_frame(frame): + # type: (Any) -> bool try: mod = frame.f_globals["__name__"] return mod.startswith("sentry_sdk.") @@ -209,6 +232,7 @@ def should_hide_frame(frame): def iter_stacks(tb): + # type: (Any) -> Iterator[Any] while tb is not None: if not should_hide_frame(tb.tb_frame): yield tb @@ -216,6 +240,7 @@ def iter_stacks(tb): def slim_string(value, length=512): + # type: (str, int) -> str if not value: return value if len(value) > length: @@ -223,25 +248,31 @@ def slim_string(value, length=512): return value[:length] -def get_lines_from_file(filename, lineno, loader=None, module=None): +def get_lines_from_file( + filename, # type: str + lineno, # type: int + loader=None, # type: Any + module=None, # type: str +): + # type: (...) -> Tuple[List[str], Optional[str], List[str]] context_lines = 5 source = None if loader is not None and hasattr(loader, "get_source"): try: - source = loader.get_source(module) + source_str = loader.get_source(module) except (ImportError, IOError): - source = None - if source is not None: - source = source.splitlines() + source_str = None + if source_str is not None: + source = source_str.splitlines() if source is None: try: source = linecache.getlines(filename) except (OSError, IOError): - return None, None, None + return [], None, [] if not source: - return None, None, None + return [], None, [] lower_bound = max(0, lineno - context_lines) upper_bound = min(lineno + 1 + context_lines, len(source)) @@ -262,6 +293,7 @@ def get_lines_from_file(filename, lineno, loader=None, module=None): def get_source_context(frame, tb_lineno): + # type: (Any, int) -> Tuple[List[str], Optional[str], List[str]] try: abs_path = frame.f_code.co_filename except Exception: @@ -281,6 +313,7 @@ def get_source_context(frame, tb_lineno): def safe_str(value): + # type: (Any) -> str try: return text_type(value) except Exception: @@ -288,6 +321,7 @@ def safe_str(value): def safe_repr(value): + # type: (Any) -> str try: rv = repr(value) if isinstance(rv, bytes): @@ -351,6 +385,7 @@ def object_to_json(obj, remaining_depth=4, memo=None): def extract_locals(frame): + # type: (Any) -> Dict[str, Any] rv = {} for key, value in frame.f_locals.items(): rv[str(key)] = object_to_json(value) @@ -358,6 +393,7 @@ def extract_locals(frame): def filename_for_module(module, abs_path): + # type: (str, str) -> str try: if abs_path.endswith(".pyc"): abs_path = abs_path[:-1] @@ -375,6 +411,7 @@ def filename_for_module(module, abs_path): def serialize_frame(frame, tb_lineno=None, with_locals=True): + # type: (Any, int, bool) -> Dict[str, Any] f_code = getattr(frame, "f_code", None) if f_code: abs_path = frame.f_code.co_filename @@ -408,6 +445,7 @@ def serialize_frame(frame, tb_lineno=None, with_locals=True): def stacktrace_from_traceback(tb=None, with_locals=True): + # type: (Any, bool) -> Dict[str, List[Dict[str, Any]]] return { "frames": [ serialize_frame( @@ -434,13 +472,23 @@ def current_stacktrace(with_locals=True): def get_errno(exc_value): + # type: (BaseException) -> Optional[Any] return getattr(exc_value, "errno", None) def single_exception_from_error_tuple( - exc_type, exc_value, tb, client_options=None, mechanism=None + exc_type, # type: Optional[type] + exc_value, # type: Optional[BaseException] + tb, # type: Optional[Any] + client_options=None, # type: Optional[ClientOptions] + mechanism=None, # type: Dict[str, Any] ): - errno = get_errno(exc_value) + # type: (...) -> Dict[str, Any] + if exc_value is not None: + errno = get_errno(exc_value) + else: + errno = None + if errno is not None: mechanism = mechanism or {} mechanism_meta = mechanism.setdefault("meta", {}) @@ -465,12 +513,17 @@ def single_exception_from_error_tuple( if HAS_CHAINED_EXCEPTIONS: def walk_exception_chain(exc_info): + # type: (ExcInfo) -> Iterator[ExcInfo] exc_type, exc_value, tb = exc_info seen_exceptions = [] - seen_exception_ids = set() + seen_exception_ids = set() # type: Set[int] - while exc_type is not None and id(exc_value) not in seen_exception_ids: + while ( + exc_type is not None + and exc_value is not None + and id(exc_value) not in seen_exception_ids + ): yield exc_type, exc_value, tb # Avoid hashing random types we don't know anything @@ -479,7 +532,7 @@ def walk_exception_chain(exc_info): seen_exceptions.append(exc_value) seen_exception_ids.add(id(exc_value)) - if exc_value.__suppress_context__: + if exc_value.__suppress_context__: # type: ignore cause = exc_value.__cause__ else: cause = exc_value.__context__ @@ -493,10 +546,16 @@ def walk_exception_chain(exc_info): else: def walk_exception_chain(exc_info): + # type: (ExcInfo) -> Iterator[ExcInfo] yield exc_info -def exceptions_from_error_tuple(exc_info, client_options=None, mechanism=None): +def exceptions_from_error_tuple( + exc_info, # type: ExcInfo + client_options=None, # type: Optional[ClientOptions] + mechanism=None, # type: Dict[str, Any] +): + # type: (...) -> List[Dict[str, Any]] exc_type, exc_value, tb = exc_info rv = [] for exc_type, exc_value, tb in walk_exception_chain(exc_info): @@ -509,6 +568,7 @@ def exceptions_from_error_tuple(exc_info, client_options=None, mechanism=None): def to_string(value): + # type: (str) -> str try: return text_type(value) except UnicodeDecodeError: @@ -516,6 +576,7 @@ def to_string(value): def iter_event_frames(event): + # type: (Dict[str, Any]) -> Iterator[Dict[str, Any]] stacktraces = [] if "stacktrace" in event: stacktraces.append(event["stacktrace"]) @@ -529,6 +590,7 @@ def iter_event_frames(event): def handle_in_app(event, in_app_exclude=None, in_app_include=None): + # type: (Dict[str, Any], List, List) -> Dict[str, Any] any_in_app = False for frame in iter_event_frames(event): in_app = frame.get("in_app") @@ -555,9 +617,10 @@ def handle_in_app(event, in_app_exclude=None, in_app_include=None): def exc_info_from_error(error): + # type: (Union[BaseException, ExcInfo]) -> ExcInfo if isinstance(error, tuple) and len(error) == 3: exc_type, exc_value, tb = error - else: + elif isinstance(error, BaseException): tb = getattr(error, "__traceback__", None) if tb is not None: exc_type = type(error) @@ -569,10 +632,18 @@ def exc_info_from_error(error): exc_value = error exc_type = type(error) + else: + raise ValueError() + return exc_type, exc_value, tb -def event_from_exception(exc_info, client_options=None, mechanism=None): +def event_from_exception( + exc_info, # type: Union[BaseException, ExcInfo] + client_options=None, # type: Optional[ClientOptions] + mechanism=None, # type: Dict[str, Any] +): + # type: (...) -> Tuple[Dict[str, Any], Dict[str, Any]] exc_info = exc_info_from_error(exc_info) hint = event_hint_with_exc_info(exc_info) return ( @@ -589,6 +660,7 @@ def event_from_exception(exc_info, client_options=None, mechanism=None): def _module_in_set(name, set): + # type: (str, Optional[List]) -> bool if not set: return False for item in set or (): @@ -599,14 +671,17 @@ def _module_in_set(name, set): class AnnotatedValue(object): def __init__(self, value, metadata): + # type: (Optional[Any], Dict[str, Any]) -> None self.value = value self.metadata = metadata def flatten_metadata(obj): + # type: (Dict[str, Any]) -> Dict[str, Any] def inner(obj): + # type: (Any) -> Any if isinstance(obj, Mapping): - rv = {} + dict_rv = {} meta = {} for k, v in obj.items(): # if we actually have "" keys in our data, throw them away. It's @@ -614,21 +689,21 @@ def inner(obj): if k == "": continue - rv[k], meta[k] = inner(v) + dict_rv[k], meta[k] = inner(v) if meta[k] is None: del meta[k] - if rv[k] is None: - del rv[k] - return rv, (meta or None) + if dict_rv[k] is None: + del dict_rv[k] + return dict_rv, (meta or None) if isinstance(obj, Sequence) and not isinstance(obj, (text_type, bytes)): - rv = [] + list_rv = [] meta = {} for i, v in enumerate(obj): new_v, meta[str(i)] = inner(v) - rv.append(new_v) + list_rv.append(new_v) if meta[str(i)] is None: del meta[str(i)] - return rv, (meta or None) + return list_rv, (meta or None) if isinstance(obj, AnnotatedValue): return obj.value, {"": obj.metadata} return obj, None @@ -640,6 +715,7 @@ def inner(obj): def strip_event_mut(event): + # type: (Dict[str, Any]) -> None strip_stacktrace_mut(event.get("stacktrace", None)) exception = event.get("exception", None) if exception: @@ -650,6 +726,7 @@ def strip_event_mut(event): def strip_stacktrace_mut(stacktrace): + # type: (Optional[Dict[str, List[Dict[str, Any]]]]) -> None if not stacktrace: return for frame in stacktrace.get("frames", None) or (): @@ -657,6 +734,7 @@ def strip_stacktrace_mut(stacktrace): def strip_request_mut(request): + # type: (Dict[str, Any]) -> None if not request: return data = request.get("data", None) @@ -666,6 +744,7 @@ def strip_request_mut(request): def strip_frame_mut(frame): + # type: (Dict[str, Any]) -> None if "vars" in frame: frame["vars"] = strip_databag(frame["vars"]) @@ -685,6 +764,7 @@ def unmemoize(self, obj): def convert_types(obj): + # type: (Any) -> Any if obj is CYCLE_MARKER: return u"" if isinstance(obj, datetime): @@ -701,6 +781,7 @@ def convert_types(obj): def strip_databag(obj, remaining_depth=20): + # type: (Any, int) -> Any assert not isinstance(obj, bytes), "bytes should have been normalized before" if remaining_depth <= 0: return AnnotatedValue(None, {"rem": [["!limit", "x"]]}) @@ -714,6 +795,7 @@ def strip_databag(obj, remaining_depth=20): def strip_string(value, max_length=512): + # type: (str, int) -> Union[AnnotatedValue, str] # TODO: read max_length from config if not value: return value @@ -787,11 +869,11 @@ def realign_remark(remark): try: - from contextvars import ContextVar + from contextvars import ContextVar # type: ignore except ImportError: from threading import local - class ContextVar(object): + class ContextVar(object): # type: ignore # Super-limited impl of ContextVar def __init__(self, name): diff --git a/sentry_sdk/worker.py b/sentry_sdk/worker.py index 8acdf537e9..00216e883d 100644 --- a/sentry_sdk/worker.py +++ b/sentry_sdk/worker.py @@ -1,50 +1,63 @@ -import threading import os +from threading import Thread, Lock from time import sleep, time from sentry_sdk._compat import queue, check_thread_support from sentry_sdk.utils import logger +if False: + from queue import Queue + from typing import Any + from typing import Optional + from typing import Callable + _TERMINATOR = object() class BackgroundWorker(object): def __init__(self): + # type: () -> None check_thread_support() - self._queue = queue.Queue(-1) - self._lock = threading.Lock() - self._thread = None - self._thread_for_pid = None + self._queue = queue.Queue(-1) # type: Queue[Any] + self._lock = Lock() + self._thread = None # type: Optional[Thread] + self._thread_for_pid = None # type: Optional[int] @property def is_alive(self): + # type: () -> bool if self._thread_for_pid != os.getpid(): return False - return self._thread and self._thread.is_alive() + if not self._thread: + return False + return self._thread.is_alive() def _ensure_thread(self): + # type: () -> None if not self.is_alive: self.start() def _timed_queue_join(self, timeout): + # type: (float) -> bool deadline = time() + timeout queue = self._queue - queue.all_tasks_done.acquire() + queue.all_tasks_done.acquire() # type: ignore try: - while queue.unfinished_tasks: + while queue.unfinished_tasks: # type: ignore delay = deadline - time() if delay <= 0: return False - queue.all_tasks_done.wait(timeout=delay) + queue.all_tasks_done.wait(timeout=delay) # type: ignore return True finally: - queue.all_tasks_done.release() + queue.all_tasks_done.release() # type: ignore def start(self): + # type: () -> None with self._lock: if not self.is_alive: - self._thread = threading.Thread( + self._thread = Thread( target=self._target, name="raven-sentry.BackgroundWorker" ) self._thread.setDaemon(True) @@ -52,6 +65,7 @@ def start(self): self._thread_for_pid = os.getpid() def kill(self): + # type: () -> None logger.debug("background worker got kill request") with self._lock: if self._thread: @@ -60,6 +74,7 @@ def kill(self): self._thread_for_pid = None def flush(self, timeout, callback=None): + # type: (float, Optional[Any]) -> None logger.debug("background worker got flush request") with self._lock: if self.is_alive and timeout > 0.0: @@ -67,6 +82,7 @@ def flush(self, timeout, callback=None): logger.debug("background worker flushed") def _wait_flush(self, timeout, callback): + # type: (float, Optional[Any]) -> None initial_timeout = min(0.1, timeout) if not self._timed_queue_join(initial_timeout): pending = self._queue.qsize() @@ -76,10 +92,12 @@ def _wait_flush(self, timeout, callback): self._timed_queue_join(timeout - initial_timeout) def submit(self, callback): + # type: (Callable) -> None self._ensure_thread() self._queue.put_nowait(callback) def _target(self): + # type: () -> None while True: callback = self._queue.get() try: diff --git a/tox.ini b/tox.ini index 279bc4e348..2661d51b6a 100644 --- a/tox.ini +++ b/tox.ini @@ -102,6 +102,10 @@ deps = linters: black linters: flake8 + + # https://github.com/PyCQA/pyflakes/pull/423 + linters: git+https://github.com/pycqa/pyflakes + linters: mypy setenv = PYTHONDONTWRITEBYTECODE=1 TESTPATH=tests @@ -142,3 +146,4 @@ commands = commands = flake8 tests sentry_sdk black --check tests sentry_sdk + mypy sentry_sdk