##// END OF EJS Templates
sshpeer: clean up API for sshpeer.__init__ (API)...
Gregory Szorc -
r35954:f8f03434 default
parent child Browse files
Show More
@@ -1,391 +1,398 b''
1 # sshpeer.py - ssh repository proxy class for mercurial
1 # sshpeer.py - ssh repository proxy class for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import re
10 import re
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 error,
14 error,
15 pycompat,
15 pycompat,
16 util,
16 util,
17 wireproto,
17 wireproto,
18 )
18 )
19
19
20 def _serverquote(s):
20 def _serverquote(s):
21 """quote a string for the remote shell ... which we assume is sh"""
21 """quote a string for the remote shell ... which we assume is sh"""
22 if not s:
22 if not s:
23 return s
23 return s
24 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
24 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
25 return s
25 return s
26 return "'%s'" % s.replace("'", "'\\''")
26 return "'%s'" % s.replace("'", "'\\''")
27
27
28 def _forwardoutput(ui, pipe):
28 def _forwardoutput(ui, pipe):
29 """display all data currently available on pipe as remote output.
29 """display all data currently available on pipe as remote output.
30
30
31 This is non blocking."""
31 This is non blocking."""
32 s = util.readpipe(pipe)
32 s = util.readpipe(pipe)
33 if s:
33 if s:
34 for l in s.splitlines():
34 for l in s.splitlines():
35 ui.status(_("remote: "), l, '\n')
35 ui.status(_("remote: "), l, '\n')
36
36
37 class doublepipe(object):
37 class doublepipe(object):
38 """Operate a side-channel pipe in addition of a main one
38 """Operate a side-channel pipe in addition of a main one
39
39
40 The side-channel pipe contains server output to be forwarded to the user
40 The side-channel pipe contains server output to be forwarded to the user
41 input. The double pipe will behave as the "main" pipe, but will ensure the
41 input. The double pipe will behave as the "main" pipe, but will ensure the
42 content of the "side" pipe is properly processed while we wait for blocking
42 content of the "side" pipe is properly processed while we wait for blocking
43 call on the "main" pipe.
43 call on the "main" pipe.
44
44
45 If large amounts of data are read from "main", the forward will cease after
45 If large amounts of data are read from "main", the forward will cease after
46 the first bytes start to appear. This simplifies the implementation
46 the first bytes start to appear. This simplifies the implementation
47 without affecting actual output of sshpeer too much as we rarely issue
47 without affecting actual output of sshpeer too much as we rarely issue
48 large read for data not yet emitted by the server.
48 large read for data not yet emitted by the server.
49
49
50 The main pipe is expected to be a 'bufferedinputpipe' from the util module
50 The main pipe is expected to be a 'bufferedinputpipe' from the util module
51 that handle all the os specific bits. This class lives in this module
51 that handle all the os specific bits. This class lives in this module
52 because it focus on behavior specific to the ssh protocol."""
52 because it focus on behavior specific to the ssh protocol."""
53
53
54 def __init__(self, ui, main, side):
54 def __init__(self, ui, main, side):
55 self._ui = ui
55 self._ui = ui
56 self._main = main
56 self._main = main
57 self._side = side
57 self._side = side
58
58
59 def _wait(self):
59 def _wait(self):
60 """wait until some data are available on main or side
60 """wait until some data are available on main or side
61
61
62 return a pair of boolean (ismainready, issideready)
62 return a pair of boolean (ismainready, issideready)
63
63
64 (This will only wait for data if the setup is supported by `util.poll`)
64 (This will only wait for data if the setup is supported by `util.poll`)
65 """
65 """
66 if getattr(self._main, 'hasbuffer', False): # getattr for classic pipe
66 if getattr(self._main, 'hasbuffer', False): # getattr for classic pipe
67 return (True, True) # main has data, assume side is worth poking at.
67 return (True, True) # main has data, assume side is worth poking at.
68 fds = [self._main.fileno(), self._side.fileno()]
68 fds = [self._main.fileno(), self._side.fileno()]
69 try:
69 try:
70 act = util.poll(fds)
70 act = util.poll(fds)
71 except NotImplementedError:
71 except NotImplementedError:
72 # non supported yet case, assume all have data.
72 # non supported yet case, assume all have data.
73 act = fds
73 act = fds
74 return (self._main.fileno() in act, self._side.fileno() in act)
74 return (self._main.fileno() in act, self._side.fileno() in act)
75
75
76 def write(self, data):
76 def write(self, data):
77 return self._call('write', data)
77 return self._call('write', data)
78
78
79 def read(self, size):
79 def read(self, size):
80 r = self._call('read', size)
80 r = self._call('read', size)
81 if size != 0 and not r:
81 if size != 0 and not r:
82 # We've observed a condition that indicates the
82 # We've observed a condition that indicates the
83 # stdout closed unexpectedly. Check stderr one
83 # stdout closed unexpectedly. Check stderr one
84 # more time and snag anything that's there before
84 # more time and snag anything that's there before
85 # letting anyone know the main part of the pipe
85 # letting anyone know the main part of the pipe
86 # closed prematurely.
86 # closed prematurely.
87 _forwardoutput(self._ui, self._side)
87 _forwardoutput(self._ui, self._side)
88 return r
88 return r
89
89
90 def readline(self):
90 def readline(self):
91 return self._call('readline')
91 return self._call('readline')
92
92
93 def _call(self, methname, data=None):
93 def _call(self, methname, data=None):
94 """call <methname> on "main", forward output of "side" while blocking
94 """call <methname> on "main", forward output of "side" while blocking
95 """
95 """
96 # data can be '' or 0
96 # data can be '' or 0
97 if (data is not None and not data) or self._main.closed:
97 if (data is not None and not data) or self._main.closed:
98 _forwardoutput(self._ui, self._side)
98 _forwardoutput(self._ui, self._side)
99 return ''
99 return ''
100 while True:
100 while True:
101 mainready, sideready = self._wait()
101 mainready, sideready = self._wait()
102 if sideready:
102 if sideready:
103 _forwardoutput(self._ui, self._side)
103 _forwardoutput(self._ui, self._side)
104 if mainready:
104 if mainready:
105 meth = getattr(self._main, methname)
105 meth = getattr(self._main, methname)
106 if data is None:
106 if data is None:
107 return meth()
107 return meth()
108 else:
108 else:
109 return meth(data)
109 return meth(data)
110
110
111 def close(self):
111 def close(self):
112 return self._main.close()
112 return self._main.close()
113
113
114 def flush(self):
114 def flush(self):
115 return self._main.flush()
115 return self._main.flush()
116
116
117 def _cleanuppipes(ui, pipei, pipeo, pipee):
117 def _cleanuppipes(ui, pipei, pipeo, pipee):
118 """Clean up pipes used by an SSH connection."""
118 """Clean up pipes used by an SSH connection."""
119 if pipeo:
119 if pipeo:
120 pipeo.close()
120 pipeo.close()
121 if pipei:
121 if pipei:
122 pipei.close()
122 pipei.close()
123
123
124 if pipee:
124 if pipee:
125 # Try to read from the err descriptor until EOF.
125 # Try to read from the err descriptor until EOF.
126 try:
126 try:
127 for l in pipee:
127 for l in pipee:
128 ui.status(_('remote: '), l)
128 ui.status(_('remote: '), l)
129 except (IOError, ValueError):
129 except (IOError, ValueError):
130 pass
130 pass
131
131
132 pipee.close()
132 pipee.close()
133
133
134 def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
134 def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
135 """Create an SSH connection to a server.
135 """Create an SSH connection to a server.
136
136
137 Returns a tuple of (process, stdin, stdout, stderr) for the
137 Returns a tuple of (process, stdin, stdout, stderr) for the
138 spawned process.
138 spawned process.
139 """
139 """
140 cmd = '%s %s %s' % (
140 cmd = '%s %s %s' % (
141 sshcmd,
141 sshcmd,
142 args,
142 args,
143 util.shellquote('%s -R %s serve --stdio' % (
143 util.shellquote('%s -R %s serve --stdio' % (
144 _serverquote(remotecmd), _serverquote(path))))
144 _serverquote(remotecmd), _serverquote(path))))
145
145
146 ui.debug('running %s\n' % cmd)
146 ui.debug('running %s\n' % cmd)
147 cmd = util.quotecommand(cmd)
147 cmd = util.quotecommand(cmd)
148
148
149 # no buffer allow the use of 'select'
149 # no buffer allow the use of 'select'
150 # feel free to remove buffering and select usage when we ultimately
150 # feel free to remove buffering and select usage when we ultimately
151 # move to threading.
151 # move to threading.
152 stdin, stdout, stderr, proc = util.popen4(cmd, bufsize=0, env=sshenv)
152 stdin, stdout, stderr, proc = util.popen4(cmd, bufsize=0, env=sshenv)
153
153
154 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
154 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
155 stdin = doublepipe(ui, stdin, stderr)
155 stdin = doublepipe(ui, stdin, stderr)
156
156
157 return proc, stdin, stdout, stderr
157 return proc, stdin, stdout, stderr
158
158
159 class sshpeer(wireproto.wirepeer):
159 class sshpeer(wireproto.wirepeer):
160 def __init__(self, ui, path, create=False, sshstate=None):
160 def __init__(self, ui, url, proc, stdin, stdout, stderr):
161 self._url = path
161 """Create a peer from an existing SSH connection.
162
163 ``proc`` is a handle on the underlying SSH process.
164 ``stdin``, ``stdout``, and ``stderr`` are handles on the stdio
165 pipes for that process.
166 """
167 self._url = url
162 self._ui = ui
168 self._ui = ui
163 # self._subprocess is unused. Keeping a handle on the process
169 # self._subprocess is unused. Keeping a handle on the process
164 # holds a reference and prevents it from being garbage collected.
170 # holds a reference and prevents it from being garbage collected.
165 self._subprocess, self._pipei, self._pipeo, self._pipee = sshstate
171 self._subprocess = proc
172 self._pipeo = stdin
173 self._pipei = stdout
174 self._pipee = stderr
166
175
167 self._validaterepo()
176 self._validaterepo()
168
177
169 # Begin of _basepeer interface.
178 # Begin of _basepeer interface.
170
179
171 @util.propertycache
180 @util.propertycache
172 def ui(self):
181 def ui(self):
173 return self._ui
182 return self._ui
174
183
175 def url(self):
184 def url(self):
176 return self._url
185 return self._url
177
186
178 def local(self):
187 def local(self):
179 return None
188 return None
180
189
181 def peer(self):
190 def peer(self):
182 return self
191 return self
183
192
184 def canpush(self):
193 def canpush(self):
185 return True
194 return True
186
195
187 def close(self):
196 def close(self):
188 pass
197 pass
189
198
190 # End of _basepeer interface.
199 # End of _basepeer interface.
191
200
192 # Begin of _basewirecommands interface.
201 # Begin of _basewirecommands interface.
193
202
194 def capabilities(self):
203 def capabilities(self):
195 return self._caps
204 return self._caps
196
205
197 # End of _basewirecommands interface.
206 # End of _basewirecommands interface.
198
207
199 def _validaterepo(self):
208 def _validaterepo(self):
200 def badresponse():
209 def badresponse():
201 msg = _("no suitable response from remote hg")
210 msg = _("no suitable response from remote hg")
202 hint = self.ui.config("ui", "ssherrorhint")
211 hint = self.ui.config("ui", "ssherrorhint")
203 self._abort(error.RepoError(msg, hint=hint))
212 self._abort(error.RepoError(msg, hint=hint))
204
213
205 try:
214 try:
206 # skip any noise generated by remote shell
215 # skip any noise generated by remote shell
207 self._callstream("hello")
216 self._callstream("hello")
208 r = self._callstream("between", pairs=("%s-%s" % ("0"*40, "0"*40)))
217 r = self._callstream("between", pairs=("%s-%s" % ("0"*40, "0"*40)))
209 except IOError:
218 except IOError:
210 badresponse()
219 badresponse()
211
220
212 lines = ["", "dummy"]
221 lines = ["", "dummy"]
213 max_noise = 500
222 max_noise = 500
214 while lines[-1] and max_noise:
223 while lines[-1] and max_noise:
215 try:
224 try:
216 l = r.readline()
225 l = r.readline()
217 self._readerr()
226 self._readerr()
218 if lines[-1] == "1\n" and l == "\n":
227 if lines[-1] == "1\n" and l == "\n":
219 break
228 break
220 if l:
229 if l:
221 self.ui.debug("remote: ", l)
230 self.ui.debug("remote: ", l)
222 lines.append(l)
231 lines.append(l)
223 max_noise -= 1
232 max_noise -= 1
224 except IOError:
233 except IOError:
225 badresponse()
234 badresponse()
226 else:
235 else:
227 badresponse()
236 badresponse()
228
237
229 self._caps = set()
238 self._caps = set()
230 for l in reversed(lines):
239 for l in reversed(lines):
231 if l.startswith("capabilities:"):
240 if l.startswith("capabilities:"):
232 self._caps.update(l[:-1].split(":")[1].split())
241 self._caps.update(l[:-1].split(":")[1].split())
233 break
242 break
234
243
235 def _readerr(self):
244 def _readerr(self):
236 _forwardoutput(self.ui, self._pipee)
245 _forwardoutput(self.ui, self._pipee)
237
246
238 def _abort(self, exception):
247 def _abort(self, exception):
239 self._cleanup()
248 self._cleanup()
240 raise exception
249 raise exception
241
250
242 def _cleanup(self):
251 def _cleanup(self):
243 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee)
252 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee)
244
253
245 __del__ = _cleanup
254 __del__ = _cleanup
246
255
247 def _submitbatch(self, req):
256 def _submitbatch(self, req):
248 rsp = self._callstream("batch", cmds=wireproto.encodebatchcmds(req))
257 rsp = self._callstream("batch", cmds=wireproto.encodebatchcmds(req))
249 available = self._getamount()
258 available = self._getamount()
250 # TODO this response parsing is probably suboptimal for large
259 # TODO this response parsing is probably suboptimal for large
251 # batches with large responses.
260 # batches with large responses.
252 toread = min(available, 1024)
261 toread = min(available, 1024)
253 work = rsp.read(toread)
262 work = rsp.read(toread)
254 available -= toread
263 available -= toread
255 chunk = work
264 chunk = work
256 while chunk:
265 while chunk:
257 while ';' in work:
266 while ';' in work:
258 one, work = work.split(';', 1)
267 one, work = work.split(';', 1)
259 yield wireproto.unescapearg(one)
268 yield wireproto.unescapearg(one)
260 toread = min(available, 1024)
269 toread = min(available, 1024)
261 chunk = rsp.read(toread)
270 chunk = rsp.read(toread)
262 available -= toread
271 available -= toread
263 work += chunk
272 work += chunk
264 yield wireproto.unescapearg(work)
273 yield wireproto.unescapearg(work)
265
274
266 def _callstream(self, cmd, **args):
275 def _callstream(self, cmd, **args):
267 args = pycompat.byteskwargs(args)
276 args = pycompat.byteskwargs(args)
268 if (self.ui.debugflag
277 if (self.ui.debugflag
269 and self.ui.configbool('devel', 'debug.peer-request')):
278 and self.ui.configbool('devel', 'debug.peer-request')):
270 dbg = self.ui.debug
279 dbg = self.ui.debug
271 line = 'devel-peer-request: %s\n'
280 line = 'devel-peer-request: %s\n'
272 dbg(line % cmd)
281 dbg(line % cmd)
273 for key, value in sorted(args.items()):
282 for key, value in sorted(args.items()):
274 if not isinstance(value, dict):
283 if not isinstance(value, dict):
275 dbg(line % ' %s: %d bytes' % (key, len(value)))
284 dbg(line % ' %s: %d bytes' % (key, len(value)))
276 else:
285 else:
277 for dk, dv in sorted(value.items()):
286 for dk, dv in sorted(value.items()):
278 dbg(line % ' %s-%s: %d' % (key, dk, len(dv)))
287 dbg(line % ' %s-%s: %d' % (key, dk, len(dv)))
279 self.ui.debug("sending %s command\n" % cmd)
288 self.ui.debug("sending %s command\n" % cmd)
280 self._pipeo.write("%s\n" % cmd)
289 self._pipeo.write("%s\n" % cmd)
281 _func, names = wireproto.commands[cmd]
290 _func, names = wireproto.commands[cmd]
282 keys = names.split()
291 keys = names.split()
283 wireargs = {}
292 wireargs = {}
284 for k in keys:
293 for k in keys:
285 if k == '*':
294 if k == '*':
286 wireargs['*'] = args
295 wireargs['*'] = args
287 break
296 break
288 else:
297 else:
289 wireargs[k] = args[k]
298 wireargs[k] = args[k]
290 del args[k]
299 del args[k]
291 for k, v in sorted(wireargs.iteritems()):
300 for k, v in sorted(wireargs.iteritems()):
292 self._pipeo.write("%s %d\n" % (k, len(v)))
301 self._pipeo.write("%s %d\n" % (k, len(v)))
293 if isinstance(v, dict):
302 if isinstance(v, dict):
294 for dk, dv in v.iteritems():
303 for dk, dv in v.iteritems():
295 self._pipeo.write("%s %d\n" % (dk, len(dv)))
304 self._pipeo.write("%s %d\n" % (dk, len(dv)))
296 self._pipeo.write(dv)
305 self._pipeo.write(dv)
297 else:
306 else:
298 self._pipeo.write(v)
307 self._pipeo.write(v)
299 self._pipeo.flush()
308 self._pipeo.flush()
300
309
301 return self._pipei
310 return self._pipei
302
311
303 def _callcompressable(self, cmd, **args):
312 def _callcompressable(self, cmd, **args):
304 return self._callstream(cmd, **args)
313 return self._callstream(cmd, **args)
305
314
306 def _call(self, cmd, **args):
315 def _call(self, cmd, **args):
307 self._callstream(cmd, **args)
316 self._callstream(cmd, **args)
308 return self._recv()
317 return self._recv()
309
318
310 def _callpush(self, cmd, fp, **args):
319 def _callpush(self, cmd, fp, **args):
311 r = self._call(cmd, **args)
320 r = self._call(cmd, **args)
312 if r:
321 if r:
313 return '', r
322 return '', r
314 for d in iter(lambda: fp.read(4096), ''):
323 for d in iter(lambda: fp.read(4096), ''):
315 self._send(d)
324 self._send(d)
316 self._send("", flush=True)
325 self._send("", flush=True)
317 r = self._recv()
326 r = self._recv()
318 if r:
327 if r:
319 return '', r
328 return '', r
320 return self._recv(), ''
329 return self._recv(), ''
321
330
322 def _calltwowaystream(self, cmd, fp, **args):
331 def _calltwowaystream(self, cmd, fp, **args):
323 r = self._call(cmd, **args)
332 r = self._call(cmd, **args)
324 if r:
333 if r:
325 # XXX needs to be made better
334 # XXX needs to be made better
326 raise error.Abort(_('unexpected remote reply: %s') % r)
335 raise error.Abort(_('unexpected remote reply: %s') % r)
327 for d in iter(lambda: fp.read(4096), ''):
336 for d in iter(lambda: fp.read(4096), ''):
328 self._send(d)
337 self._send(d)
329 self._send("", flush=True)
338 self._send("", flush=True)
330 return self._pipei
339 return self._pipei
331
340
332 def _getamount(self):
341 def _getamount(self):
333 l = self._pipei.readline()
342 l = self._pipei.readline()
334 if l == '\n':
343 if l == '\n':
335 self._readerr()
344 self._readerr()
336 msg = _('check previous remote output')
345 msg = _('check previous remote output')
337 self._abort(error.OutOfBandError(hint=msg))
346 self._abort(error.OutOfBandError(hint=msg))
338 self._readerr()
347 self._readerr()
339 try:
348 try:
340 return int(l)
349 return int(l)
341 except ValueError:
350 except ValueError:
342 self._abort(error.ResponseError(_("unexpected response:"), l))
351 self._abort(error.ResponseError(_("unexpected response:"), l))
343
352
344 def _recv(self):
353 def _recv(self):
345 return self._pipei.read(self._getamount())
354 return self._pipei.read(self._getamount())
346
355
347 def _send(self, data, flush=False):
356 def _send(self, data, flush=False):
348 self._pipeo.write("%d\n" % len(data))
357 self._pipeo.write("%d\n" % len(data))
349 if data:
358 if data:
350 self._pipeo.write(data)
359 self._pipeo.write(data)
351 if flush:
360 if flush:
352 self._pipeo.flush()
361 self._pipeo.flush()
353 self._readerr()
362 self._readerr()
354
363
355 def instance(ui, path, create):
364 def instance(ui, path, create):
356 """Create an SSH peer.
365 """Create an SSH peer.
357
366
358 The returned object conforms to the ``wireproto.wirepeer`` interface.
367 The returned object conforms to the ``wireproto.wirepeer`` interface.
359 """
368 """
360 u = util.url(path, parsequery=False, parsefragment=False)
369 u = util.url(path, parsequery=False, parsefragment=False)
361 if u.scheme != 'ssh' or not u.host or u.path is None:
370 if u.scheme != 'ssh' or not u.host or u.path is None:
362 raise error.RepoError(_("couldn't parse location %s") % path)
371 raise error.RepoError(_("couldn't parse location %s") % path)
363
372
364 util.checksafessh(path)
373 util.checksafessh(path)
365
374
366 if u.passwd is not None:
375 if u.passwd is not None:
367 raise error.RepoError(_('password in URL not supported'))
376 raise error.RepoError(_('password in URL not supported'))
368
377
369 sshcmd = ui.config('ui', 'ssh')
378 sshcmd = ui.config('ui', 'ssh')
370 remotecmd = ui.config('ui', 'remotecmd')
379 remotecmd = ui.config('ui', 'remotecmd')
371 sshaddenv = dict(ui.configitems('sshenv'))
380 sshaddenv = dict(ui.configitems('sshenv'))
372 sshenv = util.shellenviron(sshaddenv)
381 sshenv = util.shellenviron(sshaddenv)
373 remotepath = u.path or '.'
382 remotepath = u.path or '.'
374
383
375 args = util.sshargs(sshcmd, u.host, u.user, u.port)
384 args = util.sshargs(sshcmd, u.host, u.user, u.port)
376
385
377 if create:
386 if create:
378 cmd = '%s %s %s' % (sshcmd, args,
387 cmd = '%s %s %s' % (sshcmd, args,
379 util.shellquote('%s init %s' %
388 util.shellquote('%s init %s' %
380 (_serverquote(remotecmd), _serverquote(remotepath))))
389 (_serverquote(remotecmd), _serverquote(remotepath))))
381 ui.debug('running %s\n' % cmd)
390 ui.debug('running %s\n' % cmd)
382 res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv)
391 res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv)
383 if res != 0:
392 if res != 0:
384 raise error.RepoError(_('could not create remote repo'))
393 raise error.RepoError(_('could not create remote repo'))
385
394
386 proc, stdin, stdout, stderr = _makeconnection(ui, sshcmd, args, remotecmd,
395 proc, stdin, stdout, stderr = _makeconnection(ui, sshcmd, args, remotecmd,
387 remotepath, sshenv)
396 remotepath, sshenv)
388
397
389 sshstate = (proc, stdout, stdin, stderr)
398 return sshpeer(ui, path, proc, stdin, stdout, stderr)
390
391 return sshpeer(ui, path, create=create, sshstate=sshstate)
@@ -1,78 +1,78 b''
1 # Test that certain objects conform to well-defined interfaces.
1 # Test that certain objects conform to well-defined interfaces.
2
2
3 from __future__ import absolute_import, print_function
3 from __future__ import absolute_import, print_function
4
4
5 from mercurial import (
5 from mercurial import (
6 bundlerepo,
6 bundlerepo,
7 httppeer,
7 httppeer,
8 localrepo,
8 localrepo,
9 sshpeer,
9 sshpeer,
10 statichttprepo,
10 statichttprepo,
11 ui as uimod,
11 ui as uimod,
12 unionrepo,
12 unionrepo,
13 )
13 )
14
14
15 def checkobject(o):
15 def checkobject(o):
16 """Verify a constructed object conforms to interface rules.
16 """Verify a constructed object conforms to interface rules.
17
17
18 An object must have __abstractmethods__ defined.
18 An object must have __abstractmethods__ defined.
19
19
20 All "public" attributes of the object (attributes not prefixed with
20 All "public" attributes of the object (attributes not prefixed with
21 an underscore) must be in __abstractmethods__ or appear on a base class
21 an underscore) must be in __abstractmethods__ or appear on a base class
22 with __abstractmethods__.
22 with __abstractmethods__.
23 """
23 """
24 name = o.__class__.__name__
24 name = o.__class__.__name__
25
25
26 allowed = set()
26 allowed = set()
27 for cls in o.__class__.__mro__:
27 for cls in o.__class__.__mro__:
28 if not getattr(cls, '__abstractmethods__', set()):
28 if not getattr(cls, '__abstractmethods__', set()):
29 continue
29 continue
30
30
31 allowed |= cls.__abstractmethods__
31 allowed |= cls.__abstractmethods__
32 allowed |= {a for a in dir(cls) if not a.startswith('_')}
32 allowed |= {a for a in dir(cls) if not a.startswith('_')}
33
33
34 if not allowed:
34 if not allowed:
35 print('%s does not have abstract methods' % name)
35 print('%s does not have abstract methods' % name)
36 return
36 return
37
37
38 public = {a for a in dir(o) if not a.startswith('_')}
38 public = {a for a in dir(o) if not a.startswith('_')}
39
39
40 for attr in sorted(public - allowed):
40 for attr in sorted(public - allowed):
41 print('public attributes not in abstract interface: %s.%s' % (
41 print('public attributes not in abstract interface: %s.%s' % (
42 name, attr))
42 name, attr))
43
43
44 # Facilitates testing localpeer.
44 # Facilitates testing localpeer.
45 class dummyrepo(object):
45 class dummyrepo(object):
46 def __init__(self):
46 def __init__(self):
47 self.ui = uimod.ui()
47 self.ui = uimod.ui()
48 def filtered(self, name):
48 def filtered(self, name):
49 pass
49 pass
50 def _restrictcapabilities(self, caps):
50 def _restrictcapabilities(self, caps):
51 pass
51 pass
52
52
53 # Facilitates testing sshpeer without requiring an SSH server.
53 # Facilitates testing sshpeer without requiring an SSH server.
54 class testingsshpeer(sshpeer.sshpeer):
54 class testingsshpeer(sshpeer.sshpeer):
55 def _validaterepo(self, *args, **kwargs):
55 def _validaterepo(self, *args, **kwargs):
56 pass
56 pass
57
57
58 class badpeer(httppeer.httppeer):
58 class badpeer(httppeer.httppeer):
59 def __init__(self):
59 def __init__(self):
60 super(badpeer, self).__init__(uimod.ui(), 'http://localhost')
60 super(badpeer, self).__init__(uimod.ui(), 'http://localhost')
61 self.badattribute = True
61 self.badattribute = True
62
62
63 def badmethod(self):
63 def badmethod(self):
64 pass
64 pass
65
65
66 def main():
66 def main():
67 ui = uimod.ui()
67 ui = uimod.ui()
68
68
69 checkobject(badpeer())
69 checkobject(badpeer())
70 checkobject(httppeer.httppeer(ui, 'http://localhost'))
70 checkobject(httppeer.httppeer(ui, 'http://localhost'))
71 checkobject(localrepo.localpeer(dummyrepo()))
71 checkobject(localrepo.localpeer(dummyrepo()))
72 checkobject(testingsshpeer(ui, 'ssh://localhost/foo', False,
72 checkobject(testingsshpeer(ui, 'ssh://localhost/foo', None, None, None,
73 (None, None, None, None)))
73 None))
74 checkobject(bundlerepo.bundlepeer(dummyrepo()))
74 checkobject(bundlerepo.bundlepeer(dummyrepo()))
75 checkobject(statichttprepo.statichttppeer(dummyrepo()))
75 checkobject(statichttprepo.statichttppeer(dummyrepo()))
76 checkobject(unionrepo.unionpeer(dummyrepo()))
76 checkobject(unionrepo.unionpeer(dummyrepo()))
77
77
78 main()
78 main()
General Comments 0
You need to be logged in to leave comments. Login now