fileutils.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. # -*- coding: utf-8 -*-
  2. # Copyright (c) 2013, Mahmoud Hashemi
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions are
  6. # met:
  7. #
  8. # * Redistributions of source code must retain the above copyright
  9. # notice, this list of conditions and the following disclaimer.
  10. #
  11. # * Redistributions in binary form must reproduce the above
  12. # copyright notice, this list of conditions and the following
  13. # disclaimer in the documentation and/or other materials provided
  14. # with the distribution.
  15. #
  16. # * The names of the contributors may not be used to endorse or
  17. # promote products derived from this software without specific
  18. # prior written permission.
  19. #
  20. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23. # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24. # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26. # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31. """Virtually every Python programmer has used Python for wrangling
  32. disk contents, and ``fileutils`` collects solutions to some of the
  33. most commonly-found gaps in the standard library.
  34. """
  35. from __future__ import print_function
  36. import os
  37. import re
  38. import sys
  39. import stat
  40. import errno
  41. import fnmatch
  42. from shutil import copy2, copystat, Error
  43. __all__ = ['mkdir_p', 'atomic_save', 'AtomicSaver', 'FilePerms',
  44. 'iter_find_files', 'copytree']
  45. FULL_PERMS = 511 # 0777 that both Python 2 and 3 can digest
  46. RW_PERMS = 438
  47. _SINGLE_FULL_PERM = 7 # or 07 in Python 2
  48. try:
  49. basestring
  50. except NameError:
  51. unicode = str # Python 3 compat
  52. basestring = (str, bytes)
  53. def mkdir_p(path):
  54. """Creates a directory and any parent directories that may need to
  55. be created along the way, without raising errors for any existing
  56. directories. This function mimics the behavior of the ``mkdir -p``
  57. command available in Linux/BSD environments, but also works on
  58. Windows.
  59. """
  60. try:
  61. os.makedirs(path)
  62. except OSError as exc:
  63. if exc.errno == errno.EEXIST and os.path.isdir(path):
  64. return
  65. raise
  66. return
  67. class FilePerms(object):
  68. """The :class:`FilePerms` type is used to represent standard POSIX
  69. filesystem permissions:
  70. * Read
  71. * Write
  72. * Execute
  73. Across three classes of user:
  74. * Owning (u)ser
  75. * Owner's (g)roup
  76. * Any (o)ther user
  77. This class assists with computing new permissions, as well as
  78. working with numeric octal ``777``-style and ``rwx``-style
  79. permissions. Currently it only considers the bottom 9 permission
  80. bits; it does not support sticky bits or more advanced permission
  81. systems.
  82. Args:
  83. user (str): A string in the 'rwx' format, omitting characters
  84. for which owning user's permissions are not provided.
  85. group (str): A string in the 'rwx' format, omitting characters
  86. for which owning group permissions are not provided.
  87. other (str): A string in the 'rwx' format, omitting characters
  88. for which owning other/world permissions are not provided.
  89. There are many ways to use :class:`FilePerms`:
  90. >>> FilePerms(user='rwx', group='xrw', other='wxr') # note character order
  91. FilePerms(user='rwx', group='rwx', other='rwx')
  92. >>> int(FilePerms('r', 'r', ''))
  93. 288
  94. >>> oct(288)[-3:] # XXX Py3k
  95. '440'
  96. See also the :meth:`FilePerms.from_int` and
  97. :meth:`FilePerms.from_path` classmethods for useful alternative
  98. ways to construct :class:`FilePerms` objects.
  99. """
  100. # TODO: consider more than the lower 9 bits
  101. class _FilePermProperty(object):
  102. _perm_chars = 'rwx'
  103. _perm_set = frozenset('rwx')
  104. _perm_val = {'r': 4, 'w': 2, 'x': 1} # for sorting
  105. def __init__(self, attribute, offset):
  106. self.attribute = attribute
  107. self.offset = offset
  108. def __get__(self, fp_obj, type_=None):
  109. if fp_obj is None:
  110. return self
  111. return getattr(fp_obj, self.attribute)
  112. def __set__(self, fp_obj, value):
  113. cur = getattr(fp_obj, self.attribute)
  114. if cur == value:
  115. return
  116. try:
  117. invalid_chars = set(str(value)) - self._perm_set
  118. except TypeError:
  119. raise TypeError('expected string, not %r' % value)
  120. if invalid_chars:
  121. raise ValueError('got invalid chars %r in permission'
  122. ' specification %r, expected empty string'
  123. ' or one or more of %r'
  124. % (invalid_chars, value, self._perm_chars))
  125. sort_key = lambda c: self._perm_val[c]
  126. new_value = ''.join(sorted(set(value),
  127. key=sort_key, reverse=True))
  128. setattr(fp_obj, self.attribute, new_value)
  129. self._update_integer(fp_obj, new_value)
  130. def _update_integer(self, fp_obj, value):
  131. mode = 0
  132. key = 'xwr'
  133. for symbol in value:
  134. bit = 2 ** key.index(symbol)
  135. mode |= (bit << (self.offset * 3))
  136. fp_obj._integer |= mode
  137. def __init__(self, user='', group='', other=''):
  138. self._user, self._group, self._other = '', '', ''
  139. self._integer = 0
  140. self.user = user
  141. self.group = group
  142. self.other = other
  143. @classmethod
  144. def from_int(cls, i):
  145. """Create a :class:`FilePerms` object from an integer.
  146. >>> FilePerms.from_int(0o644) # note the leading zero-oh for octal
  147. FilePerms(user='rw', group='r', other='r')
  148. """
  149. i &= FULL_PERMS
  150. key = ('', 'x', 'w', 'xw', 'r', 'rx', 'rw', 'rwx')
  151. parts = []
  152. while i:
  153. parts.append(key[i & _SINGLE_FULL_PERM])
  154. i >>= 3
  155. parts.reverse()
  156. return cls(*parts)
  157. @classmethod
  158. def from_path(cls, path):
  159. """Make a new :class:`FilePerms` object based on the permissions
  160. assigned to the file or directory at *path*.
  161. Args:
  162. path (str): Filesystem path of the target file.
  163. Here's an example that holds true on most systems:
  164. >>> import tempfile
  165. >>> 'r' in FilePerms.from_path(tempfile.gettempdir()).user
  166. True
  167. """
  168. stat_res = os.stat(path)
  169. return cls.from_int(stat.S_IMODE(stat_res.st_mode))
  170. def __int__(self):
  171. return self._integer
  172. # Sphinx tip: attribute docstrings come after the attribute
  173. user = _FilePermProperty('_user', 2)
  174. "Stores the ``rwx``-formatted *user* permission."
  175. group = _FilePermProperty('_group', 1)
  176. "Stores the ``rwx``-formatted *group* permission."
  177. other = _FilePermProperty('_other', 0)
  178. "Stores the ``rwx``-formatted *other* permission."
  179. def __repr__(self):
  180. cn = self.__class__.__name__
  181. return ('%s(user=%r, group=%r, other=%r)'
  182. % (cn, self.user, self.group, self.other))
  183. ####
  184. _TEXT_OPENFLAGS = os.O_RDWR | os.O_CREAT | os.O_EXCL
  185. if hasattr(os, 'O_NOINHERIT'):
  186. _TEXT_OPENFLAGS |= os.O_NOINHERIT
  187. if hasattr(os, 'O_NOFOLLOW'):
  188. _TEXT_OPENFLAGS |= os.O_NOFOLLOW
  189. _BIN_OPENFLAGS = _TEXT_OPENFLAGS
  190. if hasattr(os, 'O_BINARY'):
  191. _BIN_OPENFLAGS |= os.O_BINARY
  192. try:
  193. import fcntl as fcntl
  194. except ImportError:
  195. def set_cloexec(fd):
  196. "Dummy set_cloexec for platforms without fcntl support"
  197. pass
  198. else:
  199. def set_cloexec(fd):
  200. """Does a best-effort :func:`fcntl.fcntl` call to set a fd to be
  201. automatically closed by any future child processes.
  202. Implementation from the :mod:`tempfile` module.
  203. """
  204. try:
  205. flags = fcntl.fcntl(fd, fcntl.F_GETFD, 0)
  206. except IOError:
  207. pass
  208. else:
  209. # flags read successfully, modify
  210. flags |= fcntl.FD_CLOEXEC
  211. fcntl.fcntl(fd, fcntl.F_SETFD, flags)
  212. return
  213. def atomic_save(dest_path, **kwargs):
  214. """A convenient interface to the :class:`AtomicSaver` type. Example:
  215. >>> try:
  216. ... with atomic_save("file.txt", text_mode=True) as fo:
  217. ... _ = fo.write('bye')
  218. ... 1/0 # will error
  219. ... fo.write('bye')
  220. ... except ZeroDivisionError:
  221. ... pass # at least our file.txt didn't get overwritten
  222. See the :class:`AtomicSaver` documentation for details.
  223. """
  224. return AtomicSaver(dest_path, **kwargs)
  225. def path_to_unicode(path):
  226. if isinstance(path, unicode):
  227. return path
  228. encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
  229. return path.decode(encoding)
  230. if os.name == 'nt':
  231. import ctypes
  232. from ctypes import c_wchar_p
  233. from ctypes.wintypes import DWORD, LPVOID
  234. _ReplaceFile = ctypes.windll.kernel32.ReplaceFile
  235. _ReplaceFile.argtypes = [c_wchar_p, c_wchar_p, c_wchar_p,
  236. DWORD, LPVOID, LPVOID]
  237. def replace(src, dst):
  238. # argument names match stdlib docs, docstring below
  239. try:
  240. # ReplaceFile fails if the dest file does not exist, so
  241. # first try to rename it into position
  242. os.rename(src, dst)
  243. return
  244. except WindowsError as we:
  245. if we.errno == errno.EEXIST:
  246. pass # continue with the ReplaceFile logic below
  247. else:
  248. raise
  249. src = path_to_unicode(src)
  250. dst = path_to_unicode(dst)
  251. res = _ReplaceFile(c_wchar_p(dst), c_wchar_p(src),
  252. None, 0, None, None)
  253. if not res:
  254. raise OSError('failed to replace %r with %r' % (dst, src))
  255. return
  256. def atomic_rename(src, dst, overwrite=False):
  257. "Rename *src* to *dst*, replacing *dst* if *overwrite is True"
  258. if overwrite:
  259. replace(src, dst)
  260. else:
  261. os.rename(src, dst)
  262. return
  263. else:
  264. # wrapper func for cross compat + docs
  265. def replace(src, dst):
  266. # os.replace does the same thing on unix
  267. return os.rename(src, dst)
  268. def atomic_rename(src, dst, overwrite=False):
  269. "Rename *src* to *dst*, replacing *dst* if *overwrite is True"
  270. if overwrite:
  271. os.rename(src, dst)
  272. else:
  273. os.link(src, dst)
  274. os.unlink(src)
  275. return
  276. _atomic_rename = atomic_rename # backwards compat
  277. replace.__doc__ = """Similar to :func:`os.replace` in Python 3.3+,
  278. this function will atomically create or replace the file at path
  279. *dst* with the file at path *src*.
  280. On Windows, this function uses the ReplaceFile API for maximum
  281. possible atomicity on a range of filesystems.
  282. """
  283. class AtomicSaver(object):
  284. """``AtomicSaver`` is a configurable `context manager`_ that provides
  285. a writable :class:`file` which will be moved into place as long as
  286. no exceptions are raised within the context manager's block. These
  287. "part files" are created in the same directory as the destination
  288. path to ensure atomic move operations (i.e., no cross-filesystem
  289. moves occur).
  290. Args:
  291. dest_path (str): The path where the completed file will be
  292. written.
  293. overwrite (bool): Whether to overwrite the destination file if
  294. it exists at completion time. Defaults to ``True``.
  295. file_perms (int): Integer representation of file permissions
  296. for the newly-created file. Defaults are, when the
  297. destination path already exists, to copy the permissions
  298. from the previous file, or if the file did not exist, to
  299. respect the user's configured `umask`_, usually resulting
  300. in octal 0644 or 0664.
  301. text_mode (bool): Whether to open the destination file in text
  302. mode (i.e., ``'w'`` not ``'wb'``). Defaults to ``False`` (``wb``).
  303. part_file (str): Name of the temporary *part_file*. Defaults
  304. to *dest_path* + ``.part``. Note that this argument is
  305. just the filename, and not the full path of the part
  306. file. To guarantee atomic saves, part files are always
  307. created in the same directory as the destination path.
  308. overwrite_part (bool): Whether to overwrite the *part_file*,
  309. should it exist at setup time. Defaults to ``False``,
  310. which results in an :exc:`OSError` being raised on
  311. pre-existing part files. Be careful of setting this to
  312. ``True`` in situations when multiple threads or processes
  313. could be writing to the same part file.
  314. rm_part_on_exc (bool): Remove *part_file* on exception cases.
  315. Defaults to ``True``, but ``False`` can be useful for
  316. recovery in some cases. Note that resumption is not
  317. automatic and by default an :exc:`OSError` is raised if
  318. the *part_file* exists.
  319. Practically, the AtomicSaver serves a few purposes:
  320. * Avoiding overwriting an existing, valid file with a partially
  321. written one.
  322. * Providing a reasonable guarantee that a part file only has one
  323. writer at a time.
  324. * Optional recovery of partial data in failure cases.
  325. .. _context manager: https://docs.python.org/2/reference/compound_stmts.html#with
  326. .. _umask: https://en.wikipedia.org/wiki/Umask
  327. """
  328. _default_file_perms = RW_PERMS
  329. # TODO: option to abort if target file modify date has changed since start?
  330. def __init__(self, dest_path, **kwargs):
  331. self.dest_path = dest_path
  332. self.overwrite = kwargs.pop('overwrite', True)
  333. self.file_perms = kwargs.pop('file_perms', None)
  334. self.overwrite_part = kwargs.pop('overwrite_part', False)
  335. self.part_filename = kwargs.pop('part_file', None)
  336. self.rm_part_on_exc = kwargs.pop('rm_part_on_exc', True)
  337. self.text_mode = kwargs.pop('text_mode', False)
  338. self.buffering = kwargs.pop('buffering', -1)
  339. if kwargs:
  340. raise TypeError('unexpected kwargs: %r' % (kwargs.keys(),))
  341. self.dest_path = os.path.abspath(self.dest_path)
  342. self.dest_dir = os.path.dirname(self.dest_path)
  343. if not self.part_filename:
  344. self.part_path = dest_path + '.part'
  345. else:
  346. self.part_path = os.path.join(self.dest_dir, self.part_filename)
  347. self.mode = 'w+' if self.text_mode else 'w+b'
  348. self.open_flags = _TEXT_OPENFLAGS if self.text_mode else _BIN_OPENFLAGS
  349. self.part_file = None
  350. def _open_part_file(self):
  351. do_chmod = True
  352. file_perms = self.file_perms
  353. if file_perms is None:
  354. try:
  355. # try to copy from file being replaced
  356. stat_res = os.stat(self.dest_path)
  357. file_perms = stat.S_IMODE(stat_res.st_mode)
  358. except (OSError, IOError):
  359. # default if no destination file exists
  360. file_perms = self._default_file_perms
  361. do_chmod = False # respect the umask
  362. fd = os.open(self.part_path, self.open_flags, file_perms)
  363. set_cloexec(fd)
  364. self.part_file = os.fdopen(fd, self.mode, self.buffering)
  365. # if default perms are overridden by the user or previous dest_path
  366. # chmod away the effects of the umask
  367. if do_chmod:
  368. try:
  369. os.chmod(self.part_path, file_perms)
  370. except (OSError, IOError):
  371. self.part_file.close()
  372. raise
  373. return
  374. def setup(self):
  375. """Called on context manager entry (the :keyword:`with` statement),
  376. the ``setup()`` method creates the temporary file in the same
  377. directory as the destination file.
  378. ``setup()`` tests for a writable directory with rename permissions
  379. early, as the part file may not be written to immediately (not
  380. using :func:`os.access` because of the potential issues of
  381. effective vs. real privileges).
  382. If the caller is not using the :class:`AtomicSaver` as a
  383. context manager, this method should be called explicitly
  384. before writing.
  385. """
  386. if os.path.lexists(self.dest_path):
  387. if not self.overwrite:
  388. raise OSError(errno.EEXIST,
  389. 'Overwrite disabled and file already exists',
  390. self.dest_path)
  391. if self.overwrite_part and os.path.lexists(self.part_path):
  392. os.unlink(self.part_path)
  393. self._open_part_file()
  394. return
  395. def __enter__(self):
  396. self.setup()
  397. return self.part_file
  398. def __exit__(self, exc_type, exc_val, exc_tb):
  399. self.part_file.close()
  400. if exc_type:
  401. if self.rm_part_on_exc:
  402. try:
  403. os.unlink(self.part_path)
  404. except Exception:
  405. pass # avoid masking original error
  406. return
  407. try:
  408. atomic_rename(self.part_path, self.dest_path,
  409. overwrite=self.overwrite)
  410. except OSError:
  411. if self.rm_part_on_exc:
  412. try:
  413. os.unlink(self.part_path)
  414. except Exception:
  415. pass # avoid masking original error
  416. raise # could not save destination file
  417. return
  418. def iter_find_files(directory, patterns, ignored=None, include_dirs=False):
  419. """Returns a generator that yields file paths under a *directory*,
  420. matching *patterns* using `glob`_ syntax (e.g., ``*.txt``). Also
  421. supports *ignored* patterns.
  422. Args:
  423. directory (str): Path that serves as the root of the
  424. search. Yielded paths will include this as a prefix.
  425. patterns (str or list): A single pattern or list of
  426. glob-formatted patterns to find under *directory*.
  427. ignored (str or list): A single pattern or list of
  428. glob-formatted patterns to ignore.
  429. include_dirs (bool): Whether to include directories that match
  430. patterns, as well. Defaults to ``False``.
  431. For example, finding Python files in the current directory:
  432. >>> _CUR_DIR = os.path.dirname(os.path.abspath(__file__))
  433. >>> filenames = sorted(iter_find_files(_CUR_DIR, '*.py'))
  434. >>> os.path.basename(filenames[-1])
  435. 'urlutils.py'
  436. Or, Python files while ignoring emacs lockfiles:
  437. >>> filenames = iter_find_files(_CUR_DIR, '*.py', ignored='.#*')
  438. .. _glob: https://en.wikipedia.org/wiki/Glob_%28programming%29
  439. """
  440. if isinstance(patterns, basestring):
  441. patterns = [patterns]
  442. pats_re = re.compile('|'.join([fnmatch.translate(p) for p in patterns]))
  443. if not ignored:
  444. ignored = []
  445. elif isinstance(ignored, basestring):
  446. ignored = [ignored]
  447. ign_re = re.compile('|'.join([fnmatch.translate(p) for p in ignored]))
  448. for root, dirs, files in os.walk(directory):
  449. if include_dirs:
  450. for basename in dirs:
  451. if pats_re.match(basename):
  452. if ignored and ign_re.match(basename):
  453. continue
  454. filename = os.path.join(root, basename)
  455. yield filename
  456. for basename in files:
  457. if pats_re.match(basename):
  458. if ignored and ign_re.match(basename):
  459. continue
  460. filename = os.path.join(root, basename)
  461. yield filename
  462. return
  463. def copy_tree(src, dst, symlinks=False, ignore=None):
  464. """The ``copy_tree`` function is an exact copy of the built-in
  465. :func:`shutil.copytree`, with one key difference: it will not
  466. raise an exception if part of the tree already exists. It achieves
  467. this by using :func:`mkdir_p`.
  468. As of Python 3.8, you may pass :func:`shutil.copytree` the
  469. `dirs_exist_ok=True` flag to achieve the same effect.
  470. Args:
  471. src (str): Path of the source directory to copy.
  472. dst (str): Destination path. Existing directories accepted.
  473. symlinks (bool): If ``True``, copy symlinks rather than their
  474. contents.
  475. ignore (callable): A callable that takes a path and directory
  476. listing, returning the files within the listing to be ignored.
  477. For more details, check out :func:`shutil.copytree` and
  478. :func:`shutil.copy2`.
  479. """
  480. names = os.listdir(src)
  481. if ignore is not None:
  482. ignored_names = ignore(src, names)
  483. else:
  484. ignored_names = set()
  485. mkdir_p(dst)
  486. errors = []
  487. for name in names:
  488. if name in ignored_names:
  489. continue
  490. srcname = os.path.join(src, name)
  491. dstname = os.path.join(dst, name)
  492. try:
  493. if symlinks and os.path.islink(srcname):
  494. linkto = os.readlink(srcname)
  495. os.symlink(linkto, dstname)
  496. elif os.path.isdir(srcname):
  497. copytree(srcname, dstname, symlinks, ignore)
  498. else:
  499. # Will raise a SpecialFileError for unsupported file types
  500. copy2(srcname, dstname)
  501. # catch the Error from the recursive copytree so that we can
  502. # continue with other files
  503. except Error as e:
  504. errors.extend(e.args[0])
  505. except EnvironmentError as why:
  506. errors.append((srcname, dstname, str(why)))
  507. try:
  508. copystat(src, dst)
  509. except OSError as why:
  510. if WindowsError is not None and isinstance(why, WindowsError):
  511. # Copying file access times may fail on Windows
  512. pass
  513. else:
  514. errors.append((src, dst, str(why)))
  515. if errors:
  516. raise Error(errors)
  517. copytree = copy_tree # alias for drop-in replacement of shutil
  518. try:
  519. file
  520. except NameError:
  521. file = object
  522. # like open(os.devnull) but with even fewer side effects
  523. class DummyFile(file):
  524. # TODO: raise ValueErrors on closed for all methods?
  525. # TODO: enforce read/write
  526. def __init__(self, path, mode='r', buffering=None):
  527. self.name = path
  528. self.mode = mode
  529. self.closed = False
  530. self.errors = None
  531. self.isatty = False
  532. self.encoding = None
  533. self.newlines = None
  534. self.softspace = 0
  535. def close(self):
  536. self.closed = True
  537. def fileno(self):
  538. return -1
  539. def flush(self):
  540. if self.closed:
  541. raise ValueError('I/O operation on a closed file')
  542. return
  543. def next(self):
  544. raise StopIteration()
  545. def read(self, size=0):
  546. if self.closed:
  547. raise ValueError('I/O operation on a closed file')
  548. return ''
  549. def readline(self, size=0):
  550. if self.closed:
  551. raise ValueError('I/O operation on a closed file')
  552. return ''
  553. def readlines(self, size=0):
  554. if self.closed:
  555. raise ValueError('I/O operation on a closed file')
  556. return []
  557. def seek(self):
  558. if self.closed:
  559. raise ValueError('I/O operation on a closed file')
  560. return
  561. def tell(self):
  562. if self.closed:
  563. raise ValueError('I/O operation on a closed file')
  564. return 0
  565. def truncate(self):
  566. if self.closed:
  567. raise ValueError('I/O operation on a closed file')
  568. return
  569. def write(self, string):
  570. if self.closed:
  571. raise ValueError('I/O operation on a closed file')
  572. return
  573. def writelines(self, list_of_strings):
  574. if self.closed:
  575. raise ValueError('I/O operation on a closed file')
  576. return
  577. def __next__(self):
  578. raise StopIteration()
  579. def __enter__(self):
  580. if self.closed:
  581. raise ValueError('I/O operation on a closed file')
  582. return
  583. def __exit__(self, exc_type, exc_val, exc_tb):
  584. return
  585. if __name__ == '__main__':
  586. with atomic_save('/tmp/final.txt') as f:
  587. f.write('rofl')
  588. f.write('\n')