1class Callback:
2    """
3    Base class and interface for callback mechanism
4
5    This class can be used directly for monitoring file transfers by
6    providing ``callback=Callback(hooks=...)`` (see the ``hooks`` argument,
7    below), or subclassed for more specialised behaviour.
8
9    Parameters
10    ----------
11    size: int (optional)
12        Nominal quantity for the value that corresponds to a complete
13        transfer, e.g., total number of tiles or total number of
14        bytes
15    value: int (0)
16        Starting internal counter value
17    hooks: dict or None
18        A dict of named functions to be called on each update. The signature
19        of these must be ``f(size, value, **kwargs)``
20    """
21
22    def __init__(self, size=None, value=0, hooks=None, **kwargs):
23        self.size = size
24        self.value = value
25        self.hooks = hooks or {}
26        self.kw = kwargs
27
28    def set_size(self, size):
29        """
30        Set the internal maximum size attribute
31
32        Usually called if not initially set at instantiation. Note that this
33        triggers a ``call()``.
34
35        Parameters
36        ----------
37        size: int
38        """
39        self.size = size
40        self.call()
41
42    def absolute_update(self, value):
43        """
44        Set the internal value state
45
46        Triggers ``call()``
47
48        Parameters
49        ----------
50        value: int
51        """
52        self.value = value
53        self.call()
54
55    def relative_update(self, inc=1):
56        """
57        Delta increment the internal cuonter
58
59        Triggers ``call()``
60
61        Parameters
62        ----------
63        inc: int
64        """
65        self.value += inc
66        self.call()
67
68    def call(self, hook_name=None, **kwargs):
69        """
70        Execute hook(s) with current state
71
72        Each function is passed the internal size and current value
73
74        Parameters
75        ----------
76        hook_name: str or None
77            If given, execute on this hook
78        kwargs: passed on to (all) hook(s)
79        """
80        if not self.hooks:
81            return
82        kw = self.kw.copy()
83        kw.update(kwargs)
84        if hook_name:
85            if hook_name not in self.hooks:
86                return
87            return self.hooks[hook_name](self.size, self.value, **kw)
88        for hook in self.hooks.values() or []:
89            hook(self.size, self.value, **kw)
90
91    def wrap(self, iterable):
92        """
93        Wrap an iterable to call ``relative_update`` on each iterations
94
95        Parameters
96        ----------
97        iterable: Iterable
98            The iterable that is being wrapped
99        """
100        for item in iterable:
101            self.relative_update()
102            yield item
103
104    def branch(self, path_1, path_2, kwargs):
105        """
106        Set callbacks for child transfers
107
108        If this callback is operating at a higher level, e.g., put, which may
109        trigger transfers that can also be monitored. The passed kwargs are
110        to be *mutated* to add ``callback=``, if this class supports branching
111        to children.
112
113        Parameters
114        ----------
115        path_1: str
116            Child's source path
117        path_2: str
118            Child's destination path
119        kwargs: dict
120            arguments passed to child method, e.g., put_file.
121
122        Returns
123        -------
124
125        """
126        return None
127
128    def no_op(self, *_, **__):
129        pass
130
131    def __getattr__(self, item):
132        """
133        If undefined methods are called on this class, nothing happens
134        """
135        return self.no_op
136
137    @classmethod
138    def as_callback(cls, maybe_callback=None):
139        """Transform callback=... into Callback instance
140
141        For the special value of ``None``, return the global instance of
142        ``NoOpCallback``. This is an alternative to including
143        ``callback=_DEFAULT_CALLBACK`` directly in a method signature.
144        """
145        if maybe_callback is None:
146            return _DEFAULT_CALLBACK
147        return maybe_callback
148
149
150class NoOpCallback(Callback):
151    """
152    This implementation of Callback does exactly nothing
153    """
154
155    def call(self, *args, **kwargs):
156        return None
157
158
159class DotPrinterCallback(Callback):
160    """
161    Simple example Callback implementation
162
163    Almost identical to Callback with a hook that prints a char; here we
164    demonstrate how the outer layer may print "#" and the inner layer "."
165    """
166
167    def __init__(self, chr_to_print="#", **kwargs):
168        self.chr = chr_to_print
169        super().__init__(**kwargs)
170
171    def branch(self, path_1, path_2, kwargs):
172        """Mutate kwargs to add new instance with different print char"""
173        kwargs["callback"] = DotPrinterCallback(".")
174
175    def call(self, **kwargs):
176        """Just outputs a character"""
177        print(self.chr, end="")
178
179
180_DEFAULT_CALLBACK = NoOpCallback()
181