Skip to content

Commit

Permalink
Feat: Add rich logging
Browse files Browse the repository at this point in the history
  • Loading branch information
gmuloc committed Jan 18, 2023
1 parent 3c574d6 commit 450353e
Show file tree
Hide file tree
Showing 15 changed files with 338 additions and 133 deletions.
31 changes: 24 additions & 7 deletions .github/workflows/pull-request-management.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,33 @@ on:
pull_request

jobs:
pylint:
lint:
name: Run pylint
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Install pylint
run: pip install pylint
- name: Run pylint
run: pylint $GITHUB_WORKSPACE/j2lint
test:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install dependencies
run: pip install tox tox-gh-actions
- name: "Run lint"
run: tox -e lint
type:
name: Run mypy
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install dependencies
run: pip install tox tox-gh-actions
- name: "Run mypy"
run: tox -e type
tox:
name: Run pytest for supported Python versions
runs-on: ubuntu-20.04
strategy:
Expand Down
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ repos:
- -d duplicate-code
additional_dependencies:
- jinja2
- rich
exclude: ^tests/

- repo: https://github.com/pycqa/isort
Expand Down
46 changes: 32 additions & 14 deletions j2lint/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@
import sys
import tempfile

from rich.console import Console
from rich.tree import Tree

from . import DESCRIPTION, NAME, VERSION
from .linter.collection import RulesCollection
from .linter.collection import DEFAULT_RULE_DIR, RulesCollection
from .linter.error import LinterError
from .linter.runner import Runner
from .logger import add_handler, logger
from .utils import get_files

RULES_DIR = f"{os.path.dirname(os.path.realpath(__file__))}/rules"

IGNORE_RULES = WARN_RULES = [
"jinja-syntax-error",
"single-space-decorator",
Expand All @@ -41,6 +42,8 @@
"V2",
]

CONSOLE = Console()


def create_parser() -> argparse.ArgumentParser:
"""Initializes a new argument parser object
Expand Down Expand Up @@ -76,7 +79,7 @@ def create_parser() -> argparse.ArgumentParser:
"--rules_dir",
dest="rules_dir",
action="append",
default=[RULES_DIR],
default=[DEFAULT_RULE_DIR],
help="rules directory",
)
parser.add_argument(
Expand Down Expand Up @@ -158,7 +161,7 @@ def print_json_output(
for _, warnings in lint_warnings.items():
for warning in warnings:
json_output["WARNINGS"].append(json.loads(str(warning.to_json())))
print(f"\n{json.dumps(json_output)}")
CONSOLE.print_json(f"\n{json.dumps(json_output)}")

return len(json_output["ERRORS"]), len(json_output["WARNINGS"])

Expand All @@ -173,13 +176,15 @@ def print_string_output(
def print_issues(
lint_issues: dict[str, list[LinterError]], issue_type: str
) -> None:
print(f"\nJINJA2 LINT {issue_type}")
CONSOLE.rule(f"[bold red]JINJA2 LINT {issue_type}")
for key, issues in lint_issues.items():
if not issues:
continue
print(f"************ File {key}")
tree = Tree(f"📄 {key}")

for j2_issue in issues:
print(f"{j2_issue.to_string(verbose)}")
tree.add(j2_issue.to_rich(verbose))
CONSOLE.print(tree)

total_lint_errors = sum(len(issues) for _, issues in lint_errors.items())
total_lint_warnings = sum(len(issues) for _, issues in lint_warnings.items())
Expand All @@ -190,9 +195,10 @@ def print_issues(
print_issues(lint_warnings, "WARNINGS")

if not total_lint_errors and not total_lint_warnings:
print("\nLinting complete. No problems found.")
if verbose:
CONSOLE.print("Linting complete. No problems found!", style="green")
else:
print(
CONSOLE.print(
f"\nJinja2 linting finished with "
f"{total_lint_errors} error(s) and {total_lint_warnings} warning(s)"
)
Expand All @@ -206,6 +212,17 @@ def remove_temporary_file(stdin_filename: str) -> None:
os.unlink(stdin_filename)


def print_string_rules(collection: RulesCollection) -> None:
"""Print active rules as string"""
CONSOLE.rule("[bold red]Rules in the Collection")
CONSOLE.print(collection.to_rich())


def print_json_rules(collection: RulesCollection) -> None:
"""Print active rules as json"""
CONSOLE.print_json(collection.to_json())


def run(args: list[str] | None = None) -> int:
"""Runs jinja2 linter
Expand Down Expand Up @@ -258,14 +275,15 @@ def run(args: list[str] | None = None) -> int:

# List lint rules
if options.list:
rules = f"Jinja2 lint rules\n{collection}\n"
print(rules)
logger.debug(rules)
if options.json:
print_json_rules(collection)
else:
print_string_rules(collection)
return 0

# Version of j2lint
if options.version:
print(f"Jinja2-Linter Version {VERSION}")
CONSOLE.print(f"Jinja2-Linter Version [bold red]{VERSION}")
return 0

# Print help message
Expand Down
57 changes: 53 additions & 4 deletions j2lint/linter/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@
"""
from __future__ import annotations

import json
import os
import pathlib
from collections.abc import Iterable

from rich.console import Group
from rich.tree import Tree

from j2lint.logger import logger
from j2lint.utils import is_rule_disabled, load_plugins

from .error import LinterError
from .rule import Rule

DEFAULT_RULE_DIR = pathlib.Path(__file__).parent.parent / "rules"


class RulesCollection:
"""RulesCollection class which checks the linting rules against a file."""
Expand Down Expand Up @@ -83,16 +90,55 @@ def run(
errors.extend(rule.checkfulltext(file_dict, text))

for error in errors:
logger.error(error.to_string())
logger.error(error.to_rich())

for warning in warnings:
logger.warning(warning.to_string())
logger.warning(warning.to_rich())

return errors, warnings

def __repr__(self) -> str:
return "\n".join(
[repr(rule) for rule in sorted(self.rules, key=lambda x: x.id)]
res = []
current_origin = None
for rule in sorted(self.rules, key=lambda x: (x.origin, x.id)):
if rule.origin != current_origin:
current_origin = rule.origin
res.append(f"Origin: {rule.origin}")
res.append(repr(rule))

return "\n".join(res)

def to_rich(self) -> Group:
"""
Return a rich Group containing a rich Tree for each different origin
for the rules
Each Tree contain the rule.to_rich() output
📄 Origin: BUILT-IN
├── S0 Jinja syntax should be correct (jinja-syntax-error)
├── S1 <description> (single-space-decorator)
└── V2 <description> (jinja-variable-format)
"""
res = []
current_origin = None
tree = None
for rule in sorted(self.rules, key=lambda x: (x.origin, x.id)):
if rule.origin != current_origin:
current_origin = rule.origin
tree = Tree(f"📄 Origin: {rule.origin}")
res.append(tree)
assert tree
tree.add(rule.to_rich())
return Group(*res)

def to_json(self) -> str:
"""Return a json representation of the collection as a list of the rules"""
return json.dumps(
[
json.loads(rule.to_json())
for rule in sorted(self.rules, key=lambda x: (x.origin, x.id))
]
)

@classmethod
Expand All @@ -117,5 +163,8 @@ def create_from_directory(
rule.ignore = True
if rule.short_description in warn_rules or rule.id in warn_rules:
rule.warn.append(rule)
if rules_dir != DEFAULT_RULE_DIR:
for rule in result.rules:
rule.origin = rules_dir
logger.info("Created collection from rules directory %s", rules_dir)
return result
36 changes: 21 additions & 15 deletions j2lint/linter/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import json
from typing import TYPE_CHECKING

from rich.text import Text

from j2lint.logger import logger

if TYPE_CHECKING:
Expand All @@ -29,25 +31,29 @@ def __init__(
self.rule = rule
self.message = message or rule.description

def to_string(self, verbose: bool = False) -> str:
def to_rich(self, verbose: bool = False) -> Text:
"""setting string output format"""
text = Text()
if not verbose:
format_str = "{2}:{3} {5} ({6})"
text.append(self.filename, "green")
text.append(":")
text.append(str(self.line_number), "red")
text.append(f" {self.message}")
text.append(f" ({self.rule.short_description})", "blue")
else:
logger.debug("Verbose mode enabled")
format_str = (
"Linting rule: {0}\nRule description: "
"{1}\nError line: {2}:{3} {4}\nError message: {5}\n"
)
return format_str.format(
self.rule.id,
self.rule.description,
self.filename,
self.line_number,
self.line,
self.message,
self.rule.short_description,
)
text.append("Linting rule: ")
text.append(f"{self.rule.id}\n", "blue")
text.append("Rule description: ")
text.append(f"{self.rule.description}\n", "blue")
text.append("Error line: ")
text.append(self.filename, "green")
text.append(":")
text.append(str(self.line_number), "red")
text.append(f" {self.line}\n")
text.append("Error message: ")
text.append(f"{self.message}\n")
return text

def to_json(self) -> str:
"""setting json output format"""
Expand Down
29 changes: 29 additions & 0 deletions j2lint/linter/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
"""
from __future__ import annotations

import json
from typing import Any

from rich.text import Text

from j2lint.linter.error import LinterError
from j2lint.logger import logger
from j2lint.utils import is_valid_file_type
Expand All @@ -21,10 +24,36 @@ class Rule:
severity: str | None = None
ignore: bool = False
warn: list[Any] = []
origin: str = "BUILT-IN"

def __repr__(self) -> str:
return f"{self.id}: {self.description}"

def to_rich(self) -> Text:
"""
Return a rich reprsentation of the rule, e.g.:
S0 Jinja syntax should be correct (jinja-syntax-error)
Where `S0` is in red and `(jinja-syntax-error)` in blue
"""
res = Text()
res.append(f"{self.id} ", "red")
res.append(self.description)
res.append(f" ({self.short_description})", "blue")
return res

def to_json(self) -> str:
"""Return a json representation of the rule"""
return json.dumps(
{
"id": self.id,
"short_description": self.short_description,
"description": self.description,
"severity": self.severity,
"origin": self.origin,
}
)

def checktext(self, file: dict[str, Any], text: str) -> list[Any]:
"""This method is expected to be overriden by child classes"""
# pylint: disable=unused-argument
Expand Down
5 changes: 3 additions & 2 deletions j2lint/logger.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""logger.py - Creates logger object.
"""
import logging
import sys
from logging import handlers

from rich.logging import RichHandler

JINJA2_LOG_FILE = "jinja2-linter.log"

logger = logging.getLogger("")
Expand All @@ -22,6 +23,6 @@ def add_handler(log: logging.Logger, stream_handler: bool, log_level: int) -> No
file_handler.setFormatter(log_format)
log.addHandler(file_handler)
else:
console_handler = logging.StreamHandler(sys.stdout)
console_handler = RichHandler()
console_handler.setFormatter(log_format)
log.addHandler(console_handler)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ classifiers = [
keywords = ["j2lint", "linter", "jinja", "lint"]
dependencies = [
"jinja2>=3.0",
"rich>=12.4.4",
]
requires-python = ">=3.8"

Expand Down
Loading

0 comments on commit 450353e

Please sign in to comment.