Skip to content
This repository has been archived by the owner on Nov 24, 2023. It is now read-only.

Commit

Permalink
Add support for CLI arguments (#2)
Browse files Browse the repository at this point in the history
* Add support for CLI arguments
  • Loading branch information
vis4rd committed Nov 24, 2023
1 parent 346e6f3 commit 248daa0
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 24 deletions.
1 change: 1 addition & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
"ms-python.pylint",
"ms-python.python",
"ms-python.vscode-pylance",
"yzhang.markdown-all-in-one"
]
}
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true,
},
"[markdown]": {
"editor.defaultFormatter": "yzhang.markdown-all-in-one",
"editor.formatOnSave": true,
"editor.wordWrap": "off",
},
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
Expand Down
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,34 @@ Generate [GitLab Code Quality report](https://docs.gitlab.com/ee/ci/testing/code
## Usage

```bash
$ pydocstyle <file_path> | pydocstyle-gitlab-code-quality
# passing pydocstyle output through stdin (output printed to stdout)
pydocstyle main.py | pydocstyle-gitlab-code-quality > codequality.json
# or
pydocstyle-gitlab-code-quality < pydocstyle_out.txt > codequality.json

# using CLI flags (output printed directly to a file)
pydocstyle-gitlab-code-quality --input pydocstyle_out.txt --output codequality.json
```

The output of this command is printed to `stdout` in JSON format, which can be used as Code Quality report.
## CLI configuration

`pydocstyle-gitlab-code-quality` allows for the following CLI arguments:

| flag | example | default | description |
| ---------------------------- | ----------------------- | ----------------- | ------------------------------------------------------------- |
| `--minor <CODE>...` | `--minor=D100,D101` | *empty* | Error codes to be displayed with MINOR severity. |
| `--major <CODE>...` | `--major=D102,D103` | *empty* | Error codes to be displayed with MAJOR severity. |
| `--critical <CODE>...` | `--critical=D104,D105` | *empty* | Error codes to be displayed with CRITICAL severity. |
| `-i, --ignore <CODE>...` | `--ignore=D106,D107` | *empty* | Error codes to be omitted from Code Quality report. |
| `-f, --file, --input <FILE>` | `-f pydocstyle_out.txt` | *empty* | Path to the file with pydocstyle output. |
| `-o, --output <FILE>` | `-o codequality.json` | *empty* | Path to the file where the Code Quality report will be saved. |
| `--no-stdout` | N/A | `False` | Do not print the Code Quality report to stdout. |
| `--log-file <FILE>` | `--log-file latest.log` | `pgcq_latest.log` | Path to the file where the log will be saved. |
| `--enable-logging` | N/A | `False` | Enable logging to a file. For debugging purposes only. |

By default, all error codes are reported with INFO severity.

In case the same error code from `pydocstyle` has been provided to many severity options, the highest severity level takes precedence.

### Example `.gitlab-ci.yml` file

Expand All @@ -37,7 +61,7 @@ codequality:
script:
- pip install pydocstyle pydocstyle-gitlab-code-quality
- pydocstyle program.py > pydocstyle-out.txt
- pydocstyle-gitlab-code-quality < pydocstyle-out.txt > codequality.json
- pydocstyle-gitlab-code-quality --input pydocstyle-out.txt --output codequality.json
artifacts:
when: always
reports:
Expand Down
55 changes: 36 additions & 19 deletions pydocstyle_gitlab_code_quality/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import json
import logging as log
import re
from hashlib import md5
from sys import stdin
from typing import Generator, TextIO
from typing import Dict, Generator, List, TextIO

from .src.config.cli_parser import CliParser
from .src.config.config import Config
from .src.cq_types import Issue, LinesStructure, LocationStructure
from .src.encoder import DataclassJSONEncoder
from .src.utils.encoder import DataclassJSONEncoder
from .src.utils.logger import initialize_logging


def get_pydocstyle_output(output: TextIO) -> Generator[dict, None, None]:
Expand All @@ -22,6 +25,9 @@ def get_pydocstyle_output(output: TextIO) -> Generator[dict, None, None]:
except StopIteration:
return

if not brief_line or not details_line:
return

brief_line = brief_line.rstrip("\n")
details_line = details_line.rstrip("\n")

Expand All @@ -35,28 +41,39 @@ def get_pydocstyle_output(output: TextIO) -> Generator[dict, None, None]:

errors = match_brief.groupdict()
errors.update(match_details.groupdict())

yield errors


def get_code_quality_issues() -> Generator:
output = get_pydocstyle_output(stdin)
def get_code_quality_issues() -> Generator[Issue, None, None]:
log.info(f"Input sink = {Config.input_sink}")
output = get_pydocstyle_output(Config.input_sink)

severity_mapper: Dict[int, str] = {0: "info", 1: "minor", 2: "major", 3: "critical"}

for entry in output:
yield Issue(
type="issue",
check_name=entry["error_code"],
description=entry["details"],
categories=["Style"],
severity="info",
location=LocationStructure(
path=entry["path"],
lines=LinesStructure(begin=int(entry["line"])),
),
fingerprint=md5("".join(entry.values()).encode("utf-8")).hexdigest(),
)
severity_index: int = Config.code_severities[entry["error_code"]]
if severity_index >= 0:
yield Issue(
type="issue",
check_name=entry["error_code"],
description=entry["details"],
categories=["Style"],
severity=severity_mapper[severity_index],
location=LocationStructure(
path=entry["path"],
lines=LinesStructure(begin=int(entry["line"])),
),
fingerprint=md5("".join(entry.values()).encode("utf-8")).hexdigest(),
)


def main() -> None:
issues: list = list(get_code_quality_issues())
CliParser.initialize()
initialize_logging()

issues: List[Issue] = list(get_code_quality_issues())
json_output: str = json.dumps(issues, indent="\t", cls=DataclassJSONEncoder)
print(json_output)

for sink in Config.output_sinks:
sink.write(json_output)
Empty file.
113 changes: 113 additions & 0 deletions pydocstyle_gitlab_code_quality/src/config/cli_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import argparse
import logging as log

from .config import Config


class CliParser:
parser: argparse.ArgumentParser = argparse.ArgumentParser(
description="Generate GitLab Code Quality report from an output of pydocstyle.",
add_help=True,
allow_abbrev=False,
)

@classmethod
def initialize(cls) -> None:
cls._setup_arguments()
cls._update_config()

@classmethod
def _setup_arguments(cls) -> None:
cls.parser.add_argument(
"--minor",
help="Error codes to be displayed with MINOR severity.",
default=[],
type=lambda s: s.split(","),
)
cls.parser.add_argument(
"--major",
help="Error codes to be displayed with MAJOR severity.",
default=[],
type=lambda s: s.split(","),
)
cls.parser.add_argument(
"--critical",
help="Error codes to be displayed with CRITICAL severity.",
default=[],
type=lambda s: s.split(","),
)
cls.parser.add_argument(
"-i",
"--ignore",
help="Error codes to be omitted from Code Quality report.",
default="",
type=lambda s: s.split(","),
)
cls.parser.add_argument(
"-f",
"--file",
"--input",
help="Path to the file with pydocstyle output.",
default="",
type=str,
)
cls.parser.add_argument(
"-o",
"--output",
help="Path to the file where the Code Quality report will be saved.",
default="",
type=str,
)
cls.parser.add_argument(
"--no-stdout",
help="Do not print the Code Quality report to stdout.",
action="store_true",
default=False,
)
cls.parser.add_argument(
"--log-file",
help="Path to the file where the log will be saved.",
default=Config.log_file,
type=str,
)
cls.parser.add_argument(
"--enable-logging",
help="Enable logging to a file.",
action="store_true",
default=False,
)

@classmethod
def _update_config(cls) -> None:
args = vars(cls.parser.parse_args())

if args.get("no_stdout", False):
Config.output_sinks.pop(0)

Config.enable_logging = args.get("enable_logging", False)
Config.log_file = args.get("log_file", Config.log_file)

if filepath := args.get("output", ""):
file = open(filepath, "w", encoding="utf-8") # pylint: disable=consider-using-with
Config.output_sinks.append(file)

if filepath := args.get("file", ""):
file = open(filepath, "r", encoding="utf-8") # pylint: disable=consider-using-with
Config.input_sink = file

if codes := args.get("minor", []):
for code in codes:
Config.code_severities[code] = 1

if codes := args.get("major", []):
for code in codes:
Config.code_severities[code] = 2

if codes := args.get("critical", []):
log.info(codes)
for code in codes:
Config.code_severities[code] = 3

if codes := args.get("ignore", []):
for code in codes:
Config.code_severities[code] = -1
18 changes: 18 additions & 0 deletions pydocstyle_gitlab_code_quality/src/config/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import logging
from sys import stdin, stdout
from typing import Dict, List, TextIO


class Config:
enable_logging: bool = False
log_file: str = "pgcq_latest.log"
log_level: int = logging.INFO

code_severities: Dict[str, int] = {}
code_severities.update({f"D1{i:02d}": 0 for i in range(0, 8)})
code_severities.update({f"D2{i:02d}": 0 for i in range(0, 16)})
code_severities.update({f"D3{i:02d}": 0 for i in range(0, 3)})
code_severities.update({f"D4{i:02d}": 0 for i in range(0, 20)})

input_sink: TextIO = stdin
output_sinks: List[TextIO] = [stdout]
Empty file.
20 changes: 20 additions & 0 deletions pydocstyle_gitlab_code_quality/src/utils/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import logging

from ..config.config import Config

# from sys import stdout


def initialize_logging() -> None:
root = logging.getLogger()
root.setLevel(Config.log_level)
root.disabled = not Config.enable_logging

# stdout_handler = logging.StreamHandler(stdout)
# stdout_handler.setLevel(Config.log_level)

if Config.enable_logging:
file_handler = logging.FileHandler(Config.log_file, mode="w", encoding="utf-8")
file_handler.setLevel(Config.log_level)

root.addHandler(file_handler)
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pydocstyle-gitlab-code-quality"
version = "0.0.3"
version = "0.1.0"
authors = [
{ name = "Aleksander Kluczka", email = "aleksander.kluczka@gmail.com" },
]
Expand Down Expand Up @@ -54,6 +54,7 @@ disable = [
"C0116", # missing method docstring
"R0902", # too many instance attributes
"R0903", # too few public methods
"W0102" # dangerous default value of an argument
"W0102", # dangerous default value of an argument
"W1203" # use lazy % formatting in logging functions
]
ignore = ["__init__.py"]

0 comments on commit 248daa0

Please sign in to comment.