LineDance 0.1.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: LineDance
3
+ Version: 0.1.1
4
+ Summary: Synchronous line-oriented IPC with command-line utilities
5
+ Author: Jeremy Hill
6
+ Author-email: jezhill@gmail.com
7
+ License: CC0-1.0
8
+ Classifier: Programming Language :: Python :: 2.7
9
+ Classifier: Programming Language :: Python :: 3
10
+ Description-Content-Type: text/x-rst
11
+ License-File: LICENSE.txt
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: classifier
15
+ Dynamic: description
16
+ Dynamic: description-content-type
17
+ Dynamic: license
18
+ Dynamic: license-file
19
+ Dynamic: summary
20
+
21
+ .. default-role:: code
22
+
23
+
24
+ LineDance
25
+ =========
26
+
27
+ LineDance provides a small, dependency-free interface for conducting synchronous,
28
+ line-oriented exchanges with a command-line utility. It supports Python 2.7 and
29
+ Python 3.
30
+
31
+ A `Partner` instance owns one subprocess. Each call writes one line to its stdin
32
+ and waits for stdout, stderr, or process termination::
33
+
34
+ from linedance import Partner
35
+
36
+ partner = Partner( "perl -ple '$|=1; tr/a-z/A-Z/'" )
37
+ partner( 'hello world' )
38
+ # 'HELLO WORLD'
39
+ partner.Close()
40
+
41
+ Consecutive reply lines that arrive close together are returned in one newline-
42
+ joined string. Output on stderr raises `PartnerError`, while failure to produce
43
+ the first reply before an optional timeout raises `PartnerTimeout`::
44
+
45
+ partner = Partner( command, timeout=10.0, interLineTimeout=0.010 )
46
+ reply = partner.Communicate( 'one request' )
47
+
48
+ The defaults are mutable attributes and may also be overridden for one call.
49
+ `timeout=None`, the default, permits an arbitrarily long computation before the
50
+ first reply. Once the first line arrives, only the resetting `interLineTimeout`
51
+ applies, allowing any number of promptly consecutive lines to be collected.
52
+
53
+ Transcript
54
+ ----------
55
+
56
+ Every sent or received line is retained in one chronological transcript::
57
+
58
+ partner.transcript
59
+ # [(timestamp, 'stdin', 'hello again'),
60
+ # (timestamp, 'stdout', 'HELLO AGAIN')]
61
+
62
+ The `stdin`, `stdout`, and `stderr` properties provide filtered views of the same
63
+ history. Empty lines are preserved. A received LF and one optional preceding CR
64
+ are stripped from each line.
65
+
66
+ Text and bytes
67
+ --------------
68
+
69
+ Native strings are encoded using the configurable `encoding` and receive the
70
+ configurable `terminator` (default `\n`) unless already terminated. On Python 3,
71
+ explicit `bytes` input is written exactly as supplied, without encoding or an
72
+ added terminator.
73
+
74
+ Protocol boundaries
75
+ -------------------
76
+
77
+ LineDance deliberately assumes a request/reply protocol. The child must flush
78
+ its output and terminate reply lines with `\n`. A command that remains alive but
79
+ produces no output has no observable reply boundary and therefore waits until its
80
+ timeout, if any. A timeout leaves the exchange desynchronized, so the `Partner`
81
+ refuses subsequent requests rather than risk attributing a late reply to the
82
+ wrong request.
83
+
84
+ Unsolicited output is retained but is not mistaken for a reply if it arrived
85
+ before the request. Protocols with prompts lacking line terminators, request IDs,
86
+ or explicit completion sentinels need a protocol-specific adapter.
87
+
88
+ LineDance is public-domain software released under CC0.
@@ -0,0 +1,6 @@
1
+ linedance.py,sha256=8KfrVtR33RAChDJ65NfFog0hdxtnsEGSXd7Ebxft0yY,18742
2
+ linedance-0.1.1.dist-info/licenses/LICENSE.txt,sha256=9Ofzc7m5lpUDN-jUGkopOcLZC3cl6brz1QhKInF60yg,7169
3
+ linedance-0.1.1.dist-info/METADATA,sha256=EpBRuquP90iCJyKP6eV-unXMUmjRy-yDJYnL9LhFdrw,3112
4
+ linedance-0.1.1.dist-info/WHEEL,sha256=TdQ5LtNwLuxTCjgxN51AgdU5w-KkB9ttmLbzjTH02pg,109
5
+ linedance-0.1.1.dist-info/top_level.txt,sha256=M6NLGoj9uKdFahiKrStWU8kYAkFZ9e2uemslqA5018E,10
6
+ linedance-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,6 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
6
+
@@ -0,0 +1,121 @@
1
+ Creative Commons Legal Code
2
+
3
+ CC0 1.0 Universal
4
+
5
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12
+ HEREUNDER.
13
+
14
+ Statement of Purpose
15
+
16
+ The laws of most jurisdictions throughout the world automatically confer
17
+ exclusive Copyright and Related Rights (defined below) upon the creator
18
+ and subsequent owner(s) (each and all, an "owner") of an original work of
19
+ authorship and/or a database (each, a "Work").
20
+
21
+ Certain owners wish to permanently relinquish those rights to a Work for
22
+ the purpose of contributing to a commons of creative, cultural and
23
+ scientific works ("Commons") that the public can reliably and without fear
24
+ of later claims of infringement build upon, modify, incorporate in other
25
+ works, reuse and redistribute as freely as possible in any form whatsoever
26
+ and for any purposes, including without limitation commercial purposes.
27
+ These owners may contribute to the Commons to promote the ideal of a free
28
+ culture and the further production of creative, cultural and scientific
29
+ works, or to gain reputation or greater distribution for their Work in
30
+ part through the use and efforts of others.
31
+
32
+ For these and/or other purposes and motivations, and without any
33
+ expectation of additional consideration or compensation, the person
34
+ associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35
+ is an owner of Copyright and Related Rights in the Work, voluntarily
36
+ elects to apply CC0 to the Work and publicly distribute the Work under its
37
+ terms, with knowledge of his or her Copyright and Related Rights in the
38
+ Work and the meaning and intended legal effect of CC0 on those rights.
39
+
40
+ 1. Copyright and Related Rights. A Work made available under CC0 may be
41
+ protected by copyright and related or neighboring rights ("Copyright and
42
+ Related Rights"). Copyright and Related Rights include, but are not
43
+ limited to, the following:
44
+
45
+ i. the right to reproduce, adapt, distribute, perform, display,
46
+ communicate, and translate a Work;
47
+ ii. moral rights retained by the original author(s) and/or performer(s);
48
+ iii. publicity and privacy rights pertaining to a person's image or
49
+ likeness depicted in a Work;
50
+ iv. rights protecting against unfair competition in regards to a Work,
51
+ subject to the limitations in paragraph 4(a), below;
52
+ v. rights protecting the extraction, dissemination, use and reuse of data
53
+ in a Work;
54
+ vi. database rights (such as those arising under Directive 96/9/EC of the
55
+ European Parliament and of the Council of 11 March 1996 on the legal
56
+ protection of databases, and under any national implementation
57
+ thereof, including any amended or successor version of such
58
+ directive); and
59
+ vii. other similar, equivalent or corresponding rights throughout the
60
+ world based on applicable law or treaty, and any national
61
+ implementations thereof.
62
+
63
+ 2. Waiver. To the greatest extent permitted by, but not in contravention
64
+ of, applicable law, Affirmer hereby overtly, fully, permanently,
65
+ irrevocably and unconditionally waives, abandons, and surrenders all of
66
+ Affirmer's Copyright and Related Rights and associated claims and causes
67
+ of action, whether now known or unknown (including existing as well as
68
+ future claims and causes of action), in the Work (i) in all territories
69
+ worldwide, (ii) for the maximum duration provided by applicable law or
70
+ treaty (including future time extensions), (iii) in any current or future
71
+ medium and for any number of copies, and (iv) for any purpose whatsoever,
72
+ including without limitation commercial, advertising or promotional
73
+ purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74
+ member of the public at large and to the detriment of Affirmer's heirs and
75
+ successors, fully intending that such Waiver shall not be subject to
76
+ revocation, rescission, cancellation, termination, or any other legal or
77
+ equitable action to disrupt the quiet enjoyment of the Work by the public
78
+ as contemplated by Affirmer's express Statement of Purpose.
79
+
80
+ 3. Public License Fallback. Should any part of the Waiver for any reason
81
+ be judged legally invalid or ineffective under applicable law, then the
82
+ Waiver shall be preserved to the maximum extent permitted taking into
83
+ account Affirmer's express Statement of Purpose. In addition, to the
84
+ extent the Waiver is so judged Affirmer hereby grants to each affected
85
+ person a royalty-free, non transferable, non sublicensable, non exclusive,
86
+ irrevocable and unconditional license to exercise Affirmer's Copyright and
87
+ Related Rights in the Work (i) in all territories worldwide, (ii) for the
88
+ maximum duration provided by applicable law or treaty (including future
89
+ time extensions), (iii) in any current or future medium and for any number
90
+ of copies, and (iv) for any purpose whatsoever, including without
91
+ limitation commercial, advertising or promotional purposes (the
92
+ "License"). The License shall be deemed effective as of the date CC0 was
93
+ applied by Affirmer to the Work. Should any part of the License for any
94
+ reason be judged legally invalid or ineffective under applicable law, such
95
+ partial invalidity or ineffectiveness shall not invalidate the remainder
96
+ of the License, and in such case Affirmer hereby affirms that he or she
97
+ will not (i) exercise any of his or her remaining Copyright and Related
98
+ Rights in the Work or (ii) assert any associated claims and causes of
99
+ action with respect to the Work, in either case contrary to Affirmer's
100
+ express Statement of Purpose.
101
+
102
+ 4. Limitations and Disclaimers.
103
+
104
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
105
+ surrendered, licensed or otherwise affected by this document.
106
+ b. Affirmer offers the Work as-is and makes no representations or
107
+ warranties of any kind concerning the Work, express, implied,
108
+ statutory or otherwise, including without limitation warranties of
109
+ title, merchantability, fitness for a particular purpose, non
110
+ infringement, or the absence of latent or other defects, accuracy, or
111
+ the present or absence of errors, whether or not discoverable, all to
112
+ the greatest extent permissible under applicable law.
113
+ c. Affirmer disclaims responsibility for clearing rights of other persons
114
+ that may apply to the Work or any use thereof, including without
115
+ limitation any person's Copyright and Related Rights in the Work.
116
+ Further, Affirmer disclaims responsibility for obtaining any necessary
117
+ consents, permissions or other rights required for any use of the
118
+ Work.
119
+ d. Affirmer understands and acknowledges that Creative Commons is not a
120
+ party to this document and has no duty or obligation with respect to
121
+ this CC0 or use of the Work.
@@ -0,0 +1 @@
1
+ linedance
linedance.py ADDED
@@ -0,0 +1,483 @@
1
+ # $BEGIN_LINEDANCE_LICENSE$
2
+ #
3
+ # This file is part of the LineDance project, a Python module for synchronous,
4
+ # line-oriented interprocess communication.
5
+ #
6
+ # Author: Jeremy Hill (2026-)
7
+ # Development was supported by the NIH, NYS SCIRB, and the Stratton VA Medical
8
+ # Center.
9
+ #
10
+ # No Copyright
11
+ # ============
12
+ # The author has dedicated this work to the public domain under the terms of
13
+ # Creative Commons' CC0 1.0 Universal legal code, waiving all of his rights to
14
+ # the work worldwide under copyright law, including all related and neighboring
15
+ # rights, to the extent allowed by law.
16
+ #
17
+ # You can copy, modify, distribute and perform the work, even for commercial
18
+ # purposes, all without asking permission. See Other Information below.
19
+ #
20
+ # Other Information
21
+ # =================
22
+ # In no way are the patent or trademark rights of any person affected by CC0,
23
+ # nor are the rights that other persons may have in the work or in how the work
24
+ # is used, such as publicity or privacy rights.
25
+ #
26
+ # The author makes no warranties about the work, and disclaims liability for
27
+ # all uses of the work, to the fullest extent permitted by applicable law. When
28
+ # using or citing the work, you are requested to preserve the author attribution
29
+ # and this copyright waiver, but you should not imply endorsement by the author.
30
+ #
31
+ # $END_LINEDANCE_LICENSE$
32
+ r"""
33
+ Synchronous, line-oriented IPC with a command-line utility.
34
+
35
+ `Partner` starts one subprocess and supports repeated REPL-like request/reply
36
+ exchanges. Each call to `Partner.Communicate()` writes one input payload and
37
+ then waits for the first subsequent stdout line, stderr line, or process
38
+ termination:
39
+
40
+ - stdout lines are returned as a native string; consecutive lines separated by
41
+ no more than `interLineTimeout` are joined with ``'\n'``;
42
+ - stderr lines are collated by the same rule and raised in `PartnerError`;
43
+ - successful termination without a reply returns `None`; and
44
+ - unsuccessful termination raises `PartnerError` with its `returnCode`.
45
+
46
+ The initial `timeout` applies only until the first reply line. Once output
47
+ begins, any number of lines may follow provided each arrives within the
48
+ resetting `interLineTimeout`. A timed-out exchange leaves the protocol
49
+ desynchronized, so that `Partner` cannot safely be used for another request.
50
+ The exchange that first observes process termination reports it normally;
51
+ subsequent communication attempts raise `RuntimeError` rather than repeatedly
52
+ reporting EOF or writing to a finished process.
53
+
54
+ Native-string input is encoded and receives the mutable `terminator` if it does
55
+ not already have one. On Python 3, explicit `bytes` bypass encoding and
56
+ termination and are written exactly as supplied. Incoming lines are expected
57
+ to end in LF; exactly that LF and one optional preceding CR are removed. Empty
58
+ lines remain real replies and transcript events.
59
+
60
+ Every stdin, stdout, and stderr line is atomically appended to `transcript` as
61
+ ``(timestamp, eventType, line)``. The `stdin`, `stdout`, and `stderr` properties
62
+ are filtered snapshots of that sequence. Unsolicited output is always recorded,
63
+ but output received before a request is not mistaken for that request's reply.
64
+
65
+ Dedicated daemon threads continuously drain stdout and stderr, avoiding pipe
66
+ deadlock without platform-specific polling and retaining Python 2.7 support.
67
+ The child must flush its output; parent-side buffering options cannot flush a
68
+ child's private buffers. Deleting a `Partner` closes stdin, briefly permits an
69
+ EOF-driven exit, and then escalates through terminate and kill while reaping the
70
+ process.
71
+ """
72
+
73
+ from __future__ import absolute_import
74
+
75
+ __version__ = '0.1.1'
76
+
77
+ __all__ = [
78
+ 'Partner',
79
+ 'PartnerError', 'PartnerTimeout',
80
+ ]
81
+
82
+ import os
83
+ import sys
84
+ import time
85
+ import shlex
86
+ import threading
87
+ import subprocess
88
+ try: import queue
89
+ except ImportError: import Queue as queue # Python 2
90
+
91
+ PY3 = ( sys.version_info[ 0 ] >= 3 )
92
+ WINDOWS = ( sys.platform.lower().startswith( 'win' ) )
93
+
94
+ class PartnerError( RuntimeError ):
95
+ """The partner wrote to stderr or terminated unsuccessfully."""
96
+
97
+ def __init__( self, command, returnCode, stderr='' ):
98
+ self.command = command
99
+ self.returnCode = returnCode
100
+ self.stderr = stderr
101
+ message = 'partner process reported an error'
102
+ if returnCode is not None: message += ' with return code %s' % returnCode
103
+ if stderr: message += ':\n' + stderr.rstrip()
104
+ RuntimeError.__init__( self, message )
105
+
106
+
107
+ class PartnerTimeout( RuntimeError ):
108
+ """The partner did not complete its response before the deadline."""
109
+
110
+ def __init__( self, timeout ):
111
+ self.timeout = timeout
112
+ RuntimeError.__init__( self, 'partner did not respond within %g seconds' % timeout )
113
+
114
+
115
+ class Partner( object ):
116
+ """
117
+ Start `command` as a subprocess, and conduct one line-oriented exchange at
118
+ a time.
119
+
120
+ `command` may be a sequence of arguments or a command string. A string is
121
+ split with `shlex` on POSIX when `shell=False`; passing a sequence is the
122
+ least ambiguous form on every platform. If `shell=True`, `command` must be
123
+ a string.
124
+
125
+ `.Communicate(line)` writes one native `str`, adding the configured terminator
126
+ if it is not already present, and then returns the first newline-stripped
127
+ native `str` received from stdout after the write begins, together with any
128
+ further lines received within its `interLineTimeout`. Earlier unsolicited
129
+ lines are ignored as replies, but remain available in `.stdout` and `.transcript`.
130
+ The method returns `None` if the process instead terminates successfully.
131
+ Output on stderr, or an unsuccessful return code, raises `PartnerError`; all
132
+ stderr text is available as the exception's `.stderr` attribute and in the
133
+ `Partner` instance's `.stderr` and `.transcript`. The `.stdin`, `.stdout`,
134
+ and `.stderr` properties are filtered views of the single chronologically
135
+ appended `(timestamp, eventType, line)` sequence in `.transcript`.
136
+
137
+ `timeout` is the default maximum wait for the first reply line, and
138
+ `interLineTimeout` is the default maximum gap between bundled reply lines.
139
+ Both are mutable attributes and may be overridden for one `.Communicate()`
140
+ call. Native-string input is encoded after appending the mutable `terminator`
141
+ if necessary. On Python 3, explicit `bytes` input is sent exactly as supplied.
142
+
143
+ The child must flush each stdout reply. No parent-side implementation can
144
+ make an independently buffered child reveal a line that it has not flushed.
145
+ A Perl child, for example, should execute `$|=1` to disable buffering.
146
+ """
147
+
148
+ def __init__( self, command, shell=False, encoding='utf-8', terminator='\n', timeout=None, interLineTimeout=0.010 ):
149
+ if shell and isinstance( command, ( list, tuple ) ):
150
+ if WINDOWS: command = ' '.join( '"' + word.replace( '"', '""' ) + '"' for word in command )
151
+ else: command = ' '.join( '"' + word.replace( '\\', '\\\\' ).replace( '"', '\\"' ) + '"' for word in command )
152
+ if not shell and hasattr( command, 'split' ):
153
+ command = shlex.split( command )
154
+
155
+ self.__command = command
156
+ self.__encoding = encoding
157
+ self.terminator = terminator
158
+ self.timeout = timeout
159
+ self.interLineTimeout = interLineTimeout
160
+ self.__events = queue.Queue()
161
+ self.__lock = threading.Lock()
162
+ self.__receptionLock = threading.Lock()
163
+ self.__generation = [ 0 ]
164
+ self.__transcript = []
165
+ self.__stdoutEOF = False
166
+ self.__stderrEOF = False
167
+ self.__desynchronized = False
168
+ self.__retired = False
169
+ self.__sp = subprocess.Popen(
170
+ command,
171
+ shell = shell,
172
+ stdin = subprocess.PIPE,
173
+ stdout = subprocess.PIPE,
174
+ stderr = subprocess.PIPE,
175
+ bufsize = 0,
176
+ )
177
+ self.__StartReader( 'stdout', self.__sp.stdout )
178
+ self.__StartReader( 'stderr', self.__sp.stderr )
179
+
180
+ def __StartReader( self, name, stream ):
181
+ # Do not let the long-lived reader target capture `self`: otherwise a reader
182
+ # blocked on the pipe would prevent an abandoned Partner from being collected.
183
+ encoding = self.__encoding
184
+ Decode = lambda value: value.decode( encoding ) if PY3 and encoding else value
185
+ StripLineTerminator = self.__StripLineTerminator
186
+ receptionLock = self.__receptionLock
187
+ generation = self.__generation
188
+ transcript = self.__transcript
189
+ events = self.__events
190
+ Clock = time.time
191
+ def ReadLines():
192
+ try:
193
+ while True:
194
+ line = stream.readline()
195
+ if not line: break
196
+ decoded = StripLineTerminator( Decode( line ) )
197
+ timestamp = Clock()
198
+ receptionLock.acquire()
199
+ try:
200
+ currentGeneration = generation[ 0 ]
201
+ transcript.append( ( timestamp, name, decoded ) )
202
+ finally:
203
+ receptionLock.release()
204
+ events.put( ( timestamp, currentGeneration, name, line ) )
205
+ finally:
206
+ try: stream.close()
207
+ except: pass
208
+ receptionLock.acquire()
209
+ try: currentGeneration = generation[ 0 ]
210
+ finally: receptionLock.release()
211
+ events.put( ( Clock(), currentGeneration, name, None ) )
212
+ thread = threading.Thread( target=ReadLines )
213
+ thread.daemon = True
214
+ thread.start()
215
+
216
+ def __Encode( self, value ):
217
+ return value.encode( self.__encoding ) if PY3 and self.__encoding else value
218
+
219
+ def __Decode( self, value ):
220
+ return value.decode( self.__encoding ) if PY3 and self.__encoding else value
221
+
222
+ @staticmethod
223
+ def __StripLineTerminator( value ):
224
+ if value.endswith( '\n' ):
225
+ value = value[ :-1 ]
226
+ if value.endswith( '\r' ): value = value[ :-1 ]
227
+ return value
228
+
229
+ def __PrepareInput( self, value ):
230
+ if PY3 and isinstance( value, bytes ):
231
+ return value, value
232
+ if not isinstance( value, str ):
233
+ raise TypeError( 'input must be a native str%s, not %s' % ( ' or bytes' if PY3 else '', type( value ).__name__ ) )
234
+ terminator = self.terminator
235
+ if terminator is None: terminator = ''
236
+ if not isinstance( terminator, str ):
237
+ raise TypeError( 'terminator must be a native str or None' )
238
+ if terminator and value.endswith( terminator ):
239
+ return self.__Encode( value ), value[ :-len( terminator ) ]
240
+ return self.__Encode( value + terminator ), value
241
+
242
+ def __Remaining( self, deadline ):
243
+ if deadline is None: return None
244
+ return max( 0.0, deadline - time.time() )
245
+
246
+ def __NextEvent( self, deadline ):
247
+ remaining = self.__Remaining( deadline )
248
+ try:
249
+ if remaining is None: return self.__events.get()
250
+ return self.__events.get( True, remaining )
251
+ except queue.Empty:
252
+ raise PartnerTimeout( 0.0 )
253
+
254
+ def __RecordEvent( self, event, stderr, minimumGeneration=None ):
255
+ timestamp, generation, name, value = event
256
+ if value is None:
257
+ if name == 'stdout': self.__stdoutEOF = True
258
+ else: self.__stderrEOF = True
259
+ elif name == 'stderr' and ( minimumGeneration is None or generation >= minimumGeneration ):
260
+ stderr.append( value )
261
+ return timestamp, generation, name, value
262
+
263
+ def __BeginRequest( self, line ):
264
+ self.__receptionLock.acquire()
265
+ try:
266
+ self.__generation[ 0 ] += 1
267
+ self.__transcript.append( ( time.time(), 'stdin', line ) )
268
+ return self.__generation[ 0 ]
269
+ finally:
270
+ self.__receptionLock.release()
271
+
272
+ def __TerminalResult( self, deadline, stderr ):
273
+ while not ( self.__stdoutEOF and self.__stderrEOF ):
274
+ self.__RecordEvent( self.__NextEvent( deadline ), stderr )
275
+ while self.__sp.poll() is None:
276
+ if deadline is not None and time.time() >= deadline:
277
+ raise PartnerTimeout( 0.0 )
278
+ time.sleep( 0.001 )
279
+ returnCode = self.__sp.returncode
280
+ self.__CloseStdin()
281
+ self.__retired = True
282
+ stderr = self.__Decode( b''.join( stderr ) )
283
+ if stderr or returnCode:
284
+ raise PartnerError( self.__command, returnCode, stderr )
285
+ return None
286
+
287
+ def __StdoutResult( self, firstTimestamp, firstLine, generation, stderr, interLineTimeout ):
288
+ lines = [ self.__StripLineTerminator( self.__Decode( firstLine ) ) ]
289
+ if not interLineTimeout: return lines[ 0 ]
290
+ cutoff = firstTimestamp + interLineTimeout
291
+ while True:
292
+ try:
293
+ timestamp, receivedGeneration, name, value = self.__RecordEvent( self.__NextEvent( cutoff ), stderr, generation )
294
+ except PartnerTimeout:
295
+ return '\n'.join( lines )
296
+ if name == 'stderr' and value is not None and receivedGeneration >= generation:
297
+ return self.__StderrResult( timestamp, generation, stderr, interLineTimeout )
298
+ if name == 'stdout' and value is not None and receivedGeneration >= generation:
299
+ if timestamp > cutoff: return '\n'.join( lines )
300
+ lines.append( self.__StripLineTerminator( self.__Decode( value ) ) )
301
+ cutoff = timestamp + interLineTimeout
302
+ if self.__stdoutEOF:
303
+ # Return the final reply once, but do not let a subsequent request be
304
+ # mistaken for the one that caused this already-observed EOF.
305
+ self.__retired = True
306
+ self.__CloseStdin()
307
+ return '\n'.join( lines )
308
+
309
+ def __StderrResult( self, firstTimestamp, generation, stderr, interLineTimeout ):
310
+ if self.__sp.poll() is not None: return self.__TerminalResult( None, stderr )
311
+ if not interLineTimeout:
312
+ raise PartnerError( self.__command, None, self.__Decode( b''.join( stderr ) ) )
313
+ cutoff = firstTimestamp + interLineTimeout
314
+ while True:
315
+ oldLength = len( stderr )
316
+ try:
317
+ timestamp, receivedGeneration, name, value = self.__RecordEvent( self.__NextEvent( cutoff ), stderr, generation )
318
+ except PartnerTimeout:
319
+ raise PartnerError( self.__command, self.__sp.poll(), self.__Decode( b''.join( stderr ) ) )
320
+ if name == 'stderr' and value is not None and receivedGeneration >= generation:
321
+ if timestamp > cutoff:
322
+ del stderr[ oldLength: ]
323
+ raise PartnerError( self.__command, self.__sp.poll(), self.__Decode( b''.join( stderr ) ) )
324
+ cutoff = timestamp + interLineTimeout
325
+ if self.__stderrEOF:
326
+ return self.__TerminalResult( None, stderr )
327
+
328
+ def __CloseStdin( self ):
329
+ try:
330
+ if not self.__sp.stdin.closed: self.__sp.stdin.close()
331
+ except: pass
332
+
333
+ def Communicate( self, line='', timeout=None, interLineTimeout=None ):
334
+ """
335
+ Send one line and return one reply, `None`, or raise `PartnerError`.
336
+ Consecutive stdout or stderr lines separated by no more than
337
+ `interLineTimeout` seconds are joined with newline characters into one
338
+ reply or one `PartnerError`, respectively.
339
+
340
+ `timeout=None` and `interLineTimeout=None` use the corresponding mutable
341
+ instance attributes. If the effective `timeout` expires before the first
342
+ reply line, `PartnerTimeout` is raised and this instance becomes unusable:
343
+ the outstanding request makes the protocol state unknowable. Once the first
344
+ line arrives, only the resetting `interLineTimeout` applies. After process
345
+ termination has been observed, subsequent calls raise `RuntimeError`
346
+ immediately.
347
+ """
348
+ self.__lock.acquire()
349
+ try:
350
+ returnCode = self.__sp.poll()
351
+ if self.__retired or returnCode is not None:
352
+ self.__retired = True
353
+ self.__CloseStdin()
354
+ raise RuntimeError( 'partner process has already terminated with return code %s' % returnCode )
355
+ if timeout is None: timeout = self.timeout
356
+ if interLineTimeout is None: interLineTimeout = self.interLineTimeout
357
+ if interLineTimeout is None: interLineTimeout = 0.0
358
+ if timeout is not None and timeout < 0.0: raise ValueError( 'timeout cannot be negative' )
359
+ if interLineTimeout < 0.0: raise ValueError( 'interLineTimeout cannot be negative' )
360
+ if self.__desynchronized:
361
+ raise RuntimeError( 'partner is desynchronized after an earlier timeout' )
362
+ deadline = None if timeout is None else time.time() + timeout
363
+ stderr = []
364
+
365
+ wireInput, recordedInput = self.__PrepareInput( line )
366
+ generation = self.__BeginRequest( recordedInput )
367
+ try:
368
+ self.__sp.stdin.write( wireInput )
369
+ self.__sp.stdin.flush()
370
+ except ( IOError, OSError ):
371
+ return self.__TerminalResult( deadline, stderr )
372
+
373
+ while True:
374
+ timestamp, receivedGeneration, name, value = self.__RecordEvent( self.__NextEvent( deadline ), stderr, generation )
375
+
376
+ if name == 'stderr' and value is not None and receivedGeneration >= generation:
377
+ return self.__StderrResult( timestamp, generation, stderr, interLineTimeout )
378
+ if name == 'stdout' and value is not None and receivedGeneration >= generation:
379
+ return self.__StdoutResult( timestamp, value, generation, stderr, interLineTimeout )
380
+ if self.__stdoutEOF:
381
+ return self.__TerminalResult( deadline, stderr )
382
+ except PartnerTimeout:
383
+ self.__desynchronized = True
384
+ raise PartnerTimeout( timeout )
385
+ finally:
386
+ self.__lock.release()
387
+
388
+ __call__ = Communicate
389
+
390
+ @property
391
+ def command( self ):
392
+ return self.__command
393
+
394
+ @property
395
+ def pid( self ):
396
+ return self.__sp.pid
397
+
398
+ @property
399
+ def returnCode( self ):
400
+ returnCode = self.__sp.poll()
401
+ if returnCode is not None:
402
+ self.__CloseStdin()
403
+ self.__retired = True
404
+ return returnCode
405
+
406
+ def __History( self, stream=None ):
407
+ self.__receptionLock.acquire()
408
+ try:
409
+ if stream is None: return list( self.__transcript )
410
+ return [ line for timestamp, eventType, line in self.__transcript if eventType == stream ]
411
+ finally: self.__receptionLock.release()
412
+
413
+ @property
414
+ def stdin( self ):
415
+ """All stdin sent so far: unterminated native strings or exact Python 3 bytes."""
416
+ return self.__History( 'stdin' )
417
+
418
+ @property
419
+ def stdout( self ):
420
+ """All stdout lines received so far, without line terminators."""
421
+ return self.__History( 'stdout' )
422
+
423
+ @property
424
+ def stderr( self ):
425
+ """All stderr lines received so far, without line terminators."""
426
+ return self.__History( 'stderr' )
427
+
428
+ @property
429
+ def transcript( self ):
430
+ """All `(timestamp, eventType, line)` tuples in atomic chronological order."""
431
+ return self.__History()
432
+
433
+ @property
434
+ def finished( self ):
435
+ returnCode = self.__sp.poll()
436
+ if returnCode is not None:
437
+ self.__CloseStdin()
438
+ self.__retired = True
439
+ return returnCode is not None
440
+
441
+ def Wait( self, timeout=None ):
442
+ deadline = None if timeout is None else time.time() + timeout
443
+ while self.__sp.poll() is None:
444
+ if deadline is not None and time.time() >= deadline:
445
+ raise PartnerTimeout( timeout )
446
+ time.sleep( 0.01 )
447
+ self.__CloseStdin()
448
+ self.__retired = True
449
+ return self.__sp.returncode
450
+
451
+ def Close( self, timeout=None ):
452
+ """Close stdin, wait for the child to finish, and return its exit code."""
453
+ self.__CloseStdin()
454
+ return self.Wait( timeout=timeout )
455
+
456
+ def Terminate( self, timeout=None ):
457
+ if self.__sp.poll() is None: self.__sp.terminate()
458
+ return self.Wait( timeout=timeout )
459
+
460
+ def Kill( self, timeout=None ):
461
+ if self.__sp.poll() is None: self.__sp.kill()
462
+ return self.Wait( timeout=timeout )
463
+
464
+ def __enter__( self ):
465
+ return self
466
+
467
+ def __exit__( self, excType, excValue, traceback ):
468
+ if self.__sp.poll() is None: self.Close()
469
+
470
+ def __del__( self ):
471
+ try:
472
+ sp = self.__sp
473
+ try: sp.stdin.closed or sp.stdin.close()
474
+ except: pass
475
+ for Action in [ sp.terminate, sp.kill ]:
476
+ deadline = time.time() + 0.050
477
+ while sp.poll() is None and time.time() < deadline:
478
+ time.sleep( 0.001 )
479
+ if sp.poll() is not None: break
480
+ Action()
481
+ if sp.poll() is None: sp.kill()
482
+ sp.wait()
483
+ except: pass