diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e5403cf..f64c046 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9] + python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, '3.10'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/lintly/backends/base.py b/lintly/backends/base.py index 7f87f55..4ee27ee 100644 --- a/lintly/backends/base.py +++ b/lintly/backends/base.py @@ -39,7 +39,7 @@ def get_pr_diff(self, pr): """ raise NotImplementedError - def create_pull_request_review(self, pr, patch, all_violations, pr_review_action): + def create_pull_request_review(self, pr, patch, all_violations, pr_review_action, has_body): """ Creates a pull request review for the given build. """ diff --git a/lintly/backends/dummy.py b/lintly/backends/dummy.py index 588ea9d..f02e7bb 100644 --- a/lintly/backends/dummy.py +++ b/lintly/backends/dummy.py @@ -17,7 +17,7 @@ def create_pull_request_comment(self, pr, comment): def delete_pull_request_comments(self, pr): pass - def create_pull_request_review(self, pr, patch, all_violations, pr_review_action): + def create_pull_request_review(self, pr, patch, all_violations, pr_review_action, has_body): pass def delete_pull_request_review_comments(self, pr): diff --git a/lintly/backends/github.py b/lintly/backends/github.py index b5579cf..1798137 100644 --- a/lintly/backends/github.py +++ b/lintly/backends/github.py @@ -7,7 +7,7 @@ from github import GithubException, UnknownObjectException, Github -from lintly.constants import LINTLY_IDENTIFIER +from lintly.config import Config from lintly.formatters import ( build_pr_review_line_comment, build_pr_review_body, @@ -124,7 +124,7 @@ def __init__(self, token, project, context): self.context = context def _should_delete_comment(self, comment): - return LINTLY_IDENTIFIER in comment.body + return Config.LINTLY_IDENTIFIER in comment.body @translate_github_exception def get_pull_request(self, pr): @@ -180,7 +180,7 @@ def _get_event(self, review_action): elif review_action == ACTION_REVIEW_APPROVE: return 'APPROVE' - def create_pull_request_review(self, pr, patch, all_violations, pr_review_action): + def create_pull_request_review(self, pr, patch, all_violations, pr_review_action, has_body): comments = [] for file_path in all_violations: violations = all_violations[file_path] @@ -209,10 +209,11 @@ def create_pull_request_review(self, pr, patch, all_violations, pr_review_action # there are no pending comments to add then we send the request if len(comments_batch) == GITHUB_PULL_REQUEST_COMMENT_LIMIT or not comments: data = { - 'body': build_pr_review_body(all_violations), 'event': self._get_event(pr_review_action), 'comments': comments_batch, } + if has_body: + data['body'] = build_pr_review_body(all_violations) url = '/repos/{owner}/{repo_name}/pulls/{pr_number}/reviews'.format( owner=self.project.owner_login, diff --git a/lintly/backends/gitlab.py b/lintly/backends/gitlab.py index 4d5e068..37769e4 100644 --- a/lintly/backends/gitlab.py +++ b/lintly/backends/gitlab.py @@ -7,7 +7,7 @@ import gitlab import requests -from lintly.constants import LINTLY_IDENTIFIER +from lintly.config import Config from .base import BaseGitBackend from .errors import ( @@ -136,14 +136,14 @@ def delete_pull_request_comments(self, pr): mr = project.mergerequests.list(iid=pr)[0] client = GitLabAPIClient(self.token, self.user, self.project) for note in mr.notes.list(all=True, per_page=DEFAULT_PER_PAGE): - if LINTLY_IDENTIFIER in note.body: + if Config.LINTLY_IDENTIFIER in note.body: url = '/projects/{project_id}/merge_requests/{mr_id}/notes/{note_id}'.format( project_id=project.id, mr_id=mr.id, note_id=note.id ) client.delete(url) @translate_gitlab_exception - def create_pull_request_review(self, pr, patch, all_violations, pr_review_action): + def create_pull_request_review(self, pr, patch, all_violations, pr_review_action, has_body): raise NotSupportedError() @translate_gitlab_exception diff --git a/lintly/builds.py b/lintly/builds.py index ebc603d..ace490b 100644 --- a/lintly/builds.py +++ b/lintly/builds.py @@ -164,7 +164,8 @@ def submit_pr_review(self, patch, pr_review_action): self.config.pr, patch, self._diff_violations, - pr_review_action + pr_review_action, + self.config.review_body ) post_pr_comment = False except GitClientError as e: diff --git a/lintly/cli.py b/lintly/cli.py index cded503..e50ced2 100644 --- a/lintly/cli.py +++ b/lintly/cli.py @@ -55,6 +55,16 @@ @click.option('--log', is_flag=True, help='Send Lintly debug logs to the console. Default false') +@click.option('--review-body/--no-review-body', + default=True, + help=('Whether Lintly should post a PR review with a body. ' + 'The body is not removed after a re-run.')) +@click.option('--comment-tag', + default='', + help='A tag used to identify comments from a previous run that should be deleted') +@click.option('--base-dir', + default='.', + help='The base git directory') @click.option('--exit-zero/--no-exit-zero', default=False, help=('Whether Lintly should exit with error code indicating ' 'amount of violations or not. Default false')) diff --git a/lintly/config.py b/lintly/config.py index ae6eac1..174646c 100644 --- a/lintly/config.py +++ b/lintly/config.py @@ -7,8 +7,14 @@ class Config(object): """A Config object that knows how to return configuration from the CLI or Continuous Integration services""" + LINTLY_IDENTIFIER = '' + BASE_DIR = '.' + def __init__(self, cli_config): self.cli_config = cli_config + if self.comment_tag: + type(self).LINTLY_IDENTIFIER = '(%s)%s' % (self.comment_tag, type(self).LINTLY_IDENTIFIER) + type(self).BASE_DIR = self.base_dir def as_dict(self): return { @@ -21,6 +27,9 @@ def as_dict(self): 'post_status': self.post_status, 'request_changes': self.request_changes, 'github_check_run_id': self.github_check_run_id, + 'review_body': self.review_body, + 'comment_tag': self.comment_tag, + 'base_dir': self.base_dir, } @property @@ -59,6 +68,18 @@ def post_status(self): def request_changes(self): return self.cli_config['request_changes'] + @property + def review_body(self): + return self.cli_config['review_body'] + + @property + def comment_tag(self): + return self.cli_config['comment_tag'] + + @property + def base_dir(self): + return self.cli_config['base_dir'] + @property def github_check_run_id(self): """The Check Run ID from GitHub Actions. diff --git a/lintly/constants.py b/lintly/constants.py index b3a0583..5c3fc42 100644 --- a/lintly/constants.py +++ b/lintly/constants.py @@ -1,11 +1,6 @@ FAIL_ON_ANY = 'any' FAIL_ON_NEW = 'new' -# Identifies that a comment came from Lintly. This is used to aid in automatically -# deleting old PR comments/reviews. This is valid Markdown that is hidden from -# users in GitHub and GitLab. -LINTLY_IDENTIFIER = '' - # These constants define the actions that lintly might take against a PR concerning reviews, ie. # not the commit status ACTION_REVIEW_USE_CHECKS = 'use_checks' diff --git a/lintly/formatters.py b/lintly/formatters.py index fdeb274..88f81a5 100644 --- a/lintly/formatters.py +++ b/lintly/formatters.py @@ -5,7 +5,7 @@ from jinja2 import Environment, FileSystemLoader -from .constants import LINTLY_IDENTIFIER +from .config import Config TEMPLATES_PATH = os.path.join(os.path.dirname(__file__), 'templates') @@ -23,7 +23,7 @@ def build_pr_comment(config, violations): :return: The comment """ template = env.get_template('pr_comment.txt') - return template.render(violations=violations, LINTLY_IDENTIFIER=LINTLY_IDENTIFIER) + return template.render(violations=violations, LINTLY_IDENTIFIER=Config.LINTLY_IDENTIFIER) def build_pr_review_line_comment(violation): @@ -32,7 +32,7 @@ def build_pr_review_line_comment(violation): :return: The comment """ template = env.get_template('pr_review_line_comment.txt') - return template.render(violation=violation, LINTLY_IDENTIFIER=LINTLY_IDENTIFIER) + return template.render(violation=violation, LINTLY_IDENTIFIER=Config.LINTLY_IDENTIFIER) def build_check_line_comment(violation): @@ -42,4 +42,4 @@ def build_check_line_comment(violation): def build_pr_review_body(violations): template = env.get_template('pr_review_body.txt') - return template.render(violations=violations, LINTLY_IDENTIFIER=LINTLY_IDENTIFIER) + return template.render(violations=violations, LINTLY_IDENTIFIER=Config.LINTLY_IDENTIFIER) diff --git a/lintly/parsers.py b/lintly/parsers.py index d34bf5e..3ba3269 100644 --- a/lintly/parsers.py +++ b/lintly/parsers.py @@ -8,6 +8,7 @@ import re from .violations import Violation +from .config import Config class BaseLintParser(object): @@ -15,15 +16,15 @@ class BaseLintParser(object): def parse_violations(self, output): raise NotImplementedError - def _get_working_dir(self): - return os.getcwd() - def _normalize_path(self, path): """ Normalizes a file path so that it returns a path relative to the root repo directory. """ norm_path = os.path.normpath(path) - return os.path.relpath(norm_path, start=self._get_working_dir()) + rel_path = os.path.relpath(norm_path, start=Config.BASE_DIR) + if os.name == 'nt': + rel_path = rel_path.replace('\\', '/') + return rel_path class LineRegexParser(BaseLintParser): @@ -58,11 +59,12 @@ def parse_violations(self, output): path = self._normalize_path(match.group('path')) + groups = match.groupdict() violation = Violation( - line=int(match.group('line')), - column=int(match.group('column')), - code=match.group('code'), - message=match.group('message') + line=int(groups.get('line') or 1), + column=int(groups.get('column') or 1), + code=(groups.get('code') or '[empty]').strip(), + message=(groups.get('message') or '[empty]').strip() ) violations[path].append(violation) @@ -296,4 +298,8 @@ def parse_violations(self, output): # cfn-nag JSON output 'cfn-nag': CfnNagParser(), + + # Mypy formatter + 'mypy': LineRegexParser(r'^(?P.*\.py):(?:(?P\d*):)?(?:(?P\d*):)? [^:]*: ' + r'(?P.*?)(?:\[(?P\S*)\])?$'), } diff --git a/requirements.txt b/requirements.txt index 0195ca8..534dd82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ -Jinja2<3.0 +Jinja2<4.0 PyGithub<2.0 cached-property<2.0 ci-py -click<7.0 +click<9.0 codecov coverage flake8 mock pytest -python-gitlab<2.0 +python-gitlab<4.0 tox diff --git a/setup.py b/setup.py index 03ddd4b..460bc7a 100644 --- a/setup.py +++ b/setup.py @@ -8,10 +8,10 @@ dependencies = [ 'ci-py', 'cached-property<2.0', - 'click<8.0', - 'Jinja2<3.0', + 'click<9.0', + 'Jinja2<4.0', 'PyGithub<2.0', - 'python-gitlab<2.0', + 'python-gitlab<4.0', 'six', ] diff --git a/tests/test_builds.py b/tests/test_builds.py index d9d844f..598d279 100644 --- a/tests/test_builds.py +++ b/tests/test_builds.py @@ -31,6 +31,8 @@ def config(format_and_context): "commit_sha": "xyz123notarealsha", "context": format_and_context[1], "post_status": True, + "comment_tag": "", + 'base_dir': ".", } return Config(cli_config) diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 3e7a479..1131410 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -2,12 +2,8 @@ import os import unittest -try: - from unittest.mock import patch -except ImportError: - from mock import patch - from lintly.parsers import PARSERS +from lintly.config import Config class ParserTestCaseMixin(object): @@ -57,6 +53,7 @@ def load_linter_output(file_name): return linter_output.read() def setUp(self): + Config.BASE_DIR = '.' self.linter_output = self.load_linter_output(self.linter_output_file_name) def test_parse_violations(self): @@ -137,8 +134,8 @@ class ESLintParserTestCase(ParserTestCaseMixin, unittest.TestCase): ] } - @patch('lintly.parsers.ESLintParser._get_working_dir', return_value='/Users/grant/project') - def test_parse_violations(self, _get_working_dir_mock): + def test_parse_violations(self): + Config.BASE_DIR = '/Users/grant/project' super(ESLintParserTestCase, self).test_parse_violations() @@ -168,8 +165,8 @@ class BlackParserTestCase(ParserTestCaseMixin, unittest.TestCase): for file_path in ['lintly/violations.py', 'lintly/parsers.py'] } - @patch('lintly.parsers.BlackParser._get_working_dir', return_value='/Users/jouyuy/Dev/workspace/Lintly') - def test_parse_violations(self, _get_working_dir_mock): + def test_parse_violations(self): + Config.BASE_DIR = '/Users/jouyuy/Dev/workspace/Lintly' super(BlackParserTestCase, self).test_parse_violations()