Skip to content

Commit 8bf3db7

Browse files
authored
Merge pull request #198 from skogsbaer/sw/fix-performance
fix performance
2 parents 74d0c24 + 5f3ab5c commit 8bf3db7

18 files changed

Lines changed: 148 additions & 37 deletions

File tree

python/code/typeguard/_checkers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,10 @@ def check_type_internal(
914914
:param memo: a memo object containing configuration and information necessary for
915915
looking up forward references
916916
"""
917+
ty = type(value)
918+
if ty == annotation or (value is None and annotation is type(None)):
919+
# some early exits for better performance
920+
return
917921
annotation = resolve_alias_chains(annotation)
918922

919923
if isinstance(annotation, ForwardRef):

python/code/wypp/errors.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,30 @@ def invalidType(ty: Any, loc: Optional[location.Loc]) -> WyppTypeError:
7878
lines.append(renderLoc(loc))
7979
raise WyppTypeError('\n'.join(lines))
8080

81+
@staticmethod
82+
def invalidRestArgType(ty: Any, loc: Optional[location.Loc]) -> WyppTypeError:
83+
lines = []
84+
tyStr = renderTy(ty)
85+
lines.append(i18n.invalidRestArgTy(tyStr))
86+
lines.append('')
87+
if loc is not None:
88+
lines.append(f'## {i18n.tr("File")} {loc.filename}')
89+
lines.append(f'## {i18n.tr("Type declared in line")} {loc.startLine}:\n')
90+
lines.append(renderLoc(loc))
91+
raise WyppTypeError('\n'.join(lines))
92+
93+
@staticmethod
94+
def invalidKwArgType(ty: Any, loc: Optional[location.Loc]) -> WyppTypeError:
95+
lines = []
96+
tyStr = renderTy(ty)
97+
lines.append(i18n.invalidKwArgTy(tyStr))
98+
lines.append('')
99+
if loc is not None:
100+
lines.append(f'## {i18n.tr("File")} {loc.filename}')
101+
lines.append(f'## {i18n.tr("Type declared in line")} {loc.startLine}:\n')
102+
lines.append(renderLoc(loc))
103+
raise WyppTypeError('\n'.join(lines))
104+
81105
@staticmethod
82106
def unknownKeywordArgument(callableName: location.CallableName, callLoc: Optional[location.Loc], name: str) -> WyppTypeError:
83107
lines = []

python/code/wypp/i18n.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ def tr(key: str, **kws) -> str:
102102

103103
'invalid type `{ty}`':
104104
'ungültiger Typ `{ty}`',
105+
'invalid type for rest argument: `{ty}`':
106+
'ungültiger Typ für Restargument: `{ty}`',
107+
'invalid type for keyword argument: `{ty}`':
108+
'ungültiger Typ für Schlüsselwort-Argument: `{ty}`',
105109
'Cannot set attribute to value of type `{ty}`.':
106110
'Das Attribut kann nicht auf einen Wert vom Typ `{ty}` gesetzt werden.',
107111
'Problematic assignment in line': 'Fehlerhafte Zuweisung in Zeile',
@@ -342,6 +346,12 @@ def noTypeAnnotationForAttribute(attrName: str, recordName: str) -> str:
342346
def invalidTy(ty: Any) -> str:
343347
return tr('invalid type `{ty}`', ty=ty)
344348

349+
def invalidRestArgTy(ty: Any) -> str:
350+
return tr('invalid type for rest argument: `{ty}`', ty=ty)
351+
352+
def invalidKwArgTy(ty: Any) -> str:
353+
return tr('invalid type for keyword argument: `{ty}`', ty=ty)
354+
345355
def didYouMean(ty: str) -> str:
346356
return tr('Did you mean `{ty}`?', ty=ty)
347357

python/code/wypp/location.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ def __init__(self, f: Callable, kind: CallableKind):
217217
self.__lineno = f.__code__.co_firstlineno
218218
self.__name = f.__name__
219219
self.__ast = parsecache.getAST(self.file)
220+
self.__async = None
220221

221222
def __repr__(self):
222223
return f'StdCallableInfo({self.name}, {self.kind})'
@@ -282,8 +283,10 @@ def getParamSourceLocation(self, paramName: str) -> Optional[Loc]:
282283

283284
@property
284285
def isAsync(self) -> bool:
285-
node = self._findDef()
286-
return isinstance(node, ast.AsyncFunctionDef)
286+
if self.__async is None:
287+
node = self._findDef()
288+
self.__async = isinstance(node, ast.AsyncFunctionDef)
289+
return self.__async
287290

288291
def classFilename(cls) -> str | None:
289292
"""Best-effort path to the file that defined `cls`."""

python/code/wypp/stacktrace.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ def __call__(self, frame: types.FrameType, event: str, arg: Any):
8989
pass
9090
case 'c_exception':
9191
pass
92+
def getReturnFrameType(self, idx: int) -> Optional[types.FrameType]:
93+
try:
94+
return self.__returnFrames[idx]
95+
except IndexError:
96+
return None
9297
def getReturnFrame(self, idx: int) -> Optional[inspect.FrameInfo]:
9398
try:
9499
f = self.__returnFrames[idx]
@@ -102,6 +107,15 @@ def getReturnFrame(self, idx: int) -> Optional[inspect.FrameInfo]:
102107
else:
103108
return None
104109

110+
def frameTypeToFrameInfo(f: Optional[types.FrameType]) -> Optional[inspect.FrameInfo]:
111+
if f:
112+
tb = inspect.getframeinfo(f, context=1)
113+
fi = inspect.FrameInfo(f, tb.filename, tb.lineno, tb.function, tb.code_context, tb.index)
114+
del f
115+
return fi
116+
else:
117+
return None
118+
105119
# when using _call_with_next_frame_removed, we have to take the second-to-last
106120
# return. Hence, we keep the two most recent returns byn setting entriesToKeep = 2.
107121
def installProfileHook(entriesToKeep: int=2) -> ReturnTracker:

python/code/wypp/typecheck.py

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22
from collections.abc import Callable
33
from typing import ParamSpec, TypeVar, Any, Optional, Literal
4+
import types
45
import inspect
56
from dataclasses import dataclass
67
import utils
@@ -24,14 +25,14 @@ def isEmptySignature(sig: inspect.Signature) -> bool:
2425
return False
2526
return isEmptyAnnotation(sig.return_annotation)
2627

27-
def handleMatchesTyResult(res: MatchesTyResult, tyLoc: Optional[location.Loc]) -> bool:
28+
def handleMatchesTyResult(res: MatchesTyResult, getTyLoc: Callable[[], Optional[location.Loc]]) -> bool:
2829
match res:
2930
case MatchesTyFailure(exc, ty):
3031
if isDebug():
3132
debug(f'Exception occurred while calling matchesTy with type {ty}, re-raising')
3233
raise exc
3334
else:
34-
raise errors.WyppTypeError.invalidType(ty, tyLoc)
35+
raise errors.WyppTypeError.invalidType(ty, getTyLoc())
3536
case b:
3637
return b
3738

@@ -63,9 +64,11 @@ def checkSignature(sig: inspect.Signature, info: location.CallableInfo, cfg: Che
6364
locDecl = info.getParamSourceLocation(name)
6465
raise errors.WyppTypeError.partialAnnotationError(location.CallableName.mk(info), name, locDecl)
6566
if p.default is not inspect.Parameter.empty:
66-
locDecl = info.getParamSourceLocation(name)
67+
locDecl = lambda: info.getParamSourceLocation(name)
6768
if not handleMatchesTyResult(matchesTy(p.default, ty, cfg.ns), locDecl):
68-
raise errors.WyppTypeError.defaultError(location.CallableName.mk(info), name, locDecl, ty, p.default)
69+
raise errors.WyppTypeError.defaultError(location.CallableName.mk(info), name,
70+
locDecl(), ty, p.default)
71+
6972

7073
def mandatoryArgCount(sig: inspect.Signature) -> int:
7174
required_kinds = {
@@ -80,24 +83,34 @@ def mandatoryArgCount(sig: inspect.Signature) -> int:
8083
return res
8184

8285
def checkArgument(p: inspect.Parameter, name: str, idx: Optional[int], a: Any,
83-
locArg: Optional[location.Loc], info: location.CallableInfo, cfg: CheckCfg):
86+
getLocArg: Callable[[], Optional[location.Loc]],
87+
info: location.CallableInfo, cfg: CheckCfg):
8488
t = p.annotation
8589
if not isEmptyAnnotation(t):
90+
locDecl = lambda: info.getParamSourceLocation(name)
8691
if p.kind == inspect.Parameter.VAR_POSITIONAL:
92+
if type(t) == str:
93+
t = eval(t)
8794
argT = None
8895
# For *args annotated as tuple[X, ...], extract the element type X
8996
origin = getattr(t, '__origin__', None)
9097
if origin is tuple:
9198
args = getattr(t, '__args__', None)
92-
if args:
99+
if args and len(args) == 2 and args[1] is Ellipsis:
100+
# tuple[X, ...] — homogeneous variadic
93101
argT = args[0]
102+
elif args:
103+
# tuple[X, Y, ...] — fixed-length tuple, no single element type to extract
104+
raise errors.WyppTypeError.invalidRestArgType(t, locDecl())
94105
elif t is tuple:
95106
# bare `tuple` without type parameters, nothing to check
96107
return
97108
else:
98-
raise ValueError(f'Invalid type for rest argument: {t}')
109+
raise errors.WyppTypeError.invalidRestArgType(t, locDecl())
99110
t = argT
100111
elif p.kind == inspect.Parameter.VAR_KEYWORD:
112+
if type(t) == str:
113+
t = eval(t)
101114
valT = None
102115
# For **kwargs annotated as dict[str, X], extract the value type X
103116
origin = getattr(t, '__origin__', None)
@@ -108,31 +121,39 @@ def checkArgument(p: inspect.Parameter, name: str, idx: Optional[int], a: Any,
108121
elif t is dict:
109122
return
110123
else:
111-
raise ValueError(f'Invalid type for keyword argument: {t}')
124+
raise errors.WyppTypeError.invalidKwArgType(t, info.getParamSourceLocation(p.name))
112125
t = valT
113-
locDecl = info.getParamSourceLocation(name)
114126
if not handleMatchesTyResult(matchesTy(a, t, cfg.ns), locDecl):
115127
cn = location.CallableName.mk(info)
116128
raise errors.WyppTypeError.argumentError(cn,
117129
name,
118130
idx,
119-
locDecl,
131+
locDecl(),
120132
t,
121133
a,
122-
locArg)
134+
getLocArg())
135+
123136
def checkArguments(sig: inspect.Signature, args: tuple, kwargs: dict,
124137
info: location.CallableInfo, cfg: CheckCfg) -> None:
125-
debug(f'Checking arguments when calling {info}')
138+
if isDebug():
139+
debug(f'Checking arguments when calling {info}')
126140
paramNames = list(sig.parameters)
127141
mandatory = mandatoryArgCount(sig)
128142
kind = getKind(cfg, paramNames)
129143
offset = 1 if kind == 'method' else 0
130-
fi = stacktrace.callerOutsideWypp()
131-
callLoc = None if not fi else location.Loc.fromFrameInfo(fi)
132144
cn = location.CallableName.mk(info)
145+
# stacktrace.callerOutsideWypp() is expensive, only access it lazily
146+
def getCallLoc() -> Optional[location.Loc]:
147+
fi = stacktrace.callerOutsideWypp()
148+
return None if not fi else location.Loc.fromFrameInfo(fi)
149+
def getLocArg(idxOrName: int | str) -> Callable[[], Optional[location.Loc]]:
150+
def f():
151+
fi = stacktrace.callerOutsideWypp()
152+
return None if fi is None else location.locationOfArgument(fi, i)
153+
return f
133154
def raiseArgMismatch():
134155
raise errors.WyppTypeError.argCountMismatch(cn,
135-
callLoc,
156+
getCallLoc(),
136157
len(paramNames) - offset,
137158
mandatory - offset,
138159
len(args) - offset)
@@ -152,46 +173,46 @@ def raiseArgMismatch():
152173
raiseArgMismatch()
153174
# Check positional args
154175
for i in range(len(args)):
155-
locArg = None if fi is None else location.locationOfArgument(fi, i)
156176
if i < len(positionalNames):
157177
name = positionalNames[i]
158178
p = sig.parameters[name]
159-
checkArgument(p, name, i - offset, args[i], locArg, info, cfg)
179+
checkArgument(p, name, i - offset, args[i], getLocArg(i), info, cfg)
160180
elif varPositionalParam is not None:
161-
checkArgument(varPositionalParam, varPositionalParam.name, i - offset, args[i], locArg, info, cfg)
181+
checkArgument(varPositionalParam, varPositionalParam.name, i - offset, args[i], getLocArg(i), info, cfg)
162182
else:
163183
raiseArgMismatch()
164184
# Check keyword args
165185
for name in kwargs:
166-
locArg = None if fi is None else location.locationOfArgument(fi, name)
167186
if name in sig.parameters and sig.parameters[name].kind not in (
168187
inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD
169188
):
170-
checkArgument(sig.parameters[name], name, None, kwargs[name], locArg, info, cfg)
189+
checkArgument(sig.parameters[name], name, None, kwargs[name], getLocArg(name), info, cfg)
171190
elif varKeywordParam is not None:
172-
checkArgument(varKeywordParam, name, None, kwargs[name], locArg, info, cfg)
191+
checkArgument(varKeywordParam, name, None, kwargs[name], getLocArg(name), info, cfg)
173192
else:
174-
raise errors.WyppTypeError.unknownKeywordArgument(cn, callLoc, name)
193+
raise errors.WyppTypeError.unknownKeywordArgument(cn, getCallLoc(), name)
175194

176-
def checkReturn(sig: inspect.Signature, returnFrame: Optional[inspect.FrameInfo],
195+
def checkReturn(sig: inspect.Signature, returnFrameType: Optional[types.FrameType],
177196
result: Any, info: location.CallableInfo, cfg: CheckCfg) -> None:
178197
if info.isAsync:
179198
return
180199
t = sig.return_annotation
181200
if isEmptyAnnotation(t):
182201
t = None
183-
debug(f'Checking return value when calling {info}, return type: {t}')
184-
locDecl = info.getResultTypeLocation()
202+
if isDebug():
203+
debug(f'Checking return value when calling {info}, return type: {t}')
204+
locDecl = lambda: info.getResultTypeLocation()
185205
if not handleMatchesTyResult(matchesTy(result, t, cfg.ns), locDecl):
186206
fi = stacktrace.callerOutsideWypp()
187207
if fi is not None:
188208
locRes = location.Loc.fromFrameInfo(fi)
189209
returnLoc = None
190210
extraFrames = []
211+
returnFrame = stacktrace.frameTypeToFrameInfo(returnFrameType)
191212
if returnFrame:
192213
returnLoc = location.Loc.fromFrameInfo(returnFrame)
193214
extraFrames = [returnFrame]
194-
raise errors.WyppTypeError.resultError(location.CallableName.mk(info), locDecl, t, returnLoc, result,
215+
raise errors.WyppTypeError.resultError(location.CallableName.mk(info), locDecl(), t, returnLoc, result,
195216
locRes, extraFrames)
196217

197218

@@ -232,9 +253,9 @@ def wrapped(*args, **kwargs) -> T:
232253
utils._call_with_frames_removed(checkArguments, sig, args, kwargs, info, checkCfg)
233254
returnTracker = stacktrace.getReturnTracker()
234255
result = utils._call_with_next_frame_removed(f, *args, **kwargs)
235-
retFrame = returnTracker.getReturnFrame(0)
256+
ft = returnTracker.getReturnFrameType(0)
236257
utils._call_with_frames_removed(
237-
checkReturn, sig, retFrame, result, info, checkCfg
258+
checkReturn, sig, ft, result, info, checkCfg
238259
)
239260
return result
240261
return wrapped

python/file-test-data/extras/args3_ok.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from wypp import *
22

3-
def f(x: int, *rest: tuple[int], **kw: dict[str, int]):
3+
def f(x: int, *rest: tuple[int, ...], **kw: dict[str, int]):
44
print(f'x={x}, kw={kw}')
55

66
f(1)

python/file-test-data/extras/args5.err

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ f(1, 10, '11', y=2, z=3)
1414

1515
## Typ deklariert in Zeile 3:
1616

17-
def f(x: int, *rest: tuple[int], **kw: dict[str, int]):
17+
def f(x: int, *rest: tuple[int, ...], **kw: dict[str, int]):

python/file-test-data/extras/args5.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from wypp import *
22

3-
def f(x: int, *rest: tuple[int], **kw: dict[str, int]):
3+
def f(x: int, *rest: tuple[int, ...], **kw: dict[str, int]):
44
print(f'x={x}, kw={kw}')
55

66
f(1)

python/file-test-data/extras/args6.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from wypp import *
22

3-
def f(x: int, *rest: tuple[int], **kw: dict[str, int]):
3+
def f(x: int, *rest: tuple[int, ...], **kw: dict[str, int]):
44
print(f'x={x}, kw={kw}')
55

66
f(1)

0 commit comments

Comments
 (0)