Skip to content

Commit

Permalink
Add support to limit checks to git diff (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
anapaulagomes authored and codingjoe committed Dec 4, 2018
1 parent 3b9446c commit 69e8a89
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 21 deletions.
24 changes: 24 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ The following command will lint all files in the current directory:
The default configuration file name is `.relint.yaml` within your working
directory, but you can provide any YAML or JSON file.

If you prefer linting changed files (cached on git) you can use the option
`--diff [-d]`:

.. code-block:: bash
git diff | relint my_file.py --diff
This option is useful for pre-commit purposes. Here an example of how to use it
with `pre-commit`_ framework:

.. code-block:: YAML
- repo: local
hooks:
- id: relint
name: relint
entry: bin/relint-pre-commit.sh
language: system
You can find an example of `relint-pre-commit.sh`_ in this repository.

Samples
-------

Expand Down Expand Up @@ -76,3 +97,6 @@ Samples
hint: "Please write to self.stdout or self.stderr in favor of using a logger."
filename:
- "*/management/commands/*.py"
.. _`pre-commit`: https://pre-commit.com/
.. _`relint-pre-commit.sh`: relint-pre-commit.sh
4 changes: 4 additions & 0 deletions relint-pre-commit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh

set -eo pipefail
git diff --staged | relint --diff ${@:1}
141 changes: 121 additions & 20 deletions relint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@
import fnmatch
import glob
import re
import sys
from collections import namedtuple
from itertools import chain

import yaml


GIT_DIFF_LINE_NUMBERS_PATTERN = re.compile(
r"@ -\d+(,\d+)? \+(\d+)(,)?(\d+)? @")
GIT_DIFF_FILENAME_PATTERN = re.compile(
r"(?:\n|^)diff --git a\/.* b\/(.*)(?:\n|$)")
GIT_DIFF_SPLIT_PATTERN = re.compile(
r"(?:\n|^)diff --git a\/.* b\/.*(?:\n|$)")


Test = namedtuple('Test', ('name', 'pattern', 'hint', 'filename', 'error'))


def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
Expand All @@ -18,19 +30,22 @@ def parse_args():
help='Path to one or multiple files to be checked.'
)
parser.add_argument(
'--config',
'-c',
'--config',
metavar='CONFIG_FILE',
type=str,
default='.relint.yml',
help='Path to config file, default: .relint.yml'
)
parser.add_argument(
'-d',
'--diff',
action='store_true',
help='Analyze content from git diff.'
)
return parser.parse_args()


Test = namedtuple('Test', ('name', 'pattern', 'hint', 'filename', 'error'))


def load_config(path):
with open(path) as fs:
for test in yaml.load(fs):
Expand All @@ -56,31 +71,77 @@ def lint_file(filename, tests):
for test in tests:
if any(fnmatch.fnmatch(filename, fp) for fp in test.filename):
for match in test.pattern.finditer(content):
yield filename, test, match
line_number = match.string[:match.start()].count('\n') + 1
yield filename, test, match, line_number


def main():
args = parse_args()
paths = {
path
for file in args.files
for path in glob.iglob(file, recursive=True)
}
def parse_line_numbers(output):
"""
Extract line numbers from ``git diff`` output.
tests = list(load_config(args.config))
Git shows which lines were changed indicating a start line
and how many lines were changed from that. If only one
line was changed, the output will display only the start line,
like this:
``@@ -54 +54 @@ import glob``
If more lines were changed from that point, it will show
how many after a comma:
``@@ -4,2 +4,2 @@ import glob``
It means that line number 4 and the following 2 lines were changed
(5 and 6).
matches = chain.from_iterable(
lint_file(path, tests)
for path in paths
)
Args:
output (int): ``git diff`` output.
Returns:
list: All changed line numbers.
"""
line_numbers = []
matches = GIT_DIFF_LINE_NUMBERS_PATTERN.finditer(output)

for match in matches:
start = int(match.group(2))
if match.group(4) is not None:
end = start + int(match.group(4))
line_numbers.extend(range(start, end))
else:
line_numbers.append(start)

return line_numbers


def parse_filenames(output):
return re.findall(GIT_DIFF_FILENAME_PATTERN, output)

_filename = ''
lines = []

def split_diff_content_by_filename(output):
"""
Split the output by filename.
Args:
output (int): ``git diff`` output.
Returns:
dict: Filename and its content.
"""
content_by_filename = {}
filenames = parse_filenames(output)
splited_content = re.split(GIT_DIFF_SPLIT_PATTERN, output)
splited_content = filter(lambda x: x != '', splited_content)

for filename, content in zip(filenames, splited_content):
content_by_filename[filename] = content
return content_by_filename


def print_culprits(matches):
exit_code = 0
_filename = ''
lines = []

for filename, test, match in matches:
for filename, test, match, _ in matches:
exit_code = test.error if exit_code == 0 else exit_code

if filename != _filename:
_filename = filename
lines = match.string.splitlines()
Expand All @@ -102,6 +163,46 @@ def main():
)
print(*match_lines, sep="\n")

return exit_code


def match_with_diff_changes(content, matches):
"""Check matches found on diff output."""
for filename, test, match, line_number in matches:
if content.get(filename) and line_number in content.get(filename):
yield filename, test, match, line_number


def parse_diff(output):
"""Parse changed content by file."""
changed_content = {}
for filename, content in split_diff_content_by_filename(output).items():
changed_line_numbers = parse_line_numbers(content)
changed_content[filename] = changed_line_numbers
return changed_content


def main():
args = parse_args()
paths = {
path
for file in args.files
for path in glob.iglob(file, recursive=True)
}

tests = list(load_config(args.config))

matches = chain.from_iterable(
lint_file(path, tests)
for path in paths
)

if args.diff:
output = sys.stdin.read()
changed_content = parse_diff(output)
matches = match_with_diff_changes(changed_content, matches)

exit_code = print_culprits(matches)
exit(exit_code)


Expand Down
59 changes: 59 additions & 0 deletions test.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
diff --git a/README.rst b/README.rst
index 43032c5..e7203b3 100644
--- a/README.rst
+++ b/README.rst
@@ -51,7 +51,7 @@ If you prefer linting changed files (cached on git) you can use the option

.. code-block:: bash

- relint my_file.py --diff
+ git diff | relint my_file.py --diff

This option is useful for pre-commit purposes.

diff --git a/relint.py b/relint.py
index 31061ec..697a3f0 100644
--- a/relint.py
+++ b/relint.py
@@ -113,7 +113,7 @@ def print_culprits(matches):
for filename, test, match, _ in matches:
exit_code = test.error if exit_code == 0 else exit_code

- if filename != _filename:
+ if filename != _filename: # TODO check this out
_filename = filename
lines = match.string.splitlines()

@@ -167,7 +167,7 @@ def main():
for path in paths
)

- if args.diff:
+ if args.diff: # TODO wow
output = sys.stdin.read()
changed_content = parse_diff(output)
matches = filter_paths_from_diff(changed_content, matches)
diff --git a/test_relint.py b/test_relint.py
index 7165fd3..249b783 100644
--- a/test_relint.py
+++ b/test_relint.py
@@ -54,8 +54,9 @@ class TestParseGitDiff:
def test_split_diff_content(self):
output = open('test.diff').read()
splited = split_diff_content(output)
+
assert isinstance(splited, dict)
- assert len(splited) == 2
+ assert len(splited) == 3

def test_return_empty_list_if_can_not_split_diff_content(self):
splited = split_diff_content('')
@@ -120,7 +121,7 @@ class TestParseGitDiff:
"@@ -1,0 +2 @@\n" \
"+# TODO: I'll do it later, promise\n"

- parsed_content = parse_diff(output)
+ parsed_content = parse_diff(output) # TODO brand new
expected = {'test_relint.py': [2]}

assert parsed_content == expected
Loading

0 comments on commit 69e8a89

Please sign in to comment.