Skip to content

Commit 8be130d

Browse files
committed
Simplify api, improve documentation and tests.
1 parent 161aec5 commit 8be130d

File tree

4 files changed

+44
-50
lines changed

4 files changed

+44
-50
lines changed

getdents/__init__.py

+25-12
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,30 @@
1515
)
1616

1717

18-
def getdents(path, buff_size=32768, close_fd=False):
19-
if hasattr(path, 'fileno'):
20-
fd = path.fileno()
21-
elif isinstance(path, str):
22-
fd = os.open(path, O_GETDENTS)
23-
close_fd = True
24-
elif isinstance(path, int):
25-
fd = path
26-
else:
27-
raise TypeError('Unsupported type: %s', type(path))
18+
def getdents(path, buff_size=32768):
19+
"""Get directory entries.
20+
21+
Wrapper around getdents_raw(), simulates ls behaviour: ignores deleted
22+
files, skips . and .. entries.
23+
24+
Note:
25+
Default buffer size is 32k, it's a default allocation size of glibc's
26+
readdir() implementation.
27+
28+
Note:
29+
Larger buffer will result in a fewer syscalls, so for really large
30+
dirs you should pick larger value.
31+
32+
Note:
33+
For better performance, set buffer size to be multiple of your block
34+
size for filesystem I/O.
35+
36+
Args:
37+
path (str): Location of the directory.
38+
buff_size (int): Buffer size in bytes for getdents64 syscall.
39+
"""
40+
41+
fd = os.open(path, O_GETDENTS)
2842

2943
try:
3044
yield from (
@@ -33,5 +47,4 @@ def getdents(path, buff_size=32768, close_fd=False):
3347
if not(type == DT_UNKNOWN or inode == 0 or name in ('.', '..'))
3448
)
3549
finally:
36-
if close_fd:
37-
os.close(fd)
50+
os.close(fd)

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setup(
99
name='getdents',
10-
version='0.1',
10+
version='0.2',
1111
description='Python binding to linux syscall getdents64.',
1212
long_description=open('README.rst').read(),
1313
classifiers=[

tests/test___init__.py

+16-34
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,36 @@
1-
from unittest.mock import Mock, patch, sentinel
1+
from unittest.mock import patch, sentinel
22

3-
from pytest import mark, raises
3+
from pytest import raises
44

55
from getdents import DT_DIR, DT_REG, DT_UNKNOWN, O_GETDENTS, getdents
66

77

8-
@patch('getdents.os')
9-
@patch('getdents.getdents_raw')
10-
def test_file_like(mock_getdents_raw, mock_os):
11-
mock_file = Mock(spec_set=['fileno'])
12-
mock_file.fileno.return_value = sentinel.fd
13-
14-
list(getdents(mock_file, sentinel.size))
15-
16-
assert mock_file.fileno.called
17-
assert mock_getdents_raw.called_once_with(sentinel.fd, sentinel.size)
18-
19-
208
@patch('getdents.os')
219
@patch('getdents.getdents_raw')
2210
def test_path(mock_getdents_raw, mock_os):
23-
mock_path = Mock(spec='/tmp')
2411
mock_os.open.return_value = sentinel.fd
2512

26-
list(getdents(mock_path, sentinel.size))
13+
list(getdents('/tmp', sentinel.size))
2714

28-
assert mock_os.open.called_once_with(mock_path, O_GETDENTS)
29-
assert mock_getdents_raw.called_once_with(sentinel.fd, sentinel.size)
15+
mock_os.open.assert_called_once_with('/tmp', O_GETDENTS)
16+
mock_getdents_raw.assert_called_once_with(sentinel.fd, sentinel.size)
17+
mock_os.close.assert_called_once_with(sentinel.fd)
3018

3119

32-
@mark.parametrize(['close_fd'], [(True,), (False,)])
3320
@patch('getdents.os')
34-
@patch('getdents.getdents_raw')
35-
def test_fd(mock_getdents_raw, mock_os, close_fd):
36-
mock_fd = Mock(spec=4)
37-
38-
list(getdents(mock_fd, sentinel.size, close_fd))
39-
40-
assert mock_getdents_raw.called_once_with(mock_fd, sentinel.size)
41-
assert mock_os.close.called is close_fd
42-
21+
@patch('getdents.getdents_raw', side_effect=[Exception])
22+
def test_path_err(mock_getdents_raw, mock_os):
23+
mock_os.open.return_value = sentinel.fd
4324

44-
@patch('getdents.getdents_raw')
45-
def test_alien(mock_getdents_raw):
46-
with raises(TypeError):
47-
list(getdents(object()))
25+
with raises(Exception):
26+
list(getdents('/tmp', sentinel.size))
4827

49-
assert not mock_getdents_raw.called
28+
mock_os.open.assert_called_once_with('/tmp', O_GETDENTS)
29+
mock_getdents_raw.assert_called_once_with(sentinel.fd, sentinel.size)
30+
mock_os.close.assert_called_once_with(sentinel.fd)
5031

5132

33+
@patch('getdents.os')
5234
@patch('getdents.getdents_raw', return_value=iter([
5335
(1, DT_DIR, '.'),
5436
(2, DT_DIR, '..'),
@@ -57,7 +39,7 @@ def test_alien(mock_getdents_raw):
5739
(5, DT_UNKNOWN, '???'),
5840
(0, DT_REG, 'deleted'),
5941
]))
60-
def test_filtering(mock_getdents_raw):
42+
def test_filtering(mock_getdents_raw, mock_os):
6143
assert list(getdents(0)) == [
6244
(3, DT_DIR, 'dir'),
6345
(4, DT_REG, 'file'),

tests/test__getdents.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from unittest.mock import ANY
44

5-
from pytest import fixture, mark, raises
5+
from pytest import fixture, raises
66

77
from getdents._getdents import (
88
DT_DIR,
@@ -45,10 +45,9 @@ def test_small_buffer(fixt_dir):
4545
getdents_raw(fixt_dir, MIN_GETDENTS_BUFF_SIZE - 1)
4646

4747

48-
@mark.skip(reason='No idea how to test this properly')
4948
def test_malloc_fail(fixt_dir):
5049
with raises(MemoryError):
51-
getdents_raw(fixt_dir, 2**63)
50+
getdents_raw(fixt_dir, 1 << 62)
5251

5352

5453
def test_getdents_raw(fixt_dir):

0 commit comments

Comments
 (0)