1 # -*- coding: utf-8 -*-
2 # This Source Code Form is subject to the terms of the Mozilla Public
3 # License, v. 2.0. If a copy of the MPL was not distributed with this
4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 # Copyright (c) 2015 Digi International Inc. All Rights Reserved.
8 # Downloaded from https://github.com/digidotcom/python-streamexpect
22 class SequenceMatch(object):
23 """Information about a match that has a concept of ordering."""
25 def __init__(self
, searcher
, match
, start
, end
):
27 :param Searcher searcher: The :class:`Searcher` that found the match
28 :param match: Portion of sequence that triggered the match
29 :param int start: Index of start of match
30 :param int end: Index of item directly after match
32 self
.searcher
= searcher
34 self
.start
= int(start
)
38 return '{}({!r}, match={!r}, start={}, end={})'.format(
39 self
.__class
__.__name
__, self
.searcher
, self
.match
, self
.start
,
43 class RegexMatch(SequenceMatch
):
44 """Information about a match from a regex."""
46 def __init__(self
, searcher
, match
, start
, end
, groups
):
48 :param Searcher searcher: The :class:`Searcher` that found the match
49 :param match: Portion of sequence that triggered the match
50 :param int start: Index of start of match
51 :param int end: Index of item directly after match
52 :param tuple groups: Contains the matched subgroups if the regex
53 contained groups, otherwise ``None``
55 super(RegexMatch
, self
).__init
__(searcher
, match
, start
, end
)
59 return '{}({!r}, match={!r}, start={}), end={}, groups={!r}'.format(
60 self
.__class
__.__name
__, self
.searcher
, self
.match
, self
.start
,
61 self
.end
, self
.groups
)
64 class ExpectTimeout(Exception):
65 """Exception raised when *expect* call exceeds a timeout."""
68 class Searcher(object):
69 """Base class for searching buffers.
71 Implements the base class for *Searcher* types, which are used by the
72 library to determine whether or not a particular buffer contains a *match*.
73 The type of the match is determined by the *Searcher* implementation: it
74 may be bytes, text, or something else entirely.
76 To conform to the *Searcher* interface, a class must implement one method
77 *search* and one read-only property *match_type*. The buffer passed to
78 the *search* method must match the type returned by the *match_type*
79 property, and *search* must raise a `TypeError` if it does not. The
80 member function :func:`_check_type` exists to provide this functionality
81 for subclass implementations.
85 return '{}()'.format(self
.__class
__.__name
__)
87 def search(self
, buf
):
88 """Search the provided buffer for a *match*.
90 Search the provided buffer for a *match*. What exactly a *match* means
91 is defined by the *Searcher* implementation. If the *match* is found,
92 returns an `SequenceMatch` object, otherwise returns ``None``.
94 :param buf: Buffer to search for a match.
96 raise NotImplementedError('search function must be provided')
100 """Read-only property that returns type matched by this *Searcher*"""
101 raise NotImplementedError('match_type must be provided')
103 def _check_type(self
, value
):
104 """Checks that *value* matches the type of this *Searcher*.
106 Checks that *value* matches the type of this *Searcher*, returning the
107 value if it does and raising a `TypeError` if it does not.
109 :return: *value* if type of *value* matches type of this *Searcher*.
110 :raises TypeError: if type of *value* does not match the type of this
113 if not isinstance(value
, self
.match_type
):
114 raise TypeError('Type ' + str(type(value
)) + ' does not match '
115 'expected type ' + str(self
.match_type
))
120 class BytesSearcher(Searcher
):
121 """Binary/ASCII searcher.
123 A binary/ASCII searcher. Matches when the pattern passed to the
124 constructor is found in the input buffer.
126 Note that this class only operates on binary types. That means that in
127 Python 3, it will fail on strings, as strings are Unicode by default. In
128 Python 2 this class will fail on the Unicode type, as strings are ASCII by
132 def __init__(self
, b
):
134 :param b: Bytes to search for. Must be a binary type (i.e. bytes)
136 self
._bytes
= self
._check
_type
(b
)
139 return '{}({!r})'.format(self
.__class
__.__name
__, self
._bytes
)
142 def match_type(self
):
143 return six
.binary_type
145 def search(self
, buf
):
146 """Search the provided buffer for matching bytes.
148 Search the provided buffer for matching bytes. If the *match* is found,
149 returns a :class:`SequenceMatch` object, otherwise returns ``None``.
151 :param buf: Buffer to search for a match.
152 :return: :class:`SequenceMatch` if matched, None if no match was found.
154 idx
= self
._check
_type
(buf
).find(self
._bytes
)
159 end
= idx
+ len(self
._bytes
)
160 return SequenceMatch(self
, buf
[start
:end
], start
, end
)
163 class TextSearcher(Searcher
):
164 """Plain text searcher.
166 A plain-text searcher. Matches when the text passed to the constructor is
167 found in the input buffer.
169 Note that this class operates only on text types (i.e. Unicode) and raises
170 a TypeError if used with binary data. Use the :class:`BytesSearcher` type
171 to search binary or ASCII text.
173 To make sure that modified (accented, grave, etc.) characters are matched
174 accurately, the input text is converted to the Unicode canonical composed
175 form before being used to match.
180 def __init__(self
, text
):
182 :param text: Text to search for. Must be a text type (i.e. Unicode)
184 super(TextSearcher
, self
).__init
__()
185 self
._check
_type
(text
)
186 self
._text
= unicodedata
.normalize(self
.FORM
, text
)
189 return '{}({!r})'.format(self
.__class
__.__name
__, self
._text
)
192 def match_type(self
):
195 def search(self
, buf
):
196 """Search the provided buffer for matching text.
198 Search the provided buffer for matching text. If the *match* is found,
199 returns a :class:`SequenceMatch` object, otherwise returns ``None``.
201 :param buf: Buffer to search for a match.
202 :return: :class:`SequenceMatch` if matched, None if no match was found.
204 self
._check
_type
(buf
)
205 normalized
= unicodedata
.normalize(self
.FORM
, buf
)
206 idx
= normalized
.find(self
._text
)
210 end
= idx
+ len(self
._text
)
211 return SequenceMatch(self
, normalized
[start
:end
], start
, end
)
214 class RegexSearcher(Searcher
):
215 """Regular expression searcher.
217 Searches for a match in the stream that matches the provided regular
220 This class follows the Python 3 model for dealing with binary versus text
221 patterns, raising a `TypeError` if mixed binary/text is used. This means
222 that a *RegexSearcher* that is instantiated with binary data will raise a
223 `TypeError` if used on text, and a *RegexSearcher* instantiated with text
224 will raise a `TypeError` on binary data.
227 def __init__(self
, pattern
, regex_options
=0):
229 :param pattern: The regex to search for, as a single compiled regex
230 or a string that will be processed as a regex.
231 :param regex_options: Options passed to the regex engine.
233 super(RegexSearcher
, self
).__init
__()
234 self
._regex
= re
.compile(pattern
, regex_options
)
237 return '{}(re.compile({!r}))'.format(self
.__class
__.__name
__,
241 def match_type(self
):
242 return type(self
._regex
.pattern
)
244 def search(self
, buf
):
245 """Search the provided buffer for a match to the object's regex.
247 Search the provided buffer for a match to the object's regex. If the
248 *match* is found, returns a :class:`RegexMatch` object, otherwise
251 :param buf: Buffer to search for a match.
252 :return: :class:`RegexMatch` if matched, None if no match was found.
254 match
= self
._regex
.search(self
._check
_type
(buf
))
255 if match
is not None:
256 start
= match
.start()
258 return RegexMatch(self
, buf
[start
:end
], start
, end
, match
.groups())
262 """Recursively flatten a mixed sequence of sub-sequences and items"""
263 if isinstance(n
, collections
.Sequence
):
265 for y
in _flatten(x
):
271 class SearcherCollection(Searcher
, list):
272 """Collect multiple `Searcher` objects into one.
274 Collect multiple `Searcher` instances into a single `Searcher` instance.
275 This is different than simply looping over a list of searchers, as this
276 class will always find the earliest match from any of its sub-searchers
277 (i.e. the match with the smallest index).
279 Note that this class requires that all of its sub-searchers have the same
283 def __init__(self
, *searchers
):
285 :param searchers: One or more :class:`Searcher` implementations.
287 super(SearcherCollection
, self
).__init
__()
288 self
.extend(_flatten(searchers
))
290 raise ValueError(self
.__class
__.__name
__ + ' requires at least '
291 'one sub-searcher to be specified')
293 # Check that all searchers are valid
294 for searcher
in self
:
296 getattr(searcher
, 'search')
297 except AttributeError:
298 raise TypeError('missing required attribute "search"')
300 getattr(searcher
, 'match_type')
301 except AttributeError:
302 raise TypeError('missing required attribute "match_type"')
304 # Check that all searchers are the same match type
305 match_type
= self
[0].match_type
306 if not all(map(lambda x
: x
.match_type
== match_type
, self
)):
307 raise ValueError(self
.__class
__.__name
__ + ' requires that all '
308 'sub-searchers implement the same match_type')
309 self
._match
_type
= match_type
312 return '{}({!r})'.format(self
.__class
__.__name
__, list(self
))
315 def match_type(self
):
316 return self
._match
_type
318 def search(self
, buf
):
319 """Search the provided buffer for a match to any sub-searchers.
321 Search the provided buffer for a match to any of this collection's
322 sub-searchers. If a single matching sub-searcher is found, returns that
323 sub-searcher's *match* object. If multiple matches are found, the match
324 with the smallest index is returned. If no matches are found, returns
327 :param buf: Buffer to search for a match.
328 :return: :class:`RegexMatch` if matched, None if no match was found.
330 self
._check
_type
(buf
)
332 best_index
= sys
.maxsize
333 for searcher
in self
:
334 match
= searcher
.search(buf
)
335 if match
and match
.start
< best_index
:
337 best_index
= match
.start
341 class StreamAdapter(object):
342 """Adapter to match varying stream objects to a single interface.
344 Despite the existence of the Python stream interface and file-like objects,
345 there are actually a number of subtly different implementations of streams
346 within Python. In addition, there are stream-like constructs like sockets
347 that use a different interface entirely (*send*/*recv* versus
350 This class provides a base adapter that can be used to convert anything
351 even remotely stream-like into a form that can consistently be used by
352 implementations of `Expecter`. The key method is :func:`poll`, which must
353 *always* provide a blocking interface to the underlying stream, and must
354 *also* provide a reliable timeout mechanism. The exact method to achieve
355 these two goals is implementation dependent, and a particular
356 implementation may be used to meet the need at hand.
358 This class also automatically delegates any non-existent attributes to the
359 underlying stream object. This allows the adapter to be used identically to
362 def __init__(self
, stream
):
363 """:param stream: Stream object to wrap over."""
366 def __getattr__(self
, attr
):
367 return getattr(self
.stream
, attr
)
370 return '{}({!r})'.format(self
.__class
__.__name
__, self
.stream
)
372 def poll(self
, timeout
):
373 """Unified blocking read access to the underlying stream.
375 All subclasses of :class:`StreamAdapter` must implement this method.
376 Once called, the method must either:
378 - Return new read data whenever it becomes available, or
379 - Raise an `ExpectTimeout` exception if timeout is exceeded.
381 The amount of data to return from each call is implementation
382 dependent, but it is important that either all data is returned from
383 the function, or that the data be somehow returned to the stream. In
384 other words, any data not returned must still be available the next
385 time the `poll` method is called.
387 Note that there is no "wait forever" functionality: either some new
388 data must be returned or an exception must occur in a finite amount of
389 time. It is also important that, if there is a timeout, the method
390 raise the exception as soon after the timeout occurred as is reasonably
393 raise NotImplementedError(self
.__class
__.__name
__ +
394 '.poll must be implemented')
397 class PollingStreamAdapterMixin(object):
398 """Add *poll_period* and *max_read* properties to a `StreamAdapter`"""
401 def poll_period(self
):
402 return self
._poll
_period
405 def poll_period(self
, value
):
408 raise ValueError('poll_period must be greater than 0')
409 self
._poll
_period
= value
413 return self
._max
_read
416 def max_read(self
, value
):
419 raise ValueError('max_read must be greater than or equal to 0')
420 self
._max
_read
= value
423 class PollingStreamAdapter(StreamAdapter
, PollingStreamAdapterMixin
):
424 """A :class:`StreamAdapter` that polls a non-blocking stream.
426 Polls a non-blocking stream of data until new data is available or a
427 timeout is exceeded. It is *VERY IMPORTANT* that the underlying stream be
431 def __init__(self
, stream
, poll_period
=0.1, max_read
=1024):
433 :param stream: Stream to poll for data.
434 :param float poll_period: Time (in seconds) between polls of the
436 :param int max_read: The maximum number of bytes/characters to read
437 from the stream at one time.
439 super(PollingStreamAdapter
, self
).__init
__(stream
)
440 self
.poll_period
= poll_period
441 self
.max_read
= max_read
443 def poll(self
, timeout
):
445 :param float timeout: Timeout in seconds.
447 timeout
= float(timeout
)
448 end_time
= time
.time() + timeout
450 # Keep reading until data is received or timeout
451 incoming
= self
.stream
.read(self
._max
_read
)
454 if (end_time
- time
.time()) < 0:
455 raise ExpectTimeout()
456 time
.sleep(self
._poll
_period
)
459 class PollingSocketStreamAdapter(StreamAdapter
, PollingStreamAdapterMixin
):
460 """A :class:`StreamAdapter` that polls a non-blocking socket.
462 Polls a non-blocking socket for data until new data is available or a
466 def __init__(self
, sock
, poll_period
=0.1, max_read
=1024):
468 :param sock: Socket to poll for data.
469 :param float poll_period: Time (in seconds) between poll of the socket.
470 :param int max_read: The maximum number of bytes/characters to read
471 from the socket at one time.
473 super(PollingSocketStreamAdapter
, self
).__init
__(sock
)
474 self
.poll_period
= poll_period
475 self
.max_read
= max_read
477 def poll(self
, timeout
):
479 :param float timeout: Timeout in seconds. A timeout that is less than
480 the poll_period will still cause a single read that may take up to
484 end_time
= now
+ float(timeout
)
485 prev_timeout
= self
.stream
.gettimeout()
486 self
.stream
.settimeout(self
._poll
_period
)
489 while (end_time
- now
) >= 0:
491 incoming
= self
.stream
.recv(self
._max
_read
)
492 except socket
.timeout
:
497 raise ExpectTimeout()
499 self
.stream
.settimeout(prev_timeout
)
502 class ExpectBytesMixin(object):
504 def expect_bytes(self
, b
, timeout
=3):
505 """Wait for a match to the bytes in *b* to appear on the stream.
507 Waits for input matching the bytes *b* for up to *timeout* seconds.
508 If a match is found, a :class:`SequenceMatch` result is returned. If
509 no match is found within *timeout* seconds, raise an
510 :class:`ExpectTimeout` exception.
512 :param b: The byte pattern to search for.
513 :param float timeout: Timeout in seconds.
514 :return: :class:`SequenceMatch` if matched, None if no match was found.
516 return self
.expect(BytesSearcher(b
), timeout
)
519 class ExpectTextMixin(object):
521 def expect_text(self
, text
, timeout
=3):
522 """Wait for a match to the text in *text* to appear on the stream.
524 Waits for input matching the text *text* for up to *timeout*
525 seconds. If a match is found, a :class:`SequenceMatch` result is
526 returned. If no match is found within *timeout* seconds, raise an
527 :class:`ExpectTimeout` exception.
529 :param text: The plain-text pattern to search for.
530 :param float timeout: Timeout in seconds.
531 :return: :class:`SequenceMatch` if matched, None if no match was found.
533 return self
.expect(TextSearcher(text
), timeout
)
536 class ExpectRegexMixin(object):
538 def expect_regex(self
, pattern
, timeout
=3, regex_options
=0):
539 """Wait for a match to the regex in *pattern* to appear on the stream.
541 Waits for input matching the regex *pattern* for up to *timeout*
542 seconds. If a match is found, a :class:`RegexMatch` result is returned.
543 If no match is found within *timeout* seconds, raise an
544 :class:`ExpectTimeout` exception.
546 :param pattern: The pattern to search for, as a single compiled regex
547 or a string that will be processed as a regex.
548 :param float timeout: Timeout in seconds.
549 :param regex_options: Options passed to the regex engine.
550 :return: :class:`RegexMatch` if matched, None if no match was found.
552 return self
.expect(RegexSearcher(pattern
, regex_options
), timeout
)
555 class Expecter(object):
556 """Base class for consuming input and waiting for a pattern to appear.
558 Implements the base class for *Expecter* types, which wrap over a
559 :class:`StreamAdapter` type and provide methods for applying a
560 :class:`Searcher` to the received data. Any attributes not part of this
561 class are delegated to the underlying :class:`StreamAdapter` type.
564 def __init__(self
, stream_adapter
, input_callback
, window
, close_adapter
):
566 :param StreamAdapter stream_adapter: The :class:`StreamAdapter` object
567 to receive data from.
568 :param function input_callback: Callback function with one parameter
569 that is called each time new data is read from the
571 :param int window: Number of historical objects (bytes, characters,
573 :param bool close_adapter: If ``True``, and the Expecter is used as a
574 context manager, closes the adapter at the end of the context
577 self
.stream_adapter
= stream_adapter
578 if not input_callback
:
579 self
.input_callback
= lambda _
: None
581 self
.input_callback
= input_callback
583 self
.close_adapter
= close_adapter
585 # Delegate undefined methods to underlying stream
586 def __getattr__(self
, attr
):
587 return getattr(self
._stream
_adapter
, attr
)
592 def __exit__(self
, type_
, value
, traceback
):
593 if self
.close_adapter
:
594 self
._stream
_adapter
.close()
598 def stream_adapter(self
):
599 return self
._stream
_adapter
601 @stream_adapter.setter
602 def stream_adapter(self
, value
):
604 getattr(value
, 'poll')
605 except AttributeError:
606 raise TypeError('stream_adapter must define "poll" method')
607 self
._stream
_adapter
= value
614 def window(self
, value
):
617 raise ValueError('window must be at least 1')
620 def expect(self
, searcher
, timeout
):
621 """Apply *searcher* to underlying :class:`StreamAdapter`
623 :param Searcher searcher: :class:`Searcher` to apply to underlying
625 :param float timeout: Timeout in seconds.
627 raise NotImplementedError('Expecter must implement "expect"')
630 class BytesExpecter(Expecter
, ExpectBytesMixin
, ExpectRegexMixin
):
631 """:class:`Expecter` interface for searching a byte-oriented stream."""
633 def __init__(self
, stream_adapter
, input_callback
=None, window
=1024,
636 :param StreamAdapter stream_adapter: The :class:`StreamAdapter` object
637 to receive data from.
638 :param function input_callback: Callback function with one parameter
639 that is called each time new data is read from the
641 :param int window: Number of historical bytes to buffer.
643 super(BytesExpecter
, self
).__init
__(stream_adapter
, input_callback
,
644 window
, close_adapter
)
645 self
._history
= six
.binary_type()
648 def expect(self
, searcher
, timeout
=3):
649 """Wait for input matching *searcher*
651 Waits for input matching *searcher* for up to *timeout* seconds. If
652 a match is found, the match result is returned (the specific type of
653 returned result depends on the :class:`Searcher` type). If no match is
654 found within *timeout* seconds, raise an :class:`ExpectTimeout`
657 :param Searcher searcher: :class:`Searcher` to apply to underlying
659 :param float timeout: Timeout in seconds.
661 timeout
= float(timeout
)
662 end
= time
.time() + timeout
663 match
= searcher
.search(self
._history
[self
._start
:])
665 # poll() will raise ExpectTimeout if time is exceeded
666 incoming
= self
._stream
_adapter
.poll(end
- time
.time())
667 self
.input_callback(incoming
)
668 self
._history
+= incoming
669 match
= searcher
.search(self
._history
[self
._start
:])
670 trimlength
= len(self
._history
) - self
._window
672 self
._start
-= trimlength
673 self
._history
= self
._history
[trimlength
:]
675 self
._start
+= match
.end
676 if (self
._start
< 0):
682 class TextExpecter(Expecter
, ExpectTextMixin
, ExpectRegexMixin
):
683 """:class:`Expecter` interface for searching a text-oriented stream."""
685 def __init__(self
, stream_adapter
, input_callback
=None, window
=1024,
688 :param StreamAdapter stream_adapter: The :class:`StreamAdapter` object
689 to receive data from.
690 :param function input_callback: Callback function with one parameter
691 that is called each time new data is read from the
693 :param int window: Number of historical characters to buffer.
695 super(TextExpecter
, self
).__init
__(stream_adapter
, input_callback
,
696 window
, close_adapter
)
697 self
._history
= six
.text_type()
700 def expect(self
, searcher
, timeout
=3):
701 """Wait for input matching *searcher*.
703 Waits for input matching *searcher* for up to *timeout* seconds. If
704 a match is found, the match result is returned (the specific type of
705 returned result depends on the :class:`Searcher` type). If no match is
706 found within *timeout* seconds, raise an :class:`ExpectTimeout`
709 :param Searcher searcher: :class:`Searcher` to apply to underlying
711 :param float timeout: Timeout in seconds.
713 timeout
= float(timeout
)
714 end
= time
.time() + timeout
715 match
= searcher
.search(self
._history
[self
._start
:])
717 # poll() will raise ExpectTimeout if time is exceeded
718 incoming
= self
._stream
_adapter
.poll(end
- time
.time())
719 self
.input_callback(incoming
)
720 self
._history
+= incoming
721 match
= searcher
.search(self
._history
[self
._start
:])
722 trimlength
= len(self
._history
) - self
._window
724 self
._start
-= trimlength
725 self
._history
= self
._history
[trimlength
:]
727 self
._start
+= match
.end
728 if (self
._start
< 0):
734 def _echo_text(value
):
735 sys
.stdout
.write(value
)
738 def _echo_bytes(value
):
739 sys
.stdout
.write(value
.decode('ascii', errors
='backslashreplace'))
742 def wrap(stream
, unicode=False, window
=1024, echo
=False, close_stream
=True):
743 """Wrap a stream to implement expect functionality.
745 This function provides a convenient way to wrap any Python stream (a
746 file-like object) or socket with an appropriate :class:`Expecter` class for
747 the stream type. The returned object adds an :func:`Expect.expect` method
748 to the stream, while passing normal stream functions like *read*/*recv*
749 and *write*/*send* through to the underlying stream.
751 Here's an example of opening and wrapping a pair of network sockets::
756 source, drain = socket.socketpair()
757 expecter = streamexpect.wrap(drain)
758 source.sendall(b'this is a test')
759 match = expecter.expect_bytes(b'test', timeout=5)
761 assert match is not None
763 :param stream: The stream/socket to wrap.
764 :param bool unicode: If ``True``, the wrapper will be configured for
765 Unicode matching, otherwise matching will be done on binary.
766 :param int window: Historical characters to buffer.
767 :param bool echo: If ``True``, echoes received characters to stdout.
768 :param bool close_stream: If ``True``, and the wrapper is used as a context
769 manager, closes the stream at the end of the context manager.
771 if hasattr(stream
, 'read'):
772 proxy
= PollingStreamAdapter(stream
)
773 elif hasattr(stream
, 'recv'):
774 proxy
= PollingSocketStreamAdapter(stream
)
776 raise TypeError('stream must have either read or recv method')
779 callback
= _echo_text
780 elif echo
and not unicode:
781 callback
= _echo_bytes
786 expecter
= TextExpecter(proxy
, input_callback
=callback
, window
=window
,
787 close_adapter
=close_stream
)
789 expecter
= BytesExpecter(proxy
, input_callback
=callback
, window
=window
,
790 close_adapter
=close_stream
)
809 'SearcherCollection',
815 # StreamAdapter types
817 'PollingStreamAdapter',
818 'PollingSocketStreamAdapter',
819 'PollingStreamAdapterMixin',