github-code-review 3.3.1__tar.gz → 3.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {github_code_review-3.3.1 → github_code_review-3.3.2}/PKG-INFO +2 -1
  2. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/cli.py +4 -3
  3. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/core.py +23 -7
  4. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/gh_api.py +3 -3
  5. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/report_struct.py +45 -29
  6. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/utils.py +4 -1
  7. {github_code_review-3.3.1 → github_code_review-3.3.2}/pyproject.toml +2 -1
  8. {github_code_review-3.3.1 → github_code_review-3.3.2}/LICENSE +0 -0
  9. {github_code_review-3.3.1 → github_code_review-3.3.2}/README.md +0 -0
  10. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/__init__.py +0 -0
  11. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/__main__.py +0 -0
  12. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/bootstrap.py +0 -0
  13. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/cli_base.py +0 -0
  14. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/__init__.py +0 -0
  15. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/deploy.py +0 -0
  16. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/fix.py +0 -0
  17. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/gh_post_review_comment.py +0 -0
  18. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/gh_react_to_comment.py +0 -0
  19. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/linear_comment.py +0 -0
  20. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/repl.py +0 -0
  21. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/commands/version.py +0 -0
  22. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/config.toml +0 -0
  23. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/constants.py +0 -0
  24. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/context.py +0 -0
  25. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/env.py +0 -0
  26. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/issue_trackers.py +0 -0
  27. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/pipeline.py +0 -0
  28. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/pipeline_steps/__init__.py +0 -0
  29. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/pipeline_steps/jira.py +0 -0
  30. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/pipeline_steps/linear.py +0 -0
  31. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/project_config.py +0 -0
  32. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/answer.j2 +0 -0
  33. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/github_workflows/components/env-vars.j2 +0 -0
  34. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/github_workflows/components/installs.j2 +0 -0
  35. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/github_workflows/gito-code-review.yml.j2 +0 -0
  36. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/github_workflows/gito-react-to-comments.yml.j2 +0 -0
  37. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/partial/aux_files.j2 +0 -0
  38. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/questions/changes_summary.j2 +0 -0
  39. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/questions/release_notes.j2 +0 -0
  40. {github_code_review-3.3.1 → github_code_review-3.3.2}/gito/tpl/questions/test_cases.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: github-code-review
3
- Version: 3.3.1
3
+ Version: 3.3.2
4
4
  Summary: AI code review tool that works with any language model provider. It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in.
5
5
  License: MIT
6
6
  Keywords: static code analysis,code review,code quality,ai,coding,assistant,llm,github,automation,devops,developer tools,github actions,workflows,git
@@ -21,6 +21,7 @@ Requires-Dist: anthropic (>=0.57.1,<0.58.0)
21
21
  Requires-Dist: ghapi (>=1.0.6,<1.1.0)
22
22
  Requires-Dist: google-generativeai (>=0.8.5,<0.9.0)
23
23
  Requires-Dist: jira (>=3.8.0,<4.0.0)
24
+ Requires-Dist: pydantic (>=2.12.3,<3.0.0)
24
25
  Requires-Dist: typer (>=0.16.0,<0.17.0)
25
26
  Requires-Dist: unidiff (>=0.7.5,<0.8.0)
26
27
  Project-URL: Homepage, https://github.com/Nayjest/Gito
@@ -202,8 +202,8 @@ def setup():
202
202
  mc.interactive_setup(HOME_ENV_PATH)
203
203
 
204
204
 
205
- @app.command(name="render", help="Render and display code review report.")
206
- @app.command(name="report", hidden=True)
205
+ @app.command(name="report", help="Render and display code review report.")
206
+ @app.command(name="render", hidden=True)
207
207
  def render(
208
208
  format: str = typer.Argument(default=Report.Format.CLI),
209
209
  source: str = typer.Option(
@@ -238,7 +238,8 @@ def files(
238
238
  f"Changed files: "
239
239
  f"{mc.ui.green(_what or 'INDEX')} vs "
240
240
  f"{mc.ui.yellow(_against or repo.remotes.origin.refs.HEAD.reference.name)}"
241
- f"{' filtered by ' + mc.ui.cyan(filters) if filters else ''}"
241
+ f"{' filtered by ' + mc.ui.cyan(filters) if filters else ''} --> "
242
+ f"{mc.ui.cyan(len(patch_set or []))} file(s)."
242
243
  )
243
244
 
244
245
  for patch in patch_set:
@@ -1,3 +1,6 @@
1
+ """
2
+ Gito core business logic.
3
+ """
1
4
  import os
2
5
  import fnmatch
3
6
  import logging
@@ -6,8 +9,6 @@ from pathlib import Path
6
9
  from functools import partial
7
10
 
8
11
  import microcore as mc
9
- from gito.constants import REFS_VALUE_ALL
10
- from gito.gh_api import gh_api
11
12
  from microcore import ui
12
13
  from git import Repo, Commit
13
14
  from git.exc import GitCommandError
@@ -16,11 +17,12 @@ from unidiff.constants import DEV_NULL
16
17
 
17
18
  from .context import Context
18
19
  from .project_config import ProjectConfig
19
- from .report_struct import Report
20
- from .constants import JSON_REPORT_FILE_NAME
20
+ from .report_struct import Report, RawIssue
21
+ from .constants import JSON_REPORT_FILE_NAME, REFS_VALUE_ALL
21
22
  from .utils import make_streaming_function
22
23
  from .pipeline import Pipeline
23
24
  from .env import Env
25
+ from .gh_api import gh_api
24
26
 
25
27
 
26
28
  def review_subject_is_index(what):
@@ -384,6 +386,19 @@ def provide_affected_code_blocks(issues: dict, repo: Repo):
384
386
  i["affected_code"] = block
385
387
 
386
388
 
389
+ def _llm_response_validator(parsed_response: list[dict]):
390
+ """
391
+ Validate that the LLM response is a list of dicts that can be converted to RawIssue.
392
+ """
393
+ if not isinstance(parsed_response, list):
394
+ raise ValueError("Response is not a list")
395
+ for item in parsed_response:
396
+ if not isinstance(item, dict):
397
+ raise ValueError("Response item is not a dict")
398
+ RawIssue(**item)
399
+ return True
400
+
401
+
387
402
  async def review(
388
403
  repo: Repo = None,
389
404
  what: str = None,
@@ -420,14 +435,15 @@ async def review(
420
435
  for file_diff in diff
421
436
  ],
422
437
  retries=cfg.retries,
423
- parse_json=True,
438
+ parse_json={"validator": _llm_response_validator},
424
439
  )
425
440
  issues = {file.path: issues for file, issues in zip(diff, responses) if issues}
426
441
  provide_affected_code_blocks(issues, repo)
427
442
  exec(cfg.post_process, {"mc": mc, **locals()})
428
443
  out_folder = Path(out_folder or repo.working_tree_dir)
429
444
  out_folder.mkdir(parents=True, exist_ok=True)
430
- report = Report(issues=issues, number_of_processed_files=len(diff))
445
+ report = Report(number_of_processed_files=len(diff))
446
+ report.register_issues(issues)
431
447
  ctx = Context(
432
448
  report=report,
433
449
  config=cfg,
@@ -496,7 +512,7 @@ def answer(
496
512
  config.max_code_tokens // 2
497
513
  )
498
514
  else:
499
- aux_files_dict = dict()
515
+ aux_files_dict = {}
500
516
 
501
517
  if not prompt_file and config.answer_prompt.startswith("tpl:"):
502
518
  prompt_file = str(config.answer_prompt)[4:]
@@ -67,9 +67,9 @@ def post_gh_comment(
67
67
  if 200 <= resp.status_code < 300:
68
68
  logging.info(f"Posted review comment to #{pr_or_issue_number} in {gh_repository}")
69
69
  return True
70
- else:
71
- logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
72
- return False
70
+
71
+ logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
72
+ return False
73
73
 
74
74
 
75
75
  def hide_gh_comment(
@@ -1,14 +1,15 @@
1
1
  import json
2
2
  import logging
3
- from dataclasses import dataclass, field, asdict
3
+ from dataclasses import field, asdict, is_dataclass
4
4
  from datetime import datetime
5
5
  from enum import StrEnum
6
6
  from pathlib import Path
7
7
 
8
+ import textwrap
8
9
  import microcore as mc
9
- from colorama import Fore, Style, Back
10
10
  from microcore.utils import file_link
11
- import textwrap
11
+ from colorama import Fore, Style, Back
12
+ from pydantic.dataclasses import dataclass
12
13
 
13
14
  from .constants import JSON_REPORT_FILE_NAME, HTML_TEXT_ICON, HTML_CR_COMMENT_MARKER
14
15
  from .project_config import ProjectConfig
@@ -16,33 +17,47 @@ from .utils import syntax_hint, block_wrap_lr, max_line_len, remove_html_comment
16
17
 
17
18
 
18
19
  @dataclass
19
- class Issue:
20
+ class RawIssue:
20
21
  @dataclass
21
22
  class AffectedCode:
22
23
  start_line: int = field()
23
24
  end_line: int | None = field(default=None)
25
+ proposal: str | None = field(default="")
26
+
27
+ title: str = field()
28
+ details: str | None = field(default="")
29
+ severity: int | None = field(default=None)
30
+ confidence: int | None = field(default=None)
31
+ tags: list[str] = field(default_factory=list)
32
+ affected_lines: list[AffectedCode] = field(default_factory=list)
33
+
34
+
35
+ @dataclass
36
+ class Issue(RawIssue):
37
+ @dataclass
38
+ class AffectedCode(RawIssue.AffectedCode):
24
39
  file: str = field(default="")
25
- proposal: str = field(default="")
26
40
  affected_code: str = field(default="")
27
41
 
28
42
  @property
29
43
  def syntax_hint(self) -> str:
30
44
  return syntax_hint(self.file)
31
45
 
32
- id: str = field()
33
- title: str = field()
34
- details: str = field(default="")
35
- severity: int | None = field(default=None)
36
- confidence: int | None = field(default=None)
37
- tags: list[str] = field(default_factory=list)
46
+ id: int | str = field(kw_only=True)
38
47
  file: str = field(default="")
39
48
  affected_lines: list[AffectedCode] = field(default_factory=list)
40
49
 
41
- def __post_init__(self):
42
- self.affected_lines = [
43
- Issue.AffectedCode(**filter_kwargs(Issue.AffectedCode, dict(file=self.file) | i))
44
- for i in self.affected_lines
45
- ]
50
+ @staticmethod
51
+ def from_raw_issue(file: str, raw_issue: RawIssue | dict, issue_id: int | str) -> "Issue":
52
+ if is_dataclass(raw_issue):
53
+ raw_issue = asdict(raw_issue)
54
+ params = filter_kwargs(Issue, raw_issue | {"file": file, "id": issue_id})
55
+ for i, obj in enumerate(params["affected_lines"]):
56
+ d = obj if isinstance(obj, dict) else asdict(obj)
57
+ params["affected_lines"][i] = Issue.AffectedCode(
58
+ **filter_kwargs(Issue.AffectedCode, {"file": file} | d)
59
+ )
60
+ return Issue(**params)
46
61
 
47
62
  def github_code_link(self, github_env: dict) -> str:
48
63
  url = (
@@ -63,7 +78,7 @@ class Report:
63
78
  MARKDOWN = "md"
64
79
  CLI = "cli"
65
80
 
66
- issues: dict[str, list[Issue | dict]] = field(default_factory=dict)
81
+ issues: dict[str, list[Issue]] = field(default_factory=dict)
67
82
  summary: str = field(default="")
68
83
  number_of_processed_files: int = field(default=0)
69
84
  total_issues: int = field(init=False)
@@ -79,19 +94,20 @@ class Report:
79
94
  for issue in issues
80
95
  ]
81
96
 
97
+ def register_issues(self, issues: dict[str, list[RawIssue | dict]]):
98
+ for file, file_issues in issues.items():
99
+ for issue in file_issues:
100
+ self.register_issue(file, issue)
101
+
102
+ def register_issue(self, file: str, issue: RawIssue | dict):
103
+ if file not in self.issues:
104
+ self.issues[file] = []
105
+ total = len(self.plain_issues)
106
+ self.issues[file].append(Issue.from_raw_issue(file, issue, issue_id=total + 1))
107
+ self.total_issues = total + 1
108
+
82
109
  def __post_init__(self):
83
- issue_id: int = 0
84
- for file in self.issues.keys():
85
- self.issues[file] = [
86
- Issue(
87
- **filter_kwargs(Issue, {
88
- "id": (issue_id := issue_id + 1),
89
- "file": file,
90
- } | issue)
91
- )
92
- for issue in self.issues[file]
93
- ]
94
- self.total_issues = issue_id
110
+ self.total_issues = len(self.plain_issues)
95
111
 
96
112
  def save(self, file_name: str = ""):
97
113
  file_name = file_name or JSON_REPORT_FILE_NAME
@@ -2,7 +2,7 @@ import logging
2
2
  import re
3
3
  import sys
4
4
  import os
5
- from dataclasses import fields
5
+ from dataclasses import fields, is_dataclass
6
6
  from pathlib import Path
7
7
  import importlib.metadata
8
8
  from typing import Optional
@@ -254,6 +254,9 @@ def filter_kwargs(cls, kwargs, log_warnings=True):
254
254
  Returns:
255
255
  A dictionary containing only the fields that are defined in the dataclass.
256
256
  """
257
+ if not is_dataclass(cls):
258
+ raise TypeError(f"{cls.__name__} is not a dataclass or pydantic dataclass")
259
+
257
260
  cls_fields = {f.name for f in fields(cls)}
258
261
  filtered = {}
259
262
  for k, v in kwargs.items():
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "github-code-review"
3
- version = "3.3.1"
3
+ version = "3.3.2"
4
4
  description = "AI code review tool that works with any language model provider. It detects issues in GitHub pull requests or local changes—instantly, reliably, and without vendor lock-in."
5
5
  authors = ["Nayjest <mail@vitaliy.in>"]
6
6
  readme = "README.md"
@@ -29,6 +29,7 @@ anthropic = "^0.57.1"
29
29
  typer = "^0.16.0"
30
30
  ghapi = "~=1.0.6"
31
31
  jira = "^3.8.0"
32
+ pydantic = "^2.12.3"
32
33
 
33
34
  [tool.poetry.group.dev.dependencies]
34
35
  flake8 = "*"