Skip to content

Commit a2b6cc7

Browse files
committed
Add support for fake os.dup, os.dup2 and os.lseek
- closes #970
1 parent a1621df commit a2b6cc7

File tree

5 files changed

+103
-5
lines changed

5 files changed

+103
-5
lines changed

CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The released versions correspond to PyPI releases.
1212
### Enhancements
1313
* added support for `O_NOFOLLOW` and `O_DIRECTORY` flags in `os.open`
1414
(see [#972](../../issues/972) and [#974](../../issues/974))
15+
* added support for fake `os.dup`, `os.dup2` and `os.lseek` (see [#970](../../issues/970))
1516

1617
### Fixes
1718
* fixed a specific problem on reloading a pandas-related module (see [#947](../../issues/947)),

pyfakefs/fake_file.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -858,15 +858,15 @@ def close(self) -> None:
858858
# if we get here, we have an open file descriptor
859859
# without write permission, which has to be closed
860860
assert self.filedes
861-
self._filesystem._close_open_file(self.filedes)
861+
self._filesystem.close_open_file(self.filedes)
862862
raise
863863

864864
if self._filesystem.is_windows_fs and self._changed:
865865
self.file_object.st_mtime = helpers.now()
866866

867867
assert self.filedes is not None
868868
if self._closefd:
869-
self._filesystem._close_open_file(self.filedes)
869+
self._filesystem.close_open_file(self.filedes)
870870
else:
871871
open_files = self._filesystem.open_files[self.filedes]
872872
assert open_files is not None
@@ -1288,7 +1288,7 @@ def fileno(self) -> int:
12881288
def close(self) -> None:
12891289
"""Close the directory."""
12901290
assert self.filedes is not None
1291-
self._filesystem._close_open_file(self.filedes)
1291+
self._filesystem.close_open_file(self.filedes)
12921292

12931293

12941294
class FakePipeWrapper:

pyfakefs/fake_filesystem.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -814,18 +814,38 @@ def _handle_utime_arg_errors(
814814
if ns is not None and len(ns) != 2:
815815
raise TypeError("utime: 'ns' must be a tuple of two ints")
816816

817-
def add_open_file(self, file_obj: AnyFileWrapper) -> int:
817+
def add_open_file(self, file_obj: AnyFileWrapper, new_fd: int = -1) -> int:
818818
"""Add file_obj to the list of open files on the filesystem.
819819
Used internally to manage open files.
820820
821821
The position in the open_files array is the file descriptor number.
822822
823823
Args:
824824
file_obj: File object to be added to open files list.
825+
new_fd: The optional new file descriptor.
825826
826827
Returns:
827828
File descriptor number for the file object.
828829
"""
830+
if new_fd >= 0:
831+
size = len(self.open_files)
832+
if new_fd < size:
833+
open_files = self.open_files[new_fd]
834+
if open_files:
835+
for f in open_files:
836+
try:
837+
f.close()
838+
except OSError:
839+
pass
840+
if new_fd in self._free_fd_heap:
841+
self._free_fd_heap.remove(new_fd)
842+
else:
843+
for fd in range(size, new_fd + 1):
844+
self.open_files.append([])
845+
heapq.heappush(self._free_fd_heap, fd)
846+
self.open_files[new_fd] = [file_obj]
847+
return new_fd
848+
829849
if self._free_fd_heap:
830850
open_fd = heapq.heappop(self._free_fd_heap)
831851
self.open_files[open_fd] = [file_obj]
@@ -834,7 +854,7 @@ def add_open_file(self, file_obj: AnyFileWrapper) -> int:
834854
self.open_files.append([file_obj])
835855
return len(self.open_files) - 1
836856

837-
def _close_open_file(self, file_des: int) -> None:
857+
def close_open_file(self, file_des: int) -> None:
838858
"""Remove file object with given descriptor from the list
839859
of open files.
840860

pyfakefs/fake_os.py

+20
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,15 @@ def dir() -> List[str]:
102102
"chmod",
103103
"chown",
104104
"close",
105+
"dup",
106+
"dup2",
105107
"fstat",
106108
"fsync",
107109
"getcwd",
108110
"lchmod",
109111
"link",
110112
"listdir",
113+
"lseek",
111114
"lstat",
112115
"makedirs",
113116
"mkdir",
@@ -321,6 +324,16 @@ def close(self, fd: int) -> None:
321324
file_handle = self.filesystem.get_open_file(fd)
322325
file_handle.close()
323326

327+
def dup(self, fd: int) -> int:
328+
file_handle = self.filesystem.get_open_file(fd)
329+
return self.filesystem.add_open_file(file_handle)
330+
331+
def dup2(self, fd: int, fd2: int, inheritable: bool = True) -> int:
332+
if fd == fd2:
333+
return fd
334+
file_handle = self.filesystem.get_open_file(fd)
335+
return self.filesystem.add_open_file(file_handle, fd2)
336+
324337
def read(self, fd: int, n: int) -> bytes:
325338
"""Read number of bytes from a file descriptor, returns bytes read.
326339
@@ -370,6 +383,13 @@ def write(self, fd: int, contents: bytes) -> int:
370383
file_handle.flush()
371384
return len(contents)
372385

386+
def lseek(self, fd: int, pos: int, whence: int):
387+
file_handle = self.filesystem.get_open_file(fd)
388+
if isinstance(file_handle, FakeFileWrapper):
389+
file_handle.seek(pos, whence)
390+
else:
391+
raise OSError(errno.EBADF, "Bad file descriptor for fseek")
392+
373393
def pipe(self) -> Tuple[int, int]:
374394
read_fd, write_fd = os.pipe()
375395
read_wrapper = FakePipeWrapper(self.filesystem, read_fd, False)

pyfakefs/tests/fake_os_test.py

+57
Original file line numberDiff line numberDiff line change
@@ -3002,6 +3002,63 @@ def test_capabilities(self):
30023002
os.stat in os.supports_effective_ids,
30033003
)
30043004

3005+
def test_dup(self):
3006+
with self.assertRaises(OSError) as cm:
3007+
self.os.dup(500)
3008+
self.assertEqual(errno.EBADF, cm.exception.errno)
3009+
file_path = self.make_path("test.txt")
3010+
self.create_file(file_path, contents="heythere")
3011+
fd1 = self.os.open(file_path, os.O_RDONLY)
3012+
fd2 = self.os.dup(fd1)
3013+
self.assertEqual(b"hey", self.os.read(fd1, 3))
3014+
self.assertEqual(b"there", self.os.read(fd1, 10))
3015+
self.os.close(fd1)
3016+
self.os.close(fd2)
3017+
3018+
def test_dup_uses_freed_fd(self):
3019+
file_path1 = self.make_path("foo.txt")
3020+
file_path2 = self.make_path("bar.txt")
3021+
self.create_file(file_path1, contents="foo here")
3022+
self.create_file(file_path2, contents="bar here")
3023+
fd1 = self.os.open(file_path1, os.O_RDONLY)
3024+
fd2 = self.os.open(file_path2, os.O_RDONLY)
3025+
self.os.close(fd1)
3026+
fd3 = self.os.dup(fd2)
3027+
self.assertEqual(fd1, fd3)
3028+
self.os.close(fd2)
3029+
3030+
def test_dup2_uses_existing_fd(self):
3031+
with self.assertRaises(OSError) as cm:
3032+
self.os.dup2(500, 501)
3033+
self.assertEqual(errno.EBADF, cm.exception.errno)
3034+
3035+
file_path1 = self.make_path("foo.txt")
3036+
file_path2 = self.make_path("bar.txt")
3037+
self.create_file(file_path1, contents="foo here")
3038+
self.create_file(file_path2, contents="bar here")
3039+
fd1 = self.os.open(file_path1, os.O_RDONLY)
3040+
fd2 = self.os.open(file_path2, os.O_RDONLY)
3041+
self.assertEqual(b"bar", self.os.read(fd2, 3))
3042+
fd2 = self.os.dup2(fd1, fd2)
3043+
self.assertEqual(b"foo", self.os.read(fd2, 3))
3044+
self.os.lseek(fd2, 0, 0)
3045+
self.assertEqual(b"foo", self.os.read(fd1, 3))
3046+
self.os.close(fd2)
3047+
3048+
def test_dup2_with_new_fd(self):
3049+
file_path1 = self.make_path("foo.txt")
3050+
file_path2 = self.make_path("bar.txt")
3051+
self.create_file(file_path1)
3052+
self.create_file(file_path2)
3053+
fd1 = self.os.open(file_path1, os.O_RDONLY)
3054+
fd2 = fd1 + 2
3055+
self.assertEqual(fd2, self.os.dup2(fd1, fd2))
3056+
fd3 = self.os.open(file_path2, os.O_RDONLY)
3057+
self.os.close(fd3)
3058+
self.os.close(fd2)
3059+
# we have a free position before fd2 that is now filled
3060+
self.assertEqual(fd1 + 1, fd3)
3061+
30053062

30063063
class RealOsModuleTest(FakeOsModuleTest):
30073064
def use_real_fs(self):

0 commit comments

Comments
 (0)