Skip to content

Commit

Permalink
Support bytes and PathLike paths
Browse files Browse the repository at this point in the history
  • Loading branch information
jwodder committed Oct 2, 2018
1 parent 2fdd094 commit fcfb7cc
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ v0.4.0 (in development)
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)
-------------------
Expand Down
6 changes: 5 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Compared to the in-place filtering implemented by the Python standard library's
Installation
============
Just use `pip <https://pip.pypa.io>`_ (You have pip, right?) to install
``in_place`` and its dependencies::
``in_place``::

pip install in_place

Expand Down Expand Up @@ -133,6 +133,10 @@ following arguments:
``newline``) will be forwarded to ``io.open()`` (or the builtin ``open`` if
``mode`` is ``None``) when opening both the input and output file strems.

``name``, ``backup``, and ``backup_ext`` can be either ``str`` or
filesystem-encoded ``bytes`` in Python 3, and in Python 3.6 or later, path-like
objects are also accepted.

Note:

Earlier versions of this library provided separate ``InPlaceText`` and
Expand Down
2 changes: 0 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,4 @@
- Give the classes decent `__repr__`s
- Make the context manager reusable
- Add a `seekable()` method that returns `False`?
- Support `os.PathLike` objects in Python 3.6+
- Support `bytes` paths in Python 3 using `os.fsdecode()`
- Should `io.open()` be used when `mode` is unset but `kwargs` is nonempty?
35 changes: 22 additions & 13 deletions in_place.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
import tempfile
from warnings import warn

try:
from os import fsdecode
except ImportError:
def fsdecode(p):
return p

__all__ = ['InPlace', 'InPlaceBytes', 'InPlaceText']

class InPlace(object):
Expand All @@ -32,9 +38,10 @@ class InPlace(object):
write ending up at the same filepath that you read from) that takes care of
all the necessary mucking about with temporary files.
:param string name: The path to the file to open & edit in-place (resolved
: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
Expand All @@ -45,14 +52,16 @@ class InPlace(object):
`open` using the default mode, and data will be read & written as `str`
objects, whatever those happen to be in your version of Python.
:param string 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
: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
:type backup: path-like
:param string 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.
: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 bool delay_open: If `True`, the newly-constructed instance will not
be open, and the user must either explicitly call the :meth:`open()`
Expand All @@ -78,11 +87,11 @@ def __init__(self, name, mode=None, backup=None, backup_ext=None,
delay_open=False, move_first=False, **kwargs):
cwd = os.getcwd()
#: The path to the file to edit in-place
self.name = name
self.name = fsdecode(name)
#: Whether to operate on the file in binary or text mode
self.mode = mode
#: The absolute path of the file to edit in-place
self.filepath = os.path.join(cwd, name)
self.filepath = os.path.join(cwd, self.name)
#: ``filepath`` with symbolic links resolved. This is set just before
#: opening the file.
self.realpath = None
Expand All @@ -91,11 +100,11 @@ def __init__(self, name, mode=None, backup=None, backup_ext=None,
raise ValueError('backup and backup_ext are mutually exclusive')
#: The absolute path of the backup file (if any) that the original
#: contents of ``realpath`` will be moved to after editing
self.backuppath = os.path.join(cwd, backup)
self.backuppath = os.path.join(cwd, fsdecode(backup))
elif backup_ext is not None:
if backup_ext == '':
if not backup_ext:
raise ValueError('backup_ext cannot be empty')
self.backuppath = self.filepath + backup_ext
self.backuppath = self.filepath + fsdecode(backup_ext)
else:
self.backuppath = None
#: Whether to move the input file before opening and create the output
Expand Down
150 changes: 150 additions & 0 deletions test/test_nonstr_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import sys
import pytest
from in_place import InPlace
from test_in_place_util import TEXT, pylistdir

class PathLike(object):
def __init__(self, path):
self.path = path

def __fspath__(self):
return self.path


@pytest.mark.skipif(sys.version_info[:2] < (3,6), reason='Python 3.6+ only')
def test_pathlike(tmpdir):
assert pylistdir(tmpdir) == []
p = tmpdir.join("file.txt")
p.write(TEXT)
with InPlace(PathLike(str(p))) as fp:
assert not fp.closed
for line in fp:
assert isinstance(line, str)
fp.write(line.swapcase())
assert not fp.closed
assert fp.closed
assert pylistdir(tmpdir) == ['file.txt']
assert p.read() == TEXT.swapcase()

@pytest.mark.skipif(sys.version_info[:2] < (3,6), reason='Python 3.6+ only')
def test_pathlike_backup_ext(tmpdir):
assert pylistdir(tmpdir) == []
p = tmpdir.join("file.txt")
p.write(TEXT)
with InPlace(PathLike(str(p)), backup_ext='~') as fp:
for line in fp:
fp.write(line.swapcase())
assert pylistdir(tmpdir) == ['file.txt', 'file.txt~']
assert p.new(ext='txt~').read() == TEXT
assert p.read() == TEXT.swapcase()

@pytest.mark.skipif(sys.version_info[:2] < (3,6), reason='Python 3.6+ only')
def test_pathlike_backup(tmpdir):
assert pylistdir(tmpdir) == []
p = tmpdir.join("file.txt")
p.write(TEXT)
bkp = tmpdir.join('backup.txt')
with InPlace(PathLike(str(p)), backup=PathLike(str(bkp))) as fp:
assert not fp.closed
for line in fp:
fp.write(line.swapcase())
assert not fp.closed
assert fp.closed
assert pylistdir(tmpdir) == ['backup.txt', 'file.txt']
assert bkp.read() == TEXT
assert p.read() == TEXT.swapcase()

@pytest.mark.skipif(sys.version_info[:2] < (3,6), reason='Python 3.6+ only')
def test_pathlike_bytes(tmpdir):
from os import fsencode
assert pylistdir(tmpdir) == []
p = tmpdir.join("file.txt")
p.write(TEXT)
with InPlace(PathLike(fsencode(str(p)))) as fp:
assert not fp.closed
for line in fp:
assert isinstance(line, str)
fp.write(line.swapcase())
assert not fp.closed
assert fp.closed
assert pylistdir(tmpdir) == ['file.txt']
assert p.read() == TEXT.swapcase()

@pytest.mark.skipif(sys.version_info[:2] < (3,6), reason='Python 3.6+ only')
def test_pathlike_bytes_backup_ext(tmpdir):
from os import fsencode
assert pylistdir(tmpdir) == []
p = tmpdir.join("file.txt")
p.write(TEXT)
with InPlace(PathLike(fsencode(str(p))), backup_ext='~') as fp:
for line in fp:
fp.write(line.swapcase())
assert pylistdir(tmpdir) == ['file.txt', 'file.txt~']
assert p.new(ext='txt~').read() == TEXT
assert p.read() == TEXT.swapcase()

@pytest.mark.skipif(sys.version_info[:2] < (3,6), reason='Python 3.6+ only')
def test_pathlike_bytes_backup(tmpdir):
from os import fsencode
assert pylistdir(tmpdir) == []
p = tmpdir.join("file.txt")
p.write(TEXT)
bkp = tmpdir.join('backup.txt')
with InPlace(
PathLike(fsencode(str(p))),
backup=PathLike(fsencode(str(bkp))),
) as fp:
assert not fp.closed
for line in fp:
fp.write(line.swapcase())
assert not fp.closed
assert fp.closed
assert pylistdir(tmpdir) == ['backup.txt', 'file.txt']
assert bkp.read() == TEXT
assert p.read() == TEXT.swapcase()

@pytest.mark.skipif(sys.version_info[0] < 3, reason='Python 3 only')
def test_bytes(tmpdir):
from os import fsencode
assert pylistdir(tmpdir) == []
p = tmpdir.join("file.txt")
p.write(TEXT)
with InPlace(fsencode(str(p))) as fp:
assert not fp.closed
for line in fp:
assert isinstance(line, str)
fp.write(line.swapcase())
assert not fp.closed
assert fp.closed
assert pylistdir(tmpdir) == ['file.txt']
assert p.read() == TEXT.swapcase()

@pytest.mark.skipif(sys.version_info[0] < 3, reason='Python 3 only')
def test_bytes_backup_ext(tmpdir):
from os import fsencode
assert pylistdir(tmpdir) == []
p = tmpdir.join("file.txt")
p.write(TEXT)
with InPlace(fsencode(str(p)), backup_ext=fsencode('~')) as fp:
for line in fp:
fp.write(line.swapcase())
assert pylistdir(tmpdir) == ['file.txt', 'file.txt~']
assert p.new(ext='txt~').read() == TEXT
assert p.read() == TEXT.swapcase()

@pytest.mark.skipif(sys.version_info[0] < 3, reason='Python 3 only')
def test_bytes_backup(tmpdir):
from os import fsencode
assert pylistdir(tmpdir) == []
p = tmpdir.join("file.txt")
p.write(TEXT)
bkp = tmpdir.join('backup.txt')
with InPlace(fsencode(str(p)), backup=fsencode(str(bkp))) as fp:
assert not fp.closed
for line in fp:
fp.write(line.swapcase())
assert not fp.closed
assert fp.closed
assert pylistdir(tmpdir) == ['backup.txt', 'file.txt']
assert bkp.read() == TEXT
assert p.read() == TEXT.swapcase()
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ deps =
pytest~=3.0
pytest-cov~=2.0
pytest-flakes~=4.0
six ~= 1.4
six~=1.4
commands = pytest in_place.py test

[pytest]
Expand Down

0 comments on commit fcfb7cc

Please sign in to comment.