From 290e56c85e37b728f8a252966e3b5ea2d16ea46e Mon Sep 17 00:00:00 2001 From: Srinivas Reddy Thatiparthy Date: Fri, 19 Jun 2020 14:55:49 +0530 Subject: [PATCH 1/3] bpo-41026: Add support for PathLike object to Mailbox constructor. --- Lib/mailbox.py | 155 +++++++++++++++++++-------------------- Lib/test/test_mailbox.py | 22 +++--- 2 files changed, 87 insertions(+), 90 deletions(-) diff --git a/Lib/mailbox.py b/Lib/mailbox.py index 70da07ed2e9e8b..2dd0e39321307e 100644 --- a/Lib/mailbox.py +++ b/Lib/mailbox.py @@ -19,6 +19,7 @@ import io import contextlib from types import GenericAlias +from pathlib import Path try: import fcntl except ImportError: @@ -36,7 +37,7 @@ class Mailbox: def __init__(self, path, factory=None, create=True): """Initialize a Mailbox instance.""" - self._path = os.path.abspath(os.path.expanduser(path)) + self._path = Path(path).expanduser().resolve() self._factory = factory def add(self, message): @@ -157,7 +158,7 @@ def pop(self, key, default=None): def popitem(self): """Delete an arbitrary (key, message) pair and return it.""" for key in self.iterkeys(): - return (key, self.pop(key)) # This is only run once. + return key, self.pop(key) # This is only run once. else: raise KeyError('No messages in mailbox') @@ -273,15 +274,15 @@ def __init__(self, dirname, factory=None, create=True): """Initialize a Maildir instance.""" Mailbox.__init__(self, dirname, factory, create) self._paths = { - 'tmp': os.path.join(self._path, 'tmp'), - 'new': os.path.join(self._path, 'new'), - 'cur': os.path.join(self._path, 'cur'), + 'tmp': self._path / 'tmp', + 'new': self._path / 'new', + 'cur': self._path / 'cur', } - if not os.path.exists(self._path): + if not self._path.exists(): if create: - os.mkdir(self._path, 0o700) + self._path.mkdir(mode=0o700) for path in self._paths.values(): - os.mkdir(path, 0o700) + path.mkdir(mode=0o700) else: raise NoSuchMailboxError(self._path) self._toc = {} @@ -307,8 +308,8 @@ def add(self, message): else: subdir = 'new' suffix = '' - uniq = os.path.basename(tmp_file.name).split(self.colon)[0] - dest = os.path.join(self._path, subdir, uniq + suffix) + uniq = Path(tmp_file.name).name.split(self.colon)[0] + dest = self._path / subdir / (uniq + suffix) if isinstance(message, MaildirMessage): os.utime(tmp_file.name, (os.path.getatime(tmp_file.name), message.get_date())) @@ -317,9 +318,9 @@ def add(self, message): # from other programs try: try: - os.link(tmp_file.name, dest) + Path(tmp_file.name).link_to(dest) except (AttributeError, PermissionError): - os.rename(tmp_file.name, dest) + Path(tmp_file.name).rename(dest) else: os.remove(tmp_file.name) except OSError as e: @@ -333,7 +334,7 @@ def add(self, message): def remove(self, key): """Remove the keyed message; raise KeyError if it doesn't exist.""" - os.remove(os.path.join(self._path, self._lookup(key))) + os.remove(self._path / self._lookup(key)) def discard(self, key): """If the keyed message exists, remove it.""" @@ -355,25 +356,25 @@ def __setitem__(self, key, message): # temp's subdir and suffix were defaults from add(). dominant_subpath = old_subpath subdir = os.path.dirname(dominant_subpath) - if self.colon in dominant_subpath: - suffix = self.colon + dominant_subpath.split(self.colon)[-1] + if self.colon in os.fspath(dominant_subpath): + suffix = self.colon + os.fspath(dominant_subpath).split(self.colon)[-1] else: suffix = '' self.discard(key) - tmp_path = os.path.join(self._path, temp_subpath) - new_path = os.path.join(self._path, subdir, key + suffix) + tmp_path = self._path / temp_subpath + new_path = self._path / subdir / (key + suffix) if isinstance(message, MaildirMessage): os.utime(tmp_path, (os.path.getatime(tmp_path), message.get_date())) # No file modification should be done after the file is moved to its # final position in order to prevent race conditions with changes # from other programs - os.rename(tmp_path, new_path) + Path(tmp_path).rename(new_path) def get_message(self, key): """Return a Message representation or raise a KeyError.""" subpath = self._lookup(key) - with open(os.path.join(self._path, subpath), 'rb') as f: + with open(self._path / subpath, 'rb') as f: if self._factory: msg = self._factory(f) else: @@ -382,17 +383,17 @@ def get_message(self, key): msg.set_subdir(subdir) if self.colon in name: msg.set_info(name.split(self.colon)[-1]) - msg.set_date(os.path.getmtime(os.path.join(self._path, subpath))) + msg.set_date(os.path.getmtime(self._path / subpath)) return msg def get_bytes(self, key): """Return a bytes representation or raise a KeyError.""" - with open(os.path.join(self._path, self._lookup(key)), 'rb') as f: + with open(self._path / self._lookup(key), 'rb') as f: return f.read().replace(linesep, b'\n') def get_file(self, key): """Return a file-like representation or raise a KeyError.""" - f = open(os.path.join(self._path, self._lookup(key)), 'rb') + f = open(self._path / self._lookup(key), 'rb') return _ProxyFile(f) def iterkeys(self): @@ -436,52 +437,50 @@ def close(self): def list_folders(self): """Return a list of folder names.""" result = [] + # TODO: Need to look into this carefully. for entry in os.listdir(self._path): if len(entry) > 1 and entry[0] == '.' and \ - os.path.isdir(os.path.join(self._path, entry)): + (self._path / entry).is_dir(): result.append(entry[1:]) return result def get_folder(self, folder): """Return a Maildir instance for the named folder.""" - return Maildir(os.path.join(self._path, '.' + folder), - factory=self._factory, - create=False) + return Maildir(self._path / ('.' + folder), + factory=self._factory, create=False) def add_folder(self, folder): """Create a folder and return a Maildir instance representing it.""" - path = os.path.join(self._path, '.' + folder) + path = self._path / ('.' + folder) result = Maildir(path, factory=self._factory) - maildirfolder_path = os.path.join(path, 'maildirfolder') - if not os.path.exists(maildirfolder_path): - os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY, - 0o666)) + maildirfolder_path = path / 'maildirfolder' + if not maildirfolder_path.exists(): + os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY, 0o666)) return result def remove_folder(self, folder): """Delete the named folder, which must be empty.""" - path = os.path.join(self._path, '.' + folder) - for entry in os.listdir(os.path.join(path, 'new')) + \ - os.listdir(os.path.join(path, 'cur')): + path = self._path / ('.' + folder) + for entry in os.listdir(path / 'new') + os.listdir(path / 'cur'): if len(entry) < 1 or entry[0] != '.': raise NotEmptyError('Folder contains message(s): %s' % folder) for entry in os.listdir(path): if entry != 'new' and entry != 'cur' and entry != 'tmp' and \ - os.path.isdir(os.path.join(path, entry)): + (path / entry).is_dir(): raise NotEmptyError("Folder contains subdirectory '%s': %s" % (folder, entry)) for root, dirs, files in os.walk(path, topdown=False): for entry in files: - os.remove(os.path.join(root, entry)) + os.remove(Path(root) / Path(entry)) for entry in dirs: - os.rmdir(os.path.join(root, entry)) + os.rmdir(Path(root) / Path(entry)) os.rmdir(path) def clean(self): """Delete old files in "tmp".""" now = time.time() - for entry in os.listdir(os.path.join(self._path, 'tmp')): - path = os.path.join(self._path, 'tmp', entry) + for entry in os.listdir(self._path / 'tmp'): + path = self._path / 'tmp' / entry if now - os.path.getatime(path) > 129600: # 60 * 60 * 36 os.remove(path) @@ -497,7 +496,7 @@ def _create_tmp(self): hostname = hostname.replace(':', r'\072') uniq = "%s.M%sP%sQ%s.%s" % (int(now), int(now % 1 * 1e6), os.getpid(), Maildir._count, hostname) - path = os.path.join(self._path, 'tmp', uniq) + path = self._path / 'tmp' / uniq try: os.stat(path) except FileNotFoundError: @@ -540,17 +539,17 @@ def _refresh(self): for subdir in self._toc_mtimes: path = self._paths[subdir] for entry in os.listdir(path): - p = os.path.join(path, entry) - if os.path.isdir(p): + p = path / entry + if p.is_dir(): continue uniq = entry.split(self.colon)[0] - self._toc[uniq] = os.path.join(subdir, entry) + self._toc[uniq] = Path(subdir) / Path(entry) self._last_read = time.time() def _lookup(self, key): - """Use TOC to return subpath for given key, or raise a KeyError.""" + """Use TOC to return subpath for a given key, or raise a KeyError.""" try: - if os.path.exists(os.path.join(self._path, self._toc[key])): + if (self._path / self._toc[key]).exists(): return self._toc[key] except KeyError: pass @@ -700,12 +699,12 @@ def flush(self): self._file.close() # Make sure the new file's mode is the same as the old file's mode = os.stat(self._path).st_mode - os.chmod(new_file.name, mode) + Path(new_file.name).chmod(mode) try: - os.rename(new_file.name, self._path) + Path(new_file.name).rename(self._path) except FileExistsError: os.remove(self._path) - os.rename(new_file.name, self._path) + Path(new_file.name).rename(self._path) self._file = open(self._path, 'rb+') self._toc = new_toc self._pending = False @@ -938,13 +937,13 @@ class MH(Mailbox): def __init__(self, path, factory=None, create=True): """Initialize an MH instance.""" Mailbox.__init__(self, path, factory, create) - if not os.path.exists(self._path): + if not self._path.exists(): if create: - os.mkdir(self._path, 0o700) - os.close(os.open(os.path.join(self._path, '.mh_sequences'), + self._path.mkdir(mode=0o700) + os.close(os.open(self._path / '.mh_sequences', os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)) else: - raise NoSuchMailboxError(self._path) + raise NoSuchMailboxError(os.fspath(self._path)) self._locked = False def add(self, message): @@ -954,7 +953,7 @@ def add(self, message): new_key = 1 else: new_key = max(keys) + 1 - new_path = os.path.join(self._path, str(new_key)) + new_path = self._path / str(new_key) f = _create_carefully(new_path) closed = False try: @@ -983,7 +982,7 @@ def add(self, message): def remove(self, key): """Remove the keyed message; raise KeyError if it doesn't exist.""" - path = os.path.join(self._path, str(key)) + path = self._path / str(key) try: f = open(path, 'rb+') except OSError as e: @@ -997,7 +996,7 @@ def remove(self, key): def __setitem__(self, key, message): """Replace the keyed message; raise KeyError if it doesn't exist.""" - path = os.path.join(self._path, str(key)) + path = self._path / str(key) try: f = open(path, 'rb+') except OSError as e: @@ -1023,9 +1022,9 @@ def get_message(self, key): """Return a Message representation or raise a KeyError.""" try: if self._locked: - f = open(os.path.join(self._path, str(key)), 'rb+') + f = open(self._path / str(key), 'rb+') else: - f = open(os.path.join(self._path, str(key)), 'rb') + f = open(self._path / str(key), 'rb') except OSError as e: if e.errno == errno.ENOENT: raise KeyError('No message with key: %s' % key) @@ -1048,9 +1047,9 @@ def get_bytes(self, key): """Return a bytes representation or raise a KeyError.""" try: if self._locked: - f = open(os.path.join(self._path, str(key)), 'rb+') + f = open(self._path / str(key), 'rb+') else: - f = open(os.path.join(self._path, str(key)), 'rb') + f = open(self._path / str(key), 'rb') except OSError as e: if e.errno == errno.ENOENT: raise KeyError('No message with key: %s' % key) @@ -1068,7 +1067,7 @@ def get_bytes(self, key): def get_file(self, key): """Return a file-like representation or raise a KeyError.""" try: - f = open(os.path.join(self._path, str(key)), 'rb') + f = open(self._path / str(key), 'rb') except OSError as e: if e.errno == errno.ENOENT: raise KeyError('No message with key: %s' % key) @@ -1083,7 +1082,7 @@ def iterkeys(self): def __contains__(self, key): """Return True if the keyed message exists, False otherwise.""" - return os.path.exists(os.path.join(self._path, str(key))) + return (self._path / str(key)).exists() def __len__(self): """Return a count of messages in the mailbox.""" @@ -1092,7 +1091,7 @@ def __len__(self): def lock(self): """Lock the mailbox.""" if not self._locked: - self._file = open(os.path.join(self._path, '.mh_sequences'), 'rb+') + self._file = open(self._path / '.mh_sequences', 'rb+') _lock_file(self._file) self._locked = True @@ -1117,36 +1116,36 @@ def list_folders(self): """Return a list of folder names.""" result = [] for entry in os.listdir(self._path): - if os.path.isdir(os.path.join(self._path, entry)): + if (self._path / entry).is_dir(): result.append(entry) return result def get_folder(self, folder): """Return an MH instance for the named folder.""" - return MH(os.path.join(self._path, folder), + return MH(self._path / folder, factory=self._factory, create=False) def add_folder(self, folder): """Create a folder and return an MH instance representing it.""" - return MH(os.path.join(self._path, folder), + return MH(self._path / folder, factory=self._factory) def remove_folder(self, folder): """Delete the named folder, which must be empty.""" - path = os.path.join(self._path, folder) + path = self._path / folder entries = os.listdir(path) if entries == ['.mh_sequences']: - os.remove(os.path.join(path, '.mh_sequences')) + os.remove(path / '.mh_sequences') elif entries == []: pass else: - raise NotEmptyError('Folder not empty: %s' % self._path) + raise NotEmptyError('Folder not empty: %s' % os.fspath(self._path)) os.rmdir(path) def get_sequences(self): """Return a name-to-key-list dictionary to define each sequence.""" results = {} - with open(os.path.join(self._path, '.mh_sequences'), 'r', encoding='ASCII') as f: + with open(self._path / '.mh_sequences', 'r', encoding='ASCII') as f: all_keys = set(self.keys()) for line in f: try: @@ -1169,7 +1168,7 @@ def get_sequences(self): def set_sequences(self, sequences): """Set sequences using the given name-to-key-list dictionary.""" - f = open(os.path.join(self._path, '.mh_sequences'), 'r+', encoding='ASCII') + f = open(self._path / '.mh_sequences', 'r+', encoding='ASCII') try: os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC)) for name, keys in sequences.items(): @@ -1205,13 +1204,11 @@ def pack(self): if key - 1 != prev: changes.append((key, prev + 1)) try: - os.link(os.path.join(self._path, str(key)), - os.path.join(self._path, str(prev + 1))) + (self._path / str(key)).link_to(self._path / str(prev + 1)) except (AttributeError, PermissionError): - os.rename(os.path.join(self._path, str(key)), - os.path.join(self._path, str(prev + 1))) + (self._path / str(key)).rename(self._path / str(prev + 1)) else: - os.unlink(os.path.join(self._path, str(key))) + (self._path / str(key)).unlink() prev += 1 self._next_key = prev + 1 if len(changes) == 0: @@ -2084,13 +2081,13 @@ def _lock_file(f, dotlock=True): raise try: try: - os.link(pre_lock.name, f.name + '.lock') + Path(pre_lock.name).link_to(f.name + '.lock') dotlock_done = True except (AttributeError, PermissionError): - os.rename(pre_lock.name, f.name + '.lock') + Path(pre_lock.name).rename(f.name + '.lock') dotlock_done = True else: - os.unlink(pre_lock.name) + Path(pre_lock.name).unlink() except FileExistsError: os.remove(pre_lock.name) raise ExternalClashError('dot lock unavailable: %s' % @@ -2106,7 +2103,7 @@ def _unlock_file(f): """Unlock file f using lockf and dot locking.""" if fcntl: fcntl.lockf(f, fcntl.LOCK_UN) - if os.path.exists(f.name + '.lock'): + if Path(f.name + '.lock').exists(): os.remove(f.name + '.lock') def _create_carefully(path): diff --git a/Lib/test/test_mailbox.py b/Lib/test/test_mailbox.py index fdda1d11d3307e..c1f7bf4b6662c9 100644 --- a/Lib/test/test_mailbox.py +++ b/Lib/test/test_mailbox.py @@ -13,7 +13,7 @@ import textwrap import mailbox import glob - +from pathlib import Path class TestBase: @@ -674,7 +674,7 @@ def test_initialize_existing(self): def _check_basics(self, factory=None): # (Used by test_open_new() and test_open_existing().) - self.assertEqual(self._box._path, os.path.abspath(self._path)) + self.assertEqual(self._box._path, Path(self._path).resolve()) self.assertEqual(self._box._factory, factory) for subdir in '', 'tmp', 'new', 'cur': path = os.path.join(self._path, subdir) @@ -791,15 +791,15 @@ def test_refresh(self): key1 = self._box.add(self._template % 1) self.assertEqual(self._box._toc, {}) self._box._refresh() - self.assertEqual(self._box._toc, {key0: os.path.join('new', key0), - key1: os.path.join('new', key1)}) + self.assertEqual(self._box._toc, {key0: Path('new', key0), + key1: Path('new', key1)}) key2 = self._box.add(self._template % 2) - self.assertEqual(self._box._toc, {key0: os.path.join('new', key0), - key1: os.path.join('new', key1)}) + self.assertEqual(self._box._toc, {key0: Path('new', key0), + key1: Path('new', key1)}) self._box._refresh() - self.assertEqual(self._box._toc, {key0: os.path.join('new', key0), - key1: os.path.join('new', key1), - key2: os.path.join('new', key2)}) + self.assertEqual(self._box._toc, {key0: Path('new', key0), + key1: Path('new', key1), + key2: Path('new', key2)}) def test_refresh_after_safety_period(self): # Issue #13254: Call _refresh after the "file system safety @@ -824,9 +824,9 @@ def test_lookup(self): # Look up message subpaths in the TOC self.assertRaises(KeyError, lambda: self._box._lookup('foo')) key0 = self._box.add(self._template % 0) - self.assertEqual(self._box._lookup(key0), os.path.join('new', key0)) + self.assertEqual(self._box._lookup(key0), Path('new', key0)) os.remove(os.path.join(self._path, 'new', key0)) - self.assertEqual(self._box._toc, {key0: os.path.join('new', key0)}) + self.assertEqual(self._box._toc, {key0: Path('new', key0)}) # Be sure that the TOC is read back from disk (see issue #6896 # about bad mtime behaviour on some systems). self._box.flush() From 793ad9a06d16003f0b38ce92b68864dcbdb6edbe Mon Sep 17 00:00:00 2001 From: Srinivas Reddy Thatiparthy Date: Fri, 19 Jun 2020 22:26:09 +0530 Subject: [PATCH 2/3] Copy blurb and docs from @loz-hurst --- Doc/library/mailbox.rst | 4 ++++ .../next/Library/2020-06-19-10-16-57.bpo-41026.hEXB-B.rst | 1 + 2 files changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2020-06-19-10-16-57.bpo-41026.hEXB-B.rst diff --git a/Doc/library/mailbox.rst b/Doc/library/mailbox.rst index f82a3b200deb7c..e784915e012f9d 100644 --- a/Doc/library/mailbox.rst +++ b/Doc/library/mailbox.rst @@ -65,6 +65,10 @@ Supported mailbox formats are Maildir, mbox, MH, Babyl, and MMDF. :exc:`KeyError` exception if the corresponding message is subsequently removed. + .. versionchanged:: 3.10 + Constructor accepts a :term:`path-like object` for *path* (*dirname* in + Maildir). + .. warning:: Be very cautious when modifying mailboxes that might be simultaneously diff --git a/Misc/NEWS.d/next/Library/2020-06-19-10-16-57.bpo-41026.hEXB-B.rst b/Misc/NEWS.d/next/Library/2020-06-19-10-16-57.bpo-41026.hEXB-B.rst new file mode 100644 index 00000000000000..5fbb62f847b09c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-06-19-10-16-57.bpo-41026.hEXB-B.rst @@ -0,0 +1 @@ +:mod:`mailbox` now supports a :term:`path-like object`. \ No newline at end of file From 8b126ffa1f66153a303dcc87b52f89d8fed27e49 Mon Sep 17 00:00:00 2001 From: Srinivas Reddy Thatiparthy Date: Sat, 20 Jun 2020 11:41:42 +0530 Subject: [PATCH 3/3] bpo-41026: Migrate some more methods of 'os', 'os.path' modules to 'pathlib' module. --- Lib/mailbox.py | 110 ++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/Lib/mailbox.py b/Lib/mailbox.py index 2dd0e39321307e..8a47298700b823 100644 --- a/Lib/mailbox.py +++ b/Lib/mailbox.py @@ -17,6 +17,7 @@ import email.message import email.generator import io +import itertools import contextlib from types import GenericAlias from pathlib import Path @@ -293,11 +294,12 @@ def __init__(self, dirname, factory=None, create=True): def add(self, message): """Add message and return assigned key.""" tmp_file = self._create_tmp() + tmp_path = Path(tmp_file.name) try: self._dump_message(message, tmp_file) except BaseException: tmp_file.close() - os.remove(tmp_file.name) + tmp_path.unlink() raise _sync_close(tmp_file) if isinstance(message, MaildirMessage): @@ -308,23 +310,23 @@ def add(self, message): else: subdir = 'new' suffix = '' - uniq = Path(tmp_file.name).name.split(self.colon)[0] + uniq = tmp_path.name.split(self.colon)[0] dest = self._path / subdir / (uniq + suffix) if isinstance(message, MaildirMessage): os.utime(tmp_file.name, - (os.path.getatime(tmp_file.name), message.get_date())) + (tmp_path.stat().st_atime, message.get_date())) # No file modification should be done after the file is moved to its # final position in order to prevent race conditions with changes # from other programs try: try: - Path(tmp_file.name).link_to(dest) + tmp_path.link_to(dest) except (AttributeError, PermissionError): - Path(tmp_file.name).rename(dest) + tmp_path.rename(dest) else: - os.remove(tmp_file.name) + tmp_path.unlink() except OSError as e: - os.remove(tmp_file.name) + tmp_path.unlink() if e.errno == errno.EEXIST: raise ExternalClashError('Name clash with existing message: %s' % dest) @@ -334,7 +336,7 @@ def add(self, message): def remove(self, key): """Remove the keyed message; raise KeyError if it doesn't exist.""" - os.remove(self._path / self._lookup(key)) + (self._path / self._lookup(key)).unlink() def discard(self, key): """If the keyed message exists, remove it.""" @@ -365,11 +367,11 @@ def __setitem__(self, key, message): new_path = self._path / subdir / (key + suffix) if isinstance(message, MaildirMessage): os.utime(tmp_path, - (os.path.getatime(tmp_path), message.get_date())) + (tmp_path.stat().st_atime, message.get_date())) # No file modification should be done after the file is moved to its # final position in order to prevent race conditions with changes # from other programs - Path(tmp_path).rename(new_path) + tmp_path.rename(new_path) def get_message(self, key): """Return a Message representation or raise a KeyError.""" @@ -383,7 +385,7 @@ def get_message(self, key): msg.set_subdir(subdir) if self.colon in name: msg.set_info(name.split(self.colon)[-1]) - msg.set_date(os.path.getmtime(self._path / subpath)) + msg.set_date((self._path / subpath).stat().st_mtime) return msg def get_bytes(self, key): @@ -437,11 +439,9 @@ def close(self): def list_folders(self): """Return a list of folder names.""" result = [] - # TODO: Need to look into this carefully. - for entry in os.listdir(self._path): - if len(entry) > 1 and entry[0] == '.' and \ - (self._path / entry).is_dir(): - result.append(entry[1:]) + for entry in self._path.iterdir(): + if len(entry.name) > 1 and entry.name[0] == '.' and entry.is_dir(): + result.append(entry.name[1:]) return result def get_folder(self, folder): @@ -461,28 +461,27 @@ def add_folder(self, folder): def remove_folder(self, folder): """Delete the named folder, which must be empty.""" path = self._path / ('.' + folder) - for entry in os.listdir(path / 'new') + os.listdir(path / 'cur'): - if len(entry) < 1 or entry[0] != '.': + for entry in itertools.chain((path / 'new').iterdir(), (path / 'cur').iterdir()): + if len(entry.name) < 1 or entry.name[0] != '.': raise NotEmptyError('Folder contains message(s): %s' % folder) - for entry in os.listdir(path): - if entry != 'new' and entry != 'cur' and entry != 'tmp' and \ - (path / entry).is_dir(): + for entry in path.iterdir(): + if entry.name not in ('new', 'cur', 'tmp') and entry.is_dir(): raise NotEmptyError("Folder contains subdirectory '%s': %s" % - (folder, entry)) + (folder, entry.name)) for root, dirs, files in os.walk(path, topdown=False): for entry in files: - os.remove(Path(root) / Path(entry)) + Path(root, entry).unlink() for entry in dirs: - os.rmdir(Path(root) / Path(entry)) - os.rmdir(path) + Path(root, entry).rmdir() + path.rmdir() def clean(self): """Delete old files in "tmp".""" now = time.time() - for entry in os.listdir(self._path / 'tmp'): + for entry in (self._path / 'tmp').iterdir(): path = self._path / 'tmp' / entry - if now - os.path.getatime(path) > 129600: # 60 * 60 * 36 - os.remove(path) + if now - path.stat().st_atime > 129600: # 60 * 60 * 36 + path.unlink() _count = 1 # This is used to generate unique file names. @@ -498,7 +497,7 @@ def _create_tmp(self): Maildir._count, hostname) path = self._path / 'tmp' / uniq try: - os.stat(path) + path.stat() except FileNotFoundError: Maildir._count += 1 try: @@ -528,7 +527,7 @@ def _refresh(self): if time.time() - self._last_read > 2 + self._skewfactor: refresh = False for subdir in self._toc_mtimes: - mtime = os.path.getmtime(self._paths[subdir]) + mtime = self._paths[subdir].stat().st_mtime if mtime > self._toc_mtimes[subdir]: refresh = True self._toc_mtimes[subdir] = mtime @@ -692,19 +691,20 @@ def flush(self): self._file_length = new_file.tell() except: new_file.close() - os.remove(new_file.name) + Path(new_file.name).unlink() raise _sync_close(new_file) # self._file is about to get replaced, so no need to sync. self._file.close() # Make sure the new file's mode is the same as the old file's mode = os.stat(self._path).st_mode - Path(new_file.name).chmod(mode) + new_path = Path(new_file.name) + new_path.chmod(mode) try: - Path(new_file.name).rename(self._path) + new_path.rename(self._path) except FileExistsError: - os.remove(self._path) - Path(new_file.name).rename(self._path) + self._path.unlink() + new_path.rename(self._path) self._file = open(self._path, 'rb+') self._toc = new_toc self._pending = False @@ -767,7 +767,6 @@ def _append_message(self, message): return offsets - class _mboxMMDF(_singlefileMailbox): """An mbox or MMDF mailbox.""" @@ -831,7 +830,7 @@ def _install_message(self, message): self._file.write(from_line + linesep) self._dump_message(message, self._file, self._mangle_from_) stop = self._file.tell() - return (start, stop) + return start, stop class mbox(_mboxMMDF): @@ -968,7 +967,7 @@ def add(self, message): _unlock_file(f) _sync_close(f) closed = True - os.remove(new_path) + new_path.unlink() raise if isinstance(message, MHMessage): self._dump_sequences(message, new_key) @@ -992,7 +991,7 @@ def remove(self, key): raise else: f.close() - os.remove(path) + path.unlink() def __setitem__(self, key, message): """Replace the keyed message; raise KeyError if it doesn't exist.""" @@ -1077,8 +1076,8 @@ def get_file(self, key): def iterkeys(self): """Return an iterator over keys.""" - return iter(sorted(int(entry) for entry in os.listdir(self._path) - if entry.isdigit())) + return iter(sorted(int(entry.name) for entry in self._path.iterdir() + if entry.name.isdigit())) def __contains__(self, key): """Return True if the keyed message exists, False otherwise.""" @@ -1115,9 +1114,9 @@ def close(self): def list_folders(self): """Return a list of folder names.""" result = [] - for entry in os.listdir(self._path): - if (self._path / entry).is_dir(): - result.append(entry) + for entry in self._path.iterdir(): + if entry.is_dir(): + result.append(entry.resolve().name) return result def get_folder(self, folder): @@ -1133,14 +1132,14 @@ def add_folder(self, folder): def remove_folder(self, folder): """Delete the named folder, which must be empty.""" path = self._path / folder - entries = os.listdir(path) - if entries == ['.mh_sequences']: - os.remove(path / '.mh_sequences') + entries = list(path.iterdir()) + if entries == [Path(path, '.mh_sequences').resolve()]: + (path / '.mh_sequences').unlink() elif entries == []: pass else: raise NotEmptyError('Folder not empty: %s' % os.fspath(self._path)) - os.rmdir(path) + path.rmdir() def get_sequences(self): """Return a name-to-key-list dictionary to define each sequence.""" @@ -1423,7 +1422,7 @@ def _install_message(self, message): if line == b'\n' or not line: break while True: - buffer = orig_buffer.read(4096) # Buffer size is arbitrary. + buffer = orig_buffer.read(4096) # Buffer size is arbitrary. if not buffer: break self._file.write(buffer.replace(b'\n', linesep)) @@ -2081,22 +2080,23 @@ def _lock_file(f, dotlock=True): raise try: try: - Path(pre_lock.name).link_to(f.name + '.lock') + pre_path = Path(pre_lock.name) + pre_path.link_to(f.name + '.lock') dotlock_done = True except (AttributeError, PermissionError): - Path(pre_lock.name).rename(f.name + '.lock') + pre_path.rename(f.name + '.lock') dotlock_done = True else: - Path(pre_lock.name).unlink() + pre_path.unlink() except FileExistsError: - os.remove(pre_lock.name) + pre_path.unlink() raise ExternalClashError('dot lock unavailable: %s' % f.name) except: if fcntl: fcntl.lockf(f, fcntl.LOCK_UN) if dotlock_done: - os.remove(f.name + '.lock') + Path(f.name + '.lock').unlink() raise def _unlock_file(f): @@ -2104,7 +2104,7 @@ def _unlock_file(f): if fcntl: fcntl.lockf(f, fcntl.LOCK_UN) if Path(f.name + '.lock').exists(): - os.remove(f.name + '.lock') + Path(f.name + '.lock').unlink() def _create_carefully(path): """Create a file if it doesn't exist and open for reading and writing."""