From f19c3d6569d698e4c5fafcc482e2c8c89535bc71 Mon Sep 17 00:00:00 2001 From: Bugale Bugalit Date: Wed, 22 Dec 2021 07:11:22 +0200 Subject: [PATCH 1/7] Issue #50 - normalize path to Unix on Windows --- lintly/parsers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lintly/parsers.py b/lintly/parsers.py index d34bf5e..d0277d1 100644 --- a/lintly/parsers.py +++ b/lintly/parsers.py @@ -23,7 +23,10 @@ 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=self._get_working_dir()) + if os.name == 'nt': + rel_path = rel_path.replace('\\', '/') + return rel_path class LineRegexParser(BaseLintParser): From c4293f9ed8fe482b4f2ebcdfd06b0e32f43aee82 Mon Sep 17 00:00:00 2001 From: Bugale Bugalit Date: Mon, 11 Jul 2022 21:57:09 +0300 Subject: [PATCH 2/7] Test against Python 3.10 --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }} From 4b2d7fb1b16fd9274d652e3e45b5c3de5c711fb3 Mon Sep 17 00:00:00 2001 From: Bugale Bugalit Date: Mon, 11 Jul 2022 21:57:29 +0300 Subject: [PATCH 3/7] Issue #54 - Upgrade dependencies pinning --- requirements.txt | 6 +++--- setup.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) 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', ] From c63e77fca8817f3e658a2486f175f60dd42533bc Mon Sep 17 00:00:00 2001 From: Bugale Bugalit Date: Mon, 11 Jul 2022 17:53:43 +0300 Subject: [PATCH 4/7] Issue #58 - Add support for posting review without body --- lintly/backends/base.py | 2 +- lintly/backends/dummy.py | 2 +- lintly/backends/github.py | 5 +++-- lintly/backends/gitlab.py | 2 +- lintly/builds.py | 3 ++- lintly/cli.py | 4 ++++ lintly/config.py | 5 +++++ 7 files changed, 17 insertions(+), 6 deletions(-) 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..5368957 100644 --- a/lintly/backends/github.py +++ b/lintly/backends/github.py @@ -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..2595c21 100644 --- a/lintly/backends/gitlab.py +++ b/lintly/backends/gitlab.py @@ -143,7 +143,7 @@ def delete_pull_request_comments(self, pr): 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..0b7d76c 100644 --- a/lintly/cli.py +++ b/lintly/cli.py @@ -55,6 +55,10 @@ @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('--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..c5a9018 100644 --- a/lintly/config.py +++ b/lintly/config.py @@ -21,6 +21,7 @@ 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, } @property @@ -59,6 +60,10 @@ 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 github_check_run_id(self): """The Check Run ID from GitHub Actions. From 2600dc80726b6ecabee21f111ae2a5ab73683fd4 Mon Sep 17 00:00:00 2001 From: Bugale Bugalit Date: Mon, 11 Jul 2022 22:08:22 +0300 Subject: [PATCH 5/7] Issue #60 - Add support for custom comment tag --- lintly/backends/github.py | 4 ++-- lintly/backends/gitlab.py | 4 ++-- lintly/cli.py | 3 +++ lintly/config.py | 9 +++++++++ lintly/constants.py | 5 ----- lintly/formatters.py | 8 ++++---- tests/test_builds.py | 1 + 7 files changed, 21 insertions(+), 13 deletions(-) diff --git a/lintly/backends/github.py b/lintly/backends/github.py index 5368957..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): diff --git a/lintly/backends/gitlab.py b/lintly/backends/gitlab.py index 2595c21..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,7 +136,7 @@ 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 ) diff --git a/lintly/cli.py b/lintly/cli.py index 0b7d76c..c47716b 100644 --- a/lintly/cli.py +++ b/lintly/cli.py @@ -59,6 +59,9 @@ 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('--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 c5a9018..daf77c9 100644 --- a/lintly/config.py +++ b/lintly/config.py @@ -7,8 +7,12 @@ class Config(object): """A Config object that knows how to return configuration from the CLI or Continuous Integration services""" + LINTLY_IDENTIFIER = '' + 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) def as_dict(self): return { @@ -22,6 +26,7 @@ def as_dict(self): 'request_changes': self.request_changes, 'github_check_run_id': self.github_check_run_id, 'review_body': self.review_body, + 'comment_tag': self.comment_tag, } @property @@ -64,6 +69,10 @@ def request_changes(self): def review_body(self): return self.cli_config['review_body'] + @property + def comment_tag(self): + return self.cli_config['comment_tag'] + @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/tests/test_builds.py b/tests/test_builds.py index d9d844f..a993e8e 100644 --- a/tests/test_builds.py +++ b/tests/test_builds.py @@ -31,6 +31,7 @@ def config(format_and_context): "commit_sha": "xyz123notarealsha", "context": format_and_context[1], "post_status": True, + "comment_tag": "", } return Config(cli_config) From 4079aef67fe53db97b6d57621705331f4064731a Mon Sep 17 00:00:00 2001 From: Bugale Bugalit Date: Mon, 11 Jul 2022 18:48:11 +0300 Subject: [PATCH 6/7] Issue #62 - Add mypy parser --- lintly/parsers.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lintly/parsers.py b/lintly/parsers.py index d0277d1..e474b1e 100644 --- a/lintly/parsers.py +++ b/lintly/parsers.py @@ -61,11 +61,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) @@ -299,4 +300,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*)\])?$'), } From 5df18e0b1446c53118efa518245f1294c613034e Mon Sep 17 00:00:00 2001 From: Bugale Bugalit Date: Mon, 11 Jul 2022 22:11:53 +0300 Subject: [PATCH 7/7] Support custom base dir --- lintly/cli.py | 3 +++ lintly/config.py | 7 +++++++ lintly/parsers.py | 6 ++---- tests/test_builds.py | 1 + tests/test_parsers.py | 15 ++++++--------- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lintly/cli.py b/lintly/cli.py index c47716b..e50ced2 100644 --- a/lintly/cli.py +++ b/lintly/cli.py @@ -62,6 +62,9 @@ @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 daf77c9..174646c 100644 --- a/lintly/config.py +++ b/lintly/config.py @@ -8,11 +8,13 @@ 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 { @@ -27,6 +29,7 @@ def as_dict(self): '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 @@ -73,6 +76,10 @@ def review_body(self): 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/parsers.py b/lintly/parsers.py index e474b1e..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,12 @@ 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) - rel_path = 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 diff --git a/tests/test_builds.py b/tests/test_builds.py index a993e8e..598d279 100644 --- a/tests/test_builds.py +++ b/tests/test_builds.py @@ -32,6 +32,7 @@ def config(format_and_context): "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()