pycodegate 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

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.
pycodegate/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Python Doctor - Diagnose your Python project's health."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.1.3"
pycodegate/cli.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import sys
6
+ from dataclasses import dataclass
6
7
 
7
8
  import click
8
9
 
@@ -16,8 +17,96 @@ from pycodegate.utils.fixer import run_ruff_fix
16
17
  from pycodegate.utils.precommit import install_precommit_hook
17
18
 
18
19
 
20
+ @dataclass
21
+ class CliOptions:
22
+ """Bundle of all CLI flags passed to main()."""
23
+
24
+ directory: str
25
+ lint: bool
26
+ dead_code: bool
27
+ verbose: bool
28
+ score_only: bool
29
+ json_output: bool
30
+ diff_base: str | None
31
+ fail_on: str
32
+ fix: bool
33
+ profile_override: str | None
34
+ badge: bool
35
+ ci_workflow: bool
36
+ precommit: bool
37
+ min_score: int | None
38
+
39
+
40
+ def _handle_early_exits(opts: CliOptions) -> bool:
41
+ """Handle flags that exit before scanning. Returns True if handled."""
42
+ if opts.ci_workflow:
43
+ click.echo(generate_ci_workflow(), nl=False)
44
+ return True
45
+
46
+ if opts.precommit:
47
+ effective_min = opts.min_score if opts.min_score is not None else 50
48
+ msg = install_precommit_hook(opts.directory, min_score=effective_min)
49
+ click.echo(msg)
50
+ return True
51
+
52
+ return False
53
+
54
+
55
+ def _maybe_run_fix(opts: CliOptions) -> None:
56
+ """Apply ruff auto-fix if --fix was passed."""
57
+ if not opts.fix:
58
+ return
59
+ fixes = run_ruff_fix(opts.directory)
60
+ if fixes == -1:
61
+ click.echo("ruff not found, skipping auto-fix")
62
+ else:
63
+ click.echo(f"Fixed {fixes} issues via ruff")
64
+
65
+
66
+ def _build_config(opts: CliOptions):
67
+ """Load config and apply CLI overrides."""
68
+ config = load_config(opts.directory)
69
+ config.lint = opts.lint
70
+ config.dead_code = opts.dead_code
71
+ config.verbose = opts.verbose
72
+ config.fail_on = opts.fail_on
73
+ if opts.profile_override is not None:
74
+ config.profile = opts.profile_override
75
+ return config
76
+
77
+
78
+ def _output_result(opts: CliOptions, result) -> None:
79
+ """Write scan result to stdout in the requested format."""
80
+ if opts.badge:
81
+ click.echo(generate_badge(result.score.value, result.score.label))
82
+ return
83
+
84
+ if opts.json_output:
85
+ output_json(result)
86
+ elif opts.score_only:
87
+ click.echo(str(result.score.value))
88
+ else:
89
+ print_scan_result(result, verbose=opts.verbose)
90
+
91
+
92
+ def _apply_exit_codes(opts: CliOptions, result) -> None:
93
+ """Exit with non-zero code based on --fail-on and --min-score."""
94
+ has_errors = any(d.severity == Severity.ERROR for d in result.diagnostics)
95
+ if (opts.fail_on == "error" and has_errors) or (
96
+ opts.fail_on == "warning" and result.diagnostics
97
+ ):
98
+ sys.exit(1)
99
+
100
+ if opts.min_score is not None and result.score.value < opts.min_score:
101
+ click.echo(
102
+ f"pycodegate: score {result.score.value} is below minimum {opts.min_score}",
103
+ err=True,
104
+ )
105
+ sys.exit(1)
106
+
107
+
19
108
  @click.command(context_settings={"help_option_names": ["-h", "--help"]})
20
- @click.version_option(__version__, "-v", "--version", prog_name="Py Gate")
109
+ @click.version_option(__version__, "-v", "--version", prog_name="PyCodeGate")
21
110
  @click.argument("directory", default=".", type=click.Path(exists=True))
22
111
  @click.option("--lint/--no-lint", default=True, help="Enable/disable lint checks.")
23
112
  @click.option(
@@ -79,53 +168,31 @@ def main(
79
168
  precommit: bool,
80
169
  min_score: int | None,
81
170
  ) -> None:
82
- """Py Gate — Diagnose your Python project's health."""
83
- if ci_workflow:
84
- click.echo(generate_ci_workflow(), nl=False)
85
- return
171
+ """PyCodeGate — Diagnose your Python project's health."""
172
+ opts = CliOptions(
173
+ directory=directory,
174
+ lint=lint,
175
+ dead_code=dead_code,
176
+ verbose=verbose,
177
+ score_only=score_only,
178
+ json_output=json_output,
179
+ diff_base=diff_base,
180
+ fail_on=fail_on,
181
+ fix=fix,
182
+ profile_override=profile_override,
183
+ badge=badge,
184
+ ci_workflow=ci_workflow,
185
+ precommit=precommit,
186
+ min_score=min_score,
187
+ )
86
188
 
87
- if precommit:
88
- effective_min = min_score if min_score is not None else 50
89
- msg = install_precommit_hook(directory, min_score=effective_min)
90
- click.echo(msg)
189
+ if _handle_early_exits(opts):
91
190
  return
92
191
 
93
- if fix:
94
- fixes = run_ruff_fix(directory)
95
- if fixes == -1:
96
- click.echo("ruff not found, skipping auto-fix")
97
- else:
98
- click.echo(f"Fixed {fixes} issues via ruff")
99
-
100
- config = load_config(directory)
101
- config.lint = lint
102
- config.dead_code = dead_code
103
- config.verbose = verbose
104
- config.fail_on = fail_on
105
- if profile_override is not None:
106
- config.profile = profile_override
192
+ _maybe_run_fix(opts)
107
193
 
194
+ config = _build_config(opts)
108
195
  result = scan_project(directory, config, diff_base=diff_base)
109
196
 
110
- if badge:
111
- click.echo(generate_badge(result.score.value, result.score.label))
112
- return
113
-
114
- if json_output:
115
- output_json(result)
116
- elif score_only:
117
- click.echo(str(result.score.value))
118
- else:
119
- print_scan_result(result, verbose=verbose)
120
-
121
- # Exit code based on --fail-on
122
- has_errors = any(d.severity == Severity.ERROR for d in result.diagnostics)
123
- if (fail_on == "error" and has_errors) or (fail_on == "warning" and result.diagnostics):
124
- sys.exit(1)
125
-
126
- if min_score is not None and result.score.value < min_score:
127
- click.echo(
128
- f"py-gate: score {result.score.value} is below minimum {min_score}",
129
- err=True,
130
- )
131
- sys.exit(1)
197
+ _output_result(opts, result)
198
+ _apply_exit_codes(opts, result)
pycodegate/config.py CHANGED
@@ -1,4 +1,4 @@
1
- """Configuration loading for py-gate."""
1
+ """Configuration loading for pycodegate."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -22,11 +22,11 @@ class Config:
22
22
 
23
23
 
24
24
  def load_config(project_path: str) -> Config:
25
- """Load config from py-gate.toml or pyproject.toml [tool.py-gate]."""
25
+ """Load config from pycodegate.toml or pyproject.toml [tool.pycodegate]."""
26
26
  root = Path(project_path)
27
27
 
28
- # py-gate.toml takes precedence
29
- doctor_toml = root / "py-gate.toml"
28
+ # pycodegate.toml takes precedence
29
+ doctor_toml = root / "pycodegate.toml"
30
30
  if doctor_toml.exists():
31
31
  return _parse_doctor_toml(doctor_toml)
32
32
 
@@ -62,7 +62,7 @@ def _parse_pyproject_toml(path: Path) -> Config:
62
62
  with open(path, "rb") as f:
63
63
  data = tomllib.load(f)
64
64
 
65
- section = data.get("tool", {}).get("py-gate", {})
65
+ section = data.get("tool", {}).get("pycodegate", {})
66
66
  if not section:
67
67
  return Config()
68
68
 
pycodegate/output.py CHANGED
@@ -138,7 +138,7 @@ def print_scan_result(result: ScanResult, verbose: bool = False) -> None:
138
138
  # Project info header
139
139
  p = result.project
140
140
  console.print()
141
- console.print(" [bold]Py Gate[/bold] — v0.1.0")
141
+ console.print(f" [bold]PyCodeGate[/bold] — v{__version__}")
142
142
  console.print()
143
143
  console.print(f" [dim]Path:[/dim] {p.path}")
144
144
  if p.python_version:
@@ -213,6 +213,47 @@ def output_json(result: ScanResult) -> None:
213
213
  sys.stdout.write(json.dumps(payload, indent=2) + "\n")
214
214
 
215
215
 
216
+ def _print_rule_details(
217
+ console: Console,
218
+ rule_diags: list[Diagnostic],
219
+ verbose: bool,
220
+ ) -> None:
221
+ """Print a single rule line and, in verbose mode, the file locations."""
222
+ rule = rule_diags[0].rule
223
+ count = len(rule_diags)
224
+ sev = rule_diags[0].severity
225
+ icon = "[red]✗[/red]" if sev == Severity.ERROR else "[yellow]⚠[/yellow]"
226
+ console.print(f" {icon} {rule} × {count}")
227
+ if not verbose:
228
+ return
229
+ for d in rule_diags[:10]:
230
+ console.print(f" [dim]{d.file_path}:{d.line}[/dim]")
231
+ if count > 10:
232
+ console.print(f" [dim]... and {count - 10} more[/dim]")
233
+
234
+
235
+ def _print_category_with_issues(
236
+ console: Console,
237
+ emoji: str,
238
+ name: str,
239
+ earned: int,
240
+ maximum: int,
241
+ cat_diags: list[Diagnostic],
242
+ verbose: bool,
243
+ ) -> None:
244
+ """Print a category header and its grouped rule findings."""
245
+ console.print(f" [bold]{emoji} {name}[/bold] [yellow]({earned}/{maximum})[/yellow]")
246
+ by_rule: dict[str, list[Diagnostic]] = defaultdict(list)
247
+ for d in cat_diags:
248
+ by_rule[d.rule].append(d)
249
+ sorted_rules = sorted(
250
+ by_rule.keys(),
251
+ key=lambda r: (0 if by_rule[r][0].severity == Severity.ERROR else 1, r),
252
+ )
253
+ for rule in sorted_rules:
254
+ _print_rule_details(console, by_rule[rule], verbose)
255
+
256
+
216
257
  def _print_category_groups(
217
258
  console: Console,
218
259
  diagnostics: list[Diagnostic],
@@ -233,38 +274,15 @@ def _print_category_groups(
233
274
  continue
234
275
  emoji, name = CATEGORY_DISPLAY[cat]
235
276
  earned, maximum = sub_scores.get(cat, (CATEGORY_WEIGHTS[cat], CATEGORY_WEIGHTS[cat]))
236
- # Recompute max from sub_scores (already normalised to 100-point scale)
237
277
  cat_diags = by_category.get(cat, [])
238
278
 
239
279
  if earned == maximum:
240
- # Perfect score for this category
241
280
  console.print(
242
281
  f" [bold]{emoji} {name}[/bold] "
243
282
  f"[green]({earned}/{maximum})[/green] [green]✓[/green]"
244
283
  )
245
284
  console.print(" [green]✓ All clear.[/green]")
246
285
  else:
247
- console.print(f" [bold]{emoji} {name}[/bold] [yellow]({earned}/{maximum})[/yellow]")
248
- # Group by rule within this category
249
- by_rule: dict[str, list[Diagnostic]] = defaultdict(list)
250
- for d in cat_diags:
251
- by_rule[d.rule].append(d)
252
-
253
- # Sort: errors first, then by rule name
254
- sorted_rules = sorted(
255
- by_rule.keys(),
256
- key=lambda r: (0 if by_rule[r][0].severity == Severity.ERROR else 1, r),
257
- )
258
- for rule in sorted_rules:
259
- rule_diags = by_rule[rule]
260
- count = len(rule_diags)
261
- sev = rule_diags[0].severity
262
- icon = "[red]✗[/red]" if sev == Severity.ERROR else "[yellow]⚠[/yellow]"
263
- console.print(f" {icon} {rule} × {count}")
264
- if verbose:
265
- for d in rule_diags[:10]:
266
- console.print(f" [dim]{d.file_path}:{d.line}[/dim]")
267
- if count > 10:
268
- console.print(f" [dim]... and {count - 10} more[/dim]")
286
+ _print_category_with_issues(console, emoji, name, earned, maximum, cat_diags, verbose)
269
287
 
270
288
  console.print()
pycodegate/profile.py CHANGED
@@ -32,6 +32,56 @@ PROFILES = {
32
32
  }
33
33
 
34
34
 
35
+ def _normalise_dep(dep: str) -> str:
36
+ """Strip version specifiers and extras from a dependency string."""
37
+ return dep.split("[")[0].split(">")[0].split("<")[0].split("=")[0].split("!")[0].strip().lower()
38
+
39
+
40
+ def _deps_from_pyproject(pyproject: Path) -> tuple[set[str], bool, bool]:
41
+ """Parse pyproject.toml and return (deps, has_build_system, has_scripts)."""
42
+ deps: set[str] = set()
43
+ has_build_system = False
44
+ has_scripts = False
45
+ try:
46
+ with open(pyproject, "rb") as f:
47
+ data = tomllib.load(f)
48
+ for dep in data.get("project", {}).get("dependencies", []):
49
+ deps.add(_normalise_dep(dep))
50
+ has_build_system = "build-system" in data
51
+ has_scripts = bool(data.get("project", {}).get("scripts"))
52
+ except (OSError, ValueError, KeyError):
53
+ pass
54
+ return deps, has_build_system, has_scripts
55
+
56
+
57
+ def _deps_from_requirements(path: Path) -> set[str]:
58
+ """Parse requirements files and return the set of normalised dep names."""
59
+ deps: set[str] = set()
60
+ for req_file in ["requirements.txt", "requirements-dev.txt"]:
61
+ req_path = path / req_file
62
+ if not req_path.exists():
63
+ continue
64
+ try:
65
+ for line in req_path.read_text().splitlines():
66
+ line = line.strip()
67
+ if line and not line.startswith("#") and not line.startswith("-"):
68
+ deps.add(_normalise_dep(line))
69
+ except (OSError, ValueError):
70
+ pass
71
+ return deps
72
+
73
+
74
+ def _classify_by_deps(deps: set[str], has_scripts: bool) -> str | None:
75
+ """Return a profile name based on dependencies, or None if undetermined."""
76
+ web_deps = {"flask", "django", "fastapi", "starlette", "tornado", "aiohttp", "sanic", "bottle"}
77
+ if deps & web_deps:
78
+ return "web"
79
+ cli_deps = {"click", "typer", "fire", "cement", "cliff", "argparse"}
80
+ if (deps & cli_deps) or has_scripts:
81
+ return "cli"
82
+ return None
83
+
84
+
35
85
  def detect_profile(project_path: str) -> Profile:
36
86
  """Auto-detect project profile from project structure and dependencies."""
37
87
  path = Path(project_path)
@@ -42,56 +92,15 @@ def detect_profile(project_path: str) -> Profile:
42
92
  has_scripts = False
43
93
 
44
94
  if pyproject.exists():
45
- try:
46
- with open(pyproject, "rb") as f:
47
- data = tomllib.load(f)
48
- # Collect dependency names
49
- for dep in data.get("project", {}).get("dependencies", []):
50
- deps.add(
51
- dep.split("[")[0]
52
- .split(">")[0]
53
- .split("<")[0]
54
- .split("=")[0]
55
- .split("!")[0]
56
- .strip()
57
- .lower()
58
- )
59
- has_build_system = "build-system" in data
60
- has_scripts = bool(data.get("project", {}).get("scripts"))
61
- except Exception:
62
- pass
95
+ pyproject_deps, has_build_system, has_scripts = _deps_from_pyproject(pyproject)
96
+ deps |= pyproject_deps
63
97
 
64
- # Also check requirements.txt
65
- for req_file in ["requirements.txt", "requirements-dev.txt"]:
66
- req_path = path / req_file
67
- if req_path.exists():
68
- try:
69
- for line in req_path.read_text().splitlines():
70
- line = line.strip()
71
- if line and not line.startswith("#") and not line.startswith("-"):
72
- deps.add(
73
- line.split("[")[0]
74
- .split(">")[0]
75
- .split("<")[0]
76
- .split("=")[0]
77
- .split("!")[0]
78
- .strip()
79
- .lower()
80
- )
81
- except Exception:
82
- pass
83
-
84
- # Web frameworks
85
- web_deps = {"flask", "django", "fastapi", "starlette", "tornado", "aiohttp", "sanic", "bottle"}
86
- if deps & web_deps:
87
- return PROFILES["web"]
98
+ deps |= _deps_from_requirements(path)
88
99
 
89
- # CLI tools
90
- cli_deps = {"click", "typer", "fire", "cement", "cliff", "argparse"}
91
- if (deps & cli_deps) or has_scripts:
92
- return PROFILES["cli"]
100
+ profile_name = _classify_by_deps(deps, has_scripts)
101
+ if profile_name is not None:
102
+ return PROFILES[profile_name]
93
103
 
94
- # Library
95
104
  if has_build_system and not has_scripts:
96
105
  return PROFILES["library"]
97
106
 
@@ -103,5 +112,4 @@ def detect_profile(project_path: str) -> Profile:
103
112
  if not has_init and len(py_files) <= 5:
104
113
  return PROFILES["script"]
105
114
 
106
- # Default to library
107
115
  return PROFILES["library"]
@@ -14,15 +14,8 @@ from pycodegate.types import Category, Diagnostic, Severity
14
14
  class DependencyRules:
15
15
  """Dependency vulnerability checks using pip-audit (optional)."""
16
16
 
17
- def check_project(self, project_path: str) -> list[Diagnostic]:
18
- if not shutil.which("pip-audit"):
19
- return []
20
-
21
- path = Path(project_path)
22
- req_file = self._find_or_export_requirements(path)
23
- if not req_file:
24
- return []
25
-
17
+ def _run_pip_audit(self, req_file: Path) -> dict | None:
18
+ """Run pip-audit against *req_file* and return parsed JSON, or None on failure."""
26
19
  try:
27
20
  result = subprocess.run(
28
21
  [
@@ -38,17 +31,14 @@ class DependencyRules:
38
31
  text=True,
39
32
  timeout=60,
40
33
  )
41
- # pip-audit exits non-zero when vulnerabilities found
42
34
  if not result.stdout.strip():
43
- return []
44
- data = json.loads(result.stdout)
35
+ return None
36
+ return json.loads(result.stdout)
45
37
  except (subprocess.TimeoutExpired, json.JSONDecodeError, OSError):
46
- return []
47
- finally:
48
- # Clean up temp file if we created one
49
- if req_file and str(req_file).startswith(tempfile.gettempdir()):
50
- req_file.unlink(missing_ok=True)
38
+ return None
51
39
 
40
+ def _build_vuln_diagnostics(self, data: dict) -> list[Diagnostic]:
41
+ """Convert pip-audit JSON output into Diagnostic objects."""
52
42
  diags: list[Diagnostic] = []
53
43
  for vuln in data.get("dependencies", []):
54
44
  pkg_name = vuln.get("name", "unknown")
@@ -71,6 +61,26 @@ class DependencyRules:
71
61
  )
72
62
  return diags
73
63
 
64
+ def check_project(self, project_path: str) -> list[Diagnostic]:
65
+ if not shutil.which("pip-audit"):
66
+ return []
67
+
68
+ path = Path(project_path)
69
+ req_file = self._find_or_export_requirements(path)
70
+ if not req_file:
71
+ return []
72
+
73
+ try:
74
+ data = self._run_pip_audit(req_file)
75
+ finally:
76
+ # Clean up temp file if we created one
77
+ if req_file and str(req_file).startswith(tempfile.gettempdir()):
78
+ req_file.unlink(missing_ok=True)
79
+
80
+ if data is None:
81
+ return []
82
+ return self._build_vuln_diagnostics(data)
83
+
74
84
  def _find_or_export_requirements(self, path: Path) -> Path | None:
75
85
  # Try existing requirements.txt
76
86
  for name in ["requirements.txt", "requirements.lock"]:
@@ -81,7 +91,7 @@ class DependencyRules:
81
91
  # Try uv export
82
92
  if shutil.which("uv") and (path / "pyproject.toml").exists():
83
93
  try:
84
- tmp = Path(tempfile.gettempdir()) / "py-gate-reqs.txt"
94
+ tmp = Path(tempfile.gettempdir()) / "pycodegate-reqs.txt"
85
95
  result = subprocess.run(
86
96
  [
87
97
  "uv",
@@ -17,6 +17,60 @@ class ImportsRules:
17
17
  """Detect circular imports across the project."""
18
18
  return self._check_circular_imports(project_path, source_files)
19
19
 
20
+ def _collect_imports_for_module(
21
+ self, filepath: str, root: str, graph: dict[str, set[str]], file_map: dict[str, str]
22
+ ) -> None:
23
+ """Parse *filepath* and populate *graph* and *file_map* in-place."""
24
+ try:
25
+ source = Path(filepath).read_text()
26
+ tree = ast.parse(source)
27
+ except (SyntaxError, OSError, UnicodeDecodeError):
28
+ return
29
+
30
+ module_name = self._file_to_module(filepath, root)
31
+ if not module_name:
32
+ return
33
+ file_map[module_name] = filepath
34
+
35
+ for node in ast.walk(tree):
36
+ if isinstance(node, ast.Import):
37
+ for alias in node.names:
38
+ graph[module_name].add(alias.name)
39
+ elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
40
+ graph[module_name].add(node.module)
41
+
42
+ def _detect_cycles(
43
+ self,
44
+ graph: dict[str, set[str]],
45
+ file_map: dict[str, str],
46
+ ) -> list[Diagnostic]:
47
+ """Return diagnostics for every direct A↔B cycle in *graph*."""
48
+ diags: list[Diagnostic] = []
49
+ seen_pairs: set[tuple[str, str]] = set()
50
+
51
+ for mod_a, imports in graph.items():
52
+ for mod_b in imports:
53
+ if mod_b not in graph or mod_a not in graph[mod_b]:
54
+ continue
55
+ pair = tuple(sorted([mod_a, mod_b]))
56
+ if pair in seen_pairs:
57
+ continue
58
+ seen_pairs.add(pair)
59
+ diags.append(
60
+ Diagnostic(
61
+ file_path=file_map.get(mod_a, mod_a),
62
+ rule="imports/circular",
63
+ severity=Severity.ERROR,
64
+ category=Category.IMPORTS,
65
+ message=f"Circular import between '{mod_a}' and '{mod_b}'",
66
+ help="Break the cycle by moving shared code to a third module",
67
+ line=1,
68
+ cost=3.0,
69
+ )
70
+ )
71
+
72
+ return diags
73
+
20
74
  def _check_circular_imports(
21
75
  self, project_path: str, source_files: list[str]
22
76
  ) -> list[Diagnostic]:
@@ -24,53 +78,12 @@ class ImportsRules:
24
78
  graph: dict[str, set[str]] = defaultdict(set)
25
79
  file_map: dict[str, str] = {} # module_name -> file_path
26
80
 
27
- root = Path(project_path).resolve()
81
+ root = str(Path(project_path).resolve())
28
82
 
29
83
  for filepath in source_files:
30
- try:
31
- source = Path(filepath).read_text()
32
- tree = ast.parse(source)
33
- except (SyntaxError, OSError, UnicodeDecodeError):
34
- continue
35
-
36
- module_name = self._file_to_module(filepath, str(root))
37
- if not module_name:
38
- continue
39
- file_map[module_name] = filepath
40
-
41
- for node in ast.walk(tree):
42
- if isinstance(node, ast.Import):
43
- for alias in node.names:
44
- graph[module_name].add(alias.name)
45
- elif isinstance(node, ast.ImportFrom):
46
- if node.module and node.level == 0:
47
- graph[module_name].add(node.module)
48
-
49
- # Detect direct A↔B cycles
50
- diags: list[Diagnostic] = []
51
- seen_pairs: set[tuple[str, str]] = set()
84
+ self._collect_imports_for_module(filepath, root, graph, file_map)
52
85
 
53
- for mod_a, imports in graph.items():
54
- for mod_b in imports:
55
- if mod_b in graph and mod_a in graph[mod_b]:
56
- pair = tuple(sorted([mod_a, mod_b]))
57
- if pair not in seen_pairs:
58
- seen_pairs.add(pair)
59
- file_a = file_map.get(mod_a, mod_a)
60
- diags.append(
61
- Diagnostic(
62
- file_path=file_a,
63
- rule="imports/circular",
64
- severity=Severity.ERROR,
65
- category=Category.IMPORTS,
66
- message=f"Circular import between '{mod_a}' and '{mod_b}'",
67
- help="Break the cycle by moving shared code to a third module",
68
- line=1,
69
- cost=3.0,
70
- )
71
- )
72
-
73
- return diags
86
+ return self._detect_cycles(graph, file_map)
74
87
 
75
88
  def _file_to_module(self, filepath: str, root: str) -> str | None:
76
89
  """Convert a file path to a dotted module name."""
@@ -49,6 +49,39 @@ class StructureRules:
49
49
  pass
50
50
  return diags
51
51
 
52
+ @staticmethod
53
+ def _count_lines(files: list[str]) -> int:
54
+ """Return total line count across *files*, ignoring unreadable files."""
55
+ return sum(len(Path(f).read_text().splitlines()) for f in files if Path(f).exists())
56
+
57
+ def _check_test_ratio(
58
+ self, path: Path, test_files: list[str], src_files: list[str]
59
+ ) -> list[Diagnostic]:
60
+ """Return a diagnostic if the test-to-source line ratio is too low."""
61
+ test_lines = self._count_lines(test_files)
62
+ src_lines = self._count_lines(src_files)
63
+ if src_lines == 0:
64
+ return []
65
+ ratio = test_lines / src_lines
66
+ if ratio < 0.1:
67
+ cost = 2.0
68
+ elif ratio < 0.3:
69
+ cost = 1.0
70
+ else:
71
+ return []
72
+ return [
73
+ Diagnostic(
74
+ file_path=str(path),
75
+ rule="structure/low-test-ratio",
76
+ severity=Severity.WARNING,
77
+ category=Category.STRUCTURE,
78
+ message=f"Test:source ratio is {ratio:.1f} (< 0.3)",
79
+ help="Add more tests to improve coverage",
80
+ line=0,
81
+ cost=cost,
82
+ )
83
+ ]
84
+
52
85
  def _check_tests(self, path: Path, source_files: list[str]) -> list[Diagnostic]:
53
86
  test_files = [
54
87
  f
@@ -71,43 +104,7 @@ class StructureRules:
71
104
  )
72
105
  ]
73
106
 
74
- # Test ratio check
75
- test_lines = sum(
76
- len(Path(f).read_text().splitlines()) for f in test_files if Path(f).exists()
77
- )
78
- src_lines = sum(
79
- len(Path(f).read_text().splitlines()) for f in src_files if Path(f).exists()
80
- )
81
-
82
- if src_lines > 0:
83
- ratio = test_lines / src_lines
84
- if ratio < 0.1:
85
- return [
86
- Diagnostic(
87
- file_path=str(path),
88
- rule="structure/low-test-ratio",
89
- severity=Severity.WARNING,
90
- category=Category.STRUCTURE,
91
- message=f"Test:source ratio is {ratio:.1f} (< 0.3)",
92
- help="Add more tests to improve coverage",
93
- line=0,
94
- cost=2.0,
95
- )
96
- ]
97
- elif ratio < 0.3:
98
- return [
99
- Diagnostic(
100
- file_path=str(path),
101
- rule="structure/low-test-ratio",
102
- severity=Severity.WARNING,
103
- category=Category.STRUCTURE,
104
- message=f"Test:source ratio is {ratio:.1f} (< 0.3)",
105
- help="Add more tests to improve coverage",
106
- line=0,
107
- cost=1.0,
108
- )
109
- ]
110
- return []
107
+ return self._check_test_ratio(path, test_files, src_files)
111
108
 
112
109
  def _check_readme(self, path: Path) -> list[Diagnostic]:
113
110
  for name in ["README.md", "README.rst", "README", "readme.md"]:
pycodegate/score.py CHANGED
@@ -15,16 +15,10 @@ from pycodegate.constants import (
15
15
  from pycodegate.types import Diagnostic, Score
16
16
 
17
17
 
18
- def calculate_score(
19
- diagnostics: list[Diagnostic], max_deduction_overrides: dict | None = None
20
- ) -> Score:
21
- """Calculate a 0-100 health score from diagnostics using category budgets.
22
-
23
- Each category has a weight that determines its maximum deduction budget.
24
- Within a category, the top 3 findings are counted at full cost; additional
25
- findings apply diminishing returns (10% each) to reward fixing top issues.
26
- """
27
- # 1. Compute max_deduction per category from weights
18
+ def _build_budget(
19
+ max_deduction_overrides: dict | None,
20
+ ) -> dict:
21
+ """Return per-category maximum deduction budget normalised to sum to 100."""
28
22
  total_weight = sum(CATEGORY_WEIGHTS.values())
29
23
  max_deductions = {cat: round(w / total_weight * 100) for cat, w in CATEGORY_WEIGHTS.items()}
30
24
 
@@ -34,19 +28,43 @@ def calculate_score(
34
28
  highest = max(CATEGORY_WEIGHTS, key=CATEGORY_WEIGHTS.get)
35
29
  max_deductions[highest] += diff
36
30
 
37
- # Apply overrides
38
31
  if max_deduction_overrides:
39
32
  for cat, val in max_deduction_overrides.items():
40
33
  if cat in max_deductions:
41
34
  max_deductions[cat] = val
42
35
 
43
- # 2. Group diagnostics by resolved category
36
+ return max_deductions
37
+
38
+
39
+ def _score_label(value: int) -> str:
40
+ """Return a human-readable label for a numeric score."""
41
+ if value >= 90:
42
+ return LABEL_EXCELLENT
43
+ if value >= 75:
44
+ return LABEL_GREAT
45
+ if value >= 50:
46
+ return LABEL_NEEDS_WORK
47
+ return LABEL_CRITICAL
48
+
49
+
50
+ def calculate_score(
51
+ diagnostics: list[Diagnostic], max_deduction_overrides: dict | None = None
52
+ ) -> Score:
53
+ """Calculate a 0-100 health score from diagnostics using category budgets.
54
+
55
+ Each category has a weight that determines its maximum deduction budget.
56
+ Within a category, the top 3 findings are counted at full cost; additional
57
+ findings apply diminishing returns (10% each) to reward fixing top issues.
58
+ """
59
+ max_deductions = _build_budget(max_deduction_overrides)
60
+
61
+ # Group diagnostics by resolved category
44
62
  by_category: dict = defaultdict(list)
45
63
  for d in diagnostics:
46
64
  resolved = FRAMEWORK_CATEGORY_MAP.get(d.category, d.category)
47
65
  by_category[resolved].append(d)
48
66
 
49
- # 3. Calculate deduction per category with diminishing returns
67
+ # Calculate deduction per category with diminishing returns
50
68
  total_deduction = 0.0
51
69
  for cat, diags in by_category.items():
52
70
  costs = sorted([d.cost for d in diags], reverse=True)
@@ -56,14 +74,4 @@ def calculate_score(
56
74
  total_deduction += min(cat_total, cap)
57
75
 
58
76
  value = max(0, round(100 - total_deduction))
59
-
60
- if value >= 90:
61
- label = LABEL_EXCELLENT
62
- elif value >= 75:
63
- label = LABEL_GREAT
64
- elif value >= 50:
65
- label = LABEL_NEEDS_WORK
66
- else:
67
- label = LABEL_CRITICAL
68
-
69
- return Score(value=value, label=label)
77
+ return Score(value=value, label=_score_label(value))
pycodegate/utils/badge.py CHANGED
@@ -17,7 +17,7 @@ def generate_badge(score: int, label: str) -> str:
17
17
  color = "red"
18
18
 
19
19
  score_text = urllib.parse.quote(f"{score}/100")
20
- return f"![py-gate score](https://img.shields.io/badge/py--gate-{score_text}-{color})"
20
+ return f"![pycodegate score](https://img.shields.io/badge/py--gate-{score_text}-{color})"
21
21
 
22
22
 
23
23
  def generate_ci_workflow() -> str:
@@ -36,16 +36,16 @@ jobs:
36
36
  steps:
37
37
  - uses: actions/checkout@v4
38
38
  - uses: astral-sh/setup-uv@v5
39
- - run: uv tool install py-gate
40
- - name: Run py-gate
39
+ - run: uv tool install pycodegate
40
+ - name: Run pycodegate
41
41
  run: |
42
- SCORE=$(py-gate . --score)
43
- BADGE=$(py-gate . --badge)
42
+ SCORE=$(pycodegate . --score)
43
+ BADGE=$(pycodegate . --badge)
44
44
  # Update badge in README
45
- sed -i "s|!\\[py-gate score\\](.*)|${BADGE}|" README.md
45
+ sed -i "s|!\\[pycodegate score\\](.*)|${BADGE}|" README.md
46
46
  - name: Commit badge update
47
47
  run: |
48
48
  git config user.name "github-actions[bot]"
49
49
  git config user.email "github-actions[bot]@users.noreply.github.com"
50
- git diff --quiet README.md || (git add README.md && git commit -m "chore: update py-gate score badge" && git push)
50
+ git diff --quiet README.md || (git add README.md && git commit -m "chore: update pycodegate score badge" && git push)
51
51
  """
pycodegate/utils/fixer.py CHANGED
@@ -21,7 +21,7 @@ def run_ruff_fix(project_path: str) -> int:
21
21
  except FileNotFoundError:
22
22
  # ruff is not installed
23
23
  return -1
24
- except Exception:
24
+ except (OSError, subprocess.SubprocessError):
25
25
  return 0
26
26
 
27
27
  # ruff prints something like "Fixed 3 errors." to stdout or stderr
@@ -6,31 +6,31 @@ from pathlib import Path
6
6
 
7
7
 
8
8
  def install_precommit_hook(project_path: str, min_score: int = 50) -> str:
9
- """Install a git pre-commit hook that runs py-gate."""
9
+ """Install a git pre-commit hook that runs pycodegate."""
10
10
  hooks_dir = Path(project_path) / ".git" / "hooks"
11
11
  if not hooks_dir.exists():
12
12
  return "Error: not a git repository"
13
13
 
14
14
  hook_path = hooks_dir / "pre-commit"
15
15
  hook_content = f"""#!/bin/sh
16
- # py-gate pre-commit hook
17
- SCORE=$(py-gate . --score 2>/dev/null)
16
+ # pycodegate pre-commit hook
17
+ SCORE=$(pycodegate . --score 2>/dev/null)
18
18
  if [ $? -ne 0 ]; then
19
- echo "py-gate not found. Install with: uvx py-gate"
19
+ echo "pycodegate not found. Install with: uvx pycodegate"
20
20
  exit 0
21
21
  fi
22
22
  if [ "$SCORE" -lt {min_score} ]; then
23
- echo "py-gate: score $SCORE is below minimum {min_score}. Fix issues before committing."
24
- py-gate . 2>/dev/null
23
+ echo "pycodegate: score $SCORE is below minimum {min_score}. Fix issues before committing."
24
+ pycodegate . 2>/dev/null
25
25
  exit 1
26
26
  fi
27
- echo "py-gate: score $SCORE/{min_score} — passed"
27
+ echo "pycodegate: score $SCORE/{min_score} — passed"
28
28
  """
29
29
 
30
30
  # Don't overwrite existing hook — append or warn
31
31
  if hook_path.exists():
32
32
  existing = hook_path.read_text()
33
- if "py-gate" in existing:
33
+ if "pycodegate" in existing:
34
34
  hook_path.write_text(hook_content)
35
35
  hook_path.chmod(0o755)
36
36
  return f"Updated pre-commit hook at {hook_path} (min-score: {min_score})"
@@ -41,7 +41,7 @@ echo "py-gate: score $SCORE/{min_score} — passed"
41
41
  body = "".join(lines[1:]) if lines[0].startswith("#!/") else hook_content
42
42
  with open(hook_path, "a") as f:
43
43
  f.write("\n" + body)
44
- return f"Appended py-gate to existing pre-commit hook at {hook_path}"
44
+ return f"Appended pycodegate to existing pre-commit hook at {hook_path}"
45
45
 
46
46
  hook_path.write_text(hook_content)
47
47
  hook_path.chmod(0o755)
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycodegate
3
- Version: 0.1.1
4
- Summary: One command. One score. Your Python quality gate.
3
+ Version: 0.1.3
4
+ Summary: Trust, but verify. The quality gate for AI-generated Python code.
5
5
  Project-URL: Homepage, https://github.com/themohitkhare/pycodegate
6
6
  Project-URL: Repository, https://github.com/themohitkhare/pycodegate
7
7
  Project-URL: Issues, https://github.com/themohitkhare/pycodegate/issues
@@ -32,22 +32,26 @@ Requires-Dist: pytest>=8.0; extra == 'dev'
32
32
  Requires-Dist: ruff>=0.4; extra == 'dev'
33
33
  Description-Content-Type: text/markdown
34
34
 
35
- <h1 align="center">Py Gate</h1>
35
+ <h1 align="center">PyCodeGate</h1>
36
36
  <p align="center">
37
- <strong>One command. One score. Your Python quality gate.</strong>
37
+ <strong>Trust, but verify. The quality gate for AI-generated Python code.</strong>
38
38
  </p>
39
39
  <p align="center">
40
40
  <a href="https://github.com/themohitkhare/pycodegate/actions/workflows/ci.yml"><img src="https://github.com/themohitkhare/pycodegate/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
41
- <a href="https://pypi.org/project/py-gate/"><img src="https://img.shields.io/pypi/v/py-gate" alt="PyPI"></a>
42
- <a href="https://pypi.org/project/py-gate/"><img src="https://img.shields.io/pypi/pyversions/py-gate" alt="Python"></a>
41
+ <a href="https://pypi.org/project/pycodegate/"><img src="https://img.shields.io/pypi/v/pycodegate" alt="PyPI"></a>
42
+ <a href="https://pypi.org/project/pycodegate/"><img src="https://img.shields.io/pypi/pyversions/pycodegate" alt="Python"></a>
43
43
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
44
44
  </p>
45
45
 
46
46
  ---
47
47
 
48
+ ## Why PyCodeGate?
49
+
50
+ AI coding agents ship code fast — but fast doesn't mean safe. Every `eval()` an LLM drops in, every mutable default it forgets, every circular import it creates is a landmine waiting to go off. PyCodeGate is the trust layer between AI-generated code and your production codebase: one command, one score, zero ambiguity.
51
+
48
52
  ## How it works
49
53
 
50
- Py Gate starts by detecting your project's context: framework (Django, FastAPI, Flask), Python version, package manager (uv, poetry, pip), and test framework. That context drives which rules are active — Django projects get SQL injection checks, FastAPI projects get async-correctness checks, and so on.
54
+ PyCodeGate detects your project's context: framework (Django, FastAPI, Flask), Python version, package manager (uv, poetry, pip), and test framework. That context drives which rules are active — Django projects get SQL injection checks, FastAPI projects get async-correctness checks, and so on.
51
55
 
52
56
  It then runs two analysis passes **in parallel**: a lint pass that evaluates 40+ rules across 8 categories (Security, Correctness, Complexity, Architecture, Performance, Structure, Imports, Dead Code), and a dead-code pass powered by [Vulture](https://github.com/jendrikseipp/vulture) that finds unused functions, classes, imports, and variables.
53
57
 
@@ -58,39 +62,39 @@ Findings are filtered through your configuration and scored using a **weighted c
58
62
  Run instantly with `uvx` — no install needed:
59
63
 
60
64
  ```bash
61
- uvx py-gate .
65
+ uvx pycodegate .
62
66
  ```
63
67
 
64
68
  Install globally with pipx or uv:
65
69
 
66
70
  ```bash
67
- pipx install py-gate
71
+ pipx install pycodegate
68
72
  # or
69
- uv tool install py-gate
73
+ uv tool install pycodegate
70
74
  # or
71
- pip install py-gate
75
+ pip install pycodegate
72
76
  ```
73
77
 
74
78
  ## Quick Start
75
79
 
76
80
  ```bash
77
81
  # Basic scan — score + summary
78
- py-gate .
82
+ pycodegate .
79
83
 
80
84
  # Show file paths and line numbers for every finding
81
- py-gate . --verbose
85
+ pycodegate . --verbose
82
86
 
83
87
  # Machine-readable output for AI agents and CI
84
- py-gate . --json
88
+ pycodegate . --json
85
89
 
86
90
  # Auto-fix ruff-fixable issues, then scan
87
- py-gate . --fix
91
+ pycodegate . --fix
88
92
 
89
93
  # Output only the numeric score (useful in scripts)
90
- py-gate . --score
94
+ pycodegate . --score
91
95
 
92
96
  # Scan only files changed vs a base branch
93
- py-gate . --diff main
97
+ pycodegate . --diff main
94
98
  ```
95
99
 
96
100
  ## JSON Output
@@ -98,7 +102,7 @@ py-gate . --diff main
98
102
  Pass `--json` to get structured output that AI agents and CI pipelines can parse:
99
103
 
100
104
  ```bash
101
- py-gate . --json
105
+ pycodegate . --json
102
106
  ```
103
107
 
104
108
  ```json
@@ -139,7 +143,7 @@ py-gate . --json
139
143
 
140
144
  ## Agent Integration
141
145
 
142
- Py Gate is designed to be used by AI coding agents. Add it to your agent's context so it runs after every Python change.
146
+ PyCodeGate is designed to be used by AI coding agents. Add it to your agent's context so it runs after every Python change.
143
147
 
144
148
  ### Claude Code
145
149
 
@@ -147,18 +151,18 @@ Add the skill to your project:
147
151
 
148
152
  ```bash
149
153
  mkdir -p .claude/skills
150
- curl -fsSL https://raw.githubusercontent.com/themohitkhare/pycodegate/main/skills/py-gate/SKILL.md \
151
- -o .claude/skills/py-gate.md
154
+ curl -fsSL https://raw.githubusercontent.com/themohitkhare/pycodegate/main/skills/pycodegate/SKILL.md \
155
+ -o .claude/skills/pycodegate.md
152
156
  ```
153
157
 
154
158
  Or copy `AGENTS.md` to your project root — Claude Code picks it up automatically.
155
159
 
156
160
  ### Cursor
157
161
 
158
- Add to `.cursor/rules/py-gate.mdc`:
162
+ Add to `.cursor/rules/pycodegate.mdc`:
159
163
 
160
164
  ```markdown
161
- After modifying Python files, run `uvx py-gate . --json` and fix all findings
165
+ After modifying Python files, run `uvx pycodegate . --json` and fix all findings
162
166
  with severity "error" before marking the task complete. Target score: 80+.
163
167
  ```
164
168
 
@@ -167,7 +171,7 @@ with severity "error" before marking the task complete. Target score: 80+.
167
171
  Add to `.windsurfrules`:
168
172
 
169
173
  ```
170
- After modifying Python files, run: uvx py-gate . --json
174
+ After modifying Python files, run: uvx pycodegate . --json
171
175
  Fix all "error" severity findings. Re-run to verify the score improved.
172
176
  ```
173
177
 
@@ -176,7 +180,7 @@ Fix all "error" severity findings. Re-run to verify the score improved.
176
180
  Add to your system prompt:
177
181
 
178
182
  ```
179
- After modifying Python files, run `uvx py-gate . --json` to check code quality.
183
+ After modifying Python files, run `uvx pycodegate . --json` to check code quality.
180
184
  Fix errors first. Target score: 80+.
181
185
  ```
182
186
 
@@ -194,7 +198,7 @@ name: Quality Gate
194
198
  on: [push, pull_request]
195
199
 
196
200
  jobs:
197
- py-gate:
201
+ pycodegate:
198
202
  runs-on: ubuntu-latest
199
203
  steps:
200
204
  - uses: actions/checkout@v4
@@ -205,10 +209,10 @@ jobs:
205
209
  with:
206
210
  python-version: "3.12"
207
211
 
208
- - name: Run Py Gate
212
+ - name: Run PyCodeGate
209
213
  run: |
210
- pip install py-gate
211
- py-gate . --verbose --diff main --fail-on error
214
+ pip install pycodegate
215
+ pycodegate . --verbose --diff main --fail-on error
212
216
  ```
213
217
 
214
218
  ## CLI Reference
@@ -244,7 +248,7 @@ Framework-specific rules (Django, FastAPI, Flask) are mapped into the Security o
244
248
 
245
249
  ## Scoring
246
250
 
247
- Py Gate uses a **weighted category-budget system**:
251
+ PyCodeGate uses a **weighted category-budget system**:
248
252
 
249
253
  1. Each category has a weight (see table above). Weights are normalized to sum to 100 points of total deduction budget.
250
254
  2. Within a category, findings are sorted by cost (errors cost more than warnings). The top 3 findings apply at full cost; every additional finding applies **diminishing returns** (10% of its cost).
@@ -260,7 +264,7 @@ Py Gate uses a **weighted category-budget system**:
260
264
 
261
265
  ## Configuration
262
266
 
263
- Create `py-gate.toml` in your project root:
267
+ Create `pycodegate.toml` in your project root:
264
268
 
265
269
  ```toml
266
270
  [options]
@@ -284,27 +288,27 @@ max-deduction.Dead Code = 0 # disable dead-code penalty entirely
284
288
  Or use `pyproject.toml`:
285
289
 
286
290
  ```toml
287
- [tool.py-gate]
291
+ [tool.pycodegate]
288
292
  lint = true
289
293
  dead_code = true
290
294
  fail_on = "error"
291
295
 
292
- [tool.py-gate.ignore]
296
+ [tool.pycodegate.ignore]
293
297
  rules = ["no-import-in-function"]
294
298
  files = ["tests/fixtures/**"]
295
299
 
296
- [tool.py-gate.per-file-ignores]
300
+ [tool.pycodegate.per-file-ignores]
297
301
  "src/legacy/*.py" = ["high-complexity"]
298
302
 
299
- [tool.py-gate.scoring]
303
+ [tool.pycodegate.scoring]
300
304
  "max-deduction" = { Security = 20, "Dead Code" = 0 }
301
305
  ```
302
306
 
303
- If both files exist, `py-gate.toml` takes precedence. CLI flags always override config values.
307
+ If both files exist, `pycodegate.toml` takes precedence. CLI flags always override config values.
304
308
 
305
309
  ## Profiles
306
310
 
307
- Py Gate auto-detects a project profile and adjusts rule weights accordingly. You can also set a profile explicitly in config (`profile = "library"`).
311
+ PyCodeGate auto-detects a project profile and adjusts rule weights accordingly. You can also set a profile explicitly in config (`profile = "library"`).
308
312
 
309
313
  | Profile | Auto-detected when | Adjustments |
310
314
  |---------|--------------------|-------------|
@@ -315,10 +319,10 @@ Py Gate auto-detects a project profile and adjusts rule weights accordingly. You
315
319
 
316
320
  ## Pre-commit Hook
317
321
 
318
- Use `--pre-commit` to run Py Gate as a pre-commit hook. It automatically scans only the staged files:
322
+ Use `--pre-commit` to run PyCodeGate as a pre-commit hook. It automatically scans only the staged files:
319
323
 
320
324
  ```bash
321
- py-gate . --pre-commit --fail-on error
325
+ pycodegate . --pre-commit --fail-on error
322
326
  ```
323
327
 
324
328
  Add to `.pre-commit-config.yaml`:
@@ -327,9 +331,9 @@ Add to `.pre-commit-config.yaml`:
327
331
  repos:
328
332
  - repo: local
329
333
  hooks:
330
- - id: py-gate
331
- name: Py Gate quality check
332
- entry: py-gate . --pre-commit --fail-on error
334
+ - id: pycodegate
335
+ name: PyCodeGate quality check
336
+ entry: pycodegate . --pre-commit --fail-on error
333
337
  language: system
334
338
  types: [python]
335
339
  pass_filenames: false
@@ -342,7 +346,7 @@ git clone https://github.com/themohitkhare/pycodegate
342
346
  cd pycodegate
343
347
  uv sync --all-extras
344
348
  uv run pytest -q
345
- uv run py-gate . --verbose # dogfood it
349
+ uv run pycodegate . --verbose # dogfood it
346
350
  ```
347
351
 
348
352
  To add a new rule:
@@ -1,14 +1,14 @@
1
- pycodegate/__init__.py,sha256=omYWGAgDvHd1VDi5Jhlxf9ZRIckJuKL7Zw_E1H6Ub_Q,84
1
+ pycodegate/__init__.py,sha256=C30sIG3GQ43ZgC_imlRU7Ym6StPA5Soq3tnxztdN24Q,84
2
2
  pycodegate/_compat.py,sha256=tnXZge9z8NFCDz-zi_vAScbJkDNJo2_vpEW_UgSqbd8,293
3
3
  pycodegate/api.py,sha256=W93EWmBlzrE2Uwrs4opr4F0QO-1v6GzRp1jN05b9rBo,631
4
- pycodegate/cli.py,sha256=1Cs5ZBhxl_WMM47aowMgIUEbedDWGNqHmQ-dcnkWRuw,4126
5
- pycodegate/config.py,sha256=8Qfs6d60FXvKYXqade6lDtuOncDAs2hHUFzkYQ37RIw,2444
4
+ pycodegate/cli.py,sha256=O0B7e6JP42dI5a5zeYuT6VbBbKXi4vtBiF1eWWJ3zxQ,5838
5
+ pycodegate/config.py,sha256=56wBpGecabUYYs3UN_olDyzG49My7SumorMTDvlxRuY,2462
6
6
  pycodegate/constants.py,sha256=HJnPIZR1OcpDGmqIsNCFan7fHzezJZtt105GT7-WTfI,788
7
7
  pycodegate/discover.py,sha256=ywIPIJD-fM5PHt6ACRhehuVoJVyhm36Wf28HFGn0E3I,3694
8
- pycodegate/output.py,sha256=bLCU2goD85ShbwvSxLRFtIkXs_Rogs4Iz1SOt8HSUE4,9723
9
- pycodegate/profile.py,sha256=XCmFHuyQlYfr1jru0RWXoxouQ3IVh-vgc46AjTEp3xc,3438
8
+ pycodegate/output.py,sha256=mUWLoHg9FtZ73wd0KNnC1QErO2kCic_9ITi0hUmMep8,9918
9
+ pycodegate/profile.py,sha256=okmh8_o_P5YpJxUnrOPGVxzDWZhBOb5WCJSkVVoO23c,3862
10
10
  pycodegate/scan.py,sha256=WEw_-a-uiDYiz3i8c3gHl8QXBRb_pVgeJciYB_E68Ws,7282
11
- pycodegate/score.py,sha256=Dy01pkWyi0p9ypa64aekVSlfZxXKSbdhKP54EgqYu34,2366
11
+ pycodegate/score.py,sha256=1ANbWGABy4raiVn_32NCQG-Pu4eNCqBIx90AvXoQL_Q,2613
12
12
  pycodegate/types.py,sha256=_5eDkdNmxTkfxDNE-g_FLKpcUF6l0ArKZfwXafhYn2I,1285
13
13
  pycodegate/rules/__init__.py,sha256=Do4SzAF41GAnEUS1tn2lPZK99wSYJ67OaLKZD62B2fY,1353
14
14
  pycodegate/rules/architecture.py,sha256=e1dh_bSNyno8SaWV2UK4YoSCVMhhDphUpFrK3TSa6Xc,5151
@@ -16,23 +16,23 @@ pycodegate/rules/base.py,sha256=u7qejC4rDfXMh5RYy_3r-htxxSVTRB5zHETEnQt6X_M,593
16
16
  pycodegate/rules/complexity.py,sha256=roYTPx3i8PCltqVpHWPt_U0jMZ2MZumJXeZ6mK6RNcU,3388
17
17
  pycodegate/rules/correctness.py,sha256=go6n1fcv4NDu89zL8j4LLX-kOBgBRvuOx78C11bxhLg,6049
18
18
  pycodegate/rules/dead_code.py,sha256=ZzawHLHbYKaNoK9zcnQQA99clUkOAdA4y64VtUDUouQ,1795
19
- pycodegate/rules/dependencies.py,sha256=3tX5wk6D09LVYuLcpqUx7XcTvEG3-orY4yctDgyjWpg,3729
19
+ pycodegate/rules/dependencies.py,sha256=NOg9rRhzEP92s6_IkYEN0o3dHDKpgERzspoQGgahRbk,4123
20
20
  pycodegate/rules/django.py,sha256=IddI4eVnSZ2hDJW5EZu3xnPyrmjVwHq0DOnrpV7XncM,6385
21
21
  pycodegate/rules/fastapi.py,sha256=16LOX97gXvVsWf0cvdqp5Ji-byzlKQP51iAy_yGX14Q,2769
22
22
  pycodegate/rules/flask.py,sha256=htKFDjV2VZtAyE4ELlxYqDQUSYM4ROJZnS7UI_PK-xA,4574
23
- pycodegate/rules/imports.py,sha256=yp5aSIrcw7AdqgljkJEywEJ_HQ2GcqZVIcDdVSK_R30,3404
23
+ pycodegate/rules/imports.py,sha256=L5o_JtG1DNvV5LdoG1PgEwjf6CrWgPsUSMzK7v1Q5g0,3751
24
24
  pycodegate/rules/performance.py,sha256=qZ9Tw0CNwjeIsqDwPulZXZu0VWWJQuU-r0TvG9odaQU,4512
25
25
  pycodegate/rules/security.py,sha256=fdISx3yLVtQ2OjVtnaDBUOPXK6Tpq4ykntRlKr_1bAs,7515
26
- pycodegate/rules/structure.py,sha256=VC19nrGyiATurP4tvnK7scBI1dZ9bDKyPYKiowzIVLI,9138
26
+ pycodegate/rules/structure.py,sha256=rURxlkMjQp6O-0xhe72fscTuUYZVyU-YxGWhbxXeSwA,8938
27
27
  pycodegate/utils/__init__.py,sha256=MTVfXf-fKi2bjxXedkveORadRqo5qSUK4pMl08WJWdA,41
28
28
  pycodegate/utils/ast_helpers.py,sha256=bRQwOsUvNASjq0wjZ_Yc0cDmVnXPGhujzxZV2Qff0Z8,538
29
- pycodegate/utils/badge.py,sha256=ZtG81MBQkPpjQWhoL4jc_dS_XzTYOKRK1OYIv70ONCs,1411
29
+ pycodegate/utils/badge.py,sha256=dh49h3LaOY402WxestPb0m38wQWhLZjNMn5NyuOT_ag,1432
30
30
  pycodegate/utils/diff.py,sha256=o8YuqBqOwiHmPDsjFKXAlPphd0m8y20EwXRrcSvaoIg,824
31
31
  pycodegate/utils/file_discovery.py,sha256=2tqW1LPoNjH2Y57UDo9sLyCioXvKBv8vFTmtmh2knpw,1472
32
- pycodegate/utils/fixer.py,sha256=EDw4dMqLqnzGaQwaGyehrnHBirFajYxRDONnzp9I6HM,890
33
- pycodegate/utils/precommit.py,sha256=E2XhpuPEjKTsI2Vj9WBN2qwHwhTm8QqJEBZ9zlT2AHY,1707
34
- pycodegate-0.1.1.dist-info/METADATA,sha256=BRaEV1iszbBxU08uKkM8CXmFN2Z2wJzO0VQLo3FZqfg,11390
35
- pycodegate-0.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
36
- pycodegate-0.1.1.dist-info/entry_points.txt,sha256=hG8TqhjIZnEsmgVOSiBNIExhqanOqGsy-byiZETILm0,81
37
- pycodegate-0.1.1.dist-info/licenses/LICENSE,sha256=YQAkET2EceAFEwM7LLUco13527CKFujpBapIIRPshTQ,1068
38
- pycodegate-0.1.1.dist-info/RECORD,,
32
+ pycodegate/utils/fixer.py,sha256=SlKH1mt9YaMxfZk-T_rFOc3FeDKNTnqTzbFdiD9eTXI,918
33
+ pycodegate/utils/precommit.py,sha256=nzcmXc0NPAaxL-ZqDkDyHuuua4SiRXK2HrJZq3HLkMc,1737
34
+ pycodegate-0.1.3.dist-info/METADATA,sha256=oexR4oMM3sHfX4vMPeoYGyCvMxtJvA-qIaKWzddxtik,11879
35
+ pycodegate-0.1.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
36
+ pycodegate-0.1.3.dist-info/entry_points.txt,sha256=oSjjP_VAA3O74Y5a-IqIHYsT7-Ljuv97wXiMQ5RdOg0,51
37
+ pycodegate-0.1.3.dist-info/licenses/LICENSE,sha256=YQAkET2EceAFEwM7LLUco13527CKFujpBapIIRPshTQ,1068
38
+ pycodegate-0.1.3.dist-info/RECORD,,
@@ -1,3 +1,2 @@
1
1
  [console_scripts]
2
- py-gate = pycodegate.cli:main
3
2
  pycodegate = pycodegate.cli:main