nk-python-style 0.1.0__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.
- nk_python/__init__.py +1 -0
- nk_python/__main__.py +3 -0
- nk_python/ast_checks.py +79 -0
- nk_python/cli.py +100 -0
- nk_python/comment_checks.py +82 -0
- nk_python/model.py +14 -0
- nk_python/ruff.toml +13 -0
- nk_python/ruff_runner.py +34 -0
- nk_python/structure_checks.py +53 -0
- nk_python_style-0.1.0.dist-info/METADATA +60 -0
- nk_python_style-0.1.0.dist-info/RECORD +14 -0
- nk_python_style-0.1.0.dist-info/WHEEL +5 -0
- nk_python_style-0.1.0.dist-info/entry_points.txt +2 -0
- nk_python_style-0.1.0.dist-info/top_level.txt +1 -0
nk_python/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Python style checks inspired by the C++ tooling in this repository."""
|
nk_python/__main__.py
ADDED
nk_python/ast_checks.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Checks that need Python syntax rather than regular expressions."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .model import Issue
|
|
7
|
+
|
|
8
|
+
MAX_LINE_LENGTH = 120
|
|
9
|
+
MAX_FILE_LINES = 120
|
|
10
|
+
MAX_FUNCTION_LINES = 40
|
|
11
|
+
MAX_PARAMETERS = 5
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def function_ranges(tree: ast.AST, source: str) -> list[tuple[int, int]]:
|
|
15
|
+
lines = source.splitlines()
|
|
16
|
+
ranges = []
|
|
17
|
+
for node in ast.walk(tree):
|
|
18
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
19
|
+
continue
|
|
20
|
+
end = node.end_lineno or node.lineno
|
|
21
|
+
for index in range(end, len(lines)):
|
|
22
|
+
stripped = lines[index].lstrip(" \t")
|
|
23
|
+
if not stripped:
|
|
24
|
+
continue
|
|
25
|
+
indentation = len(lines[index]) - len(stripped)
|
|
26
|
+
if stripped.startswith("#") and indentation > node.col_offset:
|
|
27
|
+
end = index + 1
|
|
28
|
+
continue
|
|
29
|
+
break
|
|
30
|
+
ranges.append((node.lineno, end))
|
|
31
|
+
return ranges
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_source(path: Path, source: str) -> tuple[list[Issue], list[tuple[int, int]]]:
|
|
35
|
+
issues: list[Issue] = []
|
|
36
|
+
lines = source.splitlines()
|
|
37
|
+
for number, line in enumerate(lines, 1):
|
|
38
|
+
if len(line) > MAX_LINE_LENGTH:
|
|
39
|
+
issues.append(Issue(path, number, "PY001", f"Line has {len(line)} characters (max {MAX_LINE_LENGTH})."))
|
|
40
|
+
if len(lines) > MAX_FILE_LINES:
|
|
41
|
+
issues.append(Issue(path, MAX_FILE_LINES + 1, "PY002", f"File has {len(lines)} lines (max {MAX_FILE_LINES})."))
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
tree = ast.parse(source, filename=str(path))
|
|
45
|
+
except SyntaxError as error:
|
|
46
|
+
issues.append(Issue(path, error.lineno or 1, "PY000", f"Invalid Python syntax: {error.msg}."))
|
|
47
|
+
return issues, []
|
|
48
|
+
|
|
49
|
+
parents = {child: parent for parent in ast.walk(tree) for child in ast.iter_child_nodes(parent)}
|
|
50
|
+
for node in ast.walk(tree):
|
|
51
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
52
|
+
continue
|
|
53
|
+
length = (node.end_lineno or node.lineno) - node.lineno + 1
|
|
54
|
+
if length > MAX_FUNCTION_LINES:
|
|
55
|
+
issues.append(
|
|
56
|
+
Issue(
|
|
57
|
+
path, node.lineno, "PY003", f"Function '{node.name}' has {length} lines (max {MAX_FUNCTION_LINES})."
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
count = _parameter_count(node, parents.get(node))
|
|
61
|
+
if count > MAX_PARAMETERS:
|
|
62
|
+
issues.append(
|
|
63
|
+
Issue(
|
|
64
|
+
path, node.lineno, "PY004", f"Function '{node.name}' has {count} parameters (max {MAX_PARAMETERS})."
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
return issues, function_ranges(tree, source)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _parameter_count(node: ast.FunctionDef | ast.AsyncFunctionDef, parent: ast.AST | None) -> int:
|
|
71
|
+
positional = node.args.posonlyargs + node.args.args
|
|
72
|
+
count = len(positional) + len(node.args.kwonlyargs)
|
|
73
|
+
count += int(node.args.vararg is not None) + int(node.args.kwarg is not None)
|
|
74
|
+
is_static = any(
|
|
75
|
+
isinstance(decorator, ast.Name) and decorator.id == "staticmethod" for decorator in node.decorator_list
|
|
76
|
+
)
|
|
77
|
+
if isinstance(parent, ast.ClassDef) and not is_static and positional and positional[0].arg in {"self", "cls"}:
|
|
78
|
+
count -= 1
|
|
79
|
+
return count
|
nk_python/cli.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Command line entry point for formatting and checking Python projects."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import importlib.util
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .ast_checks import check_source
|
|
10
|
+
from .comment_checks import LANGUAGE_AVAILABLE, check_comments
|
|
11
|
+
from .model import Issue
|
|
12
|
+
from .ruff_runner import run_ruff
|
|
13
|
+
from .structure_checks import discover
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str] | None = None) -> int:
|
|
17
|
+
parser = argparse.ArgumentParser(prog="nk-python")
|
|
18
|
+
parser.add_argument("command", choices=("check", "format"), help="Check without writing, or format in place.")
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"paths", nargs="*", default=["."], help="Python files or directories (default: current directory)."
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("--max-files", type=_positive_int, default=8, help="Maximum direct Python files per directory.")
|
|
23
|
+
arguments = parser.parse_args(argv)
|
|
24
|
+
|
|
25
|
+
if importlib.util.find_spec("ruff") is None:
|
|
26
|
+
print("Ruff is required. Install this tool with: pip install -e ./python", file=sys.stderr)
|
|
27
|
+
return 2
|
|
28
|
+
try:
|
|
29
|
+
files, structure_issues = discover([Path(value) for value in arguments.paths], arguments.max_files)
|
|
30
|
+
except (FileNotFoundError, ValueError) as error:
|
|
31
|
+
print(error, file=sys.stderr)
|
|
32
|
+
return 2
|
|
33
|
+
if not files:
|
|
34
|
+
print("No Python files found.", file=sys.stderr)
|
|
35
|
+
return 2
|
|
36
|
+
|
|
37
|
+
if arguments.command == "format":
|
|
38
|
+
import_status = run_ruff("check", files, "--select", "I", "--fix")
|
|
39
|
+
format_status = run_ruff("format", files)
|
|
40
|
+
return max(import_status, format_status)
|
|
41
|
+
|
|
42
|
+
return _check(files, structure_issues)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _check(files: list[Path], structure_issues: list[Issue]) -> int:
|
|
46
|
+
lint_output: list[str] = []
|
|
47
|
+
import_output: list[str] = []
|
|
48
|
+
format_output: list[str] = []
|
|
49
|
+
lint_status = run_ruff("check", files, "--ignore", "I", "--unfixable", "ALL", "--quiet", output=lint_output)
|
|
50
|
+
issues = structure_issues + _check_files(files)
|
|
51
|
+
import_status = run_ruff("check", files, "--select", "I", "--quiet", output=import_output)
|
|
52
|
+
format_status = run_ruff("format", files, "--check", output=format_output)
|
|
53
|
+
lint_text, import_text, format_text = ("".join(output) for output in (lint_output, import_output, format_output))
|
|
54
|
+
diagnostic_pattern = r"^.+:\d+:\d+: "
|
|
55
|
+
lint_count = len(re.findall(diagnostic_pattern, lint_text, re.MULTILINE)) + len(issues)
|
|
56
|
+
import_count = len(re.findall(diagnostic_pattern, import_text, re.MULTILINE))
|
|
57
|
+
format_count = sum(
|
|
58
|
+
int(count) for count in re.findall(r"^(\d+) files? would be reformatted", format_text, re.MULTILINE)
|
|
59
|
+
)
|
|
60
|
+
print("=== Check summary ===")
|
|
61
|
+
print(f"Files checked: {len(files)}")
|
|
62
|
+
print(f"Linter: {lint_count} warnings")
|
|
63
|
+
print(f"Formatter: {import_count} import diagnostics; {format_count} files to reformat")
|
|
64
|
+
if 2 in (lint_status, import_status, format_status):
|
|
65
|
+
print("Ruff could not complete the check; counts may be incomplete.")
|
|
66
|
+
elif not (lint_status or import_status or format_status or issues):
|
|
67
|
+
print("All checks passed!")
|
|
68
|
+
|
|
69
|
+
print("\n=== Linter diagnostics ===")
|
|
70
|
+
print(lint_text, end="")
|
|
71
|
+
for issue in sorted(issues):
|
|
72
|
+
print(issue)
|
|
73
|
+
if not LANGUAGE_AVAILABLE:
|
|
74
|
+
print("Comment language check skipped (install nk-python-style[comments] to enable it).")
|
|
75
|
+
|
|
76
|
+
print("\n=== Formatter: corrections available via 'format' ===")
|
|
77
|
+
print(import_text, end="")
|
|
78
|
+
print(format_text, end="")
|
|
79
|
+
return int(bool(lint_status or import_status or format_status or issues))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _positive_int(value: str) -> int:
|
|
83
|
+
result = int(value)
|
|
84
|
+
if result < 1:
|
|
85
|
+
raise argparse.ArgumentTypeError("--max-files must be positive")
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _check_files(files: list[Path]) -> list[Issue]:
|
|
90
|
+
issues: list[Issue] = []
|
|
91
|
+
for path in files:
|
|
92
|
+
try:
|
|
93
|
+
source = path.read_text(encoding="utf-8-sig")
|
|
94
|
+
except (OSError, UnicodeError) as error:
|
|
95
|
+
issues.append(Issue(path, 1, "PY000", f"Cannot read UTF-8 source: {error}."))
|
|
96
|
+
continue
|
|
97
|
+
source_issues, functions = check_source(path, source)
|
|
98
|
+
issues.extend(source_issues)
|
|
99
|
+
issues.extend(check_comments(path, source, functions))
|
|
100
|
+
return issues
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Token-based comment placement and optional language checks."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import re
|
|
5
|
+
import tokenize
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .model import Issue
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from lingua import Language, LanguageDetectorBuilder
|
|
12
|
+
except ImportError:
|
|
13
|
+
Language = None
|
|
14
|
+
LanguageDetectorBuilder = None
|
|
15
|
+
|
|
16
|
+
LANGUAGE_AVAILABLE = LanguageDetectorBuilder is not None
|
|
17
|
+
_detector = None
|
|
18
|
+
_DIRECTIVE = re.compile(
|
|
19
|
+
r"^(?:!|coding[:=]|noqa\b|type:\s*ignore\b|pyright:\s*ignore\b|"
|
|
20
|
+
r"pylint:|fmt:|ruff:|isort:|pragma:\s*no cover\b)",
|
|
21
|
+
re.IGNORECASE,
|
|
22
|
+
)
|
|
23
|
+
_WORD = re.compile(r"[A-Za-zÀ-ÖØ-öø-ÿ]+")
|
|
24
|
+
_META_WORD = re.compile(r"^(?:TODO|FIXME|XXX|HACK|NOTE|WIP)$", re.IGNORECASE)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_comments(path: Path, source: str, functions: list[tuple[int, int]]) -> list[Issue]:
|
|
28
|
+
issues: list[Issue] = []
|
|
29
|
+
source_lines = source.splitlines()
|
|
30
|
+
try:
|
|
31
|
+
tokens = tokenize.generate_tokens(io.StringIO(source).readline)
|
|
32
|
+
for token in tokens:
|
|
33
|
+
if token.type != tokenize.COMMENT:
|
|
34
|
+
continue
|
|
35
|
+
line_number, column = token.start
|
|
36
|
+
comment = token.string[1:].strip()
|
|
37
|
+
if _DIRECTIVE.match(comment):
|
|
38
|
+
continue
|
|
39
|
+
inside = any(start <= line_number <= end for start, end in functions)
|
|
40
|
+
if inside:
|
|
41
|
+
issues.append(
|
|
42
|
+
Issue(path, line_number, "PY006", "Use a docstring or clear code instead of a function comment.")
|
|
43
|
+
)
|
|
44
|
+
elif source_lines[line_number - 1][:column].strip():
|
|
45
|
+
issues.append(Issue(path, line_number, "PY005", "Place the comment on its own line."))
|
|
46
|
+
language = _comment_language(comment)
|
|
47
|
+
if language:
|
|
48
|
+
issues.append(Issue(path, line_number, "PY007", f"Comment appears to be in {language}; use English."))
|
|
49
|
+
except (tokenize.TokenError, IndentationError):
|
|
50
|
+
pass
|
|
51
|
+
return issues
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _comment_language(comment: str) -> str | None:
|
|
55
|
+
if not LANGUAGE_AVAILABLE:
|
|
56
|
+
return None
|
|
57
|
+
words = [word for word in _WORD.findall(comment) if _natural_word(word)]
|
|
58
|
+
if len(words) < 3 or len(" ".join(words)) < 20:
|
|
59
|
+
return None
|
|
60
|
+
confidences = _get_detector().compute_language_confidence_values(" ".join(words))
|
|
61
|
+
if not confidences or confidences[0].language == Language.ENGLISH:
|
|
62
|
+
return None
|
|
63
|
+
english = next((item.value for item in confidences if item.language == Language.ENGLISH), 0.0)
|
|
64
|
+
margin = 0.35 if len(words) <= 5 else 0.15
|
|
65
|
+
if confidences[0].value - english < margin:
|
|
66
|
+
return None
|
|
67
|
+
return confidences[0].language.name.title()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _natural_word(word: str) -> bool:
|
|
71
|
+
if _META_WORD.fullmatch(word) or word.isupper():
|
|
72
|
+
return False
|
|
73
|
+
return not any(left.islower() and right.isupper() for left, right in zip(word, word[1:], strict=False))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_detector():
|
|
77
|
+
global _detector
|
|
78
|
+
if _detector is None:
|
|
79
|
+
_detector = LanguageDetectorBuilder.from_languages(
|
|
80
|
+
Language.ENGLISH, Language.FRENCH, Language.SPANISH, Language.GERMAN
|
|
81
|
+
).build()
|
|
82
|
+
return _detector
|
nk_python/model.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, order=True)
|
|
7
|
+
class Issue:
|
|
8
|
+
path: Path
|
|
9
|
+
line: int
|
|
10
|
+
code: str
|
|
11
|
+
message: str
|
|
12
|
+
|
|
13
|
+
def __str__(self) -> str:
|
|
14
|
+
return f"{os.path.relpath(self.path)}:{self.line}: {self.code} {self.message}"
|
nk_python/ruff.toml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
line-length = 120
|
|
2
|
+
indent-width = 4
|
|
3
|
+
target-version = "py310"
|
|
4
|
+
output-format = "concise"
|
|
5
|
+
extend-exclude = [".git", ".venv*", "venv", "__pycache__", "build", "dist", "node_modules"]
|
|
6
|
+
|
|
7
|
+
[lint]
|
|
8
|
+
select = ["E4", "E7", "E9", "F", "I", "B", "UP", "RET504"]
|
|
9
|
+
|
|
10
|
+
[format]
|
|
11
|
+
indent-style = "space"
|
|
12
|
+
quote-style = "double"
|
|
13
|
+
line-ending = "lf"
|
nk_python/ruff_runner.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Run Ruff in batches, optionally collecting its output for a check summary."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
RUFF_CONFIG = Path(__file__).with_name("ruff.toml")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_ruff(command: str, files: list[Path], *options: str, output: list[str] | None = None) -> int:
|
|
12
|
+
status = 0
|
|
13
|
+
for offset in range(0, len(files), 50):
|
|
14
|
+
batch = files[offset : offset + 50]
|
|
15
|
+
result = subprocess.run(
|
|
16
|
+
[
|
|
17
|
+
sys.executable,
|
|
18
|
+
"-m",
|
|
19
|
+
"ruff",
|
|
20
|
+
command,
|
|
21
|
+
"--config",
|
|
22
|
+
str(RUFF_CONFIG),
|
|
23
|
+
*options,
|
|
24
|
+
*(os.path.relpath(path) for path in batch),
|
|
25
|
+
],
|
|
26
|
+
stdout=subprocess.PIPE if output is not None else None,
|
|
27
|
+
stderr=subprocess.STDOUT if output is not None else None,
|
|
28
|
+
text=True,
|
|
29
|
+
check=False,
|
|
30
|
+
)
|
|
31
|
+
if output is not None:
|
|
32
|
+
output.append(result.stdout)
|
|
33
|
+
status = max(status, result.returncode)
|
|
34
|
+
return status
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Collect Python files and check direct file counts in each directory."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from fnmatch import fnmatchcase
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .model import Issue
|
|
8
|
+
|
|
9
|
+
IGNORED_DIRECTORIES = {
|
|
10
|
+
".git",
|
|
11
|
+
".hg",
|
|
12
|
+
".mypy_cache",
|
|
13
|
+
".pytest_cache",
|
|
14
|
+
".ruff_cache",
|
|
15
|
+
".tox",
|
|
16
|
+
".venv*",
|
|
17
|
+
"venv",
|
|
18
|
+
"__pycache__",
|
|
19
|
+
"build",
|
|
20
|
+
"dist",
|
|
21
|
+
"node_modules",
|
|
22
|
+
}
|
|
23
|
+
SUFFIXES = {".py", ".pyi"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def discover(paths: list[Path], max_files: int = 8) -> tuple[list[Path], list[Issue]]:
|
|
27
|
+
files: set[Path] = set()
|
|
28
|
+
issues: list[Issue] = []
|
|
29
|
+
seen_directories: set[Path] = set()
|
|
30
|
+
for target in paths:
|
|
31
|
+
target = target.resolve()
|
|
32
|
+
if not target.exists():
|
|
33
|
+
raise FileNotFoundError(target)
|
|
34
|
+
if target.is_file():
|
|
35
|
+
if target.suffix not in SUFFIXES:
|
|
36
|
+
raise ValueError(f"Unsupported source file: {target}")
|
|
37
|
+
files.add(target)
|
|
38
|
+
continue
|
|
39
|
+
for root, directory_names, file_names in os.walk(target):
|
|
40
|
+
directory_names[:] = sorted(
|
|
41
|
+
name
|
|
42
|
+
for name in directory_names
|
|
43
|
+
if not any(fnmatchcase(name, pattern) for pattern in IGNORED_DIRECTORIES)
|
|
44
|
+
)
|
|
45
|
+
directory = Path(root).resolve()
|
|
46
|
+
source_names = sorted(name for name in file_names if Path(name).suffix in SUFFIXES)
|
|
47
|
+
files.update(directory / name for name in source_names)
|
|
48
|
+
if directory not in seen_directories and len(source_names) > max_files:
|
|
49
|
+
issues.append(
|
|
50
|
+
Issue(directory, 1, "PY008", f"Directory has {len(source_names)} Python files (max {max_files}).")
|
|
51
|
+
)
|
|
52
|
+
seen_directories.add(directory)
|
|
53
|
+
return sorted(files), sorted(issues)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nk-python-style
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python formatter and linter following the repository's conventions
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: ruff<1,>=0.16
|
|
8
|
+
Provides-Extra: comments
|
|
9
|
+
Requires-Dist: lingua-language-detector<3,>=2.2; extra == "comments"
|
|
10
|
+
|
|
11
|
+
# Python : linter et formatter
|
|
12
|
+
|
|
13
|
+
Cette version garde les limites générales du projet C++ et confie les règles Python courantes à Ruff. Elle utilise l'AST et `tokenize` pour distinguer code, chaînes, docstrings et commentaires.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Depuis ce dépôt :
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python -m pip install -e ./python
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Pour activer la détection de la langue des commentaires, installer l'extra optionnel (Lingua est volumineux) :
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python -m pip install -e "./python[comments]"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Depuis un autre projet, une installation non éditable fonctionne aussi : `python -m pip install <chemin-vers-ce-dépôt>/python`. La commande `nk-python` est ensuite disponible dans le même environnement Python. On peut également utiliser `python -m nk_python`.
|
|
30
|
+
|
|
31
|
+
## Utilisation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
nk-python check Src Tests # Ruff et règles propres, sans écriture
|
|
35
|
+
nk-python format Src Tests # trie les imports et formate en place
|
|
36
|
+
nk-python check fichier.py # fichier précis
|
|
37
|
+
nk-python check . --max-files 10 # seuil de dossier personnalisé
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Sans chemin, la commande traite le dossier courant. Les sous-dossiers de cache, d'environnement virtuel et de construction sont ignorés. Cela inclut tous les dossiers dont le nom commence par `.venv`, comme `.venv-cardd` et `.venv-vision`. `check` contrôle **aussi** le formatage Ruff et retourne un code non nul si une règle échoue. `format` applique le tri des imports Ruff et `ruff format`; relancer `check` pour les diagnostics qui ne se corrigent pas automatiquement.
|
|
41
|
+
|
|
42
|
+
Les diagnostics s'affichent sans extraits de code, avec des chemins relatifs au dossier courant.
|
|
43
|
+
La sortie de `check` commence par un résumé : fichiers analysés, nombre de warnings du linter (Ruff et règles `PY…`), problèmes d'ordre des imports et fichiers à reformater. Elle présente ensuite les diagnostics du linter, puis une section formatter à la fin : ordre des imports et fichiers à reformater. Cette dernière correspond aux corrections appliquées par `format`. Les autres corrections automatiques proposées par Ruff ne sont pas appliquées par cette commande.
|
|
44
|
+
|
|
45
|
+
## Règles
|
|
46
|
+
|
|
47
|
+
| Code | Contrôle |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `PY001` | Ligne de 120 caractères maximum, commentaires compris. |
|
|
50
|
+
| `PY002` | Fichier de 120 lignes maximum. |
|
|
51
|
+
| `PY003` | Fonction ou méthode de 40 lignes maximum, signature comprise. |
|
|
52
|
+
| `PY004` | Cinq paramètres maximum ; `self` ou `cls` est exclu pour les méthodes. |
|
|
53
|
+
| `PY005` | Commentaire de fin de ligne interdit, sauf directive d'outil. |
|
|
54
|
+
| `PY006` | Commentaire dans une fonction interdit, sauf directive d'outil ; utiliser une docstring ou clarifier le code. |
|
|
55
|
+
| `PY007` | Commentaire qui semble non anglais ; actif si l'extra `comments` est installé. Les textes trop courts ou ambigus sont ignorés. |
|
|
56
|
+
| `PY008` | Huit fichiers `.py`/`.pyi` directs maximum dans un dossier parcouru. |
|
|
57
|
+
|
|
58
|
+
Ruff vérifie en plus les erreurs Python (`E4`, `E7`, `E9`, `F`), l'ordre des imports (`I`), les erreurs fréquentes (`B`), certaines modernisations sûres (`UP`) et la variable créée uniquement pour être retournée (`RET504`). Sa configuration est fournie par l'outil, donc les mêmes règles s'appliquent depuis un autre projet. Le formatter impose une indentation de quatre espaces, des guillemets doubles et des fins de ligne LF. Les règles C++ propres aux accolades, `#include`, modules et initialiseurs désignés ne s'appliquent pas à Python.
|
|
59
|
+
|
|
60
|
+
Les directives usuelles (`# noqa`, `# type: ignore`, `# pylint:`, `# fmt:`, `# ruff:`, `# isort:` et `# pragma: no cover`) restent permises. Les docstrings ne sont pas des commentaires `#` et restent autorisées dans les fonctions.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
nk_python/__init__.py,sha256=Bzh-uQ6uWnIE5R-1opQqMpR6NfWELL4XjX2y_nR46qc,74
|
|
2
|
+
nk_python/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
|
|
3
|
+
nk_python/ast_checks.py,sha256=auhtwbCkcdzU0FmBwI_NOAMIZ6SPtdlpvbsV4s_mmNs,3139
|
|
4
|
+
nk_python/cli.py,sha256=4wL23RUARVZBz8sfsJ7z-LKFLoR2G6LOHw_41giS7Mg,4217
|
|
5
|
+
nk_python/comment_checks.py,sha256=GDJ0QzlbGLp5ulQ6cgnM_5ixpCkbG8EFeK7DixvVnCM,3051
|
|
6
|
+
nk_python/model.py,sha256=qSwiwnbXA4eWGkReSe74QBq_L2YJM6yVkd-g-AjNjLg,298
|
|
7
|
+
nk_python/ruff.toml,sha256=dkjak3D4VS6hnGcKXzDaqQ5CpA723Xbp7lEDV7Y5cYI,320
|
|
8
|
+
nk_python/ruff_runner.py,sha256=v_aLQVajD56Z51exlVvmUlLXOgJOSDfBZmD93dcbcd4,1053
|
|
9
|
+
nk_python/structure_checks.py,sha256=HCbm_g01i0g3vzGl_cDZu0tcMsoMKtjtj-DZVx-tqAw,1748
|
|
10
|
+
nk_python_style-0.1.0.dist-info/METADATA,sha256=rdKc_ry8CHQcmkh7W4-r_gFDp-naS5WNFydGVBAyLhM,4056
|
|
11
|
+
nk_python_style-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
12
|
+
nk_python_style-0.1.0.dist-info/entry_points.txt,sha256=rjz1wN9VFSj7FcgYsnWZxqoIk-UpzyfMx-_t7U39Rnc,49
|
|
13
|
+
nk_python_style-0.1.0.dist-info/top_level.txt,sha256=pGStA10dDoyVVsTrGpFRzp_cSnrOWUDP4ZNeEfWTd8E,10
|
|
14
|
+
nk_python_style-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nk_python
|