From a307fe9d25d4375a02051e1d540c3b130130035b Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 25 Jun 2019 16:18:34 +0200 Subject: [PATCH 1/7] ref: Type hints for init() --- sentry_sdk/client.py | 18 ++++-- sentry_sdk/consts.py | 118 +++++++++++++++++++--------------------- sentry_sdk/hub.py | 15 +++-- sentry_sdk/transport.py | 9 ++- sentry_sdk/utils.py | 7 +-- 5 files changed, 86 insertions(+), 81 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index e163114a96..aa5d524b38 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -24,17 +24,17 @@ from typing import Dict from typing import Optional - from sentry_sdk.consts import ClientOptions from sentry_sdk.scope import Scope from sentry_sdk.utils import Event, Hint + from sentry_sdk.consts import ClientConstructor _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): +def _get_options(*args, **kwargs): + # type: (*Optional[str], **Any) -> Dict[str, Any] + if args and (isinstance(args[0], str) or args[0] is None): dsn = args[0] # type: Optional[str] args = args[1:] else: @@ -62,6 +62,12 @@ def get_options(*args, **kwargs): return rv # type: ignore +if MYPY: + get_options = ClientConstructor() # type: ClientConstructor[Dict[str, Any]] +else: + get_options = _get_options + + class Client(object): """The client is internally responsible for capturing the events and forwarding them to sentry through the configured transport. It takes @@ -70,10 +76,10 @@ class Client(object): """ def __init__(self, *args, **kwargs): - # type: (*str, **ClientOptions) -> None + # type: (*Optional[str], **Any) -> None old_debug = _client_init_debug.get(False) try: - self.options = options = get_options(*args, **kwargs) + self.options = options = get_options(*args, **kwargs) # type: ignore _client_init_debug.set(options["debug"]) self.transport = make_transport(options) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index c697dc10ce..fb6e09ad89 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -2,84 +2,78 @@ MYPY = False if MYPY: - from mypy_extensions import TypedDict from typing import Optional from typing import Callable from typing import Union from typing import List from typing import Type + from typing import Generic + from typing import TypeVar + from typing import Dict + from typing import Any from sentry_sdk.transport import Transport from sentry_sdk.integrations import Integration from sentry_sdk.utils import Event, EventProcessor, BreadcrumbProcessor - 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[Transport], Callable[[Event], None]] - ], - "sample_rate": int, - "send_default_pii": bool, - "http_proxy": Optional[str], - "https_proxy": Optional[str], - "ignore_errors": List[Union[type, str]], - "request_bodies": str, - "before_send": Optional[EventProcessor], - "before_breadcrumb": Optional[BreadcrumbProcessor], - "debug": bool, - "attach_stacktrace": bool, - "ca_certs": Optional[str], - "propagate_traces": bool, - }, - total=False, - ) + _T = TypeVar("_T") +else: + _T = None + Generic = {None: object} -VERSION = "0.9.2" DEFAULT_SERVER_NAME = socket.gethostname() if hasattr(socket, "gethostname") else None -DEFAULT_OPTIONS = { - "dsn": None, - "with_locals": True, - "max_breadcrumbs": 100, - "release": None, - "environment": None, - "server_name": DEFAULT_SERVER_NAME, - "shutdown_timeout": 2.0, - "integrations": [], - "in_app_include": [], - "in_app_exclude": [], - "default_integrations": True, - "dist": None, - "transport": None, - "sample_rate": 1.0, - "send_default_pii": False, - "http_proxy": None, - "https_proxy": None, - "ignore_errors": [], - "request_bodies": "medium", - "before_send": None, - "before_breadcrumb": None, - "debug": False, - "attach_stacktrace": False, - "ca_certs": None, - "propagate_traces": True, -} +class ClientConstructor(Generic[_T]): + __bogus = None # type: _T + + def __call__( + self, + dsn=None, # type: Optional[str] + with_locals=True, # type: bool + max_breadcrumbs=100, # type: int + release=None, # type: Optional[str] + environment=None, # type: Optional[str] + server_name=DEFAULT_SERVER_NAME, # type: Optional[str] + shutdown_timeout=2, # type: int + integrations=[], # type: List[Integration] + in_app_include=[], # type: List[str] + in_app_exclude=[], # type: List[str] + default_integrations=True, # type: bool + dist=None, # type: Optional[str] + transport=None, # type: Optional[Union[Transport, Type[Transport], Callable[[Event], None]]] + sample_rate=1.0, # type: float + send_default_pii=False, # type: bool + http_proxy=None, # type: Optional[str] + https_proxy=None, # type: Optional[str] + ignore_errors=[], # type: List[Union[type, str]] + request_bodies="medium", # type: str + before_send=None, # type: Optional[EventProcessor] + before_breadcrumb=None, # type: Optional[BreadcrumbProcessor] + debug=False, # type: bool + attach_stacktrace=False, # type: bool + ca_certs=None, # type: Optional[str] + propagate_traces=True, # type: bool + ): + # type: (...) -> _T + return self.__bogus + + +def _get_default_options(): + # type: () -> Dict[str, Any] + import inspect + + a = inspect.getargspec(ClientConstructor().__call__) + return dict(zip(a.args[-len(a.defaults) :], a.defaults)) + + +DEFAULT_OPTIONS = _get_default_options() +del _get_default_options + + +VERSION = "0.9.2" SDK_INFO = { "name": "sentry.python", "version": VERSION, diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 45f3d4aea3..59222b409b 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -34,8 +34,10 @@ from sentry_sdk.integrations import Integration from sentry_sdk.utils import Event, Hint, Breadcrumb, BreadcrumbHint + from sentry_sdk.consts import ClientConstructor T = TypeVar("T") + else: def overload(x): @@ -71,15 +73,14 @@ def __exit__(self, exc_type, exc_value, tb): c.close() -def init(*args, **kwargs): - # type: (*str, **Any) -> ContextManager[Any] - # TODO: https://github.com/getsentry/sentry-python/issues/272 +def _init(*args, **kwargs): + # type: (*Optional[str], **Any) -> ContextManager[Any] """Initializes the SDK and optionally integrations. This takes the same arguments as the client constructor. """ global _initial_client - client = Client(*args, **kwargs) + client = Client(*args, **kwargs) # type: ignore Hub.current.bind_client(client) rv = _InitGuard(client) if client is not None: @@ -87,6 +88,12 @@ def init(*args, **kwargs): return rv +if MYPY: + init = ClientConstructor() # type: ClientConstructor[ContextManager[Any]] +else: + init = _init + + class HubMeta(type): @property def current(self): diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py index 8b867cc8b6..b05468316b 100644 --- a/sentry_sdk/transport.py +++ b/sentry_sdk/transport.py @@ -14,7 +14,6 @@ MYPY = False if MYPY: - from sentry_sdk.consts import ClientOptions from typing import Type from typing import Any from typing import Optional @@ -41,7 +40,7 @@ class Transport(object): parsed_dsn = None # type: Optional[Dsn] def __init__(self, options=None): - # type: (Optional[ClientOptions]) -> None + # type: (Optional[Dict[str, Any]]) -> None self.options = options if options and options["dsn"] is not None and options["dsn"]: self.parsed_dsn = Dsn(options["dsn"]) @@ -77,7 +76,7 @@ class HttpTransport(Transport): """The default HTTP transport.""" def __init__(self, options): - # type: (ClientOptions) -> None + # type: (Dict[str, Any]) -> None Transport.__init__(self, options) assert self.parsed_dsn is not None self._worker = BackgroundWorker() @@ -218,7 +217,7 @@ def capture_event(self, event): def make_transport(options): - # type: (ClientOptions) -> Optional[Transport] + # type: (Dict[str, Any]) -> Optional[Transport] ref_transport = options["transport"] # If no transport is given, we use the http transport class @@ -229,7 +228,7 @@ def make_transport(options): elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport): transport_cls = ref_transport elif callable(ref_transport): - return _FunctionTransport(ref_transport) + return _FunctionTransport(ref_transport) # type: ignore # if a transport class is given only instanciate it if the dsn is not # empty or None diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index d002d940d1..fa912d84be 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -21,7 +21,6 @@ from typing import Type from typing import Union - from sentry_sdk.consts import ClientOptions from sentry_sdk.hub import Hub ExcInfo = Tuple[ @@ -444,7 +443,7 @@ def single_exception_from_error_tuple( exc_type, # type: Optional[type] exc_value, # type: Optional[BaseException] tb, # type: Optional[Any] - client_options=None, # type: Optional[ClientOptions] + client_options=None, # type: Optional[dict] mechanism=None, # type: Optional[Dict[str, Any]] ): # type: (...) -> Dict[str, Any] @@ -517,7 +516,7 @@ def walk_exception_chain(exc_info): def exceptions_from_error_tuple( exc_info, # type: ExcInfo - client_options=None, # type: Optional[ClientOptions] + client_options=None, # type: Optional[dict] mechanism=None, # type: Optional[Dict[str, Any]] ): # type: (...) -> List[Dict[str, Any]] @@ -630,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] + client_options=None, # type: Optional[dict] mechanism=None, # type: Optional[Dict[str, Any]] ): # type: (...) -> Tuple[Dict[str, Any], Dict[str, Any]] From 058d27c30ecdce2a3debcaaec2d19d86eb9c682b Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 26 Jun 2019 13:40:12 +0200 Subject: [PATCH 2/7] ref: Summon satan --- sentry_sdk/client.py | 20 +++++++++++++++++--- sentry_sdk/consts.py | 14 +++++++------- sentry_sdk/hub.py | 6 +++++- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index aa5d524b38..7317995476 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -63,12 +63,16 @@ def _get_options(*args, **kwargs): if MYPY: - get_options = ClientConstructor() # type: ClientConstructor[Dict[str, Any]] + + class get_options(ClientConstructor, Dict[str, Any]): + pass + + else: get_options = _get_options -class Client(object): +class _Client(object): """The client is internally responsible for capturing the events and forwarding them to sentry through the configured transport. It takes the client options as keyword arguments and optionally the DSN as first @@ -267,9 +271,19 @@ def flush(self, timeout=None, callback=None): self.transport.flush(timeout=timeout, callback=callback) def __enter__(self): - # type: () -> Client + # type: () -> _Client return self def __exit__(self, exc_type, exc_value, tb): # type: (Any, Any, Any) -> None self.close() + + +if MYPY: + + class Client(ClientConstructor, _Client): + pass + + +else: + Client = _Client diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index fb6e09ad89..52b6b8072f 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -26,10 +26,10 @@ DEFAULT_SERVER_NAME = socket.gethostname() if hasattr(socket, "gethostname") else None -class ClientConstructor(Generic[_T]): - __bogus = None # type: _T - - def __call__( +# This type exists to trick mypy and PyCharm into thinking `init` and `Client` +# take these arguments (even though they take opaque **kwargs) +class ClientConstructor(object): + def __init__( self, dsn=None, # type: Optional[str] with_locals=True, # type: bool @@ -57,15 +57,15 @@ def __call__( ca_certs=None, # type: Optional[str] propagate_traces=True, # type: bool ): - # type: (...) -> _T - return self.__bogus + # type: (...) -> None + pass def _get_default_options(): # type: () -> Dict[str, Any] import inspect - a = inspect.getargspec(ClientConstructor().__call__) + a = inspect.getargspec(ClientConstructor.__init__) return dict(zip(a.args[-len(a.defaults) :], a.defaults)) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 59222b409b..c5d70abc6e 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -89,7 +89,11 @@ def _init(*args, **kwargs): if MYPY: - init = ClientConstructor() # type: ClientConstructor[ContextManager[Any]] + + class init(ClientConstructor, ContextManager[Any]): + pass + + else: init = _init From a4c0db6d9301530ffb2f226d34c56cd1f090b519 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 26 Jun 2019 15:06:17 +0200 Subject: [PATCH 3/7] ref: Confuse PyCharm a bit --- sentry_sdk/client.py | 4 ++-- sentry_sdk/hub.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 7317995476..a6eff6c635 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -69,7 +69,7 @@ class get_options(ClientConstructor, Dict[str, Any]): else: - get_options = _get_options + get_options = (lambda: _get_options)() class _Client(object): @@ -286,4 +286,4 @@ class Client(ClientConstructor, _Client): else: - Client = _Client + Client = (lambda: _Client)() diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index c5d70abc6e..9fb1aa3720 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -89,13 +89,22 @@ def _init(*args, **kwargs): if MYPY: + # Make mypy, PyCharm and other static analyzers think `init` is a type to + # have nicer autocompletion for params. + # + # Use `ClientConstructor` to define the argument types of `init` and + # `ContextManager[Any]` to tell static analyzers about the return type. class init(ClientConstructor, ContextManager[Any]): pass else: - init = _init + # Alias `init` for actual usage. Go through the lambda indirection to throw + # PyCharm off of the weakly typed signature (it would otherwise discover + # both the weakly typed signature of `_init` and our faked `init` type). + + init = (lambda: _init)() class HubMeta(type): From a23e873c884c75024775119feafee02243380ac4 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 26 Jun 2019 15:23:58 +0200 Subject: [PATCH 4/7] ref: More docs --- examples/basic.py | 2 +- sentry_sdk/client.py | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/examples/basic.py b/examples/basic.py index e6d928bbed..2459ab81d0 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -5,7 +5,7 @@ from sentry_sdk.integrations.stdlib import StdlibIntegration -sentry_sdk.init( +sentry_sdk.Client( dsn="https://@sentry.io/", default_integrations=False, integrations=[ diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index a6eff6c635..bce78012ba 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -13,7 +13,7 @@ ) from sentry_sdk.serializer import Serializer from sentry_sdk.transport import make_transport -from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO +from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO, ClientConstructor from sentry_sdk.integrations import setup_integrations from sentry_sdk.utils import ContextVar @@ -26,7 +26,6 @@ from sentry_sdk.scope import Scope from sentry_sdk.utils import Event, Hint - from sentry_sdk.consts import ClientConstructor _client_init_debug = ContextVar("client_init_debug") @@ -62,16 +61,6 @@ def _get_options(*args, **kwargs): return rv # type: ignore -if MYPY: - - class get_options(ClientConstructor, Dict[str, Any]): - pass - - -else: - get_options = (lambda: _get_options)() - - class _Client(object): """The client is internally responsible for capturing the events and forwarding them to sentry through the configured transport. It takes @@ -280,10 +269,24 @@ def __exit__(self, exc_type, exc_value, tb): if MYPY: + # Make mypy, PyCharm and other static analyzers think `get_options` is a + # type to have nicer autocompletion for params. + # + # Use `ClientConstructor` to define the argument types of `init` and + # `ContextManager[Any]` to tell static analyzers about the return type. + + class get_options(ClientConstructor, Dict[str, Any]): + pass class Client(ClientConstructor, _Client): pass else: + # Alias `get_options` for actual usage. Go through the lambda indirection + # to throw PyCharm off of the weakly typed signature (it would otherwise + # discover both the weakly typed signature of `_init` and our faked `init` + # type). + + get_options = (lambda: _get_options)() Client = (lambda: _Client)() From 7c72b504280835c9471167174b9487352e0843ea Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 26 Jun 2019 15:34:19 +0200 Subject: [PATCH 5/7] fix: Revert accidental changes to examples --- examples/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/basic.py b/examples/basic.py index 2459ab81d0..e6d928bbed 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -5,7 +5,7 @@ from sentry_sdk.integrations.stdlib import StdlibIntegration -sentry_sdk.Client( +sentry_sdk.init( dsn="https://@sentry.io/", default_integrations=False, integrations=[ From 036e643e3b9b7da32a9e9b26a2bdb3647665eaa1 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 26 Jun 2019 15:35:03 +0200 Subject: [PATCH 6/7] ref: Fix comments --- sentry_sdk/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index bce78012ba..9f6d8efe58 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -273,7 +273,7 @@ def __exit__(self, exc_type, exc_value, tb): # type to have nicer autocompletion for params. # # Use `ClientConstructor` to define the argument types of `init` and - # `ContextManager[Any]` to tell static analyzers about the return type. + # `Dict[str, Any]` to tell static analyzers about the return type. class get_options(ClientConstructor, Dict[str, Any]): pass From f72085fda7d6b94983fc4a483972f24057f9ede1 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 26 Jun 2019 15:38:32 +0200 Subject: [PATCH 7/7] ref: Remove dead code --- sentry_sdk/consts.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 52b6b8072f..3b1178684f 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -7,8 +7,6 @@ from typing import Union from typing import List from typing import Type - from typing import Generic - from typing import TypeVar from typing import Dict from typing import Any @@ -17,11 +15,6 @@ from sentry_sdk.utils import Event, EventProcessor, BreadcrumbProcessor - _T = TypeVar("_T") -else: - _T = None - Generic = {None: object} - DEFAULT_SERVER_NAME = socket.gethostname() if hasattr(socket, "gethostname") else None