• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pyta-uoft / pyta / 4050015085

pending completion
4050015085

Pull #875

github

GitHub
<a href="https://github.com/pyta-uoft/pyta/commit/<a class=hub.com/pyta-uoft/pyta/commit/<a class="double-link" href="https://git"><a class=hub.com/pyta-uoft/pyta/commit/<a class="double-link" href="https://git"><a class=hub.com/pyta-uoft/pyta/commit/<a class="double-link" href="https://git"><a class=hub.com/pyta-uoft/pyta/commit/<a class="double-link" href="https://git"><a class=hub.com/pyta-uoft/pyta/commit/8ae4a0eeee3ae4d9f51a119ce9f41cc59f585beb">8ae4a0eee">&lt;a href=&quot;https://github.com/pyta-uoft/pyta/commit/</a><a class="double-link" href="https://github.com/pyta-uoft/pyta/commit/&lt;a class=&quot;double-link&quot; href=&quot;https://git">&lt;a class=</a>hub.com/pyta-uoft/pyta/commit/&lt;a class=&quot;double-link&quot; href=&quot;https://git">&lt;a class=</a>hub.com/pyta-uoft/pyta/commit/&lt;a class=&quot;double-link&quot; href=&quot;https://git">&lt;a class=</a>hub.com/pyta-uoft/pyta/commit/&lt;a class=&quot;double-link&quot; href=&quot;https://git">&lt;a class=</a>hub.com/pyta-uoft/pyta/commit/&lt;a class=&quot;double-link&quot; href=&quot;https://git">&lt;a class=</a>hub.com/pyta-uoft/pyta/commit/8ae4a0eeee3ae4d9f51a119ce9f41cc59f585beb">8ae4a0eee</a><a href="https://github.com/pyta-uoft/pyta/commit/8ae4a0eeee3ae4d9f51a119ce9f41cc59f585beb">&quot;&gt;&amp;lt;a href=&amp;quot;https://github.com/pyta-uoft/pyta/commit/&lt;/a&gt;&lt;a class=&quot;double-link&quot; href=&quot;https://github.com/pyta-uoft/pyta/commit/&amp;lt;a class=&amp;quot;double-link&amp;quot; href=&amp;quot;https://git&quot;&gt;&amp;lt;a class=&lt;/a&gt;hub.com/pyta-uoft/pyta/commit/&amp;lt;a class=&amp;quot;double-link&amp;quot; href=&amp;quot;https://git&quot;&gt;&amp;lt;a class=&lt;/a&gt;hub.com/pyta-uoft/pyta/commit/&amp;lt;a class=&amp;quot;double-link&amp;quot; href=&amp;quot;https://git&quot;&gt;&amp;lt;a class=&lt;/a&gt;hub.com/pyta-uoft/pyta/commit/8ae4a0eeee3ae4d9f51a119ce9f41cc59f585beb&quot;&gt;8ae4a0eee&lt;/a&gt;&lt;a href=&quot;https://github.com/pyta-uoft/pyta/commit/8ae4a0eeee3ae4d9f51a119ce9f41cc59f585beb&quot;&gt;&amp;lt;a href=&amp;quot;https://github.com/pyta-uoft/pyta/commit/8ae4a0eeee3ae4d9f51a119ce9f41cc59f585beb&amp;quot;&amp;gt;&amp;amp;quot;&amp;amp;gt;&amp;amp;amp;lt;a href=&amp;amp;amp;quot;https://github.com/pyta-uoft/pyta/commit/&amp;amp;lt;/a&amp;amp;gt;&amp;amp;lt;a class=&amp;amp;quot;double-link&amp;amp;q... (continued)
Pull Request #875: contracts: Make ENABLE_CONTRACT_CHECKING=False skip initial decoration

5 of 5 new or added lines in 1 file covered. (100.0%)

2007 of 2306 relevant lines covered (87.03%)

3.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.04
/python_ta/contracts/__init__.py
1
"""This module provides the functionality for PythonTA contracts.
2

3
Representation invariants, preconditions, and postconditions are parsed, compiled, and stored.
4
Below are some notes on how they are stored.
5
    - Representation invariants are stored in a class attribute __representation_invariants__
6
    as a list [(assertion, compiled)].
7
    - Preconditions are stored in an attribute __preconditions__ of the function as a list
8
    [(assertion, compiled)].
9
    - Postconditions are stored in an attribute __postconditions__ of the function as a list
10
    [(assertion, compiled, return_val_var_name)].
11
"""
12
import inspect
4✔
13
import sys
4✔
14
import typing
4✔
15
from types import CodeType, FunctionType, ModuleType
4✔
16
from typing import Any, Callable, List, Optional, Set, Tuple
4✔
17

18
import wrapt
4✔
19
from typeguard import check_type
4✔
20

21
# Configuration options
22

23
ENABLE_CONTRACT_CHECKING = True
4✔
24
"""
2✔
25
Set to True to enable contract checking.
26
"""
27

28
DEBUG_CONTRACTS = False
4✔
29
"""
2✔
30
Set to True to display debugging messages when checking contracts.
31
"""
32

33
RENAME_MAIN_TO_PYDEV_UMD = True
4✔
34
"""
2✔
35
Set to False to disable workaround for PyCharm's "Run File in Python Console" action.
36
In most cases you should not need to change this!
37
"""
38

39
_PYDEV_UMD_NAME = "pydev_umd"
4✔
40

41

42
_DEFAULT_MAX_VALUE_LENGTH = 30
4✔
43
FUNCTION_RETURN_VALUE = "$return_value"
4✔
44

45

46
class PyTAContractError(Exception):
4✔
47
    """Error raised when a PyTA contract assertion is violated."""
48

49

50
def check_all_contracts(*mod_names: str, decorate_main: bool = True) -> None:
4✔
51
    """Automatically check contracts for all functions and classes in the given modules.
52

53
    By default (when called with no arguments), the current module is used.
54

55
    Args:
56
        *mod_names: The names of modules to check contracts for. These modules must have been
57
            previously imported.
58
        decorate_main: True if the module being run (where __name__ == '__main__') should
59
            have contracts checked.
60
    """
61
    if not ENABLE_CONTRACT_CHECKING:
4✔
62
        return
×
63

64
    modules = []
4✔
65
    if decorate_main:
4✔
66
        mod_names = mod_names + ("__main__",)
×
67

68
        # Also add _PYDEV_UMD_NAME, handling when the file is being run in PyCharm
69
        # with the "Run in Python Console" action.
70
        if RENAME_MAIN_TO_PYDEV_UMD:
×
71
            mod_names = mod_names + (_PYDEV_UMD_NAME,)
×
72

73
    for module_name in mod_names:
4✔
74
        modules.append(sys.modules.get(module_name, None))
4✔
75

76
    for module in modules:
4✔
77
        if not module:
4✔
78
            # Module name was passed in incorrectly.
79
            continue
×
80
        for name, value in inspect.getmembers(module):
4✔
81
            if inspect.isfunction(value) or inspect.isclass(value):
4✔
82
                module.__dict__[name] = check_contracts(value, module_names=set(mod_names))
4✔
83

84

85
@wrapt.decorator
4✔
86
def _enable_function_contracts(wrapped, instance, args, kwargs):
4✔
87
    """A decorator that enables checking contracts for a function."""
88
    try:
4✔
89
        if instance is not None and inspect.isclass(instance):
4✔
90
            # This is a class method, so there is no instance.
91
            return _check_function_contracts(wrapped, None, args, kwargs)
4✔
92
        else:
93
            return _check_function_contracts(wrapped, instance, args, kwargs)
4✔
94
    except PyTAContractError as e:
4✔
95
        raise AssertionError(str(e)) from None
4✔
96

97

98
def check_contracts(func_or_class: Any, module_names: Optional[Set[str]] = None) -> Callable:
4✔
99
    """A decorator to enable contract checking for a function or class.
100

101
    When used with a class, all methods defined within the class have contract checking enabled.
102
    If module_names is not None, only functions or classes defined in a module whose name is in module_names are checked.
103

104
    Example:
105

106
        >>> from python_ta.contracts import check_contracts
107
        >>> @check_contracts
108
        ... def divide(x: int, y: int) -> int:
109
        ...     \"\"\"Return x // y.
110
        ...
111
        ...     Preconditions:
112
        ...        - y != 0
113
        ...     \"\"\"
114
        ...     return x // y
115
    """
116
    if not ENABLE_CONTRACT_CHECKING:
4✔
117
        return func_or_class
4✔
118

119
    if module_names is not None and func_or_class.__module__ not in module_names:
4✔
120
        _debug(
4✔
121
            f"Warning: skipping contract check for {func_or_class.__name__} defined in {func_or_class.__module__} because module is not included as an argument."
122
        )
123
        return func_or_class
4✔
124
    elif inspect.isroutine(func_or_class):
4✔
125
        return _enable_function_contracts(func_or_class)
4✔
126
    elif inspect.isclass(func_or_class):
4✔
127
        add_class_invariants(func_or_class)
4✔
128
        return func_or_class
4✔
129
    else:
130
        # Default action
131
        return func_or_class
×
132

133

134
def add_class_invariants(klass: type) -> None:
4✔
135
    """Modify the given class to check representation invariants and method contracts."""
136
    if not ENABLE_CONTRACT_CHECKING or "__representation_invariants__" in klass.__dict__:
4✔
137
        # This means the class has already been decorated
138
        return
×
139

140
    # Update representation invariants from this class' docstring and those of its superclasses.
141
    rep_invariants: List[Tuple[str, CodeType]] = []
4✔
142

143
    # Iterate over all inherited classes except builtins
144
    for cls in reversed(klass.__mro__):
4✔
145
        if "__representation_invariants__" in cls.__dict__:
4✔
146
            rep_invariants.extend(cls.__representation_invariants__)
4✔
147
        elif cls.__module__ != "builtins":
4✔
148
            assertions = parse_assertions(cls, parse_token="Representation Invariant")
4✔
149
            # Try compiling assertions
150
            for assertion in assertions:
4✔
151
                try:
4✔
152
                    compiled = compile(assertion, "<string>", "eval")
4✔
153
                except:
×
154
                    _debug(
×
155
                        f"Warning: representation invariant {assertion} could not be parsed as a valid Python expression"
156
                    )
157
                    continue
×
158
                rep_invariants.append((assertion, compiled))
4✔
159

160
    setattr(klass, "__representation_invariants__", rep_invariants)
4✔
161

162
    def new_setattr(self: klass, name: str, value: Any) -> None:
4✔
163
        """Set the value of the given attribute on self to the given value.
164

165
        Check representation invariants for this class when not within an instance method of the class.
166
        """
167
        klass_mod = _get_module(klass)
4✔
168
        cls_annotations = typing.get_type_hints(klass, globalns=klass_mod.__dict__)
4✔
169

170
        if name in cls_annotations:
4✔
171
            try:
4✔
172
                _debug(f"Checking type of attribute {attr} for {klass.__qualname__} instance")
4✔
173
                check_type(name, value, cls_annotations[name])
4✔
174
            except TypeError:
4✔
175
                raise AssertionError(
4✔
176
                    f"Value {_display_value(value)} did not match type annotation for attribute "
177
                    f"{name}: {_display_annotation(cls_annotations[name])}"
178
                ) from None
179

180
        super(klass, self).__setattr__(name, value)
4✔
181
        curframe = inspect.currentframe()
4✔
182
        callframe = inspect.getouterframes(curframe, 2)
4✔
183
        frame_locals = callframe[1].frame.f_locals
4✔
184
        if self is not frame_locals.get("self"):
4✔
185
            # Only validating if the attribute is not being set in a instance/class method
186
            klass_mod = _get_module(klass)
4✔
187
            if klass_mod is not None and ENABLE_CONTRACT_CHECKING:
4✔
188
                try:
4✔
189
                    _check_invariants(self, klass, klass_mod.__dict__)
4✔
190
                except PyTAContractError as e:
4✔
191
                    raise AssertionError(str(e)) from None
4✔
192

193
    for attr, value in klass.__dict__.items():
4✔
194
        if inspect.isroutine(value):
4✔
195
            if isinstance(value, (staticmethod, classmethod)):
4✔
196
                # Don't check rep invariants for staticmethod and classmethod
197
                setattr(klass, attr, check_contracts(value))
4✔
198
            else:
199
                setattr(klass, attr, _instance_method_wrapper(value, klass))
4✔
200

201
    klass.__setattr__ = new_setattr
4✔
202

203

204
def _check_function_contracts(wrapped, instance, args, kwargs):
4✔
205
    params = wrapped.__code__.co_varnames[: wrapped.__code__.co_argcount]
4✔
206
    if instance is not None:
4✔
207
        klass_mod = _get_module(type(instance))
4✔
208
        annotations = typing.get_type_hints(wrapped, globalns=klass_mod.__dict__)
4✔
209
    else:
210
        annotations = typing.get_type_hints(wrapped)
4✔
211
    args_with_self = args if instance is None else (instance,) + args
4✔
212

213
    # Check function parameter types
214
    for arg, param in zip(args_with_self, params):
4✔
215
        if param in annotations:
4✔
216
            try:
4✔
217
                _debug(f"Checking type of parameter {param} in call to {wrapped.__qualname__}")
4✔
218
                check_type_strict(param, arg, annotations[param])
4✔
219
            except TypeError:
4✔
220
                additional_suggestions = _get_argument_suggestions(arg, annotations[param])
4✔
221

222
                raise PyTAContractError(
4✔
223
                    f"{wrapped.__name__} argument {_display_value(arg)} did not match type "
224
                    f"annotation for parameter {param}: {_display_annotation(annotations[param])}"
225
                    + (f"\n{additional_suggestions}" if additional_suggestions else "")
226
                )
227

228
    function_locals = dict(zip(params, args_with_self))
4✔
229

230
    # Check bounded function
231
    if hasattr(wrapped, "__self__"):
4✔
232
        target = wrapped.__func__
4✔
233
    else:
234
        target = wrapped
4✔
235

236
    # Check function preconditions
237
    if not hasattr(target, "__preconditions__"):
4✔
238
        target.__preconditions__: List[Tuple[str, CodeType]] = []
4✔
239
        preconditions = parse_assertions(wrapped)
4✔
240
        for precondition in preconditions:
4✔
241
            try:
4✔
242
                compiled = compile(precondition, "<string>", "eval")
4✔
243
            except:
×
244
                _debug(
×
245
                    f"Warning: precondition {precondition} could not be parsed as a valid Python expression"
246
                )
247
                continue
×
248
            target.__preconditions__.append((precondition, compiled))
4✔
249

250
    if ENABLE_CONTRACT_CHECKING:
4✔
251
        _check_assertions(wrapped, function_locals)
4✔
252

253
    # Check return type
254
    r = wrapped(*args, **kwargs)
4✔
255
    if "return" in annotations:
4✔
256
        return_type = annotations["return"]
4✔
257
        try:
4✔
258
            _debug(f"Checking return type from call to {wrapped.__qualname__}")
4✔
259
            check_type_strict("return", r, return_type)
4✔
260
        except TypeError:
4✔
261
            raise PyTAContractError(
4✔
262
                f"{wrapped.__name__}'s return value {_display_value(r)} did not match "
263
                f"return type annotation {_display_annotation(return_type)}"
264
            )
265

266
    # Check function postconditions
267
    if not hasattr(target, "__postconditions__"):
4✔
268
        target.__postconditions__: List[Tuple[str, CodeType, str]] = []
4✔
269
        return_val_var_name = _get_legal_return_val_var_name(
4✔
270
            {**wrapped.__globals__, **function_locals}
271
        )
272
        postconditions = parse_assertions(wrapped, parse_token="Postcondition")
4✔
273
        for postcondition in postconditions:
4✔
274
            assertion = _replace_return_val_assertion(postcondition, return_val_var_name)
4✔
275
            try:
4✔
276
                compiled = compile(assertion, "<string>", "eval")
4✔
277
            except:
×
278
                _debug(
×
279
                    f"Warning: postcondition {postcondition} could not be parsed as a valid Python expression"
280
                )
281
                continue
×
282
            target.__postconditions__.append((postcondition, compiled, return_val_var_name))
4✔
283

284
    if ENABLE_CONTRACT_CHECKING:
4✔
285
        _check_assertions(
4✔
286
            wrapped,
287
            function_locals,
288
            function_return_val=r,
289
            condition_type="postcondition",
290
        )
291

292
    return r
4✔
293

294

295
def check_type_strict(argname: str, value: Any, expected_type: type) -> None:
4✔
296
    """Ensure that ``value`` matches ``expected_type``.
297

298
    Differentiates between:
299
        - float vs. int
300
        - bool vs. int
301
    """
302
    if ENABLE_CONTRACT_CHECKING:
4✔
303
        if (type(value) is int and expected_type is float) or (
4✔
304
            type(value) is bool and expected_type is int
305
        ):
306
            raise TypeError(f"type of {argname} must be {expected_type}; got {value} instead")
4✔
307
        check_type(argname, value, expected_type)
4✔
308

309

310
def _get_argument_suggestions(arg: Any, annotation: type) -> str:
4✔
311
    """Returns potential suggestions for the given arg and its annotation"""
312
    try:
4✔
313
        if isinstance(arg, type) and issubclass(arg, annotation):
4✔
314
            return "Did you mean {cls}(...) instead of {cls}?".format(cls=arg.__name__)
4✔
315
    except TypeError:
4✔
316
        pass
4✔
317

318
    return ""
4✔
319

320

321
def _instance_method_wrapper(wrapped: Callable, klass: type) -> Callable:
4✔
322
    @wrapt.decorator
4✔
323
    def wrapper(wrapped, instance, args, kwargs):
4✔
324
        try:
4✔
325
            r = _check_function_contracts(wrapped, instance, args, kwargs)
4✔
326
            if _instance_init_in_callstack(instance):
4✔
327
                return r
4✔
328
            _check_class_type_annotations(klass, instance)
4✔
329
            klass_mod = _get_module(klass)
4✔
330
            if klass_mod is not None and ENABLE_CONTRACT_CHECKING:
4✔
331
                _check_invariants(instance, klass, klass_mod.__dict__)
4✔
332
        except PyTAContractError as e:
4✔
333
            raise AssertionError(str(e)) from None
4✔
334
        else:
335
            return r
4✔
336

337
    return wrapper(wrapped)
4✔
338

339

340
def _instance_init_in_callstack(instance: Any) -> bool:
4✔
341
    """Return whether instance's init is part of the current callstack
342

343
    Note: due to the nature of the check, externally defined __init__ functions with
344
    'self' defined as the first parameter may pass this check.
345
    """
346
    frame = inspect.currentframe().f_back
4✔
347
    while frame:
4✔
348
        frame_context_name = inspect.getframeinfo(frame).function
4✔
349
        frame_context_self = frame.f_locals.get("self")
4✔
350
        frame_context_vars = frame.f_code.co_varnames
4✔
351
        if (
4✔
352
            frame_context_name == "__init__"
353
            and frame_context_self is instance
354
            and frame_context_vars[0] == "self"
355
        ):
356
            return True
4✔
357
        frame = frame.f_back
4✔
358
    return False
4✔
359

360

361
def _check_class_type_annotations(klass: type, instance: Any) -> None:
4✔
362
    """Check that the type annotations for the class still hold.
363

364
    Precondition:
365
        - isinstance(instance, klass)
366
    """
367
    klass_mod = _get_module(klass)
4✔
368
    cls_annotations = typing.get_type_hints(klass, globalns=klass_mod.__dict__)
4✔
369

370
    for attr, annotation in cls_annotations.items():
4✔
371
        value = getattr(instance, attr)
4✔
372
        try:
4✔
373
            _debug(f"Checking type of attribute {attr} for {klass.__qualname__} instance")
4✔
374
            check_type(attr, value, annotation)
4✔
375
        except TypeError:
4✔
376
            raise AssertionError(
4✔
377
                f"{_display_value(value)} did not match type annotation for attribute {attr}: "
378
                f"{_display_annotation(annotation)}"
379
            )
380

381

382
def _check_invariants(instance, klass: type, global_scope: dict) -> None:
4✔
383
    """Check that the representation invariants for the instance are satisfied."""
384
    rep_invariants = getattr(klass, "__representation_invariants__", set())
4✔
385

386
    for invariant, compiled in rep_invariants:
4✔
387
        try:
4✔
388
            _debug(
4✔
389
                "Checking representation invariant for "
390
                f"{instance.__class__.__qualname__}: {invariant}"
391
            )
392
            check = eval(compiled, {**global_scope, "self": instance})
4✔
393
        except:
×
394
            _debug(f"Warning: could not evaluate representation invariant: {invariant}")
×
395
        else:
396
            if not check:
4✔
397
                curr_attributes = ", ".join(
4✔
398
                    f"{k}: {_display_value(v)}" for k, v in vars(instance).items()
399
                )
400

401
                curr_attributes = "{" + curr_attributes + "}"
4✔
402

403
                raise PyTAContractError(
4✔
404
                    f'"{instance.__class__.__name__}" representation invariant "{invariant}" was violated for'
405
                    f" instance attributes {curr_attributes}"
406
                )
407

408

409
def _get_legal_return_val_var_name(var_dict: dict) -> str:
4✔
410
    """
411
    Add '_' to the end of __function_return_value__ until a variable name that has not been used for any other
412
    variable in the function's scope is created. This is used to refer to the function's return value when evaluating
413
    postconditions.
414
    """
415
    legal_var_name = "__function_return_value__"
4✔
416

417
    while legal_var_name in var_dict:
4✔
418
        legal_var_name += "_"
×
419

420
    return legal_var_name
4✔
421

422

423
def _replace_return_val_assertion(assertion: str, return_val_var_name: Optional[str]) -> str:
4✔
424
    """
425
    Replace FUNCTION_RETURN_VALUE in the assertion with the legal python variable name generated and return the new
426
    assertion. If FUNCTION_RETURN_VALUE does not appear in assertion, then simply return the original assertion.
427

428
    Precondition: If FUNCTION_RETURN_VALUE is in assertion, then return_val_var_name is not None
429
    """
430

431
    if FUNCTION_RETURN_VALUE in assertion:
4✔
432
        return assertion.replace(FUNCTION_RETURN_VALUE, return_val_var_name)
4✔
433
    return assertion
4✔
434

435

436
def _check_assertions(
4✔
437
    wrapped: Callable[..., Any],
438
    function_locals: dict,
439
    condition_type: str = "precondition",
440
    function_return_val: Any = None,
441
) -> None:
442
    """Check that the given assertions are still satisfied."""
443
    # Check bounded function
444
    if hasattr(wrapped, "__self__"):
4✔
445
        target = wrapped.__func__
4✔
446
    else:
447
        target = wrapped
4✔
448
    assertions = []
4✔
449
    if condition_type == "precondition":
4✔
450
        assertions = target.__preconditions__
4✔
451
    elif condition_type == "postcondition":
4✔
452
        assertions = target.__postconditions__
4✔
453
    for assertion_str, compiled, *return_val_var_name in assertions:
4✔
454
        return_val_dict = {}
4✔
455
        if condition_type == "postcondition":
4✔
456
            return_val_dict = {return_val_var_name[0]: function_return_val}
4✔
457
        try:
4✔
458
            _debug(f"Checking {condition_type} for {wrapped.__qualname__}: {assertion_str}")
4✔
459
            check = eval(compiled, {**wrapped.__globals__, **function_locals, **return_val_dict})
4✔
460
        except:
×
461
            _debug(f"Warning: could not evaluate {condition_type}: {assertion_str}")
×
462
        else:
463
            if not check:
4✔
464
                arg_string = ", ".join(
4✔
465
                    f"{k}: {_display_value(v)}" for k, v in function_locals.items()
466
                )
467
                arg_string = "{" + arg_string + "}"
4✔
468

469
                return_val_string = ""
4✔
470

471
                if condition_type == "postcondition":
4✔
472
                    return_val_string = f"and return value {function_return_val}"
4✔
473
                raise PyTAContractError(
4✔
474
                    f'{wrapped.__name__} {condition_type} "{assertion_str}" was '
475
                    f"violated for arguments {arg_string} {return_val_string}"
476
                )
477

478

479
def parse_assertions(obj: Any, parse_token: str = "Precondition") -> List[str]:
4✔
480
    """Return a list of preconditions/postconditions/representation invariants parsed from the given entity's docstring.
481

482
    Uses parse_token to determine what to look for. parse_token defaults to Precondition.
483

484
    Currently only supports two forms:
485

486
    1. A single line of the form "<parse_token>: <cond>"
487
    2. A group of lines starting with "<parse_token>s:", where each subsequent
488
       line is of the form "- <cond>". Each line is considered a separate condition.
489
       The lines can be separated by blank lines, but no other text.
490
    """
491
    if hasattr(obj, "doc_node"):
4✔
492
        # Check if obj is an astroid node
493
        docstring = obj.doc_node.value
4✔
494
    else:
495
        docstring = getattr(obj, "__doc__") or ""
4✔
496
    lines = [line.strip() for line in docstring.split("\n")]
4✔
497
    assertion_lines = [
4✔
498
        i for i, line in enumerate(lines) if line.lower().startswith(parse_token.lower())
499
    ]
500

501
    if assertion_lines == []:
4✔
502
        return []
4✔
503

504
    first = assertion_lines[0]
4✔
505

506
    if lines[first].startswith(parse_token + ":"):
4✔
507
        return [lines[first][len(parse_token + ":") :].strip()]
4✔
508
    elif lines[first].startswith(parse_token + "s:"):
4✔
509
        assertions = []
4✔
510
        for line in lines[first + 1 :]:
4✔
511
            if line.startswith("-"):
4✔
512
                assertion = line[1:].strip()
4✔
513
                if hasattr(obj, "__qualname__"):
4✔
514
                    _debug(f"Adding assertion to {obj.__qualname__}: {assertion}")
4✔
515
                assertions.append(assertion)
4✔
516
            elif line != "":
4✔
517
                break
×
518
        return assertions
4✔
519
    else:
520
        return []
×
521

522

523
def _display_value(value: Any, max_length: int = _DEFAULT_MAX_VALUE_LENGTH) -> str:
4✔
524
    """Return a human-friendly representation of the given value.
525

526
    If DEBUG_CONTRACTS is False, truncate long strings to max_length characters.
527

528
    Preconditions:
529
        - max_length >= 5
530
    """
531
    s = repr(value)
4✔
532
    if not DEBUG_CONTRACTS and len(s) > max_length:
4✔
533
        i = (max_length - 3) // 2
4✔
534
        return s[:i] + "..." + s[-i:]
4✔
535
    else:
536
        return s
4✔
537

538

539
def _display_annotation(annotation: Any) -> str:
4✔
540
    """Return a human-friendly representation of the given type annotation.
541

542
    >>> _display_annotation(int)
543
    'int'
544
    >>> _display_annotation(list[int])
545
    'list[int]'
546
    >>> from typing import List
547
    >>> _display_annotation(List[int])
548
    'typing.List[int]'
549
    """
550
    if annotation is type(None):  # Use 'None' instead of 'NoneType'
4✔
551
        return "None"
×
552
    if hasattr(annotation, "__origin__"):  # Generic type annotations
4✔
553
        return repr(annotation)
4✔
554
    elif hasattr(annotation, "__name__"):
4✔
555
        return annotation.__name__
4✔
556
    else:
557
        return repr(annotation)
×
558

559

560
def _get_module(obj: Any) -> ModuleType:
4✔
561
    """Return the module where obj was defined (normally obj.__module__).
562

563
    NOTE: this function defines a special case when using PyCharm and the file
564
    defining the object is "Run in Python Console". In this case, the pydevd runner
565
    renames the '__main__' module to 'pydev_umd', and so we need to access that
566
    module instead. This behaviour can be disabled by setting RENAME_MAIN_TO_PYDEV_UMD
567
    to False.
568
    """
569
    module_name = obj.__module__
4✔
570
    module = sys.modules[module_name]
4✔
571

572
    if (
4✔
573
        module_name != "__main__"
574
        or not RENAME_MAIN_TO_PYDEV_UMD
575
        or _PYDEV_UMD_NAME not in sys.modules
576
    ):
577
        return module
4✔
578

579
    # Get a function/class name to check whether it is defined in the module
580
    if isinstance(obj, (FunctionType, type)):
×
581
        name = obj.__name__
×
582
    else:
583
        # For any other type of object, be conservative and just return the module
584
        return module
×
585

586
    if name in vars(module):
×
587
        return module
×
588
    else:
589
        return sys.modules[_PYDEV_UMD_NAME]
×
590

591

592
def _debug(msg: str) -> None:
4✔
593
    """Display a debugging message.
594

595
    Do nothing if DEBUG_CONTRACTS is False.
596
    """
597
    if not DEBUG_CONTRACTS:
4✔
598
        return
4✔
599

600
    print("[PyTA]", msg, file=sys.stderr)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc