Merge ~cjwatson/txpkgupload:remove-zope.server into txpkgupload:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 99b39a674b01700e054b07adb465308a90b17391
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/txpkgupload:remove-zope.server
Merge into: txpkgupload:master
Diff against target: 870 lines (+9/-723)
6 files modified
requirements.txt (+0/-12)
setup.py (+0/-3)
src/txpkgupload/NEWS.txt (+2/-0)
src/txpkgupload/filesystem.py (+2/-193)
src/txpkgupload/tests/filesystem.txt (+4/-515)
tox.ini (+1/-0)
Reviewer Review Type Date Requested Status
Jürgen Gmach Approve
Review via email: mp+437107@code.launchpad.net

Commit message

Remove dependencies on zope.{component,security,server}

Description of the change

Once upon a time, the code in Launchpad that grew into txpkgupload relied on the FTP server in `zope.server`. That was replaced by a dependency on Twisted in 2011 or so, but we retained a vestigial dependency on `zope.server` and a good deal of leftover support code in `UploadFileSystem` that's no longer called by anything. Get rid of all this.

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) :
review: Approve
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/requirements.txt b/requirements.txt
2index ab7562f..b2cf037 100644
3--- a/requirements.txt
4+++ b/requirements.txt
5@@ -35,7 +35,6 @@ pycparser==2.18
6 PyHamcrest==1.10.1
7 PyNaCl==1.3.0
8 python-dateutil==2.8.1
9-python-gettext==4.0
10 python-mimeparse==0.1.4
11 pytz==2017.2
12 PyYAML==3.10
13@@ -47,21 +46,10 @@ Twisted[conch]==20.3.0+lp8
14 unittest2==1.1.0+lp1
15 wadllib==1.3.2
16 zipp==1.2.0
17-zope.browser==2.3
18 zope.component==4.6.1
19-zope.configuration==4.4.0
20-zope.contenttype==4.5.0
21 zope.deferredimport==4.3.1
22 zope.deprecation==4.4.0
23 zope.event==4.4
24-zope.exceptions==4.3
25 zope.hookable==5.0.1
26-zope.i18n==4.7.0
27-zope.i18nmessageid==5.0.1
28 zope.interface==5.0.2
29-zope.location==4.2
30 zope.proxy==4.3.5
31-zope.publisher==5.2.0
32-zope.schema==6.0.0
33-zope.security==5.1.1
34-zope.server==4.0.2
35diff --git a/setup.py b/setup.py
36index 246ba61..fc284de 100755
37--- a/setup.py
38+++ b/setup.py
39@@ -61,10 +61,7 @@ setup(
40 'setuptools',
41 'six>=1.12.0',
42 'Twisted[conch_nacl]',
43- 'zope.component',
44 'zope.interface>=3.6.0',
45- 'zope.security',
46- 'zope.server',
47 ],
48 url='https://launchpad.net/txpkgupload',
49 download_url='https://launchpad.net/txpkgupload/+download',
50diff --git a/src/txpkgupload/NEWS.txt b/src/txpkgupload/NEWS.txt
51index c1fa967..59d1713 100644
52--- a/src/txpkgupload/NEWS.txt
53+++ b/src/txpkgupload/NEWS.txt
54@@ -9,6 +9,8 @@ NEWS for txpkgupload
55 - Adjust directory creation to work around a change in ``os.makedirs`` in
56 Python 3.7.
57 - Add Python 3.8 support.
58+- Remove dependencies on ``zope.component``, ``zope.security``, and
59+ ``zope.server``.
60
61 0.4 (2021-01-04)
62 ================
63diff --git a/src/txpkgupload/filesystem.py b/src/txpkgupload/filesystem.py
64index a598184..9a913e0 100644
65--- a/src/txpkgupload/filesystem.py
66+++ b/src/txpkgupload/filesystem.py
67@@ -6,16 +6,9 @@ __all__ = [
68 'UploadFileSystem',
69 ]
70
71-import datetime
72 import os
73-import stat
74
75-from zope.interface import implementer
76-from zope.security.interfaces import Unauthorized
77-from zope.server.interfaces.ftp import IFileSystem
78
79-
80-@implementer(IFileSystem)
81 class UploadFileSystem:
82
83 def __init__(self, rootpath):
84@@ -30,10 +23,8 @@ class UploadFileSystem:
85
86 def _sanitize(self, path):
87 if isinstance(path, bytes):
88- # zope.server's FTP implementation seems to decode all commands
89- # (including the paths they contain) to text using UTF-8,
90- # effectively assuming the recommendation in RFC 2640 except
91- # without the feature negotiation part. However, Twisted's SFTP
92+ # RFC 2640 recommends that paths are exchanged using UTF-8,
93+ # albeit with some feature negotiation. However, Twisted's SFTP
94 # implementation leaves paths as bytes. Since in practice
95 # legitimate uses of txpkgupload will only involve ASCII paths,
96 # and since UTF-8 has low risk of undetected decoding errors,
97@@ -47,98 +38,6 @@ class UploadFileSystem:
98 path = os.path.normpath(path)
99 return path
100
101- def type(self, path):
102- """Return the file type at path
103-
104- The 'type' command returns 'f' for a file, 'd' for a directory and
105- None if there is no file.
106- """
107- path = self._sanitize(path)
108- full_path = self._full(path)
109- if os.path.exists(full_path):
110- if os.path.isdir(full_path):
111- return 'd'
112- elif os.path.isfile(full_path):
113- return 'f'
114-
115- def names(self, path, filter=None):
116- """Return a sequence of the names in a directory
117-
118- If the filter is not None, include only those names for which
119- the filter returns a true value.
120- """
121- path = self._sanitize(path)
122- full_path = self._full(path)
123- if not os.path.exists(full_path):
124- raise OSError("Not exists:", path)
125- filenames = os.listdir(os.path.join(self.rootpath, path))
126- files = []
127- for filename in filenames:
128- if not filter or filter(filename):
129- files.append(filename)
130- return files
131-
132- def ls(self, path, filter=None):
133- """Return a sequence of information objects.
134-
135- It considers the names in the given path (returned self.name())
136- and builds file information using self.lsinfo().
137- """
138- return [self.lsinfo(name) for name in self.names(path, filter)]
139-
140- def readfile(self, path, outstream, start=0, end=None):
141- """Outputs the file at path to a stream.
142-
143- Not allowed - see filesystem.txt.
144- """
145- raise Unauthorized
146-
147- def lsinfo(self, path):
148- """Return information for a unix-style ls listing for the path
149-
150- See zope3's interfaces/ftp.py:IFileSystem for details of the
151- dictionary's content.
152- """
153- path = self._sanitize(path)
154- full_path = self._full(path)
155- if not os.path.exists(full_path):
156- raise OSError("Not exists:", path)
157-
158- info = {"owner_name": "upload",
159- "group_name": "upload",
160- "name": path.split("/")[-1]}
161-
162- s = os.stat(full_path)
163-
164- info["owner_readable"] = bool(s.st_mode & stat.S_IRUSR)
165- info["owner_writable"] = bool(s.st_mode & stat.S_IWUSR)
166- info["owner_executable"] = bool(s.st_mode & stat.S_IXUSR)
167- info["group_readable"] = bool(s.st_mode & stat.S_IRGRP)
168- info["group_writable"] = bool(s.st_mode & stat.S_IWGRP)
169- info["group_executable"] = bool(s.st_mode & stat.S_IXGRP)
170- info["other_readable"] = bool(s.st_mode & stat.S_IROTH)
171- info["other_writable"] = bool(s.st_mode & stat.S_IWOTH)
172- info["other_executable"] = bool(s.st_mode & stat.S_IXOTH)
173- info["mtime"] = datetime.datetime.fromtimestamp(self.mtime(path))
174- info["size"] = self.size(path)
175- info["type"] = self.type(path)
176- info["nlinks"] = s.st_nlink
177- return info
178-
179- def mtime(self, path):
180- """Return the modification time for the file"""
181- path = self._sanitize(path)
182- full_path = self._full(path)
183- if os.path.exists(full_path):
184- return os.path.getmtime(full_path)
185-
186- def size(self, path):
187- """Return the size of the file at path"""
188- path = self._sanitize(path)
189- full_path = self._full(path)
190- if os.path.exists(full_path):
191- return os.path.getsize(full_path)
192-
193 def mkdir(self, path):
194 """Create a directory."""
195 path = self._sanitize(path)
196@@ -156,18 +55,6 @@ class UploadFileSystem:
197 finally:
198 os.umask(old_mask)
199
200- def remove(self, path):
201- """Remove a file."""
202- path = self._sanitize(path)
203- full_path = self._full(path)
204- if os.path.exists(full_path):
205- if os.path.isfile(full_path):
206- os.unlink(full_path)
207- elif os.path.isdir(full_path):
208- raise OSError("Is a directory:", path)
209- else:
210- raise OSError("Not exists:", path)
211-
212 def rmdir(self, path):
213 """Remove a directory.
214
215@@ -179,81 +66,3 @@ class UploadFileSystem:
216 os.rmdir(full_path)
217 else:
218 raise OSError("Not exists:", path)
219-
220- def rename(self, old, new):
221- """Rename a file."""
222- old = self._sanitize(old)
223- new = self._sanitize(new)
224- full_old = self._full(old)
225- full_new = self._full(new)
226-
227- if os.path.isdir(full_new):
228- raise OSError("Is a directory:", new)
229-
230- if os.path.exists(full_old):
231- if os.path.isfile(full_old):
232- os.rename(full_old, full_new)
233- elif os.path.isdir(full_old):
234- raise OSError("Is a directory:", old)
235- else:
236- raise OSError("Not exists:", old)
237-
238- def writefile(self, path, instream, start=None, end=None, append=False):
239- """Write data to a file.
240-
241- See zope3's interfaces/ftp.py:IFileSystem for details of the
242- handling of the various arguments.
243- """
244- path = self._sanitize(path)
245- full_path = self._full(path)
246- if os.path.exists(full_path):
247- if os.path.isdir(full_path):
248- raise OSError("Is a directory:", path)
249- else:
250- dirname = os.path.dirname(full_path)
251- if dirname:
252- if not os.path.exists(dirname):
253- old_mask = os.umask(0o002)
254- try:
255- os.makedirs(dirname)
256- finally:
257- os.umask(old_mask)
258-
259- if start and start < 0:
260- raise ValueError("Negative start argument:", start)
261- if end and end < 0:
262- raise ValueError("Negative end argument:", end)
263- if start and end and end <= start:
264- return
265- if append:
266- open_flag = 'ab'
267- elif start or end:
268- open_flag = "r+b"
269- if not os.path.exists(full_path):
270- with open(full_path, 'wb'):
271- pass
272-
273- else:
274- open_flag = 'wb'
275- with open(full_path, open_flag) as outstream:
276- if start:
277- outstream.seek(start)
278- chunk = instream.read()
279- while chunk:
280- outstream.write(chunk)
281- chunk = instream.read()
282- if not end:
283- outstream.truncate()
284- instream.close()
285-
286- def writable(self, path):
287- """Return boolean indicating whether a file at path is writable."""
288- path = self._sanitize(path)
289- full_path = self._full(path)
290- if os.path.exists(full_path):
291- if os.path.isfile(full_path):
292- return True
293- elif os.path.isdir(full_path):
294- return False
295- else:
296- return True
297diff --git a/src/txpkgupload/tests/filesystem.txt b/src/txpkgupload/tests/filesystem.txt
298index 20b80d9..2dcd81c 100644
299--- a/src/txpkgupload/tests/filesystem.txt
300+++ b/src/txpkgupload/tests/filesystem.txt
301@@ -1,15 +1,8 @@
302-
303-This is an implementation of IFileSystem which the FTP Server in Zope3
304-uses to know what to do when people make FTP commands.
305+`UploadFileSystem` provides logic for the few limited interactions with the
306+part of the file system we expose over FTP/SFTP.
307
308 >>> from txpkgupload.filesystem import UploadFileSystem
309
310-The UploadFileSystem class implements the interface IFileSystem.
311-
312- >>> from zope.server.interfaces.ftp import IFileSystem
313- >>> IFileSystem.implementedBy(UploadFileSystem)
314- True
315-
316 First we need to setup our test environment.
317
318 >>> import os
319@@ -38,12 +31,6 @@ to use.
320
321 >>> ufs = UploadFileSystem(rootpath)
322
323-An UploadFileSystem object provides the interface IFileSystem.
324-
325- >>> from zope.interface.verify import verifyObject
326- >>> verifyObject(IFileSystem, ufs)
327- True
328-
329 mkdir
330 =====
331
332@@ -88,434 +75,13 @@ Check if it works as expected after the directory creation:
333 >>> os.path.exists(os.path.join(rootpath, "new-dir"))
334 False
335
336-
337-lsinfo
338-======
339-
340-Return information for a unix-style ls listing for the path.
341-
342-See zope3's interfaces/ftp.py:IFileSystem for details of the
343-dictionary's content.
344-
345-Setup a default dictionary used for generating the dictionaries we
346-expect lsinfo to return.
347-
348- >>> def clean_mtime(stat_info):
349- ... """Return a datetime from an mtime, sans microseconds."""
350- ... mtime = stat_info.st_mtime
351- ... datestamp = datetime.datetime.fromtimestamp(mtime)
352- ... datestamp.replace(microsecond=0)
353- ... return datestamp
354-
355- >>> import copy
356- >>> import datetime
357- >>> import stat
358- >>> def_exp = {"type": 'f',
359- ... "owner_name": "upload",
360- ... "owner_readable": True,
361- ... "owner_writable": True,
362- ... "owner_executable": False,
363- ... "group_name": "upload",
364- ... "group_readable": True,
365- ... "group_writable": False,
366- ... "group_executable": False,
367- ... "other_readable": True,
368- ... "other_writable": False,
369- ... "other_executable": False,
370- ... "nlinks": 1}
371- ...
372-
373- >>> os.chmod(full_testfile, stat.S_IRUSR | stat.S_IWUSR | \
374- ... stat.S_IRGRP | stat.S_IROTH)
375- >>> exp = copy.copy(def_exp)
376- >>> s = os.stat(full_testfile)
377- >>> exp["name"] = testfile
378- >>> exp["mtime"] = clean_mtime(s)
379- >>> exp["size"] = s[stat.ST_SIZE]
380- >>> info = ufs.lsinfo(testfile)
381- >>> info == exp
382- True
383-
384-ls
385-==
386-
387-`ls` a sequence of item info objects (see ls_info) for the files in a
388-directory.
389-
390- >>> expected = [exp]
391- >>> for i in [ "foo", "bar" ]:
392- ... filename = os.path.join(rootpath, i)
393- ... with open(filename, 'w'):
394- ... pass
395- ... os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR | \
396- ... stat.S_IRGRP | stat.S_IROTH)
397- ... exp = copy.copy(def_exp)
398- ... s = os.stat(filename)
399- ... exp["name"] = i
400- ... exp["mtime"] = clean_mtime(s)
401- ... exp["size"] = s[stat.ST_SIZE]
402- ... expected.append(exp)
403- ...
404-
405- >>> dir_exp = copy.copy(def_exp)
406- >>> s = os.stat(full_testdir)
407- >>> dir_exp["type"] = "d"
408- >>> dir_exp["name"] = testdir
409- >>> dir_exp["mtime"] = clean_mtime(s)
410- >>> dir_exp["size"] = s[stat.ST_SIZE]
411- >>> dir_exp["nlinks"] = s[stat.ST_NLINK]
412- >>> dir_exp["owner_executable"] = True
413- >>> dir_exp["other_executable"] = True
414- >>> dir_exp["group_executable"] = True
415- >>> expected.append(dir_exp)
416-
417-We need a helper function to turn the returned and expected data into reliably
418-sorted orders for comparison.
419-
420- >>> from operator import itemgetter
421- >>> def sorted_listings(ls_infos):
422- ... # ls_infos will be a sequence of dictionaries. They need to be
423- ... # sorted for the sequences to compare equal, so do that on the
424- ... # dictionary's 'name' key. The equality test used here
425- ... # doesn't care about the sort order of the dictionaries.
426- ... return sorted(ls_infos, key=itemgetter('name'))
427-
428- >>> returned = ufs.ls(".")
429- >>> sorted_listings(expected) == sorted_listings(returned)
430- True
431-
432-If `filter` is not None, include only those names for which `filter`
433-returns a true value.
434-
435- >>> def always_false_filter(name):
436- ... return False
437- >>> def always_true_filter(name):
438- ... return True
439- >>> def arbitrary_filter(name):
440- ... if name == "foo" or name == "baz":
441- ... return True
442- ... else:
443- ... return False
444- ...
445- >>> for i in expected:
446- ... if i["name"] == "foo":
447- ... filtered_expected = [i];
448- >>> returned = ufs.ls(".", always_true_filter)
449- >>> sorted_listings(expected) == sorted_listings(returned)
450- True
451- >>> returned = ufs.ls(".", always_false_filter)
452- >>> returned == []
453- True
454- >>> returned = ufs.ls(".", arbitrary_filter)
455- >>> sorted_listings(filtered_expected) == sorted_listings(returned)
456- True
457- >>> for i in [ "foo", "bar" ]:
458- ... ufs.remove(i)
459- ...
460-
461-readfile
462-========
463-
464-We are not implementing `readfile` as a precautionary measure, i.e. in
465-case anyone bypasses the per-session separate directories they still
466-aren't able to read any other files and therefore can't abuse the
467-server for warez/child porn etc.
468-
469-Unlike `mkdir` and `rmdir` we will raise an exception so that the
470-server returns an error to the client and the client does not receive
471-bogus or empty data.
472-
473- >>> ufs.readfile(testfile, None)
474- ... # doctest: +IGNORE_EXCEPTION_DETAIL
475- Traceback (most recent call last):
476- ...
477- Unauthorized
478-
479-The 'type' command returns 'f' for a file, 'd' for a directory and
480-None if there is no file.
481-
482- >>> ufs.type(testfile)
483- 'f'
484-
485- >>> ufs.type(testdir)
486- 'd'
487-
488- >>> ufs.type("does-not-exist") is None
489- True
490-
491-size
492-====
493-
494-The 'size' command returns the size of the file. If the file does not
495-exist None is returned.
496-
497- >>> ufs.size("does-not-exist") is None
498- True
499-
500- >>> ufs.size(testfile) == os.path.getsize(full_testfile)
501- True
502-
503- >>> ufs.size(testdir) == os.path.getsize(full_testdir)
504- True
505-
506-mtime
507-=====
508-
509-The 'mtime' command returns the mtime of the file. If the file does not
510-exist None is returned.
511-
512- >>> ufs.size("does-not-exist") is None
513- True
514-
515- >>> ufs.mtime(testfile) == os.path.getmtime(full_testfile)
516- True
517-
518- >>> ufs.mtime(testdir) == os.path.getmtime(full_testdir)
519- True
520-
521-remove
522-======
523-
524-The 'remove' command removes a file. An exception is raised if the
525-file does not exist or is a directory.
526-
527- >>> ufs.remove("does-not-exist")
528- Traceback (most recent call last):
529- ...
530- OSError: [Errno Not exists:] does-not-exist
531-
532- >>> ufs.remove(testfile)
533- >>> os.path.exists(full_testfile)
534- False
535- >>> with open(full_testfile, 'wb') as f:
536- ... _ = f.write(b"contents of the file")
537-
538- >>> ufs.remove(testdir)
539- Traceback (most recent call last):
540- ...
541- OSError: [Errno Is a directory:] testdir
542-
543-rename
544-======
545-
546-The 'rename' command renames a file. An exception is raised if the
547-old filename doesn't exist or if the old or new filename is a
548-directory.
549-
550- >>> new_testfile = "baz"
551- >>> new_full_testfile = os.path.join(rootpath, new_testfile)
552-
553- >>> ufs.rename("does-not-exist", new_testfile)
554- Traceback (most recent call last):
555- ...
556- OSError: [Errno Not exists:] does-not-exist
557-
558- >>> new_testfile = "baz"
559- >>> new_full_testfile = os.path.join(rootpath, new_testfile)
560- >>> ufs.rename(testfile, new_testfile)
561- >>> os.path.exists(full_testfile)
562- False
563- >>> os.path.exists(new_full_testfile)
564- True
565- >>> with open(new_full_testfile, "rb") as f:
566- ... f.read() == testfile_contents
567- True
568- >>> ufs.rename(new_testfile, testfile)
569-
570- >>> ufs.rename(testdir, new_testfile)
571- Traceback (most recent call last):
572- ...
573- OSError: [Errno Is a directory:] testdir
574-
575- >>> ufs.rename(testfile, testdir)
576- Traceback (most recent call last):
577- ...
578- OSError: [Errno Is a directory:] testdir
579-
580-names
581-=====
582-
583-The `names` command returns a sequence of the names in the `path`.
584-
585- >>> for name in sorted(ufs.names(".")):
586- ... print(name)
587- testdir
588- testfile
589-
590-`path` is normalized before used.
591-
592- >>> for name in sorted(ufs.names("some-directory/..")):
593- ... print(name)
594- testdir
595- testfile
596-
597-'path' under the server root is not allowed:
598-
599- >>> ufs.names("..")
600- Traceback (most recent call last):
601- ...
602- OSError: [Errno Path not allowed:] ..
603-
604-
605-If the `filter` argument is provided, each name is only returned if
606-the given `filter` function returns True for it.
607-
608- >>> ufs.names(".", always_false_filter)
609- []
610-
611- >>> for name in sorted(ufs.names(".", always_true_filter)):
612- ... print(name)
613- testdir
614- testfile
615-
616- >>> for i in [ "foo", "bar", "baz", "bat" ]:
617- ... with open(os.path.join(rootpath, i), 'w'):
618- ... pass
619- >>> names = ufs.names(".", arbitrary_filter)
620- >>> names.sort()
621- >>> names == ['baz', 'foo']
622- True
623- >>> for i in [ "foo", "bar", "baz", "bat" ]:
624- ... os.unlink(os.path.join(rootpath, i))
625-
626-writefile
627-=========
628-
629-`writefile` writes data to a file.
630-
631- >>> import io
632- >>> ufs.writefile("upload", io.BytesIO(propaganda))
633- >>> with open(os.path.join(rootpath, "upload"), "rb") as f:
634- ... f.read() == propaganda
635- True
636- >>> ufs.remove("upload")
637-
638-If neither `start` nor `end` are specified, then the file contents
639-are overwritten.
640-
641- >>> ufs.writefile(testfile, io.BytesIO(b"MOO"))
642- >>> with open(full_testfile, "rb") as f:
643- ... f.read() == b"MOO"
644- True
645- >>> ufs.writefile(testfile, io.BytesIO(testfile_contents))
646-
647-If `start` or `end` are specified, they must be non-negative.
648-
649- >>> ufs.writefile("upload", io.BytesIO(propaganda), -37)
650- Traceback (most recent call last):
651- ...
652- ValueError: ('Negative start argument:', -37)
653-
654- >>> ufs.writefile("upload", io.BytesIO(propaganda), 1, -43)
655- Traceback (most recent call last):
656- ...
657- ValueError: ('Negative end argument:', -43)
658-
659-If `start` or `end` is not None, then only part of the file is
660-written. The remainder of the file is unchanged.
661-
662- >>> ufs.writefile(testfile, io.BytesIO(b"MOO"), 9, 12)
663- >>> with open(full_testfile, "rb") as f:
664- ... f.read() == b"contents MOOthe file"
665- True
666- >>> ufs.writefile(testfile, io.BytesIO(testfile_contents))
667-
668-If `end` is None, then the file is truncated after the data are
669-written.
670-
671- >>> ufs.writefile(testfile, io.BytesIO(b"MOO"), 9)
672- >>> with open(full_testfile, "rb") as f:
673- ... f.read() == b"contents MOO"
674- True
675- >>> ufs.writefile(testfile, io.BytesIO(testfile_contents))
676-
677-If `start` is specified and the file doesn't exist or is shorter
678-than start, the file will contain undefined data before start.
679-
680- >>> ufs.writefile("didnt-exist", io.BytesIO(b"MOO"), 9)
681- >>> with open(os.path.join(rootpath, "didnt-exist"), "rb") as f:
682- ... f.read() == b"\x00\x00\x00\x00\x00\x00\x00\x00\x00MOO"
683- True
684- >>> ufs.remove("didnt-exist")
685-
686-If `end` is not None and there isn't enough data in `instream` to fill
687-out the file, then the missing data is undefined.
688-
689- >>> ufs.writefile(testfile, io.BytesIO(b"MOO"), 9, 15)
690- >>> with open(full_testfile, "rb") as f:
691- ... f.read() == b"contents MOOthe file"
692- True
693- >>> ufs.writefile(testfile, io.BytesIO(testfile_contents))
694-
695-If `end` is less than or the same as `start no data is writen to the file.
696-
697- >>> ufs.writefile(testfile, io.BytesIO(b"MOO"), 9, 4)
698- >>> with open(full_testfile, "rb") as f:
699- ... f.read() == b"contents of the file"
700- True
701-
702- >>> ufs.writefile(testfile, io.BytesIO(b"MOO"), 9, 9)
703- >>> with open(full_testfile, "rb") as f:
704- ... f.read() == b"contents of the file"
705- True
706-
707-If `append` is true the file is appended to rather than being
708-overwritten.
709-
710- >>> ufs.writefile(testfile, io.BytesIO(b"MOO"), append=True)
711- >>> with open(full_testfile, "rb") as f:
712- ... f.read() == b"contents of the fileMOO"
713- True
714- >>> ufs.writefile(testfile, io.BytesIO(testfile_contents))
715-
716-Additionally, if `append` is true, `start` and `end` are ignored.
717-
718- >>> ufs.writefile(testfile, io.BytesIO(b"MOO"), 10, 13, append=True)
719- >>> with open(full_testfile, "rb") as f:
720- ... f.read() == b"contents of the fileMOO"
721- True
722- >>> ufs.writefile(testfile, io.BytesIO(testfile_contents))
723-
724-'writefile' is able to create inexistent directories in a requested
725-path:
726-
727- >>> os.path.exists(os.path.join(rootpath, "foo"))
728- False
729- >>> ufs.writefile("foo/bar", io.BytesIO(b"fake"))
730- >>> os.path.exists(os.path.join(rootpath, "foo/bar"))
731- True
732- >>> with open(os.path.join(rootpath, "foo/bar"), "rb") as f:
733- ... f.read() == b"fake"
734- True
735-
736-
737-writable
738-========
739-
740-`writable` returns a boolean indicating whether `path` is writable or
741-not.
742-
743- >>> ufs.writable(testfile)
744- True
745-
746-`writable` returns True if `path` is a non-existent file.
747-
748- >>> ufs.writable("does-not-exist")
749- True
750-
751-`writable` returns False if `path` is a directory as we don't allow
752-the creation of sub-directories.
753-
754- >>> ufs.writable(testdir)
755- False
756-
757 path checking
758 =============
759
760 `path` arguments must be normalized.
761
762- >>> ufs.type(os.path.join("non-existent-dir", "..", testfile))
763- 'f'
764+ >>> ufs._sanitize(os.path.join("non-existent-dir", "..", testfile))
765+ 'testfile'
766
767
768 Cleanup the server root:
769@@ -528,84 +94,8 @@ Cleanup the server root:
770 ... os.remove(full_path)
771
772
773-Dealing with inexistent path:
774-
775- >>> ufs.type("foo/bar") is None
776- True
777- >>> ufs.mtime("foo/bar") is None
778- True
779- >>> ufs.size("foo/bar") is None
780- True
781- >>> ufs.writable("foo/bar")
782- True
783- >>> ufs.names("foo/bar")
784- Traceback (most recent call last):
785- ...
786- OSError: [Errno Not exists:] foo/bar
787- >>> ufs.ls("foo/bar")
788- Traceback (most recent call last):
789- ...
790- OSError: [Errno Not exists:] foo/bar
791- >>> ufs.lsinfo("foo/bar")
792- Traceback (most recent call last):
793- ...
794- OSError: [Errno Not exists:] foo/bar
795- >>> ufs.remove("foo/bar")
796- Traceback (most recent call last):
797- ...
798- OSError: [Errno Not exists:] foo/bar
799- >>> ufs.rename("foo/bar", "baz")
800- Traceback (most recent call last):
801- ...
802- OSError: [Errno Not exists:] foo/bar
803- >>> ufs.rename("baz", "foo/bar")
804- Traceback (most recent call last):
805- ...
806- OSError: [Errno Not exists:] baz
807-
808-
809 Dealing with paths outside the server root directory:
810
811- >>> ufs.type("..")
812- Traceback (most recent call last):
813- ...
814- OSError: [Errno Path not allowed:] ..
815- >>> ufs.mtime("..")
816- Traceback (most recent call last):
817- ...
818- OSError: [Errno Path not allowed:] ..
819- >>> ufs.size("..")
820- Traceback (most recent call last):
821- ...
822- OSError: [Errno Path not allowed:] ..
823- >>> ufs.writable("..")
824- Traceback (most recent call last):
825- ...
826- OSError: [Errno Path not allowed:] ..
827- >>> ufs.names("..")
828- Traceback (most recent call last):
829- ...
830- OSError: [Errno Path not allowed:] ..
831- >>> ufs.ls("..")
832- Traceback (most recent call last):
833- ...
834- OSError: [Errno Path not allowed:] ..
835- >>> ufs.lsinfo("..")
836- Traceback (most recent call last):
837- ...
838- OSError: [Errno Path not allowed:] ..
839- >>> ufs.remove("..")
840- Traceback (most recent call last):
841- ...
842- OSError: [Errno Path not allowed:] ..
843- >>> ufs.rename("..", "baz")
844- Traceback (most recent call last):
845- ...
846- OSError: [Errno Path not allowed:] ..
847- >>> ufs.rename("baz", "..")
848- Traceback (most recent call last):
849- ...
850- OSError: [Errno Path not allowed:] ..
851 >>> ufs.mkdir("..")
852 Traceback (most recent call last):
853 ...
854@@ -621,4 +111,3 @@ Dealing with paths outside the server root directory:
855 Finally, cleanup after ourselves.
856
857 >>> shutil.rmtree(rootpath)
858-
859diff --git a/tox.ini b/tox.ini
860index 85035bc..3103c49 100644
861--- a/tox.ini
862+++ b/tox.ini
863@@ -6,6 +6,7 @@ skip_missing_interpreters = true
864
865 [testenv]
866 setenv =
867+ py35: VIRTUALENV_DOWNLOAD = 0
868 py38: VIRTUALENV_PIP = 9.0.1
869 py38: VIRTUALENV_SETUPTOOLS = 44.1.1
870 py38: VIRTUALENV_WHEEL = 0.35.1

Subscribers

People subscribed via source and target branches

to all changes: