pax_global_header00006660000000000000000000000064147230547770014532gustar00rootroot0000000000000052 comment=36b9bac1bb6d028a8756da2ed4d19ac8f168592a inplace-1.0.1/000077500000000000000000000000001472305477700131445ustar00rootroot00000000000000inplace-1.0.1/.github/000077500000000000000000000000001472305477700145045ustar00rootroot00000000000000inplace-1.0.1/.github/dependabot.yml000066400000000000000000000006411472305477700173350ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: / schedule: interval: weekly commit-message: prefix: "[python]" labels: - dependencies - d:python - package-ecosystem: github-actions directory: / schedule: interval: weekly commit-message: prefix: "[gh-actions]" include: scope labels: - dependencies - d:github-actions inplace-1.0.1/.github/workflows/000077500000000000000000000000001472305477700165415ustar00rootroot00000000000000inplace-1.0.1/.github/workflows/test.yml000066400000000000000000000032761472305477700202530ustar00rootroot00000000000000name: Test on: pull_request: push: branches: - master schedule: - cron: '0 6 * * *' concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name }} cancel-in-progress: true jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - macos-latest - ubuntu-latest - windows-latest python-version: - '3.8' - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' - 'pypy-3.8' - 'pypy-3.9' - 'pypy-3.10' toxenv: [py] include: - python-version: '3.8' toxenv: lint os: ubuntu-latest - python-version: '3.8' toxenv: typing os: ubuntu-latest steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip wheel python -m pip install --upgrade --upgrade-strategy=eager tox - name: Run tests with coverage if: matrix.toxenv == 'py' run: tox -e py -- --cov-report=xml - name: Run generic tests if: matrix.toxenv != 'py' run: tox -e ${{ matrix.toxenv }} - name: Upload coverage to Codecov if: matrix.toxenv == 'py' uses: codecov/codecov-action@v5 with: fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} name: ${{ matrix.python-version }} # vim:set et sts=2: inplace-1.0.1/.gitignore000066400000000000000000000000601472305477700151300ustar00rootroot00000000000000.coverage .tox/ __pycache__/ dist/ docs/_build/ inplace-1.0.1/.pre-commit-config.yaml000066400000000000000000000012361472305477700174270ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-added-large-files - id: check-json - id: check-toml - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black rev: 24.4.2 hooks: - id: black - repo: https://github.com/PyCQA/isort rev: 5.13.2 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 rev: 7.0.0 hooks: - id: flake8 additional_dependencies: - flake8-bugbear - flake8-builtins - flake8-unused-arguments exclude: ^test/data inplace-1.0.1/CHANGELOG.md000066400000000000000000000055761472305477700147720ustar00rootroot00000000000000v1.0.1 (2024-12-01) ------------------- - Migrated from setuptools to hatch - Support Python 3.13 v1.0.0 (2023-10-12) ------------------- - Support Python 3.10, 3.11, and 3.12 - Drop support for Python 3.6 and 3.7 - **Breaking**: The `move_first` argument has been removed. Only the `move_first=False` semantics are retained. - Removed the `readall()` method. I don't think it ever worked. - **Breaking**: The `delay_open` argument and `open()` method have been removed. Filehandles will now always be created at the moment an in-place instance is constructed, just like when calling the standard library's `open()`. - **Breaking**: When the input path points to a symlink and `backup_ext` is given, the backup extension will now be appended to the resolved path rather than to the pre-resolved path. - Added type annotations - `InPlaceText` and `InPlaceBytes` (deprecated in v0.4.0) have been removed - Added `read1()` and `readinto1()` methods for binary mode - The `InPlace` constructor now immediately raises a `ValueError` if `backup` is the empty string v0.5.0 (2021-02-20) ------------------- - Support Python 3.8 and 3.9 - Drop support for Python 2.7, 3.4, and 3.5 - Support `move_first` on Windows - Get tests to pass on Windows - Use [`jaraco.windows`](https://github.com/jaraco/jaraco.windows) to handle symlinks on Windows on versions of Python prior to 3.8 v0.4.0 (2018-10-05) ------------------- - **Breaking**: Combined all classes' functionality into a single `InPlace` class that uses a `mode` argument to determine whether to operate in text or binary mode. - `InPlaceBytes` and `InPlaceText` are now deprecated and will be removed in a future version; please use `InPlace` with `mode='b'` or `mode='t'` instead. - Support fsencoded-bytes as file paths under Python 3 v0.3.0 (2018-06-28) ------------------- - Handling of symbolic links is changed: Now, if `in_place` is asked to operate on a symlink `link.txt` that points to `realfile.txt`, it will act as though it was asked to operate on `realfile.txt` instead, and the path `link.txt` will only be used when combining with `backup_ext` to construct a backup file path - Drop support for Python 2.6 and 3.3 v0.2.0 (2017-02-23) ------------------- - Renamed `InPlace` to `InPlaceText` and added a new `InPlace` class for reading & writing `str` objects (whatever those happen to be in the current Python) - **Bugfix**: If the given file does not exist and `move_first` is `True`, an empty file will no longer be left behind in the nonexistent file's place. - Specifying both `backup` and `backup_ext` will now produce a `ValueError` - Specifying an empty `backup_ext` will now produce a `ValueError` v0.1.1 (2017-01-27) ------------------- Rename package & module from "`inplace`" to "`in_place`" (I could have sworn I had already checked PyPI for name conflicts....) v0.1.0 (2017-01-27) ------------------- Initial release inplace-1.0.1/LICENSE000066400000000000000000000021071472305477700141510ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016-2024 John Thorvald Wodder II Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. inplace-1.0.1/README.rst000066400000000000000000000151761472305477700146450ustar00rootroot00000000000000|repostatus| |ci-status| |coverage| |pyversions| |conda| |license| .. |repostatus| image:: https://www.repostatus.org/badges/latest/active.svg :target: https://www.repostatus.org/#active :alt: Project Status: Active - The project has reached a stable, usable state and is being actively developed. .. |ci-status| image:: https://github.com/jwodder/inplace/actions/workflows/test.yml/badge.svg :target: https://github.com/jwodder/inplace/actions/workflows/test.yml :alt: CI Status .. |coverage| image:: https://codecov.io/gh/jwodder/inplace/branch/master/graph/badge.svg :target: https://codecov.io/gh/jwodder/inplace .. |pyversions| image:: https://img.shields.io/pypi/pyversions/in_place.svg :target: https://pypi.org/project/in_place .. |conda| image:: https://img.shields.io/conda/vn/conda-forge/in_place.svg :target: https://anaconda.org/conda-forge/in_place :alt: Conda Version .. |license| image:: https://img.shields.io/github/license/jwodder/inplace.svg?maxAge=2592000 :target: https://opensource.org/licenses/MIT :alt: MIT License `GitHub `_ | `PyPI `_ | `Issues `_ | `Changelog `_ The ``in_place`` module provides an ``InPlace`` class for reading & writing a file "in-place": data that you write ends up at the same filepath that you read from, and ``in_place`` takes care of all the necessary mucking about with temporary files for you. For example, given the file ``somefile.txt``:: 'Twas brillig, and the slithy toves Did gyre and gimble in the wabe; All mimsy were the borogoves, And the mome raths outgrabe. and the program ``disemvowel.py``: .. code:: python import in_place with in_place.InPlace("somefile.txt") as fp: for line in fp: fp.write("".join(c for c in line if c not in "AEIOUaeiou")) after running the program, ``somefile.txt`` will have been edited in place, reducing it to just:: 'Tws brllg, nd th slthy tvs Dd gyr nd gmbl n th wb; ll mmsy wr th brgvs, nd th mm rths tgrb. and no sign of those pesky vowels remains! If you want a sign of those pesky vowels to remain, you can instead save the file's original contents in, say, ``somefile.txt~`` by constructing the filehandle with: .. code:: python in_place.InPlace("somefile.txt", backup_ext="~") or save to ``someotherfile.txt`` with: .. code:: python in_place.InPlace("somefile.txt", backup="someotherfile.txt") Compared to the in-place filtering implemented by the Python standard library's |fileinput|_ module, ``in_place`` offers the following benefits: - Instead of hijacking ``sys.stdout``, a new filehandle is returned for writing. - The filehandle supports all of the standard I/O methods, not just ``readline()``. - There are options for setting the encoding, encoding error handling, and newline policy for opening the file, along with support for opening files in binary mode, and these options apply to both input and output. - The complete filename of the backup file can be specified; you aren't constrained to just adding an extension. - When used as a context manager, ``in_place`` will restore the original file if an exception occurs. - The creation of temporary files won't silently clobber innocent bystander files. .. |fileinput| replace:: ``fileinput`` .. _fileinput: https://docs.python.org/3/library/fileinput.html Installation ============ ``in_place`` requires Python 3.8 or higher. Just use `pip `_ for Python 3 (You have pip, right?) to install it:: python3 -m pip install in_place Basic Usage =========== ``in_place`` provides a single class, ``InPlace``. Its constructor takes the following arguments: ``name=`` (required) The path to the file to open & edit in-place ``mode=<"b"|"t"|None>`` Whether to operate on the file in binary or text mode. If ``mode`` is ``"b"``, the file will be opened in binary mode, and data will be read & written as ``bytes`` objects. If ``mode`` is ``"t"`` or ``None`` (the default), the file will be opened in text mode, and data will be read & written as ``str`` objects. ``backup=`` If set, the original contents of the file will be saved to the given path when the instance is closed. ``backup`` cannot be set to the empty string. ``backup_ext=`` If set, the path to the backup file will be created by appending ``backup_ext`` to the original file path. ``backup`` and ``backup_ext`` are mutually exclusive. ``backup_ext`` cannot be set to the empty string. ``**kwargs`` Any additional keyword arguments (such as ``encoding``, ``errors``, and ``newline``) will be forwarded to ``open()`` when opening both the input and output file streams. ``name``, ``backup``, and ``backup_ext`` can be ``str``, filesystem-encoded ``bytes``, or path-like objects. ``InPlace`` instances act as read-write filehandles with the usual filehandle attributes, specifically:: __iter__() __next__() closed flush() name read() read1() * readinto() * readinto1() * readline() readlines() write() writelines() * binary mode only ``InPlace`` instances also feature the following new or modified attributes: ``close()`` Close filehandles and move files to their final destinations. If called after the filehandle has already been closed, ``close()`` does nothing. Be sure to always close your instances when you're done with them by calling ``close()`` or ``rollback()`` either explicitly or implicitly (i.e., via use as a context manager). ``rollback()`` Like ``close()``, but discard the output data (keeping the original file intact) instead of replacing the original file with it ``__enter__()``, ``__exit__()`` When an ``InPlace`` instance is used as a context manager, on exiting the context, the instance will be either closed (if all went well) or rolled back (if an exception occurred). ``InPlace`` context managers are not reusable_ but are reentrant_ (as long as no further operations are performed after the innermost context ends). ``input`` The actual filehandle that data is read from, in case you need to access it directly ``output`` The actual filehandle that data is written to, in case you need to access it directly .. _reentrant: https://docs.python.org/3/library/contextlib.html#reentrant-cms .. _reusable: https://docs.python.org/3/library/contextlib.html#reusable-context-managers inplace-1.0.1/pyproject.toml000066400000000000000000000037161472305477700160670ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "in_place" dynamic = ["version"] description = "In-place file processing" readme = "README.rst" requires-python = ">=3.8" license = "MIT" license-files = { paths = ["LICENSE"] } authors = [ { name = "John Thorvald Wodder II", email = "inplace@varonathe.org" } ] keywords = [ "inplace", "in-place", "io", "open", "file", "tmpfile", "tempfile", "sed", "redirection", "fileinput", ] classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "License :: OSI Approved :: MIT License", "Intended Audience :: Developers", "Topic :: System :: Filesystems", "Topic :: Text Processing :: Filters", "Typing :: Typed", ] dependencies = [] [project.urls] "Source Code" = "https://github.com/jwodder/inplace" "Bug Tracker" = "https://github.com/jwodder/inplace/issues" [tool.hatch.version] path = "src/in_place/__init__.py" [tool.hatch.build.targets.sdist] include = [ "/docs", "/src", "/test", "CHANGELOG.*", "CONTRIBUTORS.*", "tox.ini", ] [tool.hatch.envs.default] python = "3" [tool.mypy] allow_incomplete_defs = false allow_untyped_defs = false ignore_missing_imports = false # : no_implicit_optional = true implicit_reexport = false local_partial_types = true pretty = true show_error_codes = true show_traceback = true strict_equality = true warn_redundant_casts = true warn_return_any = true warn_unreachable = true inplace-1.0.1/src/000077500000000000000000000000001472305477700137335ustar00rootroot00000000000000inplace-1.0.1/src/in_place/000077500000000000000000000000001472305477700155055ustar00rootroot00000000000000inplace-1.0.1/src/in_place/__init__.py000066400000000000000000000236211472305477700176220ustar00rootroot00000000000000""" In-place file processing The ``in_place`` module provides an ``InPlace`` class for reading & writing a file "in-place": data that you write ends up at the same filepath that you read from, and ``in_place`` takes care of all the necessary mucking about with temporary files for you. Visit for more information. """ from __future__ import annotations from collections.abc import Iterable import os import os.path import shutil import tempfile from types import TracebackType from typing import IO, TYPE_CHECKING, Any, AnyStr, Literal, Union, overload if TYPE_CHECKING: from typing_extensions import Buffer __version__ = "1.0.1" __author__ = "John Thorvald Wodder II" __author_email__ = "inplace@varonathe.org" __license__ = "MIT" __url__ = "https://github.com/jwodder/inplace" __all__ = ["InPlace"] AnyPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] class InPlace(IO[AnyStr]): """ A class for reading from & writing to a file "in-place" (with data that you write ending up at the same filepath that you read from) that takes care of all the necessary mucking about with temporary files. :param name: The path to the file to open & edit in-place (resolved relative to the current directory at the time of the instance's creation) :type name: path-like :param string mode: Whether to operate on the file in binary or text mode. If ``mode`` is ``"b"``, the file will be opened in binary mode, and data will be read & written as `bytes` objects. If ``mode`` is ``"t"`` or unset, the file will be opened in text mode, and data will be read & written as `str` objects. :param backup: The path at which to save the file's original contents once editing has finished (resolved relative to the current directory at the time of the instance's creation); if `None` (the default), no backup is saved. Cannot be empty. :type backup: path-like :param backup_ext: A string to append to ``name`` to get the path at which to save the file's original contents. Cannot be empty. ``backup`` and ``backup_ext`` are mutually exclusive. :type backup_ext: path-like :param kwargs: Additional keyword arguments to pass to `open()` """ @overload def __init__( self: InPlace[str], name: AnyPath, mode: Literal["t", None] = None, backup: AnyPath | None = None, backup_ext: AnyPath | None = None, **kwargs: Any, ) -> None: ... @overload def __init__( self: InPlace[bytes], name: AnyPath, mode: Literal["b"], backup: AnyPath | None = None, backup_ext: AnyPath | None = None, **kwargs: Any, ) -> None: ... def __init__( self, name: AnyPath, mode: Literal["t", "b", None] = None, backup: AnyPath | None = None, backup_ext: AnyPath | None = None, **kwargs: Any, ) -> None: cwd = os.getcwd() #: The path to the file to edit in-place self._name = os.fsdecode(name) #: The absolute path of the file to edit in-place, with symbolic links #: resolved self._path = os.path.realpath(os.path.join(cwd, self._name)) #: The absolute path of the backup file (if any) that the original #: contents of ``path`` will be moved to after editing self._backuppath: str | None if backup is not None: if backup_ext is not None: raise ValueError("backup and backup_ext are mutually exclusive") b = os.fsdecode(backup) if not b: raise ValueError("backup cannot be empty") self._backuppath = os.path.join(cwd, b) elif backup_ext is not None: be = os.fsdecode(backup_ext) if not be: raise ValueError("backup_ext cannot be empty") self._backuppath = self._path + be else: self._backuppath = None if mode not in (None, "t", "b"): raise ValueError(f"{mode!r}: invalid mode") #: `True` iff the filehandle is closed self._closed = False #: The absolute path to the temporary file self._tmppath = self._mktemp(self._path) try: #: The output filehandle to which data is written self.output: IO[AnyStr] if mode is None or mode == "t": self.output = open(self._tmppath, "w", **kwargs) else: self.output = open(self._tmppath, "wb", **kwargs) except Exception: try_unlink(self._tmppath) raise try: copystats(self._path, self._tmppath) except Exception: self.output.close() try_unlink(self._tmppath) raise try: #: The input filehandle from which data is read self.input: IO[AnyStr] if mode is None or mode == "t": self.input = open(self._path, "r", **kwargs) else: self.input = open(self._path, "rb", **kwargs) except Exception: self.output.close() try_unlink(self._tmppath) raise def __enter__(self) -> InPlace[AnyStr]: return self def __exit__( self, exc_type: type[BaseException] | None, _exc_val: BaseException | None, _exc_tb: TracebackType | None, ) -> None: if not self.closed: if exc_type is not None: self.rollback() else: self.close() def _mktemp(self, filepath: str) -> str: """ Create an empty temporary file in the same directory as ``filepath`` and return the path to the new file """ fd, tmppath = tempfile.mkstemp( dir=os.path.dirname(filepath), prefix="._in_place-", ) os.close(fd) return tmppath def _close(self) -> None: """Close filehandles and set them to `None`""" self._closed = True self.input.close() self.output.close() def close(self) -> None: """ Close filehandles and move affected files to their final destinations. If called after the filehandle has already been closed (with either this method or :meth:`rollback`), :meth:`close` does nothing. :return: `None` """ if not self.closed: self._close() try: if self._backuppath is not None: os.replace(self._path, self._backuppath) os.replace(self._tmppath, self._path) finally: try_unlink(self._tmppath) def rollback(self) -> None: """ Close filehandles and remove/rename temporary files so that things look like they did before the `InPlace` instance was opened :return: `None` :raises ValueError: if called after the `InPlace` instance is closed """ if not self.closed: self._close() try_unlink(self._tmppath) else: raise ValueError("Cannot rollback closed file") @property def name(self) -> str: return self._name @property def closed(self) -> bool: return self._closed def read(self, size: int = -1) -> AnyStr: return self.input.read(size) def read1(self: InPlace[bytes], size: int = -1) -> bytes: bs = self.input.read1(size) # type: ignore[attr-defined] assert isinstance(bs, bytes) return bs def readline(self, size: int = -1) -> AnyStr: return self.input.readline(size) def readlines(self, sizehint: int = -1) -> list[AnyStr]: return self.input.readlines(sizehint) def readinto(self: InPlace[bytes], b: Buffer) -> int: r = self.input.readinto(b) # type: ignore[attr-defined] assert isinstance(r, int) return r def readinto1(self: InPlace[bytes], b: Buffer) -> int: r = self.input.readinto1(b) # type: ignore[attr-defined] assert isinstance(r, int) return r def write(self, s: AnyStr) -> int: return self.output.write(s) def writelines(self, seq: Iterable[AnyStr]) -> None: self.output.writelines(seq) def __iter__(self) -> InPlace[AnyStr]: return self def __next__(self) -> AnyStr: return next(self.input) def flush(self) -> None: self.output.flush() def readable(self) -> bool: return True def writable(self) -> bool: return True def seekable(self) -> bool: return False def seek(self, _offset: int, _whence: int = 0) -> int: raise OSError(f"{type(self).__name__} does not support seek()") def tell(self) -> int: raise OSError(f"{type(self).__name__} does not support tell()") def truncate(self, _size: int | None = None) -> int: raise OSError(f"{type(self).__name__} does not support truncate()") def fileno(self) -> int: raise OSError(f"{type(self).__name__} does not support fileno()") def isatty(self) -> bool: return False def copystats(from_file: str, to_file: str) -> None: """ Copy stat info from ``from_file`` to ``to_file`` using `shutil.copystat`. If possible, also copy the user and/or group ownership information. """ shutil.copystat(from_file, to_file) if hasattr(os, "chown"): st = os.stat(from_file) # Based on GNU sed's behavior: try: os.chown(to_file, st.st_uid, st.st_gid) except IOError: try: os.chown(to_file, -1, st.st_gid) except IOError: pass def try_unlink(path: str) -> None: """ Try to delete the file at ``path``. If the file doesn't exist, do nothing; any other errors are propagated to the caller. """ try: os.unlink(path) except FileNotFoundError: pass inplace-1.0.1/src/in_place/py.typed000066400000000000000000000000001472305477700171720ustar00rootroot00000000000000inplace-1.0.1/test/000077500000000000000000000000001472305477700141235ustar00rootroot00000000000000inplace-1.0.1/test/test_encoding.py000066400000000000000000000045161472305477700173300ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path from unicodedata import normalize import pytest from in_place import InPlace from test_in_place_util import NLB, UNICODE, pylistdir def test_utf8_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(UNICODE, encoding="utf-8") with InPlace(p, "t", encoding="utf-8") as fp: txt = fp.read() assert isinstance(txt, str) assert txt == UNICODE fp.write(normalize("NFD", txt)) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text(encoding="utf-8") == "a\u030Ae\u0301i\u0302\xF8u\u0308\n" def test_utf8_as_latin1(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(UNICODE, encoding="utf-8") with InPlace(p, "t", encoding="latin-1") as fp: txt = fp.read() assert isinstance(txt, str) assert txt == "\xc3\xa5\xc3\xa9\xc3\xae\xc3\xb8\xc3\xbc\n" fp.write(UNICODE) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_bytes() == b"\xE5\xE9\xEE\xF8\xFC" + NLB def test_latin1_as_utf8(tmp_path: Path) -> None: p = tmp_path / "file.txt" p.write_text(UNICODE, encoding="latin-1") with InPlace(p, "t", encoding="utf-8") as fp: with pytest.raises(UnicodeDecodeError): fp.read() def test_latin1_as_utf8_replace(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(UNICODE, encoding="latin-1") with InPlace(p, "t", encoding="utf-8", errors="replace") as fp: txt = fp.read() assert isinstance(txt, str) assert txt == "\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\n" fp.write(txt) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text(encoding="utf-8") == "\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\n" def test_bytes_iconv_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(UNICODE, encoding="utf-8") with InPlace(p, "b") as fp: txt = fp.read() assert isinstance(txt, bytes) assert txt == b"\xc3\xa5\xc3\xa9\xc3\xae\xc3\xb8\xc3\xbc" + NLB fp.write(txt.decode("utf-8").encode("latin-1")) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_bytes() == b"\xE5\xE9\xEE\xF8\xFC" + NLB inplace-1.0.1/test/test_errors.py000066400000000000000000000177641472305477700170670ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path import platform import pytest from in_place import InPlace from test_in_place_util import TEXT, UNICODE, pylistdir def test_backup_ext_and_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" with pytest.raises(ValueError): InPlace(p, backup=bkp, backup_ext="~") assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT def test_empty_backup_ext(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with pytest.raises(ValueError): InPlace(p, backup_ext="") assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT def test_empty_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with pytest.raises(ValueError): InPlace(p, backup="") assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT @pytest.mark.parametrize("backup", [None, "backup.txt"]) def test_bad_mode(tmp_path: Path, backup: str | None) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) backup_path = tmp_path / backup if backup is not None else None with pytest.raises(ValueError, match="invalid mode"): InPlace(p, mode="q", backup=backup_path) # type: ignore[call-overload] assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT def test_early_close_and_write_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: for line in fp: fp.write(line.swapcase()) fp.close() with pytest.raises(ValueError): fp.write("And another thing...\n") assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() def test_early_close_and_write_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" with InPlace(p, backup=bkp) as fp: for line in fp: fp.write(line.swapcase()) fp.close() with pytest.raises(ValueError): fp.write("And another thing...\n") assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert bkp.read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_rollback_and_write_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: for line in fp: fp.write(line.swapcase()) fp.rollback() with pytest.raises(ValueError): fp.write("And another thing...\n") assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT def test_rollback_and_write_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" with InPlace(p, backup=bkp) as fp: for line in fp: fp.write(line.swapcase()) fp.rollback() with pytest.raises(ValueError): fp.write("And another thing...\n") assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT def test_rollback_too_late(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p, backup_ext="~") as fp: for line in fp: fp.write(line.swapcase()) with pytest.raises(ValueError): fp.rollback() assert pylistdir(tmp_path) == ["file.txt", "file.txt~"] assert p.with_suffix(".txt~").read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_backup_dirpath(tmp_path: Path) -> None: """ Assert that using a path to a directory as the backup path raises an error when closing """ assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) not_a_file = tmp_path / "not-a-file" not_a_file.mkdir() assert pylistdir(not_a_file) == [] fp = InPlace(p, backup=not_a_file) fp.write("This will be discarded.\n") with pytest.raises(OSError): fp.close() assert pylistdir(tmp_path) == ["file.txt", "not-a-file"] assert p.read_text() == TEXT assert pylistdir(not_a_file) == [] def test_backup_nosuchdir(tmp_path: Path) -> None: """ Assert that using a path to a file in a nonexistent directory as the backup path raises an error when closing """ assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) fp = InPlace(p, backup=tmp_path / "nonexistent" / "backup.txt") fp.write("This will be discarded.\n") with pytest.raises(OSError): fp.close() assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT def test_nonexistent(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" with pytest.raises(FileNotFoundError): InPlace(p) assert pylistdir(tmp_path) == [] def test_nonexistent_backup_ext(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" with pytest.raises(FileNotFoundError): InPlace(p, backup_ext="~") assert pylistdir(tmp_path) == [] def test_nonexistent_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" with pytest.raises(FileNotFoundError): InPlace(p, backup=tmp_path / "backup.txt") assert pylistdir(tmp_path) == [] @pytest.mark.skipif( platform.system() == "Windows", reason="Windows barely has file modes" ) def test_unwritable_dir(tmp_path: Path) -> None: p = tmp_path / "file.txt" p.write_text(TEXT) tmp_path.chmod(0o555) with pytest.raises(OSError): InPlace(p) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT @pytest.mark.skipif( platform.system() == "Windows", reason="Windows barely has file modes" ) def test_unreadable_file(tmp_path: Path) -> None: p = tmp_path / "file.txt" p.touch() p.chmod(0o000) with pytest.raises(OSError): InPlace(p) assert pylistdir(tmp_path) == ["file.txt"] def test_useless_after_close(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p, backup_ext="~") as fp: pass with pytest.raises(ValueError): fp.flush() with pytest.raises(ValueError): next(fp) with pytest.raises(ValueError): fp.read() with pytest.raises(ValueError): fp.readline() with pytest.raises(ValueError): fp.readlines() with pytest.raises(ValueError): fp.write("") with pytest.raises(ValueError): fp.writelines([""]) def test_not_reusable(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: pass with fp: with pytest.raises(ValueError): fp.flush() with pytest.raises(ValueError): next(fp) with pytest.raises(ValueError): fp.read() with pytest.raises(ValueError): fp.readline() with pytest.raises(ValueError): fp.readlines() with pytest.raises(ValueError): fp.write("") with pytest.raises(ValueError): fp.writelines([""]) def test_bytes_useless_after_close(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(UNICODE, encoding="utf-8") with InPlace(p, "b", backup_ext="~") as fp: pass with pytest.raises(ValueError): # type: ignore[unreachable] fp.read1() with pytest.raises(ValueError): fp.readinto(bytearray(42)) with pytest.raises(ValueError): fp.readinto1(bytearray(42)) inplace-1.0.1/test/test_general.py000066400000000000000000000354441472305477700171630ustar00rootroot00000000000000from __future__ import annotations import os from pathlib import Path import platform import pytest from in_place import InPlace from test_in_place_util import TEXT, pylistdir def test_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: assert not fp.closed files = pylistdir(tmp_path) assert len(files) == 2 assert files[0].startswith("._in_place-") assert files[1] == "file.txt" for line in fp: assert isinstance(line, str) fp.write(line.swapcase()) assert not fp.closed assert fp.closed assert pylistdir(tmp_path) == ["file.txt"] # type: ignore[unreachable] assert p.read_text() == TEXT.swapcase() def test_backup_ext(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p, backup_ext="~") as fp: assert not fp.closed files = pylistdir(tmp_path) assert len(files) == 2 assert files[0].startswith("._in_place-") assert files[1] == "file.txt" for line in fp: fp.write(line.swapcase()) assert not fp.closed assert fp.closed assert pylistdir(tmp_path) == ["file.txt", "file.txt~"] # type: ignore[unreachable] assert p.with_suffix(".txt~").read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" with InPlace(p, backup=bkp) as fp: assert not fp.closed files = pylistdir(tmp_path) assert len(files) == 2 assert files[0].startswith("._in_place-") assert files[1] == "file.txt" for line in fp: fp.write(line.swapcase()) assert not fp.closed assert fp.closed assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] # type: ignore[unreachable] assert bkp.read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_error_backup_ext(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with pytest.raises(RuntimeError): with InPlace(p, backup_ext="~") as fp: for i, line in enumerate(fp): fp.write(line.swapcase()) if i > 5: raise RuntimeError("I changed my mind.") assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT def test_pass_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p): pass assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == "" @pytest.mark.skipif( platform.system() == "Windows", reason="Cannot delete open file on Windows", ) def test_delete_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: for i, line in enumerate(fp): fp.write(line.swapcase()) if i == 5: p.unlink() assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() @pytest.mark.skipif( platform.system() == "Windows", reason="Cannot delete open file on Windows", ) def test_delete_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" with InPlace(p, backup=bkp) as fp: for i, line in enumerate(fp): fp.write(line.swapcase()) if i == 5: p.unlink() with pytest.raises(OSError): fp.close() assert pylistdir(tmp_path) == [] def test_early_close_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: for line in fp: fp.write(line.swapcase()) fp.close() assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() def test_early_close_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" with InPlace(p, backup=bkp) as fp: for line in fp: fp.write(line.swapcase()) fp.close() assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert bkp.read_text() == TEXT assert p.read_text() == TEXT.swapcase() assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert bkp.read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_late_close(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() fp.close() assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() def test_rollback_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: for line in fp: fp.write(line.swapcase()) fp.rollback() assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT def test_rollback_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" with InPlace(p, backup=bkp) as fp: for line in fp: fp.write(line.swapcase()) fp.rollback() assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT def test_rollback_then_inner_close(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: for line in fp: fp.write(line.swapcase()) fp.rollback() assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT fp.close() assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT def test_rollback_then_outer_close(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: for line in fp: fp.write(line.swapcase()) fp.rollback() assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT fp.close() assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT def test_overwrite_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" bkp.write_text("This is not the file you are looking for.\n") with InPlace(p, backup=bkp) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert bkp.read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_rollback_overwrite_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" bkp.write_text("This is not the file you are looking for.\n") with InPlace(p, backup=bkp) as fp: for line in fp: fp.write(line.swapcase()) fp.rollback() assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert bkp.read_text() == "This is not the file you are looking for.\n" assert p.read_text() == TEXT def test_prechdir_backup(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: assert pylistdir(tmp_path) == [] monkeypatch.chdir(tmp_path) p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p, backup="backup.txt") as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert (tmp_path / "backup.txt").read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_postchdir_backup(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Assert that changing directory after opening an InPlace object works""" filedir = tmp_path / "filedir" filedir.mkdir() wrongdir = tmp_path / "wrongdir" wrongdir.mkdir() p = filedir / "file.txt" p.write_text(TEXT) monkeypatch.chdir(filedir) with InPlace("file.txt", backup="backup.txt") as fp: monkeypatch.chdir(wrongdir) for line in fp: fp.write(line.swapcase()) assert os.getcwd() == str(wrongdir) assert pylistdir(wrongdir) == [] assert pylistdir(filedir) == ["backup.txt", "file.txt"] assert (filedir / "backup.txt").read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_different_dir_backup(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.chdir(tmp_path) filedir = tmp_path / "filedir" filedir.mkdir() bkpdir = tmp_path / "bkpdir" bkpdir.mkdir() p = filedir / "file.txt" p.write_text(TEXT) with InPlace( os.path.join("filedir", "file.txt"), backup=os.path.join("bkpdir", "backup.txt") ) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(filedir) == ["file.txt"] assert pylistdir(bkpdir) == ["backup.txt"] assert (bkpdir / "backup.txt").read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_different_dir_file_backup( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """ Assert that if the input filepath contains a directory component and the backup path does not, the backup file will be created in the current directory """ monkeypatch.chdir(tmp_path) filedir = tmp_path / "filedir" filedir.mkdir() p = filedir / "file.txt" p.write_text(TEXT) with InPlace( os.path.join("filedir", "file.txt"), backup="backup.txt", ) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["backup.txt", "filedir"] assert pylistdir(filedir) == ["file.txt"] assert (tmp_path / "backup.txt").read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_reentrant_backup_ext(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p, backup_ext="~") as fp: with fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt", "file.txt~"] assert p.with_suffix(".txt~").read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_use_and_reenter_backup_ext(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p, backup_ext="~") as fp: fp.write(fp.readline().swapcase()) with fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt", "file.txt~"] assert p.with_suffix(".txt~").read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_same_backup_path(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p, backup=p) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() @pytest.mark.skipif( platform.system() == "Windows", reason="Windows barely has file modes" ) def test_copy_executable_perm(tmp_path: Path) -> None: p = tmp_path / "file.txt" p.write_text(TEXT) p.chmod(0o755) with InPlace(p) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() assert p.stat().st_mode & 0o777 == 0o755 def test_empty_newline(tmp_path: Path) -> None: BYTES = ( b"'Twas brillig, and the slithy toves\n" b"\tDid gyre and gimble in the wabe;\r" b"All mimsy were the borogoves,\r\n" b"\tAnd the mome raths outgrabe.\n" ) p = tmp_path / "file.txt" p.write_bytes(BYTES) with InPlace(p, newline="") as fp: lines = fp.readlines() assert lines == [ "'Twas brillig, and the slithy toves\n", "\tDid gyre and gimble in the wabe;\r", "All mimsy were the borogoves,\r\n", "\tAnd the mome raths outgrabe.\n", ] fp.writelines(ln.swapcase() for ln in lines) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_bytes() == BYTES.swapcase() def test_unix_newline(tmp_path: Path) -> None: BYTES = ( b"'Twas brillig, and the slithy toves\n" b"\tDid gyre and gimble in the wabe;\r" b"All mimsy were the borogoves,\r\n" b"\tAnd the mome raths outgrabe.\n" ) p = tmp_path / "file.txt" p.write_bytes(BYTES) with InPlace(p, newline="\n") as fp: lines = fp.readlines() assert lines == [ "'Twas brillig, and the slithy toves\n", "\tDid gyre and gimble in the wabe;\rAll mimsy were the borogoves,\r\n", "\tAnd the mome raths outgrabe.\n", ] fp.writelines(ln.swapcase() for ln in lines) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_bytes() == BYTES.swapcase() def test_dos_newline(tmp_path: Path) -> None: BYTES = ( b"'Twas brillig, and the slithy toves\n" b"\tDid gyre and gimble in the wabe;\r" b"All mimsy were the borogoves,\r\n" b"\tAnd the mome raths outgrabe.\n" ) p = tmp_path / "file.txt" p.write_bytes(BYTES) with InPlace(p, newline="\r\n") as fp: lines = fp.readlines() assert lines == [ ( "'Twas brillig, and the slithy toves\n" "\tDid gyre and gimble in the wabe;\r" "All mimsy were the borogoves,\r\n" ), "\tAnd the mome raths outgrabe.\n", ] fp.writelines(ln.swapcase() for ln in lines) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_bytes() == ( b"'tWAS BRILLIG, AND THE SLITHY TOVES\r\n" b"\tdID GYRE AND GIMBLE IN THE WABE;\r" b"aLL MIMSY WERE THE BOROGOVES,\r\r\n" b"\taND THE MOME RATHS OUTGRABE.\r\n" ) inplace-1.0.1/test/test_in_place_util.py000066400000000000000000000026771472305477700203570ustar00rootroot00000000000000from __future__ import annotations import os from pathlib import Path def pylistdir(d: Path) -> list[str]: return sorted(p.name for p in d.iterdir()) TEXT = ( "'Twas brillig, and the slithy toves\n" "\tDid gyre and gimble in the wabe;\n" "All mimsy were the borogoves,\n" "\tAnd the mome raths outgrabe.\n" "\n" '"Beware the Jabberwock, my son!\n' "\tThe jaws that bite, the claws that catch!\n" "Beware the Jubjub bird, and shun\n" '\tThe frumious Bandersnatch!"\n' "\n" "He took his vorpal sword in hand:\n" "\tLong time the manxome foe he sought--\n" "So rested he by the Tumtum tree,\n" "\tAnd stood awhile in thought.\n" "\n" "And as in uffish thought he stood,\n" "\tThe Jabberwock, with eyes of flame,\n" "Came whiffling through the tulgey wood,\n" "\tAnd burbled as it came!\n" "\n" "One, two! One, two! And through and through\n" "\tThe vorpal blade went snicker-snack!\n" "He left it dead, and with its head\n" "\tHe went galumphing back.\n" "\n" '"And hast thou slain the Jabberwock?\n' "\tCome to my arms, my beamish boy!\n" 'O frabjous day! Callooh! Callay!"\n' "\tHe chortled in his joy.\n" "\n" "'Twas brillig, and the slithy toves\n" "\tDid gyre and gimble in the wabe;\n" "All mimsy were the borogoves,\n" "\tAnd the mome raths outgrabe.\n" ) UNICODE = "åéîøü\n" NLB = os.linesep.encode("us-ascii") inplace-1.0.1/test/test_io_methods.py000066400000000000000000000067051472305477700176760ustar00rootroot00000000000000from __future__ import annotations import os.path from pathlib import Path import pytest from in_place import InPlace from test_in_place_util import NLB, TEXT, UNICODE, pylistdir def test_print_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" with InPlace(p, backup=bkp) as fp: for line in fp: print(line.swapcase(), end="", file=fp) assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert bkp.read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_read1_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(UNICODE, encoding="utf-8") with InPlace(p, "b") as fp: bs = fp.read1(5) assert 1 <= len(bs) <= 5 assert bs[0] == 0xC3 def test_readinto_bytearray_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(UNICODE, encoding="utf-8") with InPlace(p, "b") as fp: ba = bytearray(5) assert fp.readinto(ba) == 5 assert ba == bytearray(b"\xC3\xA5\xC3\xA9\xC3") fp.write(ba) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_bytes() == b"\xC3\xA5\xC3\xA9\xC3" def test_readinto1_bytearray_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(UNICODE, encoding="utf-8") with InPlace(p, "b") as fp: ba = bytearray(5) r = fp.readinto1(ba) assert 1 <= r <= 5 assert len(ba) == r assert ba[0] == 0xC3 def test_readlines_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: assert fp.readlines() == TEXT.splitlines(True) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == "" def test_writelines_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text("") bkp = tmp_path / "backup.txt" with InPlace(p, backup=bkp) as fp: fp.writelines(TEXT.splitlines(True)) assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert bkp.read_text() == "" assert p.read_text() == TEXT def test_readline_nobackup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p) as fp: for line in iter(fp.readline, ""): fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() def test_misc(tmp_path: Path) -> None: p = tmp_path / "file.txt" p.touch() with InPlace(p) as fp: assert fp.name == os.path.realpath(p) assert fp.readable() assert fp.writable() assert not fp.seekable() assert not fp.isatty() with pytest.raises(OSError): fp.seek(0) with pytest.raises(OSError): fp.tell() with pytest.raises(OSError): fp.truncate() with pytest.raises(OSError): fp.fileno() def test_binary_iteration(tmp_path: Path) -> None: p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(p, mode="b") as fp: assert next(fp) == b"'Twas brillig, and the slithy toves" + NLB assert next(fp) == b"\tDid gyre and gimble in the wabe;" + NLB inplace-1.0.1/test/test_nonstr_path.py000066400000000000000000000076311472305477700201020ustar00rootroot00000000000000from __future__ import annotations from os import fsencode from pathlib import Path from typing import AnyStr, Generic from in_place import InPlace from test_in_place_util import TEXT, pylistdir class PathLike(Generic[AnyStr]): def __init__(self, path: AnyStr) -> None: self.path: AnyStr = path def __fspath__(self) -> AnyStr: return self.path def test_pathlike(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(PathLike(str(p))) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() def test_pathlike_backup_ext(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(PathLike(str(p)), backup_ext="~") as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt", "file.txt~"] assert p.with_suffix(".txt~").read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_pathlike_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" with InPlace(PathLike(str(p)), backup=PathLike(str(bkp))) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert bkp.read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_pathlike_bytes(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(PathLike(fsencode(p))) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() def test_pathlike_bytes_backup_ext(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(PathLike(fsencode(p)), backup_ext="~") as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt", "file.txt~"] assert p.with_suffix(".txt~").read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_pathlike_bytes_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" with InPlace(PathLike(fsencode(p)), backup=PathLike(fsencode(bkp))) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert bkp.read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_bytes(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(fsencode(p)) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt"] assert p.read_text() == TEXT.swapcase() def test_bytes_backup_ext(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) with InPlace(fsencode(p), backup_ext=fsencode("~")) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt", "file.txt~"] assert p.with_suffix(".txt~").read_text() == TEXT assert p.read_text() == TEXT.swapcase() def test_bytes_backup(tmp_path: Path) -> None: assert pylistdir(tmp_path) == [] p = tmp_path / "file.txt" p.write_text(TEXT) bkp = tmp_path / "backup.txt" with InPlace(fsencode(p), backup=fsencode(bkp)) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert bkp.read_text() == TEXT assert p.read_text() == TEXT.swapcase() inplace-1.0.1/test/test_symlinks.py000066400000000000000000000117601472305477700174120ustar00rootroot00000000000000from __future__ import annotations import os from os.path import relpath from pathlib import Path import platform import sys import pytest from in_place import InPlace from test_in_place_util import TEXT, pylistdir pytestmark = pytest.mark.xfail( platform.system() == "Windows" and hasattr(sys, "pypy_version_info") and sys.pypy_version_info < (7, 3, 12), reason="Symlinks are not implemented on PyPy on Windows before v7.3.12", ) def test_symlink_nobackup(tmp_path: Path) -> None: assert list(tmp_path.iterdir()) == [] realdir = tmp_path / "real" realdir.mkdir() real = realdir / "realfile.txt" real.write_text(TEXT) linkdir = tmp_path / "link" linkdir.mkdir() link = linkdir / "linkfile.txt" target = relpath(real, linkdir) link.symlink_to(target) with InPlace(link) as fp: for line in fp: fp.write(line.swapcase()) assert list(realdir.iterdir()) == [real] assert list(linkdir.iterdir()) == [link] assert link.is_symlink() assert os.readlink(link) == target assert link.read_text() == TEXT.swapcase() assert real.read_text() == TEXT.swapcase() def test_symlink_backup_ext(tmp_path: Path) -> None: assert list(tmp_path.iterdir()) == [] realdir = tmp_path / "real" realdir.mkdir() real = realdir / "realfile.txt" real.write_text(TEXT) linkdir = tmp_path / "link" linkdir.mkdir() link = linkdir / "linkfile.txt" target = relpath(real, linkdir) link.symlink_to(target) with InPlace(link, backup_ext="~") as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(realdir) == ["realfile.txt", "realfile.txt~"] assert not real.is_symlink() assert real.read_text() == TEXT.swapcase() assert not (realdir / "realfile.txt~").is_symlink() assert (realdir / "realfile.txt~").read_text() == TEXT assert pylistdir(linkdir) == ["linkfile.txt"] assert link.is_symlink() assert os.readlink(link) == target assert link.read_text() == TEXT.swapcase() def test_symlink_backup(tmp_path: Path) -> None: assert list(tmp_path.iterdir()) == [] realdir = tmp_path / "real" realdir.mkdir() real = realdir / "realfile.txt" real.write_text(TEXT) linkdir = tmp_path / "link" linkdir.mkdir() link = linkdir / "linkfile.txt" target = relpath(real, linkdir) link.symlink_to(target) bkp = tmp_path / "backup.txt" with InPlace(link, backup=bkp) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["backup.txt", "link", "real"] assert list(realdir.iterdir()) == [real] assert list(linkdir.iterdir()) == [link] assert link.is_symlink() assert os.readlink(link) == target assert bkp.read_text() == TEXT assert link.read_text() == TEXT.swapcase() assert real.read_text() == TEXT.swapcase() def test_backup_is_symlink(tmp_path: Path) -> None: p = tmp_path / "file.txt" p.write_text(TEXT) realdir = tmp_path / "realdir" realdir.mkdir() realfile = realdir / "realfile.txt" realfile.write_text("This is a symlinked file.\n") linkdir = tmp_path / "linkdir" linkdir.mkdir() linkfile = linkdir / "linkfile.txt" target = relpath(realfile, linkdir) linkfile.symlink_to(target) with InPlace(p, backup=linkfile) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["file.txt", "linkdir", "realdir"] assert not p.is_symlink() assert p.read_text() == TEXT.swapcase() assert pylistdir(realdir) == ["realfile.txt"] assert not realfile.is_symlink() assert realfile.read_text() == "This is a symlinked file.\n" assert pylistdir(linkdir) == ["linkfile.txt"] assert not linkfile.is_symlink() assert linkfile.read_text() == TEXT def test_file_links_to_backup(tmp_path: Path) -> None: p = tmp_path / "file.txt" backup = tmp_path / "backup.txt" backup.write_text(TEXT) target = "backup.txt" p.symlink_to(target) with InPlace(p, backup=backup) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert p.is_symlink() assert os.readlink(p) == target assert backup.read_text() == TEXT.swapcase() def test_backup_links_to_file(tmp_path: Path) -> None: p = tmp_path / "file.txt" p.write_text(TEXT) backup = tmp_path / "backup.txt" target = "file.txt" backup.symlink_to(target) with InPlace(p, backup=backup) as fp: for line in fp: fp.write(line.swapcase()) assert pylistdir(tmp_path) == ["backup.txt", "file.txt"] assert not p.is_symlink() assert p.read_text() == TEXT.swapcase() assert not backup.is_symlink() assert backup.read_text() def test_broken_symlink(tmp_path: Path) -> None: p = tmp_path / "file.txt" target = "nowhere.txt" p.symlink_to(target) with pytest.raises(OSError): InPlace(p) assert pylistdir(tmp_path) == ["file.txt"] assert os.readlink(p) == target inplace-1.0.1/tox.ini000066400000000000000000000024111472305477700144550ustar00rootroot00000000000000[tox] envlist = lint,typing,py38,py39,py310,py311,py312,py313,pypy3 skip_missing_interpreters = True isolated_build = True minversion = 3.3.0 [testenv] setenv = LC_ALL=en_US.UTF-8 deps = pytest pytest-cov commands = pytest {posargs} test [testenv:lint] skip_install = True deps = flake8 flake8-bugbear flake8-builtins flake8-unused-arguments commands = flake8 src test [testenv:typing] deps = mypy {[testenv]deps} commands = mypy src test [pytest] addopts = --cov=in_place --no-cov-on-fail filterwarnings = error [coverage:run] branch = True parallel = True [coverage:paths] source = src .tox/**/site-packages [coverage:report] precision = 2 show_missing = True exclude_lines = pragma: no cover if TYPE_CHECKING: \.\.\. [flake8] doctests = True extend-exclude = build/,dist/,test/data,venv/ max-doc-length = 100 max-line-length = 80 unused-arguments-ignore-stub-functions = True extend-select = B901,B902,B950 ignore = A003,A005,B005,B038,E203,E262,E266,E501,E704,U101,W503 [isort] atomic = True force_sort_within_sections = True honor_noqa = True known_first_party = test_in_place_util lines_between_sections = 0 profile = black reverse_relative = True sort_relative_in_force_sorted_sections = True src_paths = src