Skip to content

Add git diff content on relint check #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Dec 4, 2018
Merged
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