Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/invoke/watchers.py: 35%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import re
2import threading
3from typing import Generator, Iterable
5from .exceptions import ResponseNotAccepted
8class StreamWatcher(threading.local):
9 """
10 A class whose subclasses may act on seen stream data from subprocesses.
12 Subclasses must exhibit the following API; see `Responder` for a concrete
13 example.
15 * ``__init__`` is completely up to each subclass, though as usual,
16 subclasses *of* subclasses should be careful to make use of `super` where
17 appropriate.
18 * `submit` must accept the entire current contents of the stream being
19 watched, as a string, and may optionally return an iterable of strings
20 (or act as a generator iterator, i.e. multiple calls to ``yield
21 <string>``), which will each be written to the subprocess' standard
22 input.
24 .. note::
25 `StreamWatcher` subclasses exist in part to enable state tracking, such
26 as detecting when a submitted password didn't work & erroring (or
27 prompting a user, or etc). Such bookkeeping isn't easily achievable
28 with simple callback functions.
30 .. note::
31 `StreamWatcher` subclasses `threading.local` so that its instances can
32 be used to 'watch' both subprocess stdout and stderr in separate
33 threads.
35 .. versionadded:: 1.0
36 """
38 def submit(self, stream: str) -> Iterable[str]:
39 """
40 Act on ``stream`` data, potentially returning responses.
42 :param str stream:
43 All data read on this stream since the beginning of the session.
45 :returns:
46 An iterable of ``str`` (which may be empty).
48 .. versionadded:: 1.0
49 """
50 raise NotImplementedError
53class Responder(StreamWatcher):
54 """
55 A parameterizable object that submits responses to specific patterns.
57 Commonly used to implement password auto-responds for things like ``sudo``.
59 .. versionadded:: 1.0
60 """
62 def __init__(self, pattern: str, response: str) -> None:
63 r"""
64 Imprint this `Responder` with necessary parameters.
66 :param pattern:
67 A raw string (e.g. ``r"\[sudo\] password for .*:"``) which will be
68 turned into a regular expression.
70 :param response:
71 The string to submit to the subprocess' stdin when ``pattern`` is
72 detected.
73 """
74 # TODO: precompile the keys into regex objects
75 self.pattern = pattern
76 self.response = response
77 self.index = 0
79 def pattern_matches(
80 self, stream: str, pattern: str, index_attr: str
81 ) -> Iterable[str]:
82 """
83 Generic "search for pattern in stream, using index" behavior.
85 Used here and in some subclasses that want to track multiple patterns
86 concurrently.
88 :param str stream: The same data passed to ``submit``.
89 :param str pattern: The pattern to search for.
90 :param str index_attr: The name of the index attribute to use.
91 :returns: An iterable of string matches.
93 .. versionadded:: 1.0
94 """
95 # NOTE: generifies scanning so it can be used to scan for >1 pattern at
96 # once, e.g. in FailingResponder.
97 # Only look at stream contents we haven't seen yet, to avoid dupes.
98 index = getattr(self, index_attr)
99 new = stream[index:]
100 # Search, across lines if necessary
101 matches = re.findall(pattern, new, re.S)
102 # Update seek index if we've matched
103 if matches:
104 setattr(self, index_attr, index + len(new))
105 return matches
107 def submit(self, stream: str) -> Generator[str, None, None]:
108 # Iterate over findall() response in case >1 match occurred.
109 for _ in self.pattern_matches(stream, self.pattern, "index"):
110 yield self.response
113class FailingResponder(Responder):
114 """
115 Variant of `Responder` which is capable of detecting incorrect responses.
117 This class adds a ``sentinel`` parameter to ``__init__``, and its
118 ``submit`` will raise `.ResponseNotAccepted` if it detects that sentinel
119 value in the stream.
121 .. versionadded:: 1.0
122 """
124 def __init__(self, pattern: str, response: str, sentinel: str) -> None:
125 super().__init__(pattern, response)
126 self.sentinel = sentinel
127 self.failure_index = 0
128 self.tried = False
130 def submit(self, stream: str) -> Generator[str, None, None]:
131 # Behave like regular Responder initially
132 response = super().submit(stream)
133 # Also check stream for our failure sentinel
134 failed = self.pattern_matches(stream, self.sentinel, "failure_index")
135 # Error out if we seem to have failed after a previous response.
136 if self.tried and failed:
137 err = 'Auto-response to r"{}" failed with {!r}!'.format(
138 self.pattern, self.sentinel
139 )
140 raise ResponseNotAccepted(err)
141 # Once we see that we had a response, take note
142 if response:
143 self.tried = True
144 # Again, behave regularly by default.
145 return response