##// END OF EJS Templates
wireprototypes: convert `baseprotocolhandler.name` to an abstract property...
Matt Harbison -
r53019:e7812caa default
parent child Browse files
Show More
@@ -1,457 +1,460
1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 from __future__ import annotations
6 from __future__ import annotations
7
7
8 import abc
8 import typing
9 import typing
9
10
10 from typing import (
11 from typing import (
11 Protocol,
12 Protocol,
12 )
13 )
13
14
14 from .node import (
15 from .node import (
15 bin,
16 bin,
16 hex,
17 hex,
17 )
18 )
18 from .i18n import _
19 from .i18n import _
19 from .thirdparty import attr
20 from .thirdparty import attr
20
21
21 # Force pytype to use the non-vendored package
22 # Force pytype to use the non-vendored package
22 if typing.TYPE_CHECKING:
23 if typing.TYPE_CHECKING:
23 # noinspection PyPackageRequirements
24 # noinspection PyPackageRequirements
24 import attr
25 import attr
25
26
26 from . import (
27 from . import (
27 error,
28 error,
28 util,
29 util,
29 )
30 )
30 from .utils import compression
31 from .utils import compression
31
32
32 # Names of the SSH protocol implementations.
33 # Names of the SSH protocol implementations.
33 SSHV1 = b'ssh-v1'
34 SSHV1 = b'ssh-v1'
34
35
35 NARROWCAP = b'exp-narrow-1'
36 NARROWCAP = b'exp-narrow-1'
36 ELLIPSESCAP1 = b'exp-ellipses-1'
37 ELLIPSESCAP1 = b'exp-ellipses-1'
37 ELLIPSESCAP = b'exp-ellipses-2'
38 ELLIPSESCAP = b'exp-ellipses-2'
38 SUPPORTED_ELLIPSESCAP = (ELLIPSESCAP1, ELLIPSESCAP)
39 SUPPORTED_ELLIPSESCAP = (ELLIPSESCAP1, ELLIPSESCAP)
39
40
40 # All available wire protocol transports.
41 # All available wire protocol transports.
41 TRANSPORTS = {
42 TRANSPORTS = {
42 SSHV1: {
43 SSHV1: {
43 b'transport': b'ssh',
44 b'transport': b'ssh',
44 b'version': 1,
45 b'version': 1,
45 },
46 },
46 b'http-v1': {
47 b'http-v1': {
47 b'transport': b'http',
48 b'transport': b'http',
48 b'version': 1,
49 b'version': 1,
49 },
50 },
50 }
51 }
51
52
52
53
53 class bytesresponse:
54 class bytesresponse:
54 """A wire protocol response consisting of raw bytes."""
55 """A wire protocol response consisting of raw bytes."""
55
56
56 def __init__(self, data):
57 def __init__(self, data):
57 self.data = data
58 self.data = data
58
59
59
60
60 class ooberror:
61 class ooberror:
61 """wireproto reply: failure of a batch of operation
62 """wireproto reply: failure of a batch of operation
62
63
63 Something failed during a batch call. The error message is stored in
64 Something failed during a batch call. The error message is stored in
64 `self.message`.
65 `self.message`.
65 """
66 """
66
67
67 def __init__(self, message):
68 def __init__(self, message):
68 self.message = message
69 self.message = message
69
70
70
71
71 class pushres:
72 class pushres:
72 """wireproto reply: success with simple integer return
73 """wireproto reply: success with simple integer return
73
74
74 The call was successful and returned an integer contained in `self.res`.
75 The call was successful and returned an integer contained in `self.res`.
75 """
76 """
76
77
77 def __init__(self, res, output):
78 def __init__(self, res, output):
78 self.res = res
79 self.res = res
79 self.output = output
80 self.output = output
80
81
81
82
82 class pusherr:
83 class pusherr:
83 """wireproto reply: failure
84 """wireproto reply: failure
84
85
85 The call failed. The `self.res` attribute contains the error message.
86 The call failed. The `self.res` attribute contains the error message.
86 """
87 """
87
88
88 def __init__(self, res, output):
89 def __init__(self, res, output):
89 self.res = res
90 self.res = res
90 self.output = output
91 self.output = output
91
92
92
93
93 class streamres:
94 class streamres:
94 """wireproto reply: binary stream
95 """wireproto reply: binary stream
95
96
96 The call was successful and the result is a stream.
97 The call was successful and the result is a stream.
97
98
98 Accepts a generator containing chunks of data to be sent to the client.
99 Accepts a generator containing chunks of data to be sent to the client.
99
100
100 ``prefer_uncompressed`` indicates that the data is expected to be
101 ``prefer_uncompressed`` indicates that the data is expected to be
101 uncompressable and that the stream should therefore use the ``none``
102 uncompressable and that the stream should therefore use the ``none``
102 engine.
103 engine.
103 """
104 """
104
105
105 def __init__(self, gen=None, prefer_uncompressed=False):
106 def __init__(self, gen=None, prefer_uncompressed=False):
106 self.gen = gen
107 self.gen = gen
107 self.prefer_uncompressed = prefer_uncompressed
108 self.prefer_uncompressed = prefer_uncompressed
108
109
109
110
110 class streamreslegacy:
111 class streamreslegacy:
111 """wireproto reply: uncompressed binary stream
112 """wireproto reply: uncompressed binary stream
112
113
113 The call was successful and the result is a stream.
114 The call was successful and the result is a stream.
114
115
115 Accepts a generator containing chunks of data to be sent to the client.
116 Accepts a generator containing chunks of data to be sent to the client.
116
117
117 Like ``streamres``, but sends an uncompressed data for "version 1" clients
118 Like ``streamres``, but sends an uncompressed data for "version 1" clients
118 using the application/mercurial-0.1 media type.
119 using the application/mercurial-0.1 media type.
119 """
120 """
120
121
121 def __init__(self, gen=None):
122 def __init__(self, gen=None):
122 self.gen = gen
123 self.gen = gen
123
124
124
125
125 # list of nodes encoding / decoding
126 # list of nodes encoding / decoding
126 def decodelist(l, sep=b' '):
127 def decodelist(l, sep=b' '):
127 if l:
128 if l:
128 return [bin(v) for v in l.split(sep)]
129 return [bin(v) for v in l.split(sep)]
129 return []
130 return []
130
131
131
132
132 def encodelist(l, sep=b' '):
133 def encodelist(l, sep=b' '):
133 try:
134 try:
134 return sep.join(map(hex, l))
135 return sep.join(map(hex, l))
135 except TypeError:
136 except TypeError:
136 raise
137 raise
137
138
138
139
139 # batched call argument encoding
140 # batched call argument encoding
140
141
141
142
142 def escapebatcharg(plain):
143 def escapebatcharg(plain):
143 return (
144 return (
144 plain.replace(b':', b':c')
145 plain.replace(b':', b':c')
145 .replace(b',', b':o')
146 .replace(b',', b':o')
146 .replace(b';', b':s')
147 .replace(b';', b':s')
147 .replace(b'=', b':e')
148 .replace(b'=', b':e')
148 )
149 )
149
150
150
151
151 def unescapebatcharg(escaped):
152 def unescapebatcharg(escaped):
152 return (
153 return (
153 escaped.replace(b':e', b'=')
154 escaped.replace(b':e', b'=')
154 .replace(b':s', b';')
155 .replace(b':s', b';')
155 .replace(b':o', b',')
156 .replace(b':o', b',')
156 .replace(b':c', b':')
157 .replace(b':c', b':')
157 )
158 )
158
159
159
160
160 # mapping of options accepted by getbundle and their types
161 # mapping of options accepted by getbundle and their types
161 #
162 #
162 # Meant to be extended by extensions. It is the extension's responsibility to
163 # Meant to be extended by extensions. It is the extension's responsibility to
163 # ensure such options are properly processed in exchange.getbundle.
164 # ensure such options are properly processed in exchange.getbundle.
164 #
165 #
165 # supported types are:
166 # supported types are:
166 #
167 #
167 # :nodes: list of binary nodes, transmitted as space-separated hex nodes
168 # :nodes: list of binary nodes, transmitted as space-separated hex nodes
168 # :csv: list of values, transmitted as comma-separated values
169 # :csv: list of values, transmitted as comma-separated values
169 # :scsv: set of values, transmitted as comma-separated values
170 # :scsv: set of values, transmitted as comma-separated values
170 # :plain: string with no transformation needed.
171 # :plain: string with no transformation needed.
171 GETBUNDLE_ARGUMENTS = {
172 GETBUNDLE_ARGUMENTS = {
172 b'heads': b'nodes',
173 b'heads': b'nodes',
173 b'bookmarks': b'boolean',
174 b'bookmarks': b'boolean',
174 b'common': b'nodes',
175 b'common': b'nodes',
175 b'obsmarkers': b'boolean',
176 b'obsmarkers': b'boolean',
176 b'phases': b'boolean',
177 b'phases': b'boolean',
177 b'bundlecaps': b'scsv',
178 b'bundlecaps': b'scsv',
178 b'listkeys': b'csv',
179 b'listkeys': b'csv',
179 b'cg': b'boolean',
180 b'cg': b'boolean',
180 b'cbattempted': b'boolean',
181 b'cbattempted': b'boolean',
181 b'stream': b'boolean',
182 b'stream': b'boolean',
182 b'includepats': b'csv',
183 b'includepats': b'csv',
183 b'excludepats': b'csv',
184 b'excludepats': b'csv',
184 }
185 }
185
186
186
187
187 class baseprotocolhandler(Protocol):
188 class baseprotocolhandler(Protocol):
188 """Abstract base class for wire protocol handlers.
189 """Abstract base class for wire protocol handlers.
189
190
190 A wire protocol handler serves as an interface between protocol command
191 A wire protocol handler serves as an interface between protocol command
191 handlers and the wire protocol transport layer. Protocol handlers provide
192 handlers and the wire protocol transport layer. Protocol handlers provide
192 methods to read command arguments, redirect stdio for the duration of
193 methods to read command arguments, redirect stdio for the duration of
193 the request, handle response types, etc.
194 the request, handle response types, etc.
194 """
195 """
195
196
196 name: bytes
197 @property
197 """The name of the protocol implementation.
198 @abc.abstractmethod
199 def name(self) -> bytes:
200 """The name of the protocol implementation.
198
201
199 Used for uniquely identifying the transport type.
202 Used for uniquely identifying the transport type.
200 """
203 """
201
204
202 def getargs(self, args):
205 def getargs(self, args):
203 """return the value for arguments in <args>
206 """return the value for arguments in <args>
204
207
205 For version 1 transports, returns a list of values in the same
208 For version 1 transports, returns a list of values in the same
206 order they appear in ``args``. For version 2 transports, returns
209 order they appear in ``args``. For version 2 transports, returns
207 a dict mapping argument name to value.
210 a dict mapping argument name to value.
208 """
211 """
209
212
210 def getprotocaps(self):
213 def getprotocaps(self):
211 """Returns the list of protocol-level capabilities of client
214 """Returns the list of protocol-level capabilities of client
212
215
213 Returns a list of capabilities as declared by the client for
216 Returns a list of capabilities as declared by the client for
214 the current request (or connection for stateful protocol handlers)."""
217 the current request (or connection for stateful protocol handlers)."""
215
218
216 def getpayload(self):
219 def getpayload(self):
217 """Provide a generator for the raw payload.
220 """Provide a generator for the raw payload.
218
221
219 The caller is responsible for ensuring that the full payload is
222 The caller is responsible for ensuring that the full payload is
220 processed.
223 processed.
221 """
224 """
222
225
223 def mayberedirectstdio(self):
226 def mayberedirectstdio(self):
224 """Context manager to possibly redirect stdio.
227 """Context manager to possibly redirect stdio.
225
228
226 The context manager yields a file-object like object that receives
229 The context manager yields a file-object like object that receives
227 stdout and stderr output when the context manager is active. Or it
230 stdout and stderr output when the context manager is active. Or it
228 yields ``None`` if no I/O redirection occurs.
231 yields ``None`` if no I/O redirection occurs.
229
232
230 The intent of this context manager is to capture stdio output
233 The intent of this context manager is to capture stdio output
231 so it may be sent in the response. Some transports support streaming
234 so it may be sent in the response. Some transports support streaming
232 stdio to the client in real time. For these transports, stdio output
235 stdio to the client in real time. For these transports, stdio output
233 won't be captured.
236 won't be captured.
234 """
237 """
235
238
236 def client(self):
239 def client(self):
237 """Returns a string representation of this client (as bytes)."""
240 """Returns a string representation of this client (as bytes)."""
238
241
239 def addcapabilities(self, repo, caps):
242 def addcapabilities(self, repo, caps):
240 """Adds advertised capabilities specific to this protocol.
243 """Adds advertised capabilities specific to this protocol.
241
244
242 Receives the list of capabilities collected so far.
245 Receives the list of capabilities collected so far.
243
246
244 Returns a list of capabilities. The passed in argument can be returned.
247 Returns a list of capabilities. The passed in argument can be returned.
245 """
248 """
246
249
247 def checkperm(self, perm):
250 def checkperm(self, perm):
248 """Validate that the client has permissions to perform a request.
251 """Validate that the client has permissions to perform a request.
249
252
250 The argument is the permission required to proceed. If the client
253 The argument is the permission required to proceed. If the client
251 doesn't have that permission, the exception should raise or abort
254 doesn't have that permission, the exception should raise or abort
252 in a protocol specific manner.
255 in a protocol specific manner.
253 """
256 """
254
257
255
258
256 class commandentry:
259 class commandentry:
257 """Represents a declared wire protocol command."""
260 """Represents a declared wire protocol command."""
258
261
259 def __init__(
262 def __init__(
260 self,
263 self,
261 func,
264 func,
262 args=b'',
265 args=b'',
263 transports=None,
266 transports=None,
264 permission=b'push',
267 permission=b'push',
265 cachekeyfn=None,
268 cachekeyfn=None,
266 extracapabilitiesfn=None,
269 extracapabilitiesfn=None,
267 ):
270 ):
268 self.func = func
271 self.func = func
269 self.args = args
272 self.args = args
270 self.transports = transports or set()
273 self.transports = transports or set()
271 self.permission = permission
274 self.permission = permission
272 self.cachekeyfn = cachekeyfn
275 self.cachekeyfn = cachekeyfn
273 self.extracapabilitiesfn = extracapabilitiesfn
276 self.extracapabilitiesfn = extracapabilitiesfn
274
277
275 def _merge(self, func, args):
278 def _merge(self, func, args):
276 """Merge this instance with an incoming 2-tuple.
279 """Merge this instance with an incoming 2-tuple.
277
280
278 This is called when a caller using the old 2-tuple API attempts
281 This is called when a caller using the old 2-tuple API attempts
279 to replace an instance. The incoming values are merged with
282 to replace an instance. The incoming values are merged with
280 data not captured by the 2-tuple and a new instance containing
283 data not captured by the 2-tuple and a new instance containing
281 the union of the two objects is returned.
284 the union of the two objects is returned.
282 """
285 """
283 return commandentry(
286 return commandentry(
284 func,
287 func,
285 args=args,
288 args=args,
286 transports=set(self.transports),
289 transports=set(self.transports),
287 permission=self.permission,
290 permission=self.permission,
288 )
291 )
289
292
290 # Old code treats instances as 2-tuples. So expose that interface.
293 # Old code treats instances as 2-tuples. So expose that interface.
291 def __iter__(self):
294 def __iter__(self):
292 yield self.func
295 yield self.func
293 yield self.args
296 yield self.args
294
297
295 def __getitem__(self, i):
298 def __getitem__(self, i):
296 if i == 0:
299 if i == 0:
297 return self.func
300 return self.func
298 elif i == 1:
301 elif i == 1:
299 return self.args
302 return self.args
300 else:
303 else:
301 raise IndexError(b'can only access elements 0 and 1')
304 raise IndexError(b'can only access elements 0 and 1')
302
305
303
306
304 class commanddict(dict):
307 class commanddict(dict):
305 """Container for registered wire protocol commands.
308 """Container for registered wire protocol commands.
306
309
307 It behaves like a dict. But __setitem__ is overwritten to allow silent
310 It behaves like a dict. But __setitem__ is overwritten to allow silent
308 coercion of values from 2-tuples for API compatibility.
311 coercion of values from 2-tuples for API compatibility.
309 """
312 """
310
313
311 def __setitem__(self, k, v):
314 def __setitem__(self, k, v):
312 if isinstance(v, commandentry):
315 if isinstance(v, commandentry):
313 pass
316 pass
314 # Cast 2-tuples to commandentry instances.
317 # Cast 2-tuples to commandentry instances.
315 elif isinstance(v, tuple):
318 elif isinstance(v, tuple):
316 if len(v) != 2:
319 if len(v) != 2:
317 raise ValueError(b'command tuples must have exactly 2 elements')
320 raise ValueError(b'command tuples must have exactly 2 elements')
318
321
319 # It is common for extensions to wrap wire protocol commands via
322 # It is common for extensions to wrap wire protocol commands via
320 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
323 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
321 # doing this aren't aware of the new API that uses objects to store
324 # doing this aren't aware of the new API that uses objects to store
322 # command entries, we automatically merge old state with new.
325 # command entries, we automatically merge old state with new.
323 if k in self:
326 if k in self:
324 v = self[k]._merge(v[0], v[1])
327 v = self[k]._merge(v[0], v[1])
325 else:
328 else:
326 # Use default values from @wireprotocommand.
329 # Use default values from @wireprotocommand.
327 v = commandentry(
330 v = commandentry(
328 v[0],
331 v[0],
329 args=v[1],
332 args=v[1],
330 transports=set(TRANSPORTS),
333 transports=set(TRANSPORTS),
331 permission=b'push',
334 permission=b'push',
332 )
335 )
333 else:
336 else:
334 raise ValueError(
337 raise ValueError(
335 b'command entries must be commandentry instances '
338 b'command entries must be commandentry instances '
336 b'or 2-tuples'
339 b'or 2-tuples'
337 )
340 )
338
341
339 return super(commanddict, self).__setitem__(k, v)
342 return super(commanddict, self).__setitem__(k, v)
340
343
341 def commandavailable(self, command, proto):
344 def commandavailable(self, command, proto):
342 """Determine if a command is available for the requested protocol."""
345 """Determine if a command is available for the requested protocol."""
343 assert proto.name in TRANSPORTS
346 assert proto.name in TRANSPORTS
344
347
345 entry = self.get(command)
348 entry = self.get(command)
346
349
347 if not entry:
350 if not entry:
348 return False
351 return False
349
352
350 if proto.name not in entry.transports:
353 if proto.name not in entry.transports:
351 return False
354 return False
352
355
353 return True
356 return True
354
357
355
358
356 def supportedcompengines(ui, role):
359 def supportedcompengines(ui, role):
357 """Obtain the list of supported compression engines for a request."""
360 """Obtain the list of supported compression engines for a request."""
358 assert role in (compression.CLIENTROLE, compression.SERVERROLE)
361 assert role in (compression.CLIENTROLE, compression.SERVERROLE)
359
362
360 compengines = compression.compengines.supportedwireengines(role)
363 compengines = compression.compengines.supportedwireengines(role)
361
364
362 # Allow config to override default list and ordering.
365 # Allow config to override default list and ordering.
363 if role == compression.SERVERROLE:
366 if role == compression.SERVERROLE:
364 configengines = ui.configlist(b'server', b'compressionengines')
367 configengines = ui.configlist(b'server', b'compressionengines')
365 config = b'server.compressionengines'
368 config = b'server.compressionengines'
366 else:
369 else:
367 # This is currently implemented mainly to facilitate testing. In most
370 # This is currently implemented mainly to facilitate testing. In most
368 # cases, the server should be in charge of choosing a compression engine
371 # cases, the server should be in charge of choosing a compression engine
369 # because a server has the most to lose from a sub-optimal choice. (e.g.
372 # because a server has the most to lose from a sub-optimal choice. (e.g.
370 # CPU DoS due to an expensive engine or a network DoS due to poor
373 # CPU DoS due to an expensive engine or a network DoS due to poor
371 # compression ratio).
374 # compression ratio).
372 configengines = ui.configlist(
375 configengines = ui.configlist(
373 b'experimental', b'clientcompressionengines'
376 b'experimental', b'clientcompressionengines'
374 )
377 )
375 config = b'experimental.clientcompressionengines'
378 config = b'experimental.clientcompressionengines'
376
379
377 # No explicit config. Filter out the ones that aren't supposed to be
380 # No explicit config. Filter out the ones that aren't supposed to be
378 # advertised and return default ordering.
381 # advertised and return default ordering.
379 if not configengines:
382 if not configengines:
380 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
383 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
381 return [
384 return [
382 e for e in compengines if getattr(e.wireprotosupport(), attr) > 0
385 e for e in compengines if getattr(e.wireprotosupport(), attr) > 0
383 ]
386 ]
384
387
385 # If compression engines are listed in the config, assume there is a good
388 # If compression engines are listed in the config, assume there is a good
386 # reason for it (like server operators wanting to achieve specific
389 # reason for it (like server operators wanting to achieve specific
387 # performance characteristics). So fail fast if the config references
390 # performance characteristics). So fail fast if the config references
388 # unusable compression engines.
391 # unusable compression engines.
389 validnames = {e.name() for e in compengines}
392 validnames = {e.name() for e in compengines}
390 invalidnames = {e for e in configengines if e not in validnames}
393 invalidnames = {e for e in configengines if e not in validnames}
391 if invalidnames:
394 if invalidnames:
392 raise error.Abort(
395 raise error.Abort(
393 _(b'invalid compression engine defined in %s: %s')
396 _(b'invalid compression engine defined in %s: %s')
394 % (config, b', '.join(sorted(invalidnames)))
397 % (config, b', '.join(sorted(invalidnames)))
395 )
398 )
396
399
397 compengines = [e for e in compengines if e.name() in configengines]
400 compengines = [e for e in compengines if e.name() in configengines]
398 compengines = sorted(
401 compengines = sorted(
399 compengines, key=lambda e: configengines.index(e.name())
402 compengines, key=lambda e: configengines.index(e.name())
400 )
403 )
401
404
402 if not compengines:
405 if not compengines:
403 raise error.Abort(
406 raise error.Abort(
404 _(
407 _(
405 b'%s config option does not specify any known '
408 b'%s config option does not specify any known '
406 b'compression engines'
409 b'compression engines'
407 )
410 )
408 % config,
411 % config,
409 hint=_(b'usable compression engines: %s')
412 hint=_(b'usable compression engines: %s')
410 % b', '.sorted(validnames), # pytype: disable=attribute-error
413 % b', '.sorted(validnames), # pytype: disable=attribute-error
411 )
414 )
412
415
413 return compengines
416 return compengines
414
417
415
418
416 @attr.s
419 @attr.s
417 class encodedresponse:
420 class encodedresponse:
418 """Represents response data that is already content encoded.
421 """Represents response data that is already content encoded.
419
422
420 Wire protocol version 2 only.
423 Wire protocol version 2 only.
421
424
422 Commands typically emit Python objects that are encoded and sent over the
425 Commands typically emit Python objects that are encoded and sent over the
423 wire. If commands emit an object of this type, the encoding step is bypassed
426 wire. If commands emit an object of this type, the encoding step is bypassed
424 and the content from this object is used instead.
427 and the content from this object is used instead.
425 """
428 """
426
429
427 data = attr.ib()
430 data = attr.ib()
428
431
429
432
430 @attr.s
433 @attr.s
431 class alternatelocationresponse:
434 class alternatelocationresponse:
432 """Represents a response available at an alternate location.
435 """Represents a response available at an alternate location.
433
436
434 Instances are sent in place of actual response objects when the server
437 Instances are sent in place of actual response objects when the server
435 is sending a "content redirect" response.
438 is sending a "content redirect" response.
436
439
437 Only compatible with wire protocol version 2.
440 Only compatible with wire protocol version 2.
438 """
441 """
439
442
440 url = attr.ib()
443 url = attr.ib()
441 mediatype = attr.ib()
444 mediatype = attr.ib()
442 size = attr.ib(default=None)
445 size = attr.ib(default=None)
443 fullhashes = attr.ib(default=None)
446 fullhashes = attr.ib(default=None)
444 fullhashseed = attr.ib(default=None)
447 fullhashseed = attr.ib(default=None)
445 serverdercerts = attr.ib(default=None)
448 serverdercerts = attr.ib(default=None)
446 servercadercerts = attr.ib(default=None)
449 servercadercerts = attr.ib(default=None)
447
450
448
451
449 @attr.s
452 @attr.s
450 class indefinitebytestringresponse:
453 class indefinitebytestringresponse:
451 """Represents an object to be encoded to an indefinite length bytestring.
454 """Represents an object to be encoded to an indefinite length bytestring.
452
455
453 Instances are initialized from an iterable of chunks, with each chunk being
456 Instances are initialized from an iterable of chunks, with each chunk being
454 a bytes instance.
457 a bytes instance.
455 """
458 """
456
459
457 chunks = attr.ib()
460 chunks = attr.ib()
General Comments 0
You need to be logged in to leave comments. Login now