diff --git a/examples/basic.py b/examples/basic.py new file mode 100644 index 0000000000..e6d928bbed --- /dev/null +++ b/examples/basic.py @@ -0,0 +1,35 @@ +import sentry_sdk +from sentry_sdk.integrations.excepthook import ExcepthookIntegration +from sentry_sdk.integrations.atexit import AtexitIntegration +from sentry_sdk.integrations.dedupe import DedupeIntegration +from sentry_sdk.integrations.stdlib import StdlibIntegration + + +sentry_sdk.init( + dsn="https://@sentry.io/", + default_integrations=False, + integrations=[ + ExcepthookIntegration(), + AtexitIntegration(), + DedupeIntegration(), + StdlibIntegration(), + ], + environment="Production", + release="1.0.0", + send_default_pii=False, + max_breadcrumbs=5, +) + +with sentry_sdk.push_scope() as scope: + scope.user = {"email": "john.doe@example.com"} + scope.set_tag("page_locale", "de-at") + scope.set_extra("request", {"id": "d5cf8a0fd85c494b9c6453c4fba8ab17"}) + scope.level = "warning" + sentry_sdk.capture_message("Something went wrong!") + +sentry_sdk.add_breadcrumb(category="auth", message="Authenticated user", level="info") + +try: + 1 / 0 +except Exception as e: + sentry_sdk.capture_exception(e) diff --git a/mypy.ini b/mypy.ini index 93cfc91474..065eb2fad7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,10 +1,45 @@ [mypy] allow_redefinition = True +check_untyped_defs = True +; disallow_any_decorated = True +; disallow_any_explicit = True +; disallow_any_expr = True disallow_any_generics = True +; disallow_any_unimported = True +disallow_incomplete_defs = True +; disallow_subclassing_any = True +; disallow_untyped_calls = True +disallow_untyped_decorators = True +; disallow_untyped_defs = True +no_implicit_optional = True +strict_equality = True +strict_optional = True warn_redundant_casts = True +; warn_return_any = True +; warn_unused_configs = True +; warn_unused_ignores = True [mypy-sentry_sdk.integrations.*] disallow_any_generics = False [mypy-sentry_sdk.utils] disallow_any_generics = False + +[mypy-django.*] +ignore_missing_imports = True +[mypy-pyramid.*] +ignore_missing_imports = True +[mypy-psycopg2.*] +ignore_missing_imports = True +[mypy-pytest.*] +ignore_missing_imports = True +[mypy-aiohttp.*] +ignore_missing_imports = True +[mypy-sanic.*] +ignore_missing_imports = True +[mypy-tornado.*] +ignore_missing_imports = True +[mypy-fakeredis.*] +ignore_missing_imports = True +[mypy-rq.*] +ignore_missing_imports = True diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index cc6ea14d6a..c8812df94d 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -10,33 +10,42 @@ from typing import Optional from typing import overload from typing import Callable - from typing import Dict + from typing import TypeVar from contextlib import ContextManager + + from sentry_sdk.utils import Event, Hint, Breadcrumb, BreadcrumbHint + + F = TypeVar("F", bound=Callable[..., Any]) else: def overload(x): return x -__all__ = [] - - -def public(f): - __all__.append(f.__name__) - return f +__all__ = [ + "capture_event", + "capture_message", + "capture_exception", + "add_breadcrumb", + "configure_scope", + "push_scope", + "flush", + "last_event_id", +] def hubmethod(f): + # type: (F) -> F f.__doc__ = "%s\n\n%s" % ( "Alias for `Hub.%s`" % f.__name__, inspect.getdoc(getattr(Hub, f.__name__)), ) - return public(f) + return f @hubmethod def capture_event(event, hint=None): - # type: (Dict[str, Any], Dict[str, Any]) -> Optional[str] + # type: (Event, Optional[Hint]) -> Optional[str] hub = Hub.current if hub is not None: return hub.capture_event(event, hint) @@ -45,7 +54,7 @@ def capture_event(event, hint=None): @hubmethod def capture_message(message, level=None): - # type: (str, Optional[Any]) -> Optional[str] + # type: (str, Optional[str]) -> Optional[str] hub = Hub.current if hub is not None: return hub.capture_message(message, level) @@ -63,7 +72,7 @@ def capture_exception(error=None): @hubmethod def add_breadcrumb(crumb=None, hint=None, **kwargs): - # type: (Dict[str, Any], Dict[str, Any], **Any) -> None + # type: (Optional[Breadcrumb], Optional[BreadcrumbHint], **Any) -> None hub = Hub.current if hub is not None: return hub.add_breadcrumb(crumb, hint, **kwargs) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index e5e6d9f708..e224a1a183 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -47,7 +47,7 @@ def get_options(*args, **kwargs): for key, value in iteritems(options): if key not in rv: raise TypeError("Unknown option %r" % (key,)) - rv[key] = value # type: ignore + rv[key] = value if rv["dsn"] is None: rv["dsn"] = os.environ.get("SENTRY_DSN") @@ -108,9 +108,10 @@ def _prepare_event( hint = dict(hint or ()) # type: Hint if scope is not None: - event = scope.apply_to_event(event, hint) - if event is None: - return + event_ = scope.apply_to_event(event, hint) + if event_ is None: + return None + event = event_ if ( self.options["attach_stacktrace"] @@ -178,7 +179,7 @@ def _is_ignored_error(self, event, hint): if errcls == full_name or errcls == type_name: return True else: - if issubclass(exc_info[0], errcls): + if issubclass(exc_info[0], errcls): # type: ignore return True return False @@ -187,7 +188,7 @@ def _should_capture( self, event, # type: Event hint, # type: Hint - scope=None, # type: Scope + scope=None, # type: Optional[Scope] ): # type: (...) -> bool if scope is not None and not scope._should_capture: @@ -205,7 +206,7 @@ def _should_capture( return True def capture_event(self, event, hint=None, scope=None): - # type: (Dict[str, Any], Any, Scope) -> Optional[str] + # type: (Dict[str, Any], Optional[Any], Optional[Scope]) -> Optional[str] """Captures an event. This takes the ready made event and an optional hint and scope. The @@ -225,7 +226,7 @@ def capture_event(self, event, hint=None, scope=None): event["event_id"] = rv = uuid.uuid4().hex if not self._should_capture(event, hint, scope): return None - event = self._prepare_event(event, hint, scope) # type: ignore + event = self._prepare_event(event, hint, scope) if event is None: return None self.transport.capture_event(event) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 904c8a5e48..11ad3d0de4 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -35,7 +35,7 @@ "send_default_pii": bool, "http_proxy": Optional[str], "https_proxy": Optional[str], - "ignore_errors": List[type], + "ignore_errors": List[Union[type, str]], "request_bodies": str, "before_send": Optional[EventProcessor], "before_breadcrumb": Optional[BreadcrumbProcessor], diff --git a/sentry_sdk/debug.py b/sentry_sdk/debug.py index 7ceb822675..5a7038adee 100644 --- a/sentry_sdk/debug.py +++ b/sentry_sdk/debug.py @@ -20,12 +20,14 @@ def filter(self, record): def init_debug_support(): + # type: () -> None if not logger.handlers: configure_logger() configure_debug_hub() def configure_logger(): + # type: () -> None _handler = logging.StreamHandler(sys.stderr) _handler.setFormatter(logging.Formatter(" [sentry] %(levelname)s: %(message)s")) logger.addHandler(_handler) @@ -34,6 +36,7 @@ def configure_logger(): def configure_debug_hub(): + # type: () -> None def _get_debug_hub(): return Hub.current diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 978420bc8a..7a82623cd0 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -18,6 +18,7 @@ if False: from contextlib import ContextManager + from sys import _OptExcInfo from typing import Union from typing import Any @@ -26,6 +27,7 @@ from typing import List from typing import Callable from typing import Generator + from typing import Type from typing import overload from sentry_sdk.integrations import Integration @@ -36,8 +38,8 @@ def overload(x): return x -_local = ContextVar("sentry_current_hub") -_initial_client = None +_local = ContextVar("sentry_current_hub") # type: ignore +_initial_client = None # type: Optional[weakref.ReferenceType[Client]] def _should_send_default_pii(): @@ -50,9 +52,11 @@ def _should_send_default_pii(): class _InitGuard(object): def __init__(self, client): + # type: (Client) -> None self._client = client def __enter__(self): + # type: () -> _InitGuard return self def __exit__(self, exc_type, exc_value, tb): @@ -103,6 +107,7 @@ def __exit__(self, exc_type, exc_value, tb): class _ScopeManager(object): def __init__(self, hub): + # type: (Hub) -> None self._hub = hub self._original_len = len(hub._stack) self._layer = hub._stack[-1] @@ -157,8 +162,13 @@ class Hub(with_metaclass(HubMeta)): # type: ignore _stack = None # type: List[Tuple[Optional[Client], Scope]] + # Mypy doesn't pick up on the metaclass. + if False: + current = None # type: Hub + main = None # type: Hub + def __init__(self, client_or_hub=None, scope=None): - # type: (Union[Hub, Client], Optional[Any]) -> None + # type: (Optional[Union[Hub, Client]], Optional[Any]) -> None if isinstance(client_or_hub, Hub): hub = client_or_hub client, other_scope = hub._stack[-1] @@ -197,7 +207,7 @@ def run(self, callback): return callback() def get_integration(self, name_or_class): - # type: (Union[str, Integration]) -> Any + # type: (Union[str, Type[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. @@ -218,14 +228,15 @@ def get_integration(self, name_or_class): if rv is not None: return rv - initial_client = _initial_client - if initial_client is not None: - initial_client = initial_client() + if _initial_client is not None: + initial_client = _initial_client() + else: + initial_client = None if ( initial_client is not None and initial_client is not client - and initial_client.integrations.get(name_or_class) is not None + and initial_client.integrations.get(integration_name) is not None ): warning = ( "Integration %r attempted to run but it was only " @@ -254,7 +265,7 @@ def bind_client(self, new): self._stack[-1] = (new, top[1]) def capture_event(self, event, hint=None): - # type: (Event, Hint) -> Optional[str] + # type: (Event, Optional[Hint]) -> 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 @@ -306,9 +317,10 @@ def capture_exception(self, error=None): return None def _capture_internal_exception(self, exc_info): + # type: (_OptExcInfo) -> Any """Capture an exception that is likely caused by a bug in the SDK itself.""" - logger.error("Internal error in sentry_sdk", exc_info=exc_info) + logger.error("Internal error in sentry_sdk", exc_info=exc_info) # type: ignore def add_breadcrumb(self, crumb=None, hint=None, **kwargs): # type: (Optional[Breadcrumb], Optional[BreadcrumbHint], **Any) -> None diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index a60dfb4134..85d663fd89 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -33,9 +33,7 @@ def iter_default_integrations(): if isinstance(iter_default_integrations.__doc__, str): for import_string in import_strings: - iter_default_integrations.__doc__ += "\n- `{}`".format( # type: ignore - import_string - ) + iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) return iter_default_integrations @@ -72,7 +70,7 @@ def setup_integrations(integrations, with_defaults=True): instance = integration_cls() integrations[instance.identifier] = instance - for identifier, integration in iteritems(integrations): + for identifier, integration in iteritems(integrations): # type: ignore with _installer_lock: if identifier not in _installed_integrations: logger.debug( @@ -113,6 +111,7 @@ class Integration(object): @staticmethod def setup_once(): + # type: () -> None """ Initialize the integration. diff --git a/sentry_sdk/integrations/atexit.py b/sentry_sdk/integrations/atexit.py index a9e13be052..18bdc224d3 100644 --- a/sentry_sdk/integrations/atexit.py +++ b/sentry_sdk/integrations/atexit.py @@ -9,6 +9,7 @@ from sentry_sdk.integrations import Integration if False: + from typing import Any from typing import Optional @@ -20,6 +21,7 @@ def default_callback(pending, timeout): """ def echo(msg): + # type: (str) -> None sys.stderr.write(msg + "\n") echo("Sentry is attempting to send %i pending error messages" % pending) @@ -47,4 +49,7 @@ def _shutdown(): integration = hub.get_integration(AtexitIntegration) if integration is not None: logger.debug("atexit: shutting down client") - hub.client.close(callback=integration.callback) + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + client.close(callback=integration.callback) diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index 1ddef70c4d..246a120d14 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -11,6 +11,9 @@ from sentry_sdk.integrations import Integration from sentry_sdk.integrations._wsgi_common import _filter_headers +if False: + from typing import Any + def _wrap_handler(handler): def sentry_handler(event, context, *args, **kwargs): @@ -19,6 +22,9 @@ def sentry_handler(event, context, *args, **kwargs): if integration is None: return handler(event, context, *args, **kwargs) + # If an integration is there, a client has to be there. + client = hub.client # type: Any + with hub.push_scope() as scope: with capture_internal_exceptions(): scope.clear_breadcrumbs() @@ -31,7 +37,7 @@ def sentry_handler(event, context, *args, **kwargs): exc_info = sys.exc_info() event, hint = event_from_exception( exc_info, - client_options=hub.client.options, + client_options=client.options, mechanism={"type": "aws_lambda", "handled": False}, ) hub.capture_event(event, hint=hint) @@ -47,7 +53,7 @@ def _drain_queue(): if integration is not None: # Flush out the event queue before AWS kills the # process. - hub.client.flush() + hub.flush() class AwsLambdaIntegration(Integration): @@ -55,6 +61,7 @@ class AwsLambdaIntegration(Integration): @staticmethod def setup_once(): + # type: () -> None import __main__ as lambda_bootstrap # type: ignore pre_37 = True # Python 3.6 or 2.7 diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py index d6bb7b9431..0942efcad6 100644 --- a/sentry_sdk/integrations/bottle.py +++ b/sentry_sdk/integrations/bottle.py @@ -11,6 +11,7 @@ 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 @@ -98,11 +99,14 @@ def patched_make_callback(self, *args, **kwargs): if integration is None: return prepared_callback + # If an integration is there, a client has to be there. + client = hub.client # type: Any + def wrapped_callback(*args, **kwargs): def capture_exception(exception): event, hint = event_from_exception( exception, - client_options=hub.client.options, + client_options=client.options, mechanism={"type": "bottle", "handled": False}, ) hub.capture_event(event, hint=hint) diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 8a6c63cdd7..59ad6c980c 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -28,6 +28,7 @@ def __init__(self, propagate_traces=True): @staticmethod def setup_once(): + # type: () -> None import celery.app.trace as trace # type: ignore old_build_tracer = trace.build_tracer diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 3d74849ac8..fe24c8efe7 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -224,9 +224,13 @@ def _got_request_exception(request=None, **kwargs): hub = Hub.current integration = hub.get_integration(DjangoIntegration) if integration is not None: + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + event, hint = event_from_exception( sys.exc_info(), - client_options=hub.client.options, + client_options=client.options, mechanism={"type": "django", "handled": False}, ) hub.capture_event(event, hint=hint) diff --git a/sentry_sdk/integrations/excepthook.py b/sentry_sdk/integrations/excepthook.py index 5be077064f..bdd1f47083 100644 --- a/sentry_sdk/integrations/excepthook.py +++ b/sentry_sdk/integrations/excepthook.py @@ -6,6 +6,7 @@ if False: from typing import Callable + from typing import Any class ExcepthookIntegration(Integration): @@ -36,10 +37,13 @@ def sentry_sdk_excepthook(exctype, value, traceback): integration = hub.get_integration(ExcepthookIntegration) if integration is not None and _should_send(integration.always_run): + # If an integration is there, a client has to be there. + client = hub.client # type: Any + with capture_internal_exceptions(): event, hint = event_from_exception( (exctype, value, traceback), - client_options=hub.client.options, + client_options=client.options, mechanism={"type": "excepthook", "handled": False}, ) hub.capture_event(event, hint=hint) @@ -50,6 +54,7 @@ def sentry_sdk_excepthook(exctype, value, traceback): def _should_send(always_run=False): + # type: (bool) -> bool if always_run: return True diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index da641088ba..efd26772b9 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -122,9 +122,12 @@ def sentry_patched_handle_exception(self, *args): integration = hub.get_integration(FalconIntegration) if integration is not None and not _is_falcon_http_error(ex): + # If an integration is there, a client has to be there. + client = hub.client # type: Any + event, hint = event_from_exception( ex, - client_options=hub.client.options, + client_options=client.options, mechanism={"type": "falcon", "handled": False}, ) hub.capture_event(event, hint=hint) diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index 437e9fed0b..9814afeed9 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -9,6 +9,7 @@ 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 @@ -176,9 +177,13 @@ def _capture_exception(sender, exception, **kwargs): hub = Hub.current if hub.get_integration(FlaskIntegration) is None: return + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + event, hint = event_from_exception( exception, - client_options=hub.client.options, + client_options=client.options, mechanism={"type": "flask", "handled": False}, ) diff --git a/sentry_sdk/integrations/gnu_backtrace.py b/sentry_sdk/integrations/gnu_backtrace.py index 55091f8d86..840d6c7543 100644 --- a/sentry_sdk/integrations/gnu_backtrace.py +++ b/sentry_sdk/integrations/gnu_backtrace.py @@ -37,6 +37,7 @@ class GnuBacktraceIntegration(Integration): @staticmethod def setup_once(): + # type: () -> None @add_global_event_processor def process_gnu_backtrace(event, hint): with capture_internal_exceptions(): diff --git a/sentry_sdk/integrations/pyramid.py b/sentry_sdk/integrations/pyramid.py index 2bec0a73d3..3e98b9d758 100644 --- a/sentry_sdk/integrations/pyramid.py +++ b/sentry_sdk/integrations/pyramid.py @@ -126,9 +126,13 @@ def _capture_exception(exc_info): hub = Hub.current if hub.get_integration(PyramidIntegration) is None: return + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + event, hint = event_from_exception( exc_info, - client_options=hub.client.options, + client_options=client.options, mechanism={"type": "pyramid", "handled": False}, ) diff --git a/sentry_sdk/integrations/rq.py b/sentry_sdk/integrations/rq.py index e525dfe51f..815bc5c448 100644 --- a/sentry_sdk/integrations/rq.py +++ b/sentry_sdk/integrations/rq.py @@ -37,6 +37,9 @@ def sentry_patched_perform_job(self, job, *args, **kwargs): if integration is None: return old_perform_job(self, job, *args, **kwargs) + client = hub.client + assert client is not None + with hub.push_scope() as scope: scope.clear_breadcrumbs() scope.add_event_processor(_make_event_processor(weakref.ref(job))) @@ -46,7 +49,7 @@ def sentry_patched_perform_job(self, job, *args, **kwargs): # We're inside of a forked process and RQ is # about to call `os._exit`. Make sure that our # events get sent out. - hub.client.flush() + client.flush() return rv @@ -55,7 +58,7 @@ def sentry_patched_perform_job(self, job, *args, **kwargs): old_handle_exception = Worker.handle_exception def sentry_patched_handle_exception(self, job, *exc_info, **kwargs): - _capture_exception(exc_info) + _capture_exception(exc_info) # type: ignore return old_handle_exception(self, job, *exc_info, **kwargs) Worker.handle_exception = sentry_patched_handle_exception @@ -95,9 +98,13 @@ def _capture_exception(exc_info, **kwargs): hub = Hub.current if hub.get_integration(RqIntegration) is None: return + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + event, hint = event_from_exception( exc_info, - client_options=hub.client.options, + client_options=client.options, mechanism={"type": "rq", "handled": False}, ) diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 6e5ed35c2d..4a06b2fb65 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -133,10 +133,13 @@ def _capture_exception(exception): if integration is None: return + # If an integration is there, a client has to be there. + client = hub.client # type: Any + with capture_internal_exceptions(): event, hint = event_from_exception( exception, - client_options=hub.client.options, + client_options=client.options, mechanism={"type": "sanic", "handled": False}, ) hub.capture_event(event, hint=hint) diff --git a/sentry_sdk/integrations/threading.py b/sentry_sdk/integrations/threading.py index 244f121ad2..2365bf76a6 100644 --- a/sentry_sdk/integrations/threading.py +++ b/sentry_sdk/integrations/threading.py @@ -9,6 +9,9 @@ from sentry_sdk.utils import event_from_exception from sentry_sdk.integrations import Integration +if False: + from typing import Any + class ThreadingIntegration(Integration): identifier = "threading" @@ -18,22 +21,23 @@ def __init__(self, propagate_hub=False): @staticmethod def setup_once(): + # type: () -> None old_start = Thread.start def sentry_start(self, *a, **kw): hub = Hub.current integration = hub.get_integration(ThreadingIntegration) if integration is not None: - if integration.propagate_hub: - hub = Hub(hub) + if not integration.propagate_hub: + hub_ = None else: - hub = None + hub_ = Hub(hub) - self.run = _wrap_run(hub, self.run) + self.run = _wrap_run(hub_, self.run) - return old_start(self, *a, **kw) + return old_start(self, *a, **kw) # type: ignore - Thread.start = sentry_start + Thread.start = sentry_start # type: ignore def _wrap_run(parent_hub, old_run): @@ -54,9 +58,12 @@ def _capture_exception(): exc_info = sys.exc_info() if hub.get_integration(ThreadingIntegration) is not None: + # If an integration is there, a client has to be there. + client = hub.client # type: Any + event, hint = event_from_exception( exc_info, - client_options=hub.client.options, + client_options=client.options, mechanism={"type": "threading", "handled": False}, ) hub.capture_event(event, hint=hint) diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index e02fafef70..e51747b3e7 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -109,9 +109,12 @@ def _capture_exception(ty, value, tb): if isinstance(value, HTTPError): return + # If an integration is there, a client has to be there. + client = hub.client # type: Any + event, hint = event_from_exception( (ty, value, tb), - client_options=hub.client.options, + client_options=client.options, mechanism={"type": "tornado", "handled": False}, ) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 4bf1ac2dd8..ef6a7d5820 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -11,9 +11,13 @@ from typing import Optional from typing import Deque from typing import List + from typing import Callable + from typing import TypeVar from sentry_sdk.utils import Breadcrumb, Event, EventProcessor, ErrorProcessor, Hint + F = TypeVar("F", bound=Callable[..., Any]) + global_event_processors = [] # type: List[EventProcessor] @@ -28,6 +32,7 @@ def _attr_setter(fn): def _disable_capture(fn): + # type: (F) -> F @wraps(fn) def wrapper(self, *args, **kwargs): # type: (Any, *Dict[str, Any], **Any) -> Any @@ -39,7 +44,7 @@ def wrapper(self, *args, **kwargs): finally: self._should_capture = True - return wrapper + return wrapper # type: ignore class Scope(object): @@ -64,10 +69,11 @@ class Scope(object): ) def __init__(self): + # type: () -> None self._event_processors = [] # type: List[EventProcessor] self._error_processors = [] # type: List[ErrorProcessor] - self._name = None + self._name = None # type: Optional[str] self.clear() @_attr_setter @@ -95,26 +101,32 @@ def set_span_context(self, span_context): self._span = span_context def set_tag(self, key, value): + # type: (str, Any) -> None """Sets a tag for a key to a specific value.""" self._tags[key] = value def remove_tag(self, key): + # type: (str) -> None """Removes a specific tag.""" self._tags.pop(key, None) def set_context(self, key, value): + # type: (str, Any) -> None """Binds a context at a certain key to a specific value.""" self._contexts[key] = value def remove_context(self, key): + # type: (str) -> None """Removes a context.""" self._contexts.pop(key, None) def set_extra(self, key, value): + # type: (str, Any) -> None """Sets an extra key to a specific value.""" self._extras[key] = value def remove_extra(self, key): + # type: (str) -> None """Removes a specific extra key.""" self._extras.pop(key, None) @@ -156,11 +168,12 @@ def add_error_processor(self, func, cls=None): invoked with the original exception info triple as second argument. """ if cls is not None: + cls_ = cls # For mypy. real_func = func def func(event, exc_info): try: - is_inst = isinstance(exc_info[1], cls) + is_inst = isinstance(exc_info[1], cls_) except Exception: is_inst = False if is_inst: @@ -227,7 +240,7 @@ def _drop(event, cause, ty): def __copy__(self): # type: () -> Scope - rv = object.__new__(self.__class__) + rv = object.__new__(self.__class__) # type: Scope rv._level = self._level rv._name = self._name @@ -249,6 +262,7 @@ def __copy__(self): return rv def __repr__(self): + # type: () -> str return "<%s id=%s name=%s>" % ( self.__class__.__name__, hex(id(self)), diff --git a/sentry_sdk/serializer.py b/sentry_sdk/serializer.py index 69712269c9..efbf964b04 100644 --- a/sentry_sdk/serializer.py +++ b/sentry_sdk/serializer.py @@ -49,20 +49,23 @@ class MetaNode(object): ) def __init__(self): + # type: () -> None self._parent = None self._segment = None - self._depth = 0 - self._data = None - self._is_databag = None - self._should_repr_strings = None + self._depth = 0 # type: int + self._data = None # type: Optional[Dict[str, Any]] + self._is_databag = None # type: Optional[bool] + self._should_repr_strings = None # type: Optional[bool] def startswith_path(self, path): + # type: (List[Optional[str]]) -> bool if len(path) > self._depth: return False return self.is_path(path + [None] * (self._depth - len(path))) def is_path(self, path): + # type: (List[Optional[str]]) -> bool if len(path) != self._depth: return False @@ -70,6 +73,7 @@ def is_path(self, path): for segment in reversed(path): if segment is not None and segment != cur._segment: return False + assert cur._parent is not None cur = cur._parent return cur._segment is None @@ -82,6 +86,7 @@ def enter(self, segment): return rv def _create_annotations(self): + # type: () -> None if self._data is not None: return @@ -91,10 +96,13 @@ def _create_annotations(self): self._parent._data[str(self._segment)] = self._data def annotate(self, **meta): + # type: (Any) -> None self._create_annotations() + assert self._data is not None self._data.setdefault("", {}).update(meta) def should_repr_strings(self): + # type: () -> bool if self._should_repr_strings is None: self._should_repr_strings = ( self.startswith_path( @@ -109,6 +117,7 @@ def should_repr_strings(self): return self._should_repr_strings def is_databag(self): + # type: () -> bool if self._is_databag is None: self._is_databag = ( self.startswith_path(["request", "data"]) @@ -127,6 +136,7 @@ def is_databag(self): def _flatten_annotated(obj, meta_node): + # type: (Any, MetaNode) -> Any if isinstance(obj, AnnotatedValue): meta_node.annotate(**obj.metadata) obj = obj.value @@ -135,7 +145,8 @@ def _flatten_annotated(obj, meta_node): class Memo(object): def __init__(self): - self._inner = {} + # type: () -> None + self._inner = {} # type: Dict[int, Any] @contextlib.contextmanager def memoize(self, obj): @@ -150,6 +161,7 @@ def memoize(self, obj): class Serializer(object): def __init__(self): + # type: () -> None self.memo = Memo() self.meta_node = MetaNode() diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index d11315e231..4d2aec9e08 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -21,7 +21,7 @@ from typing import Union from typing import Callable from urllib3.poolmanager import PoolManager # type: ignore - from urllib3.poolmanager import ProxyManager # type: ignore + from urllib3.poolmanager import ProxyManager from sentry_sdk.utils import Event @@ -37,23 +37,25 @@ class Transport(object): A transport is used to send an event to sentry. """ - parsed_dsn = None # type: Dsn + parsed_dsn = None # type: Optional[Dsn] def __init__(self, options=None): # type: (Optional[ClientOptions]) -> None self.options = options - if options and options["dsn"]: + if options and options["dsn"] is not None and options["dsn"]: self.parsed_dsn = Dsn(options["dsn"]) else: - self.parsed_dsn = None # type: ignore + self.parsed_dsn = None def capture_event(self, event): + # type: (Event) -> None """This gets invoked with the event dictionary when an event should be sent to sentry. """ raise NotImplementedError() def flush(self, timeout, callback=None): + # type: (float, Optional[Any]) -> None """Wait `timeout` seconds for the current events to be sent out.""" pass @@ -76,6 +78,7 @@ class HttpTransport(Transport): def __init__(self, options): # type: (ClientOptions) -> None Transport.__init__(self, options) + assert self.parsed_dsn is not None self._worker = BackgroundWorker() self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION) self._disabled_until = None # type: Optional[datetime] @@ -104,6 +107,7 @@ def _send_event(self, event): with gzip.GzipFile(fileobj=body, mode="w") as f: f.write(json.dumps(event, allow_nan=False).encode("utf-8")) + assert self.parsed_dsn is not None logger.debug( "Sending %s event [%s] to %s project:%s" % ( diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 8b8f68e999..05e49b90df 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -21,6 +21,7 @@ from typing import Union from sentry_sdk.consts import ClientOptions + from sentry_sdk.hub import Hub ExcInfo = Tuple[ Optional[Type[BaseException]], Optional[BaseException], Optional[Any] @@ -47,6 +48,7 @@ def _get_debug_hub(): + # type: () -> Optional[Hub] # This function is replaced by debug.py pass @@ -63,11 +65,12 @@ def capture_internal_exceptions(): def to_timestamp(value): + # type: (datetime) -> float return (value - epoch).total_seconds() def event_hint_with_exc_info(exc_info=None): - # type: (ExcInfo) -> Dict[str, Optional[ExcInfo]] + # type: (Optional[ExcInfo]) -> Dict[str, Optional[ExcInfo]] """Creates a hint with the exc info filled in.""" if exc_info is None: exc_info = sys.exc_info() @@ -87,6 +90,7 @@ class Dsn(object): """Represents a DSN.""" def __init__(self, value): + # type: (Union[Dsn, str]) -> None if isinstance(value, Dsn): self.__dict__ = dict(value.__dict__) return @@ -114,6 +118,7 @@ def __init__(self, value): @property def netloc(self): + # type: () -> str """The netloc part of a DSN.""" rv = self.host if (self.scheme, self.port) not in (("http", 80), ("https", 443)): @@ -121,6 +126,7 @@ def netloc(self): return rv def to_auth(self, client=None): + # type: (Optional[Any]) -> Auth """Returns the auth info object for this dsn.""" return Auth( scheme=self.scheme, @@ -133,6 +139,7 @@ def to_auth(self, client=None): ) def __str__(self): + # type: () -> str return "%s://%s%s@%s%s%s" % ( self.scheme, self.public_key, @@ -157,6 +164,7 @@ def __init__( client=None, path="/", ): + # type: (str, str, str, str, Optional[str], int, Optional[Any], str) -> None self.scheme = scheme self.host = host self.path = path @@ -168,6 +176,7 @@ def __init__( @property def store_api_url(self): + # type: () -> str """Returns the API url for storing events.""" return "%s://%s%sapi/%s/store/" % ( self.scheme, @@ -177,6 +186,7 @@ def store_api_url(self): ) def to_header(self, timestamp=None): + # type: (Optional[datetime]) -> str """Returns the auth header a string.""" rv = [("sentry_key", self.public_key), ("sentry_version", self.version)] if timestamp is not None: @@ -246,8 +256,8 @@ def slim_string(value, length=MAX_STRING_LENGTH): def get_lines_from_file( filename, # type: str lineno, # type: int - loader=None, # type: Any - module=None, # type: str + loader=None, # type: Optional[Any] + module=None, # type: Optional[str] ): # type: (...) -> Tuple[List[str], Optional[str], List[str]] context_lines = 5 @@ -362,7 +372,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] + # type: (Any, Optional[int], bool) -> Dict[str, Any] f_code = getattr(frame, "f_code", None) if f_code: abs_path = frame.f_code.co_filename @@ -408,6 +418,7 @@ def stacktrace_from_traceback(tb=None, with_locals=True): def current_stacktrace(with_locals=True): + # type: (bool) -> Any __tracebackhide__ = True frames = [] @@ -432,7 +443,7 @@ def single_exception_from_error_tuple( exc_value, # type: Optional[BaseException] tb, # type: Optional[Any] client_options=None, # type: Optional[ClientOptions] - mechanism=None, # type: Dict[str, Any] + mechanism=None, # type: Optional[Dict[str, Any]] ): # type: (...) -> Dict[str, Any] if exc_value is not None: @@ -484,7 +495,7 @@ def walk_exception_chain(exc_info): seen_exceptions.append(exc_value) seen_exception_ids.add(id(exc_value)) - if exc_value.__suppress_context__: # type: ignore + if exc_value.__suppress_context__: cause = exc_value.__cause__ else: cause = exc_value.__context__ @@ -505,7 +516,7 @@ def walk_exception_chain(exc_info): def exceptions_from_error_tuple( exc_info, # type: ExcInfo client_options=None, # type: Optional[ClientOptions] - mechanism=None, # type: Dict[str, Any] + mechanism=None, # type: Optional[Dict[str, Any]] ): # type: (...) -> List[Dict[str, Any]] exc_type, exc_value, tb = exc_info @@ -552,7 +563,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] + # type: (Dict[str, Any], Optional[List], Optional[List]) -> Dict[str, Any] for stacktrace in iter_event_stacktraces(event): handle_in_app_impl( stacktrace.get("frames"), @@ -564,8 +575,9 @@ def handle_in_app(event, in_app_exclude=None, in_app_include=None): def handle_in_app_impl(frames, in_app_exclude, in_app_include): + # type: (Any, Optional[List], Optional[List]) -> Optional[Any] if not frames: - return + return None any_in_app = False for frame in frames: @@ -617,7 +629,7 @@ def exc_info_from_error(error): def event_from_exception( exc_info, # type: Union[BaseException, ExcInfo] client_options=None, # type: Optional[ClientOptions] - mechanism=None, # type: Dict[str, Any] + mechanism=None, # type: Optional[Dict[str, Any]] ): # type: (...) -> Tuple[Dict[str, Any], Dict[str, Any]] exc_info = exc_info_from_error(exc_info) @@ -676,10 +688,10 @@ def format_and_strip( raise ValueError("No formatting placeholders found") params = list(reversed(params)) - rv_remarks = [] + rv_remarks = [] # type: List[Any] rv_original_length = 0 rv_length = 0 - rv = [] + rv = [] # type: List[str] def realign_remark(remark): return [ @@ -710,14 +722,14 @@ def realign_remark(remark): rv_length += len(chunks[-1]) rv_original_length += len(chunks[-1]) - rv = u"".join(rv) - assert len(rv) == rv_length + rv_joined = u"".join(rv) + assert len(rv_joined) == rv_length if not rv_remarks: - return rv + return rv_joined return AnnotatedValue( - value=rv, metadata={"len": rv_original_length, "rem": rv_remarks} + value=rv_joined, metadata={"len": rv_original_length, "rem": rv_remarks} ) @@ -727,7 +739,7 @@ def realign_remark(remark): from contextvars import ContextVar # type: ignore if not PY2 and sys.version_info < (3, 7): - import aiocontextvars # type: ignore # noqa + import aiocontextvars # type: ignore # noqa except ImportError: HAS_REAL_CONTEXTVARS = False @@ -748,11 +760,12 @@ def set(self, value): def transaction_from_function(func): + # type: (Callable[..., Any]) -> Optional[str] # Methods in Python 2 try: return "%s.%s.%s" % ( - func.im_class.__module__, - func.im_class.__name__, + func.im_class.__module__, # type: ignore + func.im_class.__name__, # type: ignore func.__name__, ) except Exception: @@ -760,7 +773,7 @@ def transaction_from_function(func): func_qualname = ( getattr(func, "__qualname__", None) or getattr(func, "__name__", None) or None - ) + ) # type: Optional[str] if not func_qualname: # No idea what it is diff --git a/tox.ini b/tox.ini index 0dc3795a17..af53b4914d 100644 --- a/tox.ini +++ b/tox.ini @@ -173,6 +173,6 @@ commands = [testenv:linters] commands = - flake8 tests sentry_sdk - black --check tests sentry_sdk - mypy sentry_sdk + flake8 tests examples sentry_sdk + black --check tests examples sentry_sdk + mypy examples sentry_sdk