1# This file is part of Hypothesis, which may be found at
2# https://github.com/HypothesisWorks/hypothesis/
3#
4# Copyright the Hypothesis Authors.
5# Individual contributors are listed in AUTHORS.rst and the git log.
6#
7# This Source Code Form is subject to the terms of the Mozilla Public License,
8# v. 2.0. If a copy of the MPL was not distributed with this file, You can
9# obtain one at https://mozilla.org/MPL/2.0/.
10
11"""This file can approximately be considered the collection of hypothesis going
12to really unreasonable lengths to produce pretty output."""
13
14import ast
15import hashlib
16import inspect
17import linecache
18import os
19import re
20import sys
21import textwrap
22import types
23import warnings
24from collections.abc import MutableMapping, Sequence
25from functools import partial, wraps
26from inspect import Parameter, Signature
27from io import StringIO
28from keyword import iskeyword
29from random import _inst as global_random_instance
30from tokenize import COMMENT, detect_encoding, generate_tokens, untokenize
31from types import ModuleType
32from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
33from unittest.mock import _patch as PatchType
34from weakref import WeakKeyDictionary
35
36from hypothesis.errors import HypothesisWarning
37from hypothesis.internal.compat import EllipsisType, is_typed_named_tuple
38from hypothesis.utils.conventions import not_set
39from hypothesis.vendor.pretty import pretty
40
41if TYPE_CHECKING:
42 from hypothesis.strategies._internal.strategies import SearchStrategy
43
44T = TypeVar("T")
45
46READTHEDOCS = os.environ.get("READTHEDOCS", None) == "True"
47LAMBDA_SOURCE_CACHE: MutableMapping[Callable, str] = WeakKeyDictionary()
48
49
50def is_mock(obj: object) -> bool:
51 """Determine if the given argument is a mock type."""
52
53 # We want to be able to detect these when dealing with various test
54 # args. As they are sneaky and can look like almost anything else,
55 # we'll check this by looking for an attribute with a name that it's really
56 # unlikely to implement accidentally, and that anyone who implements it
57 # deliberately should know what they're doing. This is more robust than
58 # looking for types.
59 return hasattr(obj, "hypothesis_internal_is_this_a_mock_check")
60
61
62def _clean_source(src: str) -> bytes:
63 """Return the source code as bytes, without decorators or comments.
64
65 Because this is part of our database key, we reduce the cache invalidation
66 rate by ignoring decorators, comments, trailing whitespace, and empty lines.
67 We can't just use the (dumped) AST directly because it changes between Python
68 versions (e.g. ast.Constant)
69 """
70 # Get the (one-indexed) line number of the function definition, and drop preceding
71 # lines - i.e. any decorators, so that adding `@example()`s keeps the same key.
72 try:
73 funcdef = ast.parse(src).body[0]
74 src = "".join(src.splitlines(keepends=True)[funcdef.lineno - 1 :])
75 except Exception:
76 pass
77 # Remove blank lines and use the tokenize module to strip out comments,
78 # so that those can be changed without changing the database key.
79 try:
80 src = untokenize(
81 t for t in generate_tokens(StringIO(src).readline) if t.type != COMMENT
82 )
83 except Exception:
84 pass
85 # Finally, remove any trailing whitespace and empty lines as a last cleanup.
86 return "\n".join(x.rstrip() for x in src.splitlines() if x.rstrip()).encode()
87
88
89def function_digest(function: Any) -> bytes:
90 """Returns a string that is stable across multiple invocations across
91 multiple processes and is prone to changing significantly in response to
92 minor changes to the function.
93
94 No guarantee of uniqueness though it usually will be. Digest collisions
95 lead to unfortunate but not fatal problems during database replay.
96 """
97 hasher = hashlib.sha384()
98 try:
99 src = inspect.getsource(function)
100 except (OSError, TypeError):
101 # If we can't actually get the source code, try for the name as a fallback.
102 # NOTE: We might want to change this to always adding function.__qualname__,
103 # to differentiate f.x. two classes having the same function implementation
104 # with class-dependent behaviour.
105 try:
106 hasher.update(function.__name__.encode())
107 except AttributeError:
108 pass
109 else:
110 hasher.update(_clean_source(src))
111 try:
112 # This is additional to the source code because it can include the effects
113 # of decorators, or of post-hoc assignment to the .__signature__ attribute.
114 hasher.update(repr(get_signature(function)).encode())
115 except Exception:
116 pass
117 try:
118 # We set this in order to distinguish e.g. @pytest.mark.parametrize cases.
119 hasher.update(function._hypothesis_internal_add_digest)
120 except AttributeError:
121 pass
122 return hasher.digest()
123
124
125def check_signature(sig: Signature) -> None:
126 # Backport from Python 3.11; see https://github.com/python/cpython/pull/92065
127 for p in sig.parameters.values():
128 if iskeyword(p.name) and p.kind is not p.POSITIONAL_ONLY:
129 raise ValueError(
130 f"Signature {sig!r} contains a parameter named {p.name!r}, "
131 f"but this is a SyntaxError because `{p.name}` is a keyword. "
132 "You, or a library you use, must have manually created an "
133 "invalid signature - this will be an error in Python 3.11+"
134 )
135
136
137def get_signature(
138 target: Any, *, follow_wrapped: bool = True, eval_str: bool = False
139) -> Signature:
140 # Special case for use of `@unittest.mock.patch` decorator, mimicking the
141 # behaviour of getfullargspec instead of reporting unusable arguments.
142 patches = getattr(target, "patchings", None)
143 if isinstance(patches, list) and all(isinstance(p, PatchType) for p in patches):
144 return Signature(
145 [
146 Parameter("args", Parameter.VAR_POSITIONAL),
147 Parameter("keywargs", Parameter.VAR_KEYWORD),
148 ]
149 )
150
151 if isinstance(getattr(target, "__signature__", None), Signature):
152 # This special case covers unusual codegen like Pydantic models
153 sig = target.__signature__
154 check_signature(sig)
155 # And *this* much more complicated block ignores the `self` argument
156 # if that's been (incorrectly) included in the custom signature.
157 if sig.parameters and (inspect.isclass(target) or inspect.ismethod(target)):
158 selfy = next(iter(sig.parameters.values()))
159 if (
160 selfy.name == "self"
161 and selfy.default is Parameter.empty
162 and selfy.kind.name.startswith("POSITIONAL_")
163 ):
164 return sig.replace(
165 parameters=[v for k, v in sig.parameters.items() if k != "self"]
166 )
167 return sig
168 # eval_str is only supported by Python 3.10 and newer
169 if sys.version_info[:2] >= (3, 10):
170 sig = inspect.signature(
171 target, follow_wrapped=follow_wrapped, eval_str=eval_str
172 )
173 else:
174 sig = inspect.signature(
175 target, follow_wrapped=follow_wrapped
176 ) # pragma: no cover
177 check_signature(sig)
178 return sig
179
180
181def arg_is_required(param: Parameter) -> bool:
182 return param.default is Parameter.empty and param.kind in (
183 Parameter.POSITIONAL_OR_KEYWORD,
184 Parameter.KEYWORD_ONLY,
185 )
186
187
188def required_args(
189 target: Callable[..., Any],
190 args: tuple["SearchStrategy[Any]", ...] = (),
191 kwargs: Optional[dict[str, Union["SearchStrategy[Any]", EllipsisType]]] = None,
192) -> set[str]:
193 """Return a set of names of required args to target that were not supplied
194 in args or kwargs.
195
196 This is used in builds() to determine which arguments to attempt to
197 fill from type hints. target may be any callable (including classes
198 and bound methods). args and kwargs should be as they are passed to
199 builds() - that is, a tuple of values and a dict of names: values.
200 """
201 kwargs = {} if kwargs is None else kwargs
202 # We start with a workaround for NamedTuples, which don't have nice inits
203 if inspect.isclass(target) and is_typed_named_tuple(target):
204 provided = set(kwargs) | set(target._fields[: len(args)])
205 return set(target._fields) - provided
206 # Then we try to do the right thing with inspect.signature
207 try:
208 sig = get_signature(target)
209 except (ValueError, TypeError):
210 return set()
211 return {
212 name
213 for name, param in list(sig.parameters.items())[len(args) :]
214 if arg_is_required(param) and name not in kwargs
215 }
216
217
218def convert_keyword_arguments(
219 function: Any, args: Sequence[object], kwargs: dict[str, object]
220) -> tuple[tuple[object, ...], dict[str, object]]:
221 """Returns a pair of a tuple and a dictionary which would be equivalent
222 passed as positional and keyword args to the function. Unless function has
223 kwonlyargs or **kwargs the dictionary will always be empty.
224 """
225 sig = inspect.signature(function, follow_wrapped=False)
226 bound = sig.bind(*args, **kwargs)
227 return bound.args, bound.kwargs
228
229
230def convert_positional_arguments(
231 function: Any, args: Sequence[object], kwargs: dict[str, object]
232) -> tuple[tuple[object, ...], dict[str, object]]:
233 """Return a tuple (new_args, new_kwargs) where all possible arguments have
234 been moved to kwargs.
235
236 new_args will only be non-empty if function has pos-only args or *args.
237 """
238 sig = inspect.signature(function, follow_wrapped=False)
239 bound = sig.bind(*args, **kwargs)
240 new_args = []
241 new_kwargs = dict(bound.arguments)
242 for p in sig.parameters.values():
243 if p.name in new_kwargs:
244 if p.kind is p.POSITIONAL_ONLY:
245 new_args.append(new_kwargs.pop(p.name))
246 elif p.kind is p.VAR_POSITIONAL:
247 new_args.extend(new_kwargs.pop(p.name))
248 elif p.kind is p.VAR_KEYWORD:
249 assert set(new_kwargs[p.name]).isdisjoint(set(new_kwargs) - {p.name})
250 new_kwargs.update(new_kwargs.pop(p.name))
251 return tuple(new_args), new_kwargs
252
253
254def ast_arguments_matches_signature(args: ast.arguments, sig: Signature) -> bool:
255 expected: list[tuple[str, int]] = []
256 for node in args.posonlyargs:
257 expected.append((node.arg, Parameter.POSITIONAL_ONLY))
258 for node in args.args:
259 expected.append((node.arg, Parameter.POSITIONAL_OR_KEYWORD))
260 if args.vararg is not None:
261 expected.append((args.vararg.arg, Parameter.VAR_POSITIONAL))
262 for node in args.kwonlyargs:
263 expected.append((node.arg, Parameter.KEYWORD_ONLY))
264 if args.kwarg is not None:
265 expected.append((args.kwarg.arg, Parameter.VAR_KEYWORD))
266 return expected == [(p.name, p.kind) for p in sig.parameters.values()]
267
268
269def is_first_param_referenced_in_function(f: Any) -> bool:
270 """Is the given name referenced within f?"""
271 try:
272 tree = ast.parse(textwrap.dedent(inspect.getsource(f)))
273 except Exception:
274 return True # Assume it's OK unless we know otherwise
275 name = next(iter(get_signature(f).parameters))
276 return any(
277 isinstance(node, ast.Name)
278 and node.id == name
279 and isinstance(node.ctx, ast.Load)
280 for node in ast.walk(tree)
281 )
282
283
284def extract_all_lambdas(tree, matching_signature):
285 lambdas = []
286
287 class Visitor(ast.NodeVisitor):
288 def visit_Lambda(self, node):
289 if ast_arguments_matches_signature(node.args, matching_signature):
290 lambdas.append(node)
291
292 Visitor().visit(tree)
293
294 return lambdas
295
296
297LINE_CONTINUATION = re.compile(r"\\\n")
298WHITESPACE = re.compile(r"\s+")
299PROBABLY_A_COMMENT = re.compile("""#[^'"]*$""")
300SPACE_FOLLOWS_OPEN_BRACKET = re.compile(r"\( ")
301SPACE_PRECEDES_CLOSE_BRACKET = re.compile(r" \)")
302
303
304def _extract_lambda_source(f):
305 """Extracts a single lambda expression from the string source. Returns a
306 string indicating an unknown body if it gets confused in any way.
307
308 This is not a good function and I am sorry for it. Forgive me my
309 sins, oh lord
310 """
311 # You might be wondering how a lambda can have a return-type annotation?
312 # The answer is that we add this at runtime, in new_given_signature(),
313 # and we do support strange choices as applying @given() to a lambda.
314 sig = inspect.signature(f)
315 assert sig.return_annotation in (Parameter.empty, None), sig
316
317 # Using pytest-xdist on Python 3.13, there's an entry in the linecache for
318 # file "<string>", which then returns nonsense to getsource. Discard it.
319 linecache.cache.pop("<string>", None)
320
321 if sig.parameters:
322 if_confused = f"lambda {str(sig)[1:-1]}: <unknown>"
323 else:
324 if_confused = "lambda: <unknown>"
325 try:
326 source = inspect.getsource(f)
327 except OSError:
328 return if_confused
329
330 source = LINE_CONTINUATION.sub(" ", source)
331 source = WHITESPACE.sub(" ", source)
332 source = source.strip()
333 if "lambda" not in source and sys.platform == "emscripten": # pragma: no cover
334 return if_confused # work around Pyodide bug in inspect.getsource()
335 assert "lambda" in source, source
336
337 tree = None
338
339 try:
340 tree = ast.parse(source)
341 except SyntaxError:
342 for i in range(len(source) - 1, len("lambda"), -1):
343 prefix = source[:i]
344 if "lambda" not in prefix:
345 break
346 try:
347 tree = ast.parse(prefix)
348 source = prefix
349 break
350 except SyntaxError:
351 continue
352 if tree is None and source.startswith(("@", ".")):
353 # This will always eventually find a valid expression because the
354 # decorator or chained operator must be a valid Python function call,
355 # so will eventually be syntactically valid and break out of the loop.
356 # Thus, this loop can never terminate normally.
357 for i in range(len(source) + 1):
358 p = source[1:i]
359 if "lambda" in p:
360 try:
361 tree = ast.parse(p)
362 source = p
363 break
364 except SyntaxError:
365 pass
366 else:
367 raise NotImplementedError("expected to be unreachable")
368
369 if tree is None:
370 return if_confused
371
372 aligned_lambdas = extract_all_lambdas(tree, matching_signature=sig)
373 if len(aligned_lambdas) != 1:
374 return if_confused
375 lambda_ast = aligned_lambdas[0]
376 assert lambda_ast.lineno == 1
377
378 # If the source code contains Unicode characters, the bytes of the original
379 # file don't line up with the string indexes, and `col_offset` doesn't match
380 # the string we're using. We need to convert the source code into bytes
381 # before slicing.
382 #
383 # Under the hood, the inspect module is using `tokenize.detect_encoding` to
384 # detect the encoding of the original source file. We'll use the same
385 # approach to get the source code as bytes.
386 #
387 # See https://github.com/HypothesisWorks/hypothesis/issues/1700 for an
388 # example of what happens if you don't correct for this.
389 #
390 # Note: if the code doesn't come from a file (but, for example, a doctest),
391 # `getsourcefile` will return `None` and the `open()` call will fail with
392 # an OSError. Or if `f` is a built-in function, in which case we get a
393 # TypeError. In both cases, fall back to splitting the Unicode string.
394 # It's not perfect, but it's the best we can do.
395 try:
396 with open(inspect.getsourcefile(f), "rb") as src_f:
397 encoding, _ = detect_encoding(src_f.readline)
398
399 source_bytes = source.encode(encoding)
400 source_bytes = source_bytes[lambda_ast.col_offset :].strip()
401 source = source_bytes.decode(encoding)
402 except (OSError, TypeError):
403 source = source[lambda_ast.col_offset :].strip()
404
405 # This ValueError can be thrown in Python 3 if:
406 #
407 # - There's a Unicode character in the line before the Lambda, and
408 # - For some reason we can't detect the source encoding of the file
409 #
410 # because slicing on `lambda_ast.col_offset` will account for bytes, but
411 # the slice will be on Unicode characters.
412 #
413 # In practice this seems relatively rare, so we just give up rather than
414 # trying to recover.
415 try:
416 source = source[source.index("lambda") :]
417 except ValueError:
418 return if_confused
419
420 for i in range(len(source), len("lambda"), -1): # pragma: no branch
421 try:
422 parsed = ast.parse(source[:i])
423 assert len(parsed.body) == 1
424 assert parsed.body
425 if isinstance(parsed.body[0].value, ast.Lambda):
426 source = source[:i]
427 break
428 except SyntaxError:
429 pass
430 lines = source.split("\n")
431 lines = [PROBABLY_A_COMMENT.sub("", l) for l in lines]
432 source = "\n".join(lines)
433
434 source = WHITESPACE.sub(" ", source)
435 source = SPACE_FOLLOWS_OPEN_BRACKET.sub("(", source)
436 source = SPACE_PRECEDES_CLOSE_BRACKET.sub(")", source)
437 return source.strip()
438
439
440def extract_lambda_source(f):
441 try:
442 return LAMBDA_SOURCE_CACHE[f]
443 except KeyError:
444 pass
445
446 source = _extract_lambda_source(f)
447 LAMBDA_SOURCE_CACHE[f] = source
448 return source
449
450
451def get_pretty_function_description(f: object) -> str:
452 if isinstance(f, partial):
453 return pretty(f)
454 if not hasattr(f, "__name__"):
455 return repr(f)
456 name = f.__name__ # type: ignore
457 if name == "<lambda>":
458 return extract_lambda_source(f)
459 elif isinstance(f, (types.MethodType, types.BuiltinMethodType)):
460 self = f.__self__
461 # Some objects, like `builtins.abs` are of BuiltinMethodType but have
462 # their module as __self__. This might include c-extensions generally?
463 if not (self is None or inspect.isclass(self) or inspect.ismodule(self)):
464 if self is global_random_instance:
465 return f"random.{name}"
466 return f"{self!r}.{name}"
467 elif isinstance(name, str) and getattr(dict, name, object()) is f:
468 # special case for keys/values views in from_type() / ghostwriter output
469 return f"dict.{name}"
470 return name
471
472
473def nicerepr(v: Any) -> str:
474 if inspect.isfunction(v):
475 return get_pretty_function_description(v)
476 elif isinstance(v, type):
477 return v.__name__
478 else:
479 # With TypeVar T, show List[T] instead of TypeError on List[~T]
480 return re.sub(r"(\[)~([A-Z][a-z]*\])", r"\g<1>\g<2>", pretty(v))
481
482
483def repr_call(
484 f: Any, args: Sequence[object], kwargs: dict[str, object], *, reorder: bool = True
485) -> str:
486 # Note: for multi-line pretty-printing, see RepresentationPrinter.repr_call()
487 if reorder:
488 args, kwargs = convert_positional_arguments(f, args, kwargs)
489
490 bits = [nicerepr(x) for x in args]
491
492 for p in get_signature(f).parameters.values():
493 if p.name in kwargs and not p.kind.name.startswith("VAR_"):
494 bits.append(f"{p.name}={nicerepr(kwargs.pop(p.name))}")
495 if kwargs:
496 for a in sorted(kwargs):
497 bits.append(f"{a}={nicerepr(kwargs[a])}")
498
499 rep = nicerepr(f)
500 if rep.startswith("lambda") and ":" in rep:
501 rep = f"({rep})"
502 repr_len = len(rep) + sum(len(b) for b in bits) # approx
503 if repr_len > 30000:
504 warnings.warn(
505 "Generating overly large repr. This is an expensive operation, and with "
506 f"a length of {repr_len//1000} kB is unlikely to be useful. Use -Wignore "
507 "to ignore the warning, or -Werror to get a traceback.",
508 HypothesisWarning,
509 stacklevel=2,
510 )
511 return rep + "(" + ", ".join(bits) + ")"
512
513
514def check_valid_identifier(identifier: str) -> None:
515 if not identifier.isidentifier():
516 raise ValueError(f"{identifier!r} is not a valid python identifier")
517
518
519eval_cache: dict[str, ModuleType] = {}
520
521
522def source_exec_as_module(source: str) -> ModuleType:
523 try:
524 return eval_cache[source]
525 except KeyError:
526 pass
527
528 hexdigest = hashlib.sha384(source.encode()).hexdigest()
529 result = ModuleType("hypothesis_temporary_module_" + hexdigest)
530 assert isinstance(source, str)
531 exec(source, result.__dict__)
532 eval_cache[source] = result
533 return result
534
535
536COPY_SIGNATURE_SCRIPT = """
537from hypothesis.utils.conventions import not_set
538
539def accept({funcname}):
540 def {name}{signature}:
541 return {funcname}({invocation})
542 return {name}
543""".lstrip()
544
545
546def get_varargs(
547 sig: Signature, kind: int = Parameter.VAR_POSITIONAL
548) -> Optional[Parameter]:
549 for p in sig.parameters.values():
550 if p.kind is kind:
551 return p
552 return None
553
554
555def define_function_signature(name, docstring, signature):
556 """A decorator which sets the name, signature and docstring of the function
557 passed into it."""
558 if name == "<lambda>":
559 name = "_lambda_"
560 check_valid_identifier(name)
561 for a in signature.parameters:
562 check_valid_identifier(a)
563
564 used_names = {*signature.parameters, name}
565
566 newsig = signature.replace(
567 parameters=[
568 p if p.default is signature.empty else p.replace(default=not_set)
569 for p in (
570 p.replace(annotation=signature.empty)
571 for p in signature.parameters.values()
572 )
573 ],
574 return_annotation=signature.empty,
575 )
576
577 pos_args = [
578 p
579 for p in signature.parameters.values()
580 if p.kind.name.startswith("POSITIONAL_")
581 ]
582
583 def accept(f):
584 fsig = inspect.signature(f, follow_wrapped=False)
585 must_pass_as_kwargs = []
586 invocation_parts = []
587 for p in pos_args:
588 if p.name not in fsig.parameters and get_varargs(fsig) is None:
589 must_pass_as_kwargs.append(p.name)
590 else:
591 invocation_parts.append(p.name)
592 if get_varargs(signature) is not None:
593 invocation_parts.append("*" + get_varargs(signature).name)
594 for k in must_pass_as_kwargs:
595 invocation_parts.append(f"{k}={k}")
596 for p in signature.parameters.values():
597 if p.kind is p.KEYWORD_ONLY:
598 invocation_parts.append(f"{p.name}={p.name}")
599 varkw = get_varargs(signature, kind=Parameter.VAR_KEYWORD)
600 if varkw:
601 invocation_parts.append("**" + varkw.name)
602
603 candidate_names = ["f"] + [f"f_{i}" for i in range(1, len(used_names) + 2)]
604
605 for funcname in candidate_names: # pragma: no branch
606 if funcname not in used_names:
607 break
608
609 source = COPY_SIGNATURE_SCRIPT.format(
610 name=name,
611 funcname=funcname,
612 signature=str(newsig),
613 invocation=", ".join(invocation_parts),
614 )
615 result = source_exec_as_module(source).accept(f)
616 result.__doc__ = docstring
617 result.__defaults__ = tuple(
618 p.default
619 for p in signature.parameters.values()
620 if p.default is not signature.empty and "POSITIONAL" in p.kind.name
621 )
622 kwdefaults = {
623 p.name: p.default
624 for p in signature.parameters.values()
625 if p.default is not signature.empty and p.kind is p.KEYWORD_ONLY
626 }
627 if kwdefaults:
628 result.__kwdefaults__ = kwdefaults
629 annotations = {
630 p.name: p.annotation
631 for p in signature.parameters.values()
632 if p.annotation is not signature.empty
633 }
634 if signature.return_annotation is not signature.empty:
635 annotations["return"] = signature.return_annotation
636 if annotations:
637 result.__annotations__ = annotations
638 return result
639
640 return accept
641
642
643def impersonate(target):
644 """Decorator to update the attributes of a function so that to external
645 introspectors it will appear to be the target function.
646
647 Note that this updates the function in place, it doesn't return a
648 new one.
649 """
650
651 def accept(f):
652 # Lie shamelessly about where this code comes from, to hide the hypothesis
653 # internals from pytest, ipython, and other runtime introspection.
654 f.__code__ = f.__code__.replace(
655 co_filename=target.__code__.co_filename,
656 co_firstlineno=target.__code__.co_firstlineno,
657 )
658 f.__name__ = target.__name__
659 f.__module__ = target.__module__
660 f.__doc__ = target.__doc__
661 f.__globals__["__hypothesistracebackhide__"] = True
662 return f
663
664 return accept
665
666
667def proxies(target: T) -> Callable[[Callable], T]:
668 replace_sig = define_function_signature(
669 target.__name__.replace("<lambda>", "_lambda_"), # type: ignore
670 target.__doc__,
671 get_signature(target, follow_wrapped=False),
672 )
673
674 def accept(proxy):
675 return impersonate(target)(wraps(target)(replace_sig(proxy)))
676
677 return accept
678
679
680def is_identity_function(f: object) -> bool:
681 # TODO: pattern-match the AST to handle `def ...` identity functions too
682 return bool(re.fullmatch(r"lambda (\w+): \1", get_pretty_function_description(f)))