Skip to content

Commit 40c537b

Browse files
authored
Add parse_gitignore_str() so you don't need a file (#74)
1 parent 6e88b18 commit 40c537b

3 files changed

Lines changed: 69 additions & 50 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ Then:
2929
>>> matches('/home/michael/project/__pycache__')
3030
True
3131

32+
Alternatively, you can use the `parse_gitignore_str` function:
33+
34+
>>> from gitignore_parser import parse_gitignore_str
35+
>>> matches = parse_gitignore_str(
36+
'__pycache__/\n*.py[cod]', base_dir='/home/michael/project')
37+
>>> matches('/home/michael/project/main.py')
38+
False
39+
>>> matches('/home/michael/project/main.pyc')
40+
True
41+
3242
## Motivation
3343

3444
I couldn't find a good library for doing the above on PyPI. There are

gitignore_parser.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import re
44

5-
from os.path import abspath, dirname
5+
from os.path import abspath, dirname, join
66
from pathlib import Path
77
from typing import Reversible, Union
88

@@ -15,16 +15,22 @@ def handle_negation(file_path, rules: Reversible["IgnoreRule"]):
1515
def parse_gitignore(full_path, base_dir=None):
1616
if base_dir is None:
1717
base_dir = dirname(full_path)
18-
rules = []
1918
with open(full_path) as ignore_file:
20-
counter = 0
21-
for line in ignore_file:
22-
counter += 1
23-
line = line.rstrip('\n')
24-
rule = rule_from_pattern(line, base_path=_normalize_path(base_dir),
25-
source=(full_path, counter))
26-
if rule:
27-
rules.append(rule)
19+
return _parse_gitignore_lines(ignore_file, full_path, base_dir)
20+
21+
def parse_gitignore_str(gitignore_str, base_dir):
22+
full_path = join(base_dir, '.gitignore')
23+
lines = gitignore_str.splitlines()
24+
return _parse_gitignore_lines(lines, full_path, base_dir)
25+
26+
def _parse_gitignore_lines(lines, full_path, base_dir):
27+
base_dir = _normalize_path(base_dir)
28+
rules = []
29+
for line_no, line in enumerate(lines, start=1):
30+
rule = rule_from_pattern(
31+
line.rstrip('\n'), base_path=base_dir, source=(full_path, line_no))
32+
if rule:
33+
rules.append(rule)
2834
if not any(r.negation for r in rules):
2935
return lambda file_path: any(r.match(file_path) for r in rules)
3036
else:
@@ -100,7 +106,7 @@ def rule_from_pattern(pattern, base_path=None, source=None):
100106
negation=negation,
101107
directory_only=directory_only,
102108
anchored=anchored,
103-
base_path=_normalize_path(base_path) if base_path else None,
109+
base_path=base_path if base_path else None,
104110
source=source
105111
)
106112

tests.py

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,35 @@
22
from pathlib import Path
33
from tempfile import TemporaryDirectory
44

5-
from gitignore_parser import parse_gitignore
5+
from gitignore_parser import parse_gitignore, parse_gitignore_str
66

77
from unittest import TestCase, main
88

99

1010
class Test(TestCase):
1111
def test_simple(self):
12-
matches = _parse_gitignore_string(
12+
matches = parse_gitignore_str(
1313
'__pycache__/\n'
1414
'*.py[cod]',
15-
fake_base_dir='/home/michael'
15+
base_dir='/home/michael'
1616
)
1717
self.assertFalse(matches('/home/michael/main.py'))
1818
self.assertTrue(matches('/home/michael/main.pyc'))
1919
self.assertTrue(matches('/home/michael/dir/main.pyc'))
2020
self.assertTrue(matches('/home/michael/__pycache__'))
2121

22+
def test_simple_parse_file(self):
23+
with patch('builtins.open', mock_open(read_data=
24+
'__pycache__/\n'
25+
'*.py[cod]')):
26+
matches = parse_gitignore('/home/michael/.gitignore')
27+
self.assertFalse(matches('/home/michael/main.py'))
28+
self.assertTrue(matches('/home/michael/main.pyc'))
29+
self.assertTrue(matches('/home/michael/dir/main.pyc'))
30+
self.assertTrue(matches('/home/michael/__pycache__'))
31+
2232
def test_incomplete_filename(self):
23-
matches = _parse_gitignore_string('o.py', fake_base_dir='/home/michael')
33+
matches = parse_gitignore_str('o.py', base_dir='/home/michael')
2434
self.assertTrue(matches('/home/michael/o.py'))
2535
self.assertFalse(matches('/home/michael/foo.py'))
2636
self.assertFalse(matches('/home/michael/o.pyc'))
@@ -29,9 +39,9 @@ def test_incomplete_filename(self):
2939
self.assertFalse(matches('/home/michael/dir/o.pyc'))
3040

3141
def test_wildcard(self):
32-
matches = _parse_gitignore_string(
42+
matches = parse_gitignore_str(
3343
'hello.*',
34-
fake_base_dir='/home/michael'
44+
base_dir='/home/michael'
3545
)
3646
self.assertTrue(matches('/home/michael/hello.txt'))
3747
self.assertTrue(matches('/home/michael/hello.foobar/'))
@@ -41,22 +51,22 @@ def test_wildcard(self):
4151
self.assertFalse(matches('/home/michael/helloX'))
4252

4353
def test_anchored_wildcard(self):
44-
matches = _parse_gitignore_string(
54+
matches = parse_gitignore_str(
4555
'/hello.*',
46-
fake_base_dir='/home/michael'
56+
base_dir='/home/michael'
4757
)
4858
self.assertTrue(matches('/home/michael/hello.txt'))
4959
self.assertTrue(matches('/home/michael/hello.c'))
5060
self.assertFalse(matches('/home/michael/a/hello.java'))
5161

5262
def test_trailingspaces(self):
53-
matches = _parse_gitignore_string(
63+
matches = parse_gitignore_str(
5464
'ignoretrailingspace \n'
5565
'notignoredspace\\ \n'
5666
'partiallyignoredspace\\ \n'
5767
'partiallyignoredspace2 \\ \n'
5868
'notignoredmultiplespace\\ \\ \\ ',
59-
fake_base_dir='/home/michael'
69+
base_dir='/home/michael'
6070
)
6171
self.assertTrue(matches('/home/michael/ignoretrailingspace'))
6272
self.assertFalse(matches('/home/michael/ignoretrailingspace '))
@@ -73,12 +83,12 @@ def test_trailingspaces(self):
7383
self.assertFalse(matches('/home/michael/notignoredmultiplespace'))
7484

7585
def test_comment(self):
76-
matches = _parse_gitignore_string(
86+
matches = parse_gitignore_str(
7787
'somematch\n'
7888
'#realcomment\n'
7989
'othermatch\n'
8090
'\\#imnocomment',
81-
fake_base_dir='/home/michael'
91+
base_dir='/home/michael'
8292
)
8393
self.assertTrue(matches('/home/michael/somematch'))
8494
self.assertFalse(matches('/home/michael/#realcomment'))
@@ -87,7 +97,7 @@ def test_comment(self):
8797

8898
def test_ignore_directory(self):
8999
matches = \
90-
_parse_gitignore_string('.venv/', fake_base_dir='/home/michael')
100+
parse_gitignore_str('.venv/', base_dir='/home/michael')
91101
self.assertTrue(matches('/home/michael/.venv'))
92102
self.assertTrue(matches('/home/michael/.venv/folder'))
93103
self.assertTrue(matches('/home/michael/.venv/file.txt'))
@@ -96,34 +106,34 @@ def test_ignore_directory(self):
96106

97107
def test_ignore_directory_asterisk(self):
98108
matches = \
99-
_parse_gitignore_string('.venv/*', fake_base_dir='/home/michael')
109+
parse_gitignore_str('.venv/*', base_dir='/home/michael')
100110
self.assertFalse(matches('/home/michael/.venv'))
101111
self.assertTrue(matches('/home/michael/.venv/folder'))
102112
self.assertTrue(matches('/home/michael/.venv/file.txt'))
103113

104114
def test_negation(self):
105-
matches = _parse_gitignore_string(
115+
matches = parse_gitignore_str(
106116
'''
107117
*.ignore
108118
!keep.ignore
109119
''',
110-
fake_base_dir='/home/michael'
120+
base_dir='/home/michael'
111121
)
112122
self.assertTrue(matches('/home/michael/trash.ignore'))
113123
self.assertFalse(matches('/home/michael/keep.ignore'))
114124
self.assertTrue(matches('/home/michael/waste.ignore'))
115125

116126
def test_literal_exclamation_mark(self):
117-
matches = _parse_gitignore_string(
118-
'\\!ignore_me!', fake_base_dir='/home/michael'
127+
matches = parse_gitignore_str(
128+
'\\!ignore_me!', base_dir='/home/michael'
119129
)
120130
self.assertTrue(matches('/home/michael/!ignore_me!'))
121131
self.assertFalse(matches('/home/michael/ignore_me!'))
122132
self.assertFalse(matches('/home/michael/ignore_me'))
123133

124134
def test_double_asterisks(self):
125-
matches = _parse_gitignore_string(
126-
'foo/**/Bar', fake_base_dir='/home/michael'
135+
matches = parse_gitignore_str(
136+
'foo/**/Bar', base_dir='/home/michael'
127137
)
128138
self.assertTrue(matches('/home/michael/foo/hello/Bar'))
129139
self.assertTrue(matches('/home/michael/foo/world/Bar'))
@@ -132,7 +142,7 @@ def test_double_asterisks(self):
132142

133143
def test_double_asterisk_without_slashes_handled_like_single_asterisk(self):
134144
matches = \
135-
_parse_gitignore_string('a/b**c/d', fake_base_dir='/home/michael')
145+
parse_gitignore_str('a/b**c/d', base_dir='/home/michael')
136146
self.assertTrue(matches('/home/michael/a/bc/d'))
137147
self.assertTrue(matches('/home/michael/a/bXc/d'))
138148
self.assertTrue(matches('/home/michael/a/bbc/d'))
@@ -144,22 +154,22 @@ def test_double_asterisk_without_slashes_handled_like_single_asterisk(self):
144154

145155
def test_more_asterisks_handled_like_single_asterisk(self):
146156
matches = \
147-
_parse_gitignore_string('***a/b', fake_base_dir='/home/michael')
157+
parse_gitignore_str('***a/b', base_dir='/home/michael')
148158
self.assertTrue(matches('/home/michael/XYZa/b'))
149159
self.assertFalse(matches('/home/michael/foo/a/b'))
150160
matches = \
151-
_parse_gitignore_string('a/b***', fake_base_dir='/home/michael')
161+
parse_gitignore_str('a/b***', base_dir='/home/michael')
152162
self.assertTrue(matches('/home/michael/a/bXYZ'))
153163
self.assertFalse(matches('/home/michael/a/b/foo'))
154164

155165
def test_directory_only_negation(self):
156-
matches = _parse_gitignore_string('''
166+
matches = parse_gitignore_str('''
157167
data/**
158168
!data/**/
159169
!.gitkeep
160170
!data/01_raw/*
161171
''',
162-
fake_base_dir='/home/michael'
172+
base_dir='/home/michael'
163173
)
164174
self.assertFalse(matches('/home/michael/data/01_raw/'))
165175
self.assertFalse(matches('/home/michael/data/01_raw/.gitkeep'))
@@ -171,21 +181,21 @@ def test_directory_only_negation(self):
171181
)
172182

173183
def test_single_asterisk(self):
174-
matches = _parse_gitignore_string('*', fake_base_dir='/home/michael')
184+
matches = parse_gitignore_str('*', base_dir='/home/michael')
175185
self.assertTrue(matches('/home/michael/file.txt'))
176186
self.assertTrue(matches('/home/michael/directory'))
177187
self.assertTrue(matches('/home/michael/directory-trailing/'))
178188

179189
def test_supports_path_type_argument(self):
180-
matches = _parse_gitignore_string(
181-
'file1\n!file2', fake_base_dir='/home/michael'
190+
matches = parse_gitignore_str(
191+
'file1\n!file2', base_dir='/home/michael'
182192
)
183193
self.assertTrue(matches(Path('/home/michael/file1')))
184194
self.assertFalse(matches(Path('/home/michael/file2')))
185195

186196
def test_slash_in_range_does_not_match_dirs(self):
187-
matches = _parse_gitignore_string(
188-
'abc[X-Z/]def', fake_base_dir='/home/michael'
197+
matches = parse_gitignore_str(
198+
'abc[X-Z/]def', base_dir='/home/michael'
189199
)
190200
self.assertFalse(matches('/home/michael/abcdef'))
191201
self.assertTrue(matches('/home/michael/abcXdef'))
@@ -197,8 +207,7 @@ def test_slash_in_range_does_not_match_dirs(self):
197207
def test_symlink_to_another_directory(self):
198208
with TemporaryDirectory() as project_dir:
199209
with TemporaryDirectory() as another_dir:
200-
matches = \
201-
_parse_gitignore_string('link', fake_base_dir=project_dir)
210+
matches = parse_gitignore_str('link', base_dir=project_dir)
202211

203212
# Create a symlink to another directory.
204213
link = Path(project_dir, 'link')
@@ -217,15 +226,9 @@ def test_symlink_to_symlink_directory(self):
217226
link = Path(link_dir, 'link')
218227
link.symlink_to(project_dir)
219228
file = Path(link, 'file.txt')
220-
matches = \
221-
_parse_gitignore_string('file.txt', fake_base_dir=str(link))
229+
matches = parse_gitignore_str('file.txt', base_dir=str(link_dir))
222230
self.assertTrue(matches(file))
223231

224232

225-
def _parse_gitignore_string(data: str, fake_base_dir: str = None):
226-
with patch('builtins.open', mock_open(read_data=data)):
227-
success = parse_gitignore(f'{fake_base_dir}/.gitignore', fake_base_dir)
228-
return success
229-
230233
if __name__ == '__main__':
231234
main()

0 commit comments

Comments
 (0)