/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/transport/local.py

  • Committer: Jelmer Vernooij
  • Date: 2018-05-19 13:16:11 UTC
  • mto: (6968.4.3 git-archive)
  • mto: This revision was merged to the branch mainline in revision 6972.
  • Revision ID: jelmer@jelmer.uk-20180519131611-l9h9ud41j7qg1m03
Move tar/zip to breezy.archive.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2012, 2016 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Transport for the local filesystem.
 
18
 
 
19
This is a fairly thin wrapper on regular file IO.
 
20
"""
 
21
 
 
22
from __future__ import absolute_import
 
23
 
 
24
import os
 
25
from stat import ST_MODE, S_ISDIR, S_IMODE
 
26
import sys
 
27
 
 
28
from ..lazy_import import lazy_import
 
29
lazy_import(globals(), """
 
30
import errno
 
31
import shutil
 
32
 
 
33
from breezy import (
 
34
    atomicfile,
 
35
    osutils,
 
36
    urlutils,
 
37
    )
 
38
from breezy.transport import LateReadError
 
39
""")
 
40
 
 
41
from .. import transport
 
42
 
 
43
 
 
44
_append_flags = os.O_CREAT | os.O_APPEND | os.O_WRONLY | osutils.O_BINARY | osutils.O_NOINHERIT
 
45
_put_non_atomic_flags = os.O_CREAT | os.O_TRUNC | os.O_WRONLY | osutils.O_BINARY | osutils.O_NOINHERIT
 
46
 
 
47
 
 
48
class LocalTransport(transport.Transport):
 
49
    """This is the transport agent for local filesystem access."""
 
50
 
 
51
    def __init__(self, base):
 
52
        """Set the base path where files will be stored."""
 
53
        if not base.startswith('file://'):
 
54
            raise AssertionError("not a file:// url: %r" % base)
 
55
        if base[-1] != '/':
 
56
            base = base + '/'
 
57
 
 
58
        # Special case : windows has no "root", but does have
 
59
        # multiple lettered drives inside it. #240910
 
60
        if sys.platform == 'win32' and base == 'file:///':
 
61
            base = ''
 
62
            self._local_base = ''
 
63
            super(LocalTransport, self).__init__(base)
 
64
            return
 
65
 
 
66
        super(LocalTransport, self).__init__(base)
 
67
        self._local_base = urlutils.local_path_from_url(base)
 
68
        if self._local_base[-1] != '/':
 
69
            self._local_base = self._local_base + '/'
 
70
 
 
71
    def clone(self, offset=None):
 
72
        """Return a new LocalTransport with root at self.base + offset
 
73
        Because the local filesystem does not require a connection,
 
74
        we can just return a new object.
 
75
        """
 
76
        if offset is None:
 
77
            return LocalTransport(self.base)
 
78
        else:
 
79
            abspath = self.abspath(offset)
 
80
            if abspath == 'file://':
 
81
                # fix upwalk for UNC path
 
82
                # when clone from //HOST/path updir recursively
 
83
                # we should stop at least at //HOST part
 
84
                abspath = self.base
 
85
            return LocalTransport(abspath)
 
86
 
 
87
    def _abspath(self, relative_reference):
 
88
        """Return a path for use in os calls.
 
89
 
 
90
        Several assumptions are made:
 
91
         - relative_reference does not contain '..'
 
92
         - relative_reference is url escaped.
 
93
        """
 
94
        if relative_reference in ('.', ''):
 
95
            # _local_base normally has a trailing slash; strip it so that stat
 
96
            # on a transport pointing to a symlink reads the link not the
 
97
            # referent but be careful of / and c:\
 
98
            return osutils.split(self._local_base)[0]
 
99
        return self._local_base + urlutils.unescape(relative_reference)
 
100
 
 
101
    def abspath(self, relpath):
 
102
        """Return the full url to the given relative URL."""
 
103
        # TODO: url escape the result. RBC 20060523.
 
104
        # jam 20060426 Using normpath on the real path, because that ensures
 
105
        #       proper handling of stuff like
 
106
        path = osutils.normpath(osutils.pathjoin(
 
107
                    self._local_base, urlutils.unescape(relpath)))
 
108
        # on windows, our _local_base may or may not have a drive specified
 
109
        # (ie, it may be "/" or "c:/foo").
 
110
        # If 'relpath' is '/' we *always* get back an abspath without
 
111
        # the drive letter - but if our transport already has a drive letter,
 
112
        # we want our abspaths to have a drive letter too - so handle that
 
113
        # here.
 
114
        if (sys.platform == "win32" and self._local_base[1:2] == ":"
 
115
            and path == '/'):
 
116
            path = self._local_base[:3]
 
117
 
 
118
        return urlutils.local_path_to_url(path)
 
119
 
 
120
    def local_abspath(self, relpath):
 
121
        """Transform the given relative path URL into the actual path on disk
 
122
 
 
123
        This function only exists for the LocalTransport, since it is
 
124
        the only one that has direct local access.
 
125
        This is mostly for stuff like WorkingTree which needs to know
 
126
        the local working directory.  The returned path will always contain
 
127
        forward slashes as the path separator, regardless of the platform.
 
128
 
 
129
        This function is quite expensive: it calls realpath which resolves
 
130
        symlinks.
 
131
        """
 
132
        absurl = self.abspath(relpath)
 
133
        # mutter(u'relpath %s => base: %s, absurl %s', relpath, self.base, absurl)
 
134
        return urlutils.local_path_from_url(absurl)
 
135
 
 
136
    def relpath(self, abspath):
 
137
        """Return the local path portion from a given absolute path.
 
138
        """
 
139
        if abspath is None:
 
140
            abspath = u'.'
 
141
 
 
142
        return urlutils.file_relpath(self.base, abspath)
 
143
 
 
144
    def has(self, relpath):
 
145
        return os.access(self._abspath(relpath), os.F_OK)
 
146
 
 
147
    def get(self, relpath):
 
148
        """Get the file at the given relative path.
 
149
 
 
150
        :param relpath: The relative path to the file
 
151
        """
 
152
        canonical_url = self.abspath(relpath)
 
153
        if canonical_url in transport._file_streams:
 
154
            transport._file_streams[canonical_url].flush()
 
155
        try:
 
156
            path = self._abspath(relpath)
 
157
            return osutils.open_file(path, 'rb')
 
158
        except (IOError, OSError) as e:
 
159
            if e.errno == errno.EISDIR:
 
160
                return LateReadError(relpath)
 
161
            self._translate_error(e, path)
 
162
 
 
163
    def put_file(self, relpath, f, mode=None):
 
164
        """Copy the file-like object into the location.
 
165
 
 
166
        :param relpath: Location to put the contents, relative to base.
 
167
        :param f:       File-like object.
 
168
        :param mode: The mode for the newly created file,
 
169
                     None means just use the default
 
170
        """
 
171
 
 
172
        path = relpath
 
173
        try:
 
174
            path = self._abspath(relpath)
 
175
            osutils.check_legal_path(path)
 
176
            fp = atomicfile.AtomicFile(path, 'wb', new_mode=mode)
 
177
        except (IOError, OSError) as e:
 
178
            self._translate_error(e, path)
 
179
        try:
 
180
            length = self._pump(f, fp)
 
181
            fp.commit()
 
182
        finally:
 
183
            fp.close()
 
184
        return length
 
185
 
 
186
    def put_bytes(self, relpath, raw_bytes, mode=None):
 
187
        """Copy the string into the location.
 
188
 
 
189
        :param relpath: Location to put the contents, relative to base.
 
190
        :param raw_bytes:   String
 
191
        """
 
192
        if not isinstance(raw_bytes, bytes):
 
193
            raise TypeError(
 
194
                'raw_bytes must be bytes, not %s' % type(raw_bytes))
 
195
        path = relpath
 
196
        try:
 
197
            path = self._abspath(relpath)
 
198
            osutils.check_legal_path(path)
 
199
            fp = atomicfile.AtomicFile(path, 'wb', new_mode=mode)
 
200
        except (IOError, OSError) as e:
 
201
            self._translate_error(e, path)
 
202
        try:
 
203
            if bytes:
 
204
                fp.write(raw_bytes)
 
205
            fp.commit()
 
206
        finally:
 
207
            fp.close()
 
208
 
 
209
    def _put_non_atomic_helper(self, relpath, writer,
 
210
                               mode=None,
 
211
                               create_parent_dir=False,
 
212
                               dir_mode=None):
 
213
        """Common functionality information for the put_*_non_atomic.
 
214
 
 
215
        This tracks all the create_parent_dir stuff.
 
216
 
 
217
        :param relpath: the path we are putting to.
 
218
        :param writer: A function that takes an os level file descriptor
 
219
            and writes whatever data it needs to write there.
 
220
        :param mode: The final file mode.
 
221
        :param create_parent_dir: Should we be creating the parent directory
 
222
            if it doesn't exist?
 
223
        """
 
224
        abspath = self._abspath(relpath)
 
225
        if mode is None:
 
226
            # os.open() will automatically use the umask
 
227
            local_mode = 0o666
 
228
        else:
 
229
            local_mode = mode
 
230
        try:
 
231
            fd = os.open(abspath, _put_non_atomic_flags, local_mode)
 
232
        except (IOError, OSError) as e:
 
233
            # We couldn't create the file, maybe we need to create
 
234
            # the parent directory, and try again
 
235
            if (not create_parent_dir
 
236
                or e.errno not in (errno.ENOENT, errno.ENOTDIR)):
 
237
                self._translate_error(e, relpath)
 
238
            parent_dir = os.path.dirname(abspath)
 
239
            if not parent_dir:
 
240
                self._translate_error(e, relpath)
 
241
            self._mkdir(parent_dir, mode=dir_mode)
 
242
            # We created the parent directory, lets try to open the
 
243
            # file again
 
244
            try:
 
245
                fd = os.open(abspath, _put_non_atomic_flags, local_mode)
 
246
            except (IOError, OSError) as e:
 
247
                self._translate_error(e, relpath)
 
248
        try:
 
249
            st = os.fstat(fd)
 
250
            if mode is not None and mode != S_IMODE(st.st_mode):
 
251
                # Because of umask, we may still need to chmod the file.
 
252
                # But in the general case, we won't have to
 
253
                osutils.chmod_if_possible(abspath, mode)
 
254
            writer(fd)
 
255
        finally:
 
256
            os.close(fd)
 
257
 
 
258
    def put_file_non_atomic(self, relpath, f, mode=None,
 
259
                            create_parent_dir=False,
 
260
                            dir_mode=None):
 
261
        """Copy the file-like object into the target location.
 
262
 
 
263
        This function is not strictly safe to use. It is only meant to
 
264
        be used when you already know that the target does not exist.
 
265
        It is not safe, because it will open and truncate the remote
 
266
        file. So there may be a time when the file has invalid contents.
 
267
 
 
268
        :param relpath: The remote location to put the contents.
 
269
        :param f:       File-like object.
 
270
        :param mode:    Possible access permissions for new file.
 
271
                        None means do not set remote permissions.
 
272
        :param create_parent_dir: If we cannot create the target file because
 
273
                        the parent directory does not exist, go ahead and
 
274
                        create it, and then try again.
 
275
        """
 
276
        def writer(fd):
 
277
            self._pump_to_fd(f, fd)
 
278
        self._put_non_atomic_helper(relpath, writer, mode=mode,
 
279
                                    create_parent_dir=create_parent_dir,
 
280
                                    dir_mode=dir_mode)
 
281
 
 
282
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
283
                             create_parent_dir=False, dir_mode=None):
 
284
        def writer(fd):
 
285
            if bytes:
 
286
                os.write(fd, bytes)
 
287
        self._put_non_atomic_helper(relpath, writer, mode=mode,
 
288
                                    create_parent_dir=create_parent_dir,
 
289
                                    dir_mode=dir_mode)
 
290
 
 
291
    def iter_files_recursive(self):
 
292
        """Iter the relative paths of files in the transports sub-tree."""
 
293
        queue = list(self.list_dir(u'.'))
 
294
        while queue:
 
295
            relpath = queue.pop(0)
 
296
            st = self.stat(relpath)
 
297
            if S_ISDIR(st[ST_MODE]):
 
298
                for i, basename in enumerate(self.list_dir(relpath)):
 
299
                    queue.insert(i, relpath+'/'+basename)
 
300
            else:
 
301
                yield relpath
 
302
 
 
303
    def _mkdir(self, abspath, mode=None):
 
304
        """Create a real directory, filtering through mode"""
 
305
        if mode is None:
 
306
            # os.mkdir() will filter through umask
 
307
            local_mode = 0o777
 
308
        else:
 
309
            local_mode = mode
 
310
        try:
 
311
            os.mkdir(abspath, local_mode)
 
312
        except (IOError, OSError) as e:
 
313
            self._translate_error(e, abspath)
 
314
        if mode is not None:
 
315
            try:
 
316
                osutils.chmod_if_possible(abspath, mode)
 
317
            except (IOError, OSError) as e:
 
318
                self._translate_error(e, abspath)
 
319
 
 
320
    def mkdir(self, relpath, mode=None):
 
321
        """Create a directory at the given path."""
 
322
        self._mkdir(self._abspath(relpath), mode=mode)
 
323
 
 
324
    def open_write_stream(self, relpath, mode=None):
 
325
        """See Transport.open_write_stream."""
 
326
        abspath = self._abspath(relpath)
 
327
        try:
 
328
            handle = osutils.open_file(abspath, 'wb')
 
329
        except (IOError, OSError) as e:
 
330
            self._translate_error(e, abspath)
 
331
        handle.truncate()
 
332
        if mode is not None:
 
333
            self._check_mode_and_size(abspath, handle.fileno(), mode)
 
334
        transport._file_streams[self.abspath(relpath)] = handle
 
335
        return transport.FileFileStream(self, relpath, handle)
 
336
 
 
337
    def _get_append_file(self, relpath, mode=None):
 
338
        """Call os.open() for the given relpath"""
 
339
        file_abspath = self._abspath(relpath)
 
340
        if mode is None:
 
341
            # os.open() will automatically use the umask
 
342
            local_mode = 0o666
 
343
        else:
 
344
            local_mode = mode
 
345
        try:
 
346
            return file_abspath, os.open(file_abspath, _append_flags, local_mode)
 
347
        except (IOError, OSError) as e:
 
348
            self._translate_error(e, relpath)
 
349
 
 
350
    def _check_mode_and_size(self, file_abspath, fd, mode=None):
 
351
        """Check the mode of the file, and return the current size"""
 
352
        st = os.fstat(fd)
 
353
        if mode is not None and mode != S_IMODE(st.st_mode):
 
354
            # Because of umask, we may still need to chmod the file.
 
355
            # But in the general case, we won't have to
 
356
            osutils.chmod_if_possible(file_abspath, mode)
 
357
        return st.st_size
 
358
 
 
359
    def append_file(self, relpath, f, mode=None):
 
360
        """Append the text in the file-like object into the final location."""
 
361
        file_abspath, fd = self._get_append_file(relpath, mode=mode)
 
362
        try:
 
363
            result = self._check_mode_and_size(file_abspath, fd, mode=mode)
 
364
            self._pump_to_fd(f, fd)
 
365
        finally:
 
366
            os.close(fd)
 
367
        return result
 
368
 
 
369
    def append_bytes(self, relpath, bytes, mode=None):
 
370
        """Append the text in the string into the final location."""
 
371
        file_abspath, fd = self._get_append_file(relpath, mode=mode)
 
372
        try:
 
373
            result = self._check_mode_and_size(file_abspath, fd, mode=mode)
 
374
            if bytes:
 
375
                os.write(fd, bytes)
 
376
        finally:
 
377
            os.close(fd)
 
378
        return result
 
379
 
 
380
    def _pump_to_fd(self, fromfile, to_fd):
 
381
        """Copy contents of one file to another."""
 
382
        BUFSIZE = 32768
 
383
        while True:
 
384
            b = fromfile.read(BUFSIZE)
 
385
            if not b:
 
386
                break
 
387
            os.write(to_fd, b)
 
388
 
 
389
    def copy(self, rel_from, rel_to):
 
390
        """Copy the item at rel_from to the location at rel_to"""
 
391
        path_from = self._abspath(rel_from)
 
392
        path_to = self._abspath(rel_to)
 
393
        try:
 
394
            shutil.copy(path_from, path_to)
 
395
        except (IOError, OSError) as e:
 
396
            # TODO: What about path_to?
 
397
            self._translate_error(e, path_from)
 
398
 
 
399
    def rename(self, rel_from, rel_to):
 
400
        path_from = self._abspath(rel_from)
 
401
        path_to = self._abspath(rel_to)
 
402
        try:
 
403
            # *don't* call breezy.osutils.rename, because we want to
 
404
            # detect conflicting names on rename, and osutils.rename tries to
 
405
            # mask cross-platform differences there
 
406
            os.rename(path_from, path_to)
 
407
        except (IOError, OSError) as e:
 
408
            # TODO: What about path_to?
 
409
            self._translate_error(e, path_from)
 
410
 
 
411
    def move(self, rel_from, rel_to):
 
412
        """Move the item at rel_from to the location at rel_to"""
 
413
        path_from = self._abspath(rel_from)
 
414
        path_to = self._abspath(rel_to)
 
415
 
 
416
        try:
 
417
            # this version will delete the destination if necessary
 
418
            osutils.rename(path_from, path_to)
 
419
        except (IOError, OSError) as e:
 
420
            # TODO: What about path_to?
 
421
            self._translate_error(e, path_from)
 
422
 
 
423
    def delete(self, relpath):
 
424
        """Delete the item at relpath"""
 
425
        path = relpath
 
426
        try:
 
427
            path = self._abspath(relpath)
 
428
            os.remove(path)
 
429
        except (IOError, OSError) as e:
 
430
            self._translate_error(e, path)
 
431
 
 
432
    def external_url(self):
 
433
        """See breezy.transport.Transport.external_url."""
 
434
        # File URL's are externally usable.
 
435
        return self.base
 
436
 
 
437
    def copy_to(self, relpaths, other, mode=None, pb=None):
 
438
        """Copy a set of entries from self into another Transport.
 
439
 
 
440
        :param relpaths: A list/generator of entries to be copied.
 
441
        """
 
442
        if isinstance(other, LocalTransport):
 
443
            # Both from & to are on the local filesystem
 
444
            # Unfortunately, I can't think of anything faster than just
 
445
            # copying them across, one by one :(
 
446
            total = self._get_total(relpaths)
 
447
            count = 0
 
448
            for path in relpaths:
 
449
                self._update_pb(pb, 'copy-to', count, total)
 
450
                try:
 
451
                    mypath = self._abspath(path)
 
452
                    otherpath = other._abspath(path)
 
453
                    shutil.copy(mypath, otherpath)
 
454
                    if mode is not None:
 
455
                        osutils.chmod_if_possible(otherpath, mode)
 
456
                except (IOError, OSError) as e:
 
457
                    self._translate_error(e, path)
 
458
                count += 1
 
459
            return count
 
460
        else:
 
461
            return super(LocalTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
 
462
 
 
463
    def listable(self):
 
464
        """See Transport.listable."""
 
465
        return True
 
466
 
 
467
    def list_dir(self, relpath):
 
468
        """Return a list of all files at the given location.
 
469
        WARNING: many transports do not support this, so trying avoid using
 
470
        it if at all possible.
 
471
        """
 
472
        path = self._abspath(relpath)
 
473
        try:
 
474
            entries = os.listdir(path)
 
475
        except (IOError, OSError) as e:
 
476
            self._translate_error(e, path)
 
477
        return [urlutils.escape(entry) for entry in entries]
 
478
 
 
479
    def stat(self, relpath):
 
480
        """Return the stat information for a file.
 
481
        """
 
482
        path = relpath
 
483
        try:
 
484
            path = self._abspath(relpath)
 
485
            return os.lstat(path)
 
486
        except (IOError, OSError) as e:
 
487
            self._translate_error(e, path)
 
488
 
 
489
    def lock_read(self, relpath):
 
490
        """Lock the given file for shared (read) access.
 
491
        :return: A lock object, which should be passed to Transport.unlock()
 
492
        """
 
493
        from breezy.lock import ReadLock
 
494
        path = relpath
 
495
        try:
 
496
            path = self._abspath(relpath)
 
497
            return ReadLock(path)
 
498
        except (IOError, OSError) as e:
 
499
            self._translate_error(e, path)
 
500
 
 
501
    def lock_write(self, relpath):
 
502
        """Lock the given file for exclusive (write) access.
 
503
        WARNING: many transports do not support this, so trying avoid using it
 
504
 
 
505
        :return: A lock object, which should be passed to Transport.unlock()
 
506
        """
 
507
        from breezy.lock import WriteLock
 
508
        return WriteLock(self._abspath(relpath))
 
509
 
 
510
    def rmdir(self, relpath):
 
511
        """See Transport.rmdir."""
 
512
        path = relpath
 
513
        try:
 
514
            path = self._abspath(relpath)
 
515
            os.rmdir(path)
 
516
        except (IOError, OSError) as e:
 
517
            self._translate_error(e, path)
 
518
 
 
519
    if osutils.host_os_dereferences_symlinks():
 
520
        def readlink(self, relpath):
 
521
            """See Transport.readlink."""
 
522
            return osutils.readlink(self._abspath(relpath))
 
523
 
 
524
    if osutils.hardlinks_good():
 
525
        def hardlink(self, source, link_name):
 
526
            """See Transport.link."""
 
527
            try:
 
528
                os.link(self._abspath(source), self._abspath(link_name))
 
529
            except (IOError, OSError) as e:
 
530
                self._translate_error(e, source)
 
531
 
 
532
    if osutils.has_symlinks():
 
533
        def symlink(self, source, link_name):
 
534
            """See Transport.symlink."""
 
535
            abs_link_dirpath = urlutils.dirname(self.abspath(link_name))
 
536
            source_rel = urlutils.file_relpath(
 
537
                abs_link_dirpath, self.abspath(source))
 
538
 
 
539
            try:
 
540
                os.symlink(source_rel, self._abspath(link_name))
 
541
            except (IOError, OSError) as e:
 
542
                self._translate_error(e, source_rel)
 
543
 
 
544
    def _can_roundtrip_unix_modebits(self):
 
545
        if sys.platform == 'win32':
 
546
            # anyone else?
 
547
            return False
 
548
        else:
 
549
            return True
 
550
 
 
551
 
 
552
class EmulatedWin32LocalTransport(LocalTransport):
 
553
    """Special transport for testing Win32 [UNC] paths on non-windows"""
 
554
 
 
555
    def __init__(self, base):
 
556
        if base[-1] != '/':
 
557
            base = base + '/'
 
558
        super(LocalTransport, self).__init__(base)
 
559
        self._local_base = urlutils._win32_local_path_from_url(base)
 
560
 
 
561
    def abspath(self, relpath):
 
562
        path = osutils._win32_normpath(osutils.pathjoin(
 
563
                    self._local_base, urlutils.unescape(relpath)))
 
564
        return urlutils._win32_local_path_to_url(path)
 
565
 
 
566
    def clone(self, offset=None):
 
567
        """Return a new LocalTransport with root at self.base + offset
 
568
        Because the local filesystem does not require a connection,
 
569
        we can just return a new object.
 
570
        """
 
571
        if offset is None:
 
572
            return EmulatedWin32LocalTransport(self.base)
 
573
        else:
 
574
            abspath = self.abspath(offset)
 
575
            if abspath == 'file://':
 
576
                # fix upwalk for UNC path
 
577
                # when clone from //HOST/path updir recursively
 
578
                # we should stop at least at //HOST part
 
579
                abspath = self.base
 
580
            return EmulatedWin32LocalTransport(abspath)
 
581
 
 
582
 
 
583
def get_test_permutations():
 
584
    """Return the permutations to be used in testing."""
 
585
    from ..tests import test_server
 
586
    return [(LocalTransport, test_server.LocalURLServer),]