File: helpers.py

package info (click to toggle)
python-b2sdk 2.10.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,228 kB
  • sloc: python: 32,094; sh: 13; makefile: 8
file content (78 lines) | stat: -rw-r--r-- 2,495 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
######################################################################
#
# File: test/helpers.py
#
# Copyright 2023 Backblaze Inc. All Rights Reserved.
#
# License https://www.backblaze.com/using_b2_code.html
#
######################################################################
from __future__ import annotations

import contextlib
import inspect
import io
from unittest.mock import patch

from b2sdk._internal.types import pydantic


@contextlib.contextmanager
def patch_bind_params(instance, method_name):
    """
    Patch a method of instance.

    In addition to `patch.object(instance, method_name)` would provide, it also adds get_bound_call_args method
    on the returned mock.
    This allows to get the arguments that were passed to the method, after binding.

    :param instance: instance to patch
    :param method_name: name of the method of instance to patch
    :return: patched method mock
    """
    signature = inspect.signature(getattr(instance, method_name))
    with patch.object(instance, method_name, autospec=True) as mock_method:
        mock_method.get_bound_call_args = lambda: signature.bind(
            *mock_method.call_args[0], **mock_method.call_args[1]
        ).arguments
        yield mock_method


class NonSeekableIO(io.BytesIO):
    """Emulate a non-seekable file"""

    def seek(self, *args, **kwargs):
        raise OSError('not seekable')

    def seekable(self):
        return False


def type_validator_factory(type_):
    """
    Equivalent of `TypeAdapter(type_).validate_python` and noop under Python <3.8.

    To be removed when we drop support for Python <3.8.
    """
    if pydantic:
        return pydantic.TypeAdapter(type_).validate_python
    return lambda *args, **kwargs: None


def deep_cast_dict(actual, expected):
    """
    For composite objects `actual` and `expected`, return a copy of `actual` (with all dicts and lists deeply copied)
    with all keys of dicts not appearing in `expected` (comparing dicts on any level) removed. Useful for assertions
    in tests ignoring extra keys.
    """
    if isinstance(expected, dict) and isinstance(actual, dict):
        return {k: deep_cast_dict(actual[k], expected[k]) for k in expected if k in actual}

    elif isinstance(expected, list) and isinstance(actual, list):
        return [deep_cast_dict(a, e) for a, e in zip(actual, expected)]

    return actual


def assert_dict_equal_ignore_extra(actual, expected):
    assert deep_cast_dict(actual, expected) == expected