diff --git a/README.md b/README.md index 6e406d8..979dd36 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,16 @@ Then: >>> matches('/home/michael/project/__pycache__') True +Alternatively, you can use the `parse_gitignore_str` function: + + >>> from gitignore_parser import parse_gitignore_str + >>> matches = parse_gitignore_str( + '__pycache__/\n*.py[cod]', base_dir='/home/michael/project') + >>> matches('/home/michael/project/main.py') + False + >>> matches('/home/michael/project/main.pyc') + True + ## Motivation I couldn't find a good library for doing the above on PyPI. There are diff --git a/gitignore_parser.py b/gitignore_parser.py index 98c4afe..ef91b2b 100644 --- a/gitignore_parser.py +++ b/gitignore_parser.py @@ -2,7 +2,7 @@ import os import re -from os.path import abspath, dirname +from os.path import abspath, dirname, join from pathlib import Path from typing import Reversible, Union @@ -15,16 +15,22 @@ def handle_negation(file_path, rules: Reversible["IgnoreRule"]): def parse_gitignore(full_path, base_dir=None): if base_dir is None: base_dir = dirname(full_path) - rules = [] with open(full_path) as ignore_file: - counter = 0 - for line in ignore_file: - counter += 1 - line = line.rstrip('\n') - rule = rule_from_pattern(line, base_path=_normalize_path(base_dir), - source=(full_path, counter)) - if rule: - rules.append(rule) + return _parse_gitignore_lines(ignore_file, full_path, base_dir) + +def parse_gitignore_str(gitignore_str, base_dir): + full_path = join(base_dir, '.gitignore') + lines = gitignore_str.splitlines() + return _parse_gitignore_lines(lines, full_path, base_dir) + +def _parse_gitignore_lines(lines, full_path, base_dir): + base_dir = _normalize_path(base_dir) + rules = [] + for line_no, line in enumerate(lines, start=1): + rule = rule_from_pattern( + line.rstrip('\n'), base_path=base_dir, source=(full_path, line_no)) + if rule: + rules.append(rule) if not any(r.negation for r in rules): return lambda file_path: any(r.match(file_path) for r in rules) else: @@ -100,7 +106,7 @@ def rule_from_pattern(pattern, base_path=None, source=None): negation=negation, directory_only=directory_only, anchored=anchored, - base_path=_normalize_path(base_path) if base_path else None, + base_path=base_path if base_path else None, source=source ) diff --git a/tests.py b/tests.py index 5ccd3af..bd63855 100644 --- a/tests.py +++ b/tests.py @@ -2,25 +2,35 @@ from pathlib import Path from tempfile import TemporaryDirectory -from gitignore_parser import parse_gitignore +from gitignore_parser import parse_gitignore, parse_gitignore_str from unittest import TestCase, main class Test(TestCase): def test_simple(self): - matches = _parse_gitignore_string( + matches = parse_gitignore_str( '__pycache__/\n' '*.py[cod]', - fake_base_dir='/home/michael' + base_dir='/home/michael' ) self.assertFalse(matches('/home/michael/main.py')) self.assertTrue(matches('/home/michael/main.pyc')) self.assertTrue(matches('/home/michael/dir/main.pyc')) self.assertTrue(matches('/home/michael/__pycache__')) + def test_simple_parse_file(self): + with patch('builtins.open', mock_open(read_data= + '__pycache__/\n' + '*.py[cod]')): + matches = parse_gitignore('/home/michael/.gitignore') + self.assertFalse(matches('/home/michael/main.py')) + self.assertTrue(matches('/home/michael/main.pyc')) + self.assertTrue(matches('/home/michael/dir/main.pyc')) + self.assertTrue(matches('/home/michael/__pycache__')) + def test_incomplete_filename(self): - matches = _parse_gitignore_string('o.py', fake_base_dir='/home/michael') + matches = parse_gitignore_str('o.py', base_dir='/home/michael') self.assertTrue(matches('/home/michael/o.py')) self.assertFalse(matches('/home/michael/foo.py')) self.assertFalse(matches('/home/michael/o.pyc')) @@ -29,9 +39,9 @@ def test_incomplete_filename(self): self.assertFalse(matches('/home/michael/dir/o.pyc')) def test_wildcard(self): - matches = _parse_gitignore_string( + matches = parse_gitignore_str( 'hello.*', - fake_base_dir='/home/michael' + base_dir='/home/michael' ) self.assertTrue(matches('/home/michael/hello.txt')) self.assertTrue(matches('/home/michael/hello.foobar/')) @@ -41,22 +51,22 @@ def test_wildcard(self): self.assertFalse(matches('/home/michael/helloX')) def test_anchored_wildcard(self): - matches = _parse_gitignore_string( + matches = parse_gitignore_str( '/hello.*', - fake_base_dir='/home/michael' + base_dir='/home/michael' ) self.assertTrue(matches('/home/michael/hello.txt')) self.assertTrue(matches('/home/michael/hello.c')) self.assertFalse(matches('/home/michael/a/hello.java')) def test_trailingspaces(self): - matches = _parse_gitignore_string( + matches = parse_gitignore_str( 'ignoretrailingspace \n' 'notignoredspace\\ \n' 'partiallyignoredspace\\ \n' 'partiallyignoredspace2 \\ \n' 'notignoredmultiplespace\\ \\ \\ ', - fake_base_dir='/home/michael' + base_dir='/home/michael' ) self.assertTrue(matches('/home/michael/ignoretrailingspace')) self.assertFalse(matches('/home/michael/ignoretrailingspace ')) @@ -73,12 +83,12 @@ def test_trailingspaces(self): self.assertFalse(matches('/home/michael/notignoredmultiplespace')) def test_comment(self): - matches = _parse_gitignore_string( + matches = parse_gitignore_str( 'somematch\n' '#realcomment\n' 'othermatch\n' '\\#imnocomment', - fake_base_dir='/home/michael' + base_dir='/home/michael' ) self.assertTrue(matches('/home/michael/somematch')) self.assertFalse(matches('/home/michael/#realcomment')) @@ -87,7 +97,7 @@ def test_comment(self): def test_ignore_directory(self): matches = \ - _parse_gitignore_string('.venv/', fake_base_dir='/home/michael') + parse_gitignore_str('.venv/', base_dir='/home/michael') self.assertTrue(matches('/home/michael/.venv')) self.assertTrue(matches('/home/michael/.venv/folder')) self.assertTrue(matches('/home/michael/.venv/file.txt')) @@ -96,34 +106,34 @@ def test_ignore_directory(self): def test_ignore_directory_asterisk(self): matches = \ - _parse_gitignore_string('.venv/*', fake_base_dir='/home/michael') + parse_gitignore_str('.venv/*', base_dir='/home/michael') self.assertFalse(matches('/home/michael/.venv')) self.assertTrue(matches('/home/michael/.venv/folder')) self.assertTrue(matches('/home/michael/.venv/file.txt')) def test_negation(self): - matches = _parse_gitignore_string( + matches = parse_gitignore_str( ''' *.ignore !keep.ignore ''', - fake_base_dir='/home/michael' + base_dir='/home/michael' ) self.assertTrue(matches('/home/michael/trash.ignore')) self.assertFalse(matches('/home/michael/keep.ignore')) self.assertTrue(matches('/home/michael/waste.ignore')) def test_literal_exclamation_mark(self): - matches = _parse_gitignore_string( - '\\!ignore_me!', fake_base_dir='/home/michael' + matches = parse_gitignore_str( + '\\!ignore_me!', base_dir='/home/michael' ) self.assertTrue(matches('/home/michael/!ignore_me!')) self.assertFalse(matches('/home/michael/ignore_me!')) self.assertFalse(matches('/home/michael/ignore_me')) def test_double_asterisks(self): - matches = _parse_gitignore_string( - 'foo/**/Bar', fake_base_dir='/home/michael' + matches = parse_gitignore_str( + 'foo/**/Bar', base_dir='/home/michael' ) self.assertTrue(matches('/home/michael/foo/hello/Bar')) self.assertTrue(matches('/home/michael/foo/world/Bar')) @@ -132,7 +142,7 @@ def test_double_asterisks(self): def test_double_asterisk_without_slashes_handled_like_single_asterisk(self): matches = \ - _parse_gitignore_string('a/b**c/d', fake_base_dir='/home/michael') + parse_gitignore_str('a/b**c/d', base_dir='/home/michael') self.assertTrue(matches('/home/michael/a/bc/d')) self.assertTrue(matches('/home/michael/a/bXc/d')) self.assertTrue(matches('/home/michael/a/bbc/d')) @@ -144,22 +154,22 @@ def test_double_asterisk_without_slashes_handled_like_single_asterisk(self): def test_more_asterisks_handled_like_single_asterisk(self): matches = \ - _parse_gitignore_string('***a/b', fake_base_dir='/home/michael') + parse_gitignore_str('***a/b', base_dir='/home/michael') self.assertTrue(matches('/home/michael/XYZa/b')) self.assertFalse(matches('/home/michael/foo/a/b')) matches = \ - _parse_gitignore_string('a/b***', fake_base_dir='/home/michael') + parse_gitignore_str('a/b***', base_dir='/home/michael') self.assertTrue(matches('/home/michael/a/bXYZ')) self.assertFalse(matches('/home/michael/a/b/foo')) def test_directory_only_negation(self): - matches = _parse_gitignore_string(''' + matches = parse_gitignore_str(''' data/** !data/**/ !.gitkeep !data/01_raw/* ''', - fake_base_dir='/home/michael' + base_dir='/home/michael' ) self.assertFalse(matches('/home/michael/data/01_raw/')) self.assertFalse(matches('/home/michael/data/01_raw/.gitkeep')) @@ -171,21 +181,21 @@ def test_directory_only_negation(self): ) def test_single_asterisk(self): - matches = _parse_gitignore_string('*', fake_base_dir='/home/michael') + matches = parse_gitignore_str('*', base_dir='/home/michael') self.assertTrue(matches('/home/michael/file.txt')) self.assertTrue(matches('/home/michael/directory')) self.assertTrue(matches('/home/michael/directory-trailing/')) def test_supports_path_type_argument(self): - matches = _parse_gitignore_string( - 'file1\n!file2', fake_base_dir='/home/michael' + matches = parse_gitignore_str( + 'file1\n!file2', base_dir='/home/michael' ) self.assertTrue(matches(Path('/home/michael/file1'))) self.assertFalse(matches(Path('/home/michael/file2'))) def test_slash_in_range_does_not_match_dirs(self): - matches = _parse_gitignore_string( - 'abc[X-Z/]def', fake_base_dir='/home/michael' + matches = parse_gitignore_str( + 'abc[X-Z/]def', base_dir='/home/michael' ) self.assertFalse(matches('/home/michael/abcdef')) self.assertTrue(matches('/home/michael/abcXdef')) @@ -197,8 +207,7 @@ def test_slash_in_range_does_not_match_dirs(self): def test_symlink_to_another_directory(self): with TemporaryDirectory() as project_dir: with TemporaryDirectory() as another_dir: - matches = \ - _parse_gitignore_string('link', fake_base_dir=project_dir) + matches = parse_gitignore_str('link', base_dir=project_dir) # Create a symlink to another directory. link = Path(project_dir, 'link') @@ -217,15 +226,9 @@ def test_symlink_to_symlink_directory(self): link = Path(link_dir, 'link') link.symlink_to(project_dir) file = Path(link, 'file.txt') - matches = \ - _parse_gitignore_string('file.txt', fake_base_dir=str(link)) + matches = parse_gitignore_str('file.txt', base_dir=str(link_dir)) self.assertTrue(matches(file)) -def _parse_gitignore_string(data: str, fake_base_dir: str = None): - with patch('builtins.open', mock_open(read_data=data)): - success = parse_gitignore(f'{fake_base_dir}/.gitignore', fake_base_dir) - return success - if __name__ == '__main__': main()