Skip to content

Commit 5726d1b

Browse files
authored
ref: Type hints for init() (#401)
Fix #272
1 parent f897ac5 commit 5726d1b

File tree

5 files changed

+113
-85
lines changed

5 files changed

+113
-85
lines changed

sentry_sdk/client.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)
1414
from sentry_sdk.serializer import Serializer
1515
from sentry_sdk.transport import make_transport
16-
from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO
16+
from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO, ClientConstructor
1717
from sentry_sdk.integrations import setup_integrations
1818
from sentry_sdk.utils import ContextVar
1919

@@ -24,17 +24,16 @@
2424
from typing import Dict
2525
from typing import Optional
2626

27-
from sentry_sdk.consts import ClientOptions
2827
from sentry_sdk.scope import Scope
2928
from sentry_sdk.utils import Event, Hint
3029

3130

3231
_client_init_debug = ContextVar("client_init_debug")
3332

3433

35-
def get_options(*args, **kwargs):
36-
# type: (*str, **ClientOptions) -> ClientOptions
37-
if args and (isinstance(args[0], string_types) or args[0] is None):
34+
def _get_options(*args, **kwargs):
35+
# type: (*Optional[str], **Any) -> Dict[str, Any]
36+
if args and (isinstance(args[0], str) or args[0] is None):
3837
dsn = args[0] # type: Optional[str]
3938
args = args[1:]
4039
else:
@@ -62,18 +61,18 @@ def get_options(*args, **kwargs):
6261
return rv # type: ignore
6362

6463

65-
class Client(object):
64+
class _Client(object):
6665
"""The client is internally responsible for capturing the events and
6766
forwarding them to sentry through the configured transport. It takes
6867
the client options as keyword arguments and optionally the DSN as first
6968
argument.
7069
"""
7170

7271
def __init__(self, *args, **kwargs):
73-
# type: (*str, **ClientOptions) -> None
72+
# type: (*Optional[str], **Any) -> None
7473
old_debug = _client_init_debug.get(False)
7574
try:
76-
self.options = options = get_options(*args, **kwargs)
75+
self.options = options = get_options(*args, **kwargs) # type: ignore
7776
_client_init_debug.set(options["debug"])
7877
self.transport = make_transport(options)
7978

@@ -261,9 +260,33 @@ def flush(self, timeout=None, callback=None):
261260
self.transport.flush(timeout=timeout, callback=callback)
262261

263262
def __enter__(self):
264-
# type: () -> Client
263+
# type: () -> _Client
265264
return self
266265

267266
def __exit__(self, exc_type, exc_value, tb):
268267
# type: (Any, Any, Any) -> None
269268
self.close()
269+
270+
271+
if MYPY:
272+
# Make mypy, PyCharm and other static analyzers think `get_options` is a
273+
# type to have nicer autocompletion for params.
274+
#
275+
# Use `ClientConstructor` to define the argument types of `init` and
276+
# `Dict[str, Any]` to tell static analyzers about the return type.
277+
278+
class get_options(ClientConstructor, Dict[str, Any]):
279+
pass
280+
281+
class Client(ClientConstructor, _Client):
282+
pass
283+
284+
285+
else:
286+
# Alias `get_options` for actual usage. Go through the lambda indirection
287+
# to throw PyCharm off of the weakly typed signature (it would otherwise
288+
# discover both the weakly typed signature of `_init` and our faked `init`
289+
# type).
290+
291+
get_options = (lambda: _get_options)()
292+
Client = (lambda: _Client)()

sentry_sdk/consts.py

Lines changed: 50 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,84 +2,71 @@
22

33
MYPY = False
44
if MYPY:
5-
from mypy_extensions import TypedDict
65
from typing import Optional
76
from typing import Callable
87
from typing import Union
98
from typing import List
109
from typing import Type
10+
from typing import Dict
11+
from typing import Any
1112

1213
from sentry_sdk.transport import Transport
1314
from sentry_sdk.integrations import Integration
1415

1516
from sentry_sdk.utils import Event, EventProcessor, BreadcrumbProcessor
1617

17-
ClientOptions = TypedDict(
18-
"ClientOptions",
19-
{
20-
"dsn": Optional[str],
21-
"with_locals": bool,
22-
"max_breadcrumbs": int,
23-
"release": Optional[str],
24-
"environment": Optional[str],
25-
"server_name": Optional[str],
26-
"shutdown_timeout": int,
27-
"integrations": List[Integration],
28-
"in_app_include": List[str],
29-
"in_app_exclude": List[str],
30-
"default_integrations": bool,
31-
"dist": Optional[str],
32-
"transport": Optional[
33-
Union[Transport, Type[Transport], Callable[[Event], None]]
34-
],
35-
"sample_rate": int,
36-
"send_default_pii": bool,
37-
"http_proxy": Optional[str],
38-
"https_proxy": Optional[str],
39-
"ignore_errors": List[Union[type, str]],
40-
"request_bodies": str,
41-
"before_send": Optional[EventProcessor],
42-
"before_breadcrumb": Optional[BreadcrumbProcessor],
43-
"debug": bool,
44-
"attach_stacktrace": bool,
45-
"ca_certs": Optional[str],
46-
"propagate_traces": bool,
47-
},
48-
total=False,
49-
)
5018

51-
52-
VERSION = "0.9.2"
5319
DEFAULT_SERVER_NAME = socket.gethostname() if hasattr(socket, "gethostname") else None
54-
DEFAULT_OPTIONS = {
55-
"dsn": None,
56-
"with_locals": True,
57-
"max_breadcrumbs": 100,
58-
"release": None,
59-
"environment": None,
60-
"server_name": DEFAULT_SERVER_NAME,
61-
"shutdown_timeout": 2.0,
62-
"integrations": [],
63-
"in_app_include": [],
64-
"in_app_exclude": [],
65-
"default_integrations": True,
66-
"dist": None,
67-
"transport": None,
68-
"sample_rate": 1.0,
69-
"send_default_pii": False,
70-
"http_proxy": None,
71-
"https_proxy": None,
72-
"ignore_errors": [],
73-
"request_bodies": "medium",
74-
"before_send": None,
75-
"before_breadcrumb": None,
76-
"debug": False,
77-
"attach_stacktrace": False,
78-
"ca_certs": None,
79-
"propagate_traces": True,
80-
}
8120

8221

22+
# This type exists to trick mypy and PyCharm into thinking `init` and `Client`
23+
# take these arguments (even though they take opaque **kwargs)
24+
class ClientConstructor(object):
25+
def __init__(
26+
self,
27+
dsn=None, # type: Optional[str]
28+
with_locals=True, # type: bool
29+
max_breadcrumbs=100, # type: int
30+
release=None, # type: Optional[str]
31+
environment=None, # type: Optional[str]
32+
server_name=DEFAULT_SERVER_NAME, # type: Optional[str]
33+
shutdown_timeout=2, # type: int
34+
integrations=[], # type: List[Integration]
35+
in_app_include=[], # type: List[str]
36+
in_app_exclude=[], # type: List[str]
37+
default_integrations=True, # type: bool
38+
dist=None, # type: Optional[str]
39+
transport=None, # type: Optional[Union[Transport, Type[Transport], Callable[[Event], None]]]
40+
sample_rate=1.0, # type: float
41+
send_default_pii=False, # type: bool
42+
http_proxy=None, # type: Optional[str]
43+
https_proxy=None, # type: Optional[str]
44+
ignore_errors=[], # type: List[Union[type, str]]
45+
request_bodies="medium", # type: str
46+
before_send=None, # type: Optional[EventProcessor]
47+
before_breadcrumb=None, # type: Optional[BreadcrumbProcessor]
48+
debug=False, # type: bool
49+
attach_stacktrace=False, # type: bool
50+
ca_certs=None, # type: Optional[str]
51+
propagate_traces=True, # type: bool
52+
):
53+
# type: (...) -> None
54+
pass
55+
56+
57+
def _get_default_options():
58+
# type: () -> Dict[str, Any]
59+
import inspect
60+
61+
a = inspect.getargspec(ClientConstructor.__init__)
62+
return dict(zip(a.args[-len(a.defaults) :], a.defaults))
63+
64+
65+
DEFAULT_OPTIONS = _get_default_options()
66+
del _get_default_options
67+
68+
69+
VERSION = "0.9.2"
8370
SDK_INFO = {
8471
"name": "sentry.python",
8572
"version": VERSION,

sentry_sdk/hub.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434

3535
from sentry_sdk.integrations import Integration
3636
from sentry_sdk.utils import Event, Hint, Breadcrumb, BreadcrumbHint
37+
from sentry_sdk.consts import ClientConstructor
3738

3839
T = TypeVar("T")
40+
3941
else:
4042

4143
def overload(x):
@@ -71,22 +73,40 @@ def __exit__(self, exc_type, exc_value, tb):
7173
c.close()
7274

7375

74-
def init(*args, **kwargs):
75-
# type: (*str, **Any) -> ContextManager[Any]
76-
# TODO: https://github.com/getsentry/sentry-python/issues/272
76+
def _init(*args, **kwargs):
77+
# type: (*Optional[str], **Any) -> ContextManager[Any]
7778
"""Initializes the SDK and optionally integrations.
7879
7980
This takes the same arguments as the client constructor.
8081
"""
8182
global _initial_client
82-
client = Client(*args, **kwargs)
83+
client = Client(*args, **kwargs) # type: ignore
8384
Hub.current.bind_client(client)
8485
rv = _InitGuard(client)
8586
if client is not None:
8687
_initial_client = weakref.ref(client)
8788
return rv
8889

8990

91+
if MYPY:
92+
# Make mypy, PyCharm and other static analyzers think `init` is a type to
93+
# have nicer autocompletion for params.
94+
#
95+
# Use `ClientConstructor` to define the argument types of `init` and
96+
# `ContextManager[Any]` to tell static analyzers about the return type.
97+
98+
class init(ClientConstructor, ContextManager[Any]):
99+
pass
100+
101+
102+
else:
103+
# Alias `init` for actual usage. Go through the lambda indirection to throw
104+
# PyCharm off of the weakly typed signature (it would otherwise discover
105+
# both the weakly typed signature of `_init` and our faked `init` type).
106+
107+
init = (lambda: _init)()
108+
109+
90110
class HubMeta(type):
91111
@property
92112
def current(self):

sentry_sdk/transport.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
MYPY = False
1616
if MYPY:
17-
from sentry_sdk.consts import ClientOptions
1817
from typing import Type
1918
from typing import Any
2019
from typing import Optional
@@ -41,7 +40,7 @@ class Transport(object):
4140
parsed_dsn = None # type: Optional[Dsn]
4241

4342
def __init__(self, options=None):
44-
# type: (Optional[ClientOptions]) -> None
43+
# type: (Optional[Dict[str, Any]]) -> None
4544
self.options = options
4645
if options and options["dsn"] is not None and options["dsn"]:
4746
self.parsed_dsn = Dsn(options["dsn"])
@@ -77,7 +76,7 @@ class HttpTransport(Transport):
7776
"""The default HTTP transport."""
7877

7978
def __init__(self, options):
80-
# type: (ClientOptions) -> None
79+
# type: (Dict[str, Any]) -> None
8180
Transport.__init__(self, options)
8281
assert self.parsed_dsn is not None
8382
self._worker = BackgroundWorker()
@@ -218,7 +217,7 @@ def capture_event(self, event):
218217

219218

220219
def make_transport(options):
221-
# type: (ClientOptions) -> Optional[Transport]
220+
# type: (Dict[str, Any]) -> Optional[Transport]
222221
ref_transport = options["transport"]
223222

224223
# If no transport is given, we use the http transport class
@@ -229,7 +228,7 @@ def make_transport(options):
229228
elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport):
230229
transport_cls = ref_transport
231230
elif callable(ref_transport):
232-
return _FunctionTransport(ref_transport)
231+
return _FunctionTransport(ref_transport) # type: ignore
233232

234233
# if a transport class is given only instanciate it if the dsn is not
235234
# empty or None

sentry_sdk/utils.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from typing import Type
2222
from typing import Union
2323

24-
from sentry_sdk.consts import ClientOptions
2524
from sentry_sdk.hub import Hub
2625

2726
ExcInfo = Tuple[
@@ -444,7 +443,7 @@ def single_exception_from_error_tuple(
444443
exc_type, # type: Optional[type]
445444
exc_value, # type: Optional[BaseException]
446445
tb, # type: Optional[Any]
447-
client_options=None, # type: Optional[ClientOptions]
446+
client_options=None, # type: Optional[dict]
448447
mechanism=None, # type: Optional[Dict[str, Any]]
449448
):
450449
# type: (...) -> Dict[str, Any]
@@ -517,7 +516,7 @@ def walk_exception_chain(exc_info):
517516

518517
def exceptions_from_error_tuple(
519518
exc_info, # type: ExcInfo
520-
client_options=None, # type: Optional[ClientOptions]
519+
client_options=None, # type: Optional[dict]
521520
mechanism=None, # type: Optional[Dict[str, Any]]
522521
):
523522
# type: (...) -> List[Dict[str, Any]]
@@ -630,7 +629,7 @@ def exc_info_from_error(error):
630629

631630
def event_from_exception(
632631
exc_info, # type: Union[BaseException, ExcInfo]
633-
client_options=None, # type: Optional[ClientOptions]
632+
client_options=None, # type: Optional[dict]
634633
mechanism=None, # type: Optional[Dict[str, Any]]
635634
):
636635
# type: (...) -> Tuple[Dict[str, Any], Dict[str, Any]]

0 commit comments

Comments
 (0)