transformers-mlinter 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.
mlinter/__init__.py ADDED
@@ -0,0 +1,105 @@
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Supported public API for transformers-mlinter.
16
+
17
+ Import public helpers from ``mlinter`` instead of implementation modules such as
18
+ ``mlinter.mlinter`` or ``mlinter._helpers``.
19
+ """
20
+
21
+ from ._helpers import (
22
+ MODELS_ROOT,
23
+ Violation,
24
+ full_name,
25
+ is_self_method_call,
26
+ is_super_method_call,
27
+ iter_pretrained_classes,
28
+ )
29
+ from ._helpers import (
30
+ _collect_class_bases as collect_class_bases,
31
+ )
32
+ from ._helpers import (
33
+ _has_rule_suppression as has_rule_suppression,
34
+ )
35
+ from ._helpers import (
36
+ _inherits_pretrained_model as inherits_pretrained_model,
37
+ )
38
+ from ._helpers import (
39
+ _model_dir_name as model_dir_name,
40
+ )
41
+ from ._version import __version__
42
+ from .mlinter import (
43
+ DEFAULT_ENABLED_TRF_RULES,
44
+ TRF_MODEL_DIR_ALLOWLISTS,
45
+ TRF_RULE_CHECKS,
46
+ TRF_RULE_SPECS,
47
+ TRF_RULES,
48
+ analyze_file,
49
+ colored_error_message,
50
+ emit_violation,
51
+ format_rule_details,
52
+ format_rule_summary,
53
+ format_violation,
54
+ get_changed_modeling_files,
55
+ iter_modeling_files,
56
+ main,
57
+ maybe_handle_rule_docs_cli,
58
+ parse_args,
59
+ render_rules_reference,
60
+ resolve_enabled_rules,
61
+ should_show_progress,
62
+ )
63
+ from .mlinter import (
64
+ _is_rule_allowlisted_for_file as is_rule_allowlisted_for_file,
65
+ )
66
+
67
+
68
+ __all__ = [
69
+ "DEFAULT_ENABLED_TRF_RULES",
70
+ "MODELS_ROOT",
71
+ "TRF_MODEL_DIR_ALLOWLISTS",
72
+ "TRF_RULE_CHECKS",
73
+ "TRF_RULE_SPECS",
74
+ "TRF_RULES",
75
+ "Violation",
76
+ "__version__",
77
+ "analyze_file",
78
+ "collect_class_bases",
79
+ "colored_error_message",
80
+ "emit_violation",
81
+ "format_rule_details",
82
+ "format_rule_summary",
83
+ "format_violation",
84
+ "full_name",
85
+ "get_changed_modeling_files",
86
+ "has_rule_suppression",
87
+ "inherits_pretrained_model",
88
+ "is_rule_allowlisted_for_file",
89
+ "is_self_method_call",
90
+ "is_super_method_call",
91
+ "iter_modeling_files",
92
+ "iter_pretrained_classes",
93
+ "main",
94
+ "maybe_handle_rule_docs_cli",
95
+ "model_dir_name",
96
+ "parse_args",
97
+ "render_rules_reference",
98
+ "resolve_enabled_rules",
99
+ "should_show_progress",
100
+ ]
101
+
102
+ for rule_id in sorted(TRF_RULE_CHECKS):
103
+ globals()[rule_id] = rule_id
104
+ __all__.append(rule_id)
105
+ del rule_id
mlinter/__main__.py ADDED
@@ -0,0 +1,18 @@
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from . import main
16
+
17
+
18
+ raise SystemExit(main())
mlinter/_helpers.py ADDED
@@ -0,0 +1,194 @@
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Shared AST helper functions used across mlinter rule modules."""
16
+
17
+ import ast
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+
21
+
22
+ MODELS_ROOT = Path("src/transformers/models")
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Violation:
27
+ file_path: Path
28
+ line_number: int
29
+ message: str
30
+ rule_id: str | None = None
31
+
32
+
33
+ def full_name(node: ast.AST):
34
+ """Return full dotted name from an Attribute or Name node."""
35
+ if isinstance(node, ast.Name):
36
+ return node.id
37
+ elif isinstance(node, ast.Attribute):
38
+ return full_name(node.value) + "." + node.attr
39
+ else:
40
+ raise ValueError("Not a Name or Attribute node")
41
+
42
+
43
+ def _simple_name(name: str) -> str:
44
+ return name.split(".")[-1]
45
+
46
+
47
+ def _model_dir_name(file_path: Path) -> str | None:
48
+ try:
49
+ relative = file_path.resolve().relative_to(MODELS_ROOT.resolve())
50
+ except ValueError:
51
+ try:
52
+ relative = file_path.relative_to(MODELS_ROOT)
53
+ except ValueError:
54
+ return None
55
+ if len(relative.parts) < 2:
56
+ return None
57
+ return relative.parts[0]
58
+
59
+
60
+ def _known_model_dirs() -> set[str]:
61
+ return {path.name for path in MODELS_ROOT.iterdir() if path.is_dir()}
62
+
63
+
64
+ def _has_rule_suppression(lines: list[str], rule_id: str, line_number: int) -> bool:
65
+ if line_number <= 0:
66
+ return False
67
+ token = f"trf-ignore: {rule_id}".lower()
68
+ candidate_indexes = (line_number - 1, line_number - 2)
69
+ for idx in candidate_indexes:
70
+ if 0 <= idx < len(lines) and token in lines[idx].lower():
71
+ return True
72
+ return False
73
+
74
+
75
+ def _collect_class_bases(tree: ast.Module) -> dict[str, list[str]]:
76
+ class_to_bases: dict[str, list[str]] = {}
77
+ for node in tree.body:
78
+ if not isinstance(node, ast.ClassDef):
79
+ continue
80
+ base_names = []
81
+ for base in node.bases:
82
+ try:
83
+ base_names.append(full_name(base))
84
+ except ValueError:
85
+ continue
86
+ class_to_bases[node.name] = base_names
87
+ return class_to_bases
88
+
89
+
90
+ def _inherits_pretrained_model(
91
+ class_name: str, class_to_bases: dict[str, list[str]], visiting: set[str] | None = None
92
+ ) -> bool:
93
+ if visiting is None:
94
+ visiting = set()
95
+ if class_name in visiting:
96
+ return False
97
+ visiting.add(class_name)
98
+
99
+ for base_name in class_to_bases.get(class_name, []):
100
+ simple_base_name = _simple_name(base_name)
101
+ if simple_base_name.endswith("PreTrainedModel"):
102
+ return True
103
+ if simple_base_name in class_to_bases and _inherits_pretrained_model(
104
+ simple_base_name, class_to_bases, visiting
105
+ ):
106
+ return True
107
+ return False
108
+
109
+
110
+ def iter_pretrained_classes(tree: ast.Module, source_lines: list[str], rule_id: str) -> list[ast.ClassDef]:
111
+ """Yield ClassDef nodes that inherit from PreTrainedModel (transitively), skipping suppressed ones."""
112
+ class_to_bases = _collect_class_bases(tree)
113
+ results = []
114
+ for node in tree.body:
115
+ if not isinstance(node, ast.ClassDef):
116
+ continue
117
+ if not _inherits_pretrained_model(node.name, class_to_bases):
118
+ continue
119
+ if _has_rule_suppression(source_lines, rule_id, node.lineno):
120
+ continue
121
+ results.append(node)
122
+ return results
123
+
124
+
125
+ def _get_class_assignments(class_node: ast.ClassDef) -> dict[str, ast.AST]:
126
+ assignments: dict[str, ast.AST] = {}
127
+ for item in class_node.body:
128
+ if isinstance(item, ast.Assign) and len(item.targets) == 1 and isinstance(item.targets[0], ast.Name):
129
+ assignments[item.targets[0].id] = item.value
130
+ elif isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name) and item.value is not None:
131
+ assignments[item.target.id] = item.value
132
+ return assignments
133
+
134
+
135
+ def _class_methods(class_node: ast.ClassDef) -> dict[str, ast.FunctionDef]:
136
+ return {item.name: item for item in class_node.body if isinstance(item, ast.FunctionDef)}
137
+
138
+
139
+ def _function_argument_names(function_node: ast.FunctionDef) -> set[str]:
140
+ names = {arg.arg for arg in function_node.args.args}
141
+ names.update(arg.arg for arg in function_node.args.kwonlyargs)
142
+ if function_node.args.vararg is not None:
143
+ names.add(function_node.args.vararg.arg)
144
+ if function_node.args.kwarg is not None:
145
+ names.add(function_node.args.kwarg.arg)
146
+ return names
147
+
148
+
149
+ def _function_uses_name(function_node: ast.FunctionDef, name: str) -> bool:
150
+ return any(
151
+ isinstance(node, ast.Name) and node.id == name and isinstance(node.ctx, ast.Load)
152
+ for node in ast.walk(function_node)
153
+ )
154
+
155
+
156
+ def is_self_method_call(node: ast.AST, method: str) -> bool:
157
+ """Check if `node` is a method call on `self`, such as `self.method(...)`"""
158
+ return (
159
+ isinstance(node, ast.Call)
160
+ and isinstance(node.func, ast.Attribute)
161
+ and isinstance(node.func.value, ast.Name)
162
+ and node.func.value.id == "self"
163
+ and node.func.attr == method
164
+ )
165
+
166
+
167
+ def is_super_method_call(node: ast.AST, method: str) -> bool:
168
+ """Check if `node` is a call to `super().method(...)`"""
169
+ return (
170
+ isinstance(node, ast.Call)
171
+ and isinstance(node.func, ast.Attribute)
172
+ and isinstance(node.func.value, ast.Call)
173
+ and isinstance(node.func.value.func, ast.Name)
174
+ and node.func.value.func.id == "super"
175
+ and node.func.attr == method
176
+ )
177
+
178
+
179
+ def _is_direct_pretrained_config_subclass(class_node: ast.ClassDef) -> bool:
180
+ for base in class_node.bases:
181
+ try:
182
+ if _simple_name(full_name(base)) in {"PreTrainedConfig", "PretrainedConfig"}:
183
+ return True
184
+ except ValueError:
185
+ continue
186
+ return False
187
+
188
+
189
+ def _has_strict_decorator(class_node: ast.ClassDef) -> bool:
190
+ for decorator in class_node.decorator_list:
191
+ if isinstance(decorator, ast.Name) and decorator.id == "strict":
192
+ return True
193
+
194
+ return False
mlinter/_version.py ADDED
@@ -0,0 +1,127 @@
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import subprocess
17
+ from importlib.metadata import PackageNotFoundError, distribution
18
+ from pathlib import Path
19
+
20
+
21
+ try:
22
+ import tomllib # Python >= 3.11
23
+ except ModuleNotFoundError:
24
+ import tomli as tomllib # Python 3.10 fallback
25
+
26
+
27
+ PACKAGE_NAME = "transformers-mlinter"
28
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
29
+ DEFAULT_BASE_VERSION = "0.1.0"
30
+
31
+
32
+ def _read_version_from_pyproject() -> str | None:
33
+ pyproject_path = PROJECT_ROOT / "pyproject.toml"
34
+ try:
35
+ data = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
36
+ except (FileNotFoundError, OSError, tomllib.TOMLDecodeError):
37
+ return None
38
+
39
+ project = data.get("project")
40
+ if not isinstance(project, dict):
41
+ return None
42
+
43
+ version = project.get("version")
44
+ if not isinstance(version, str) or not version.strip():
45
+ return None
46
+ return version
47
+
48
+
49
+ def _installed_distribution():
50
+ try:
51
+ return distribution(PACKAGE_NAME)
52
+ except PackageNotFoundError:
53
+ return None
54
+
55
+
56
+ def _short_commit_id(commit_id: str | None) -> str | None:
57
+ if commit_id is None:
58
+ return None
59
+
60
+ commit_id = commit_id.strip()
61
+ if not commit_id:
62
+ return None
63
+ return commit_id[:7]
64
+
65
+
66
+ def _read_git_hash_from_direct_url(dist) -> str | None:
67
+ if dist is None:
68
+ return None
69
+
70
+ direct_url = dist.read_text("direct_url.json")
71
+ if not direct_url:
72
+ return None
73
+
74
+ try:
75
+ direct_url_data = json.loads(direct_url)
76
+ except json.JSONDecodeError:
77
+ return None
78
+
79
+ vcs_info = direct_url_data.get("vcs_info")
80
+ if not isinstance(vcs_info, dict) or vcs_info.get("vcs") != "git":
81
+ return None
82
+
83
+ commit_id = vcs_info.get("commit_id")
84
+ if not isinstance(commit_id, str):
85
+ return None
86
+ return _short_commit_id(commit_id)
87
+
88
+
89
+ def _read_git_hash_from_checkout() -> str | None:
90
+ if not (PROJECT_ROOT / ".git").exists():
91
+ return None
92
+
93
+ try:
94
+ result = subprocess.run(
95
+ ["git", "-C", str(PROJECT_ROOT), "rev-parse", "--short", "HEAD"],
96
+ capture_output=True,
97
+ check=False,
98
+ text=True,
99
+ )
100
+ except OSError:
101
+ return None
102
+
103
+ if result.returncode != 0:
104
+ return None
105
+ return _short_commit_id(result.stdout)
106
+
107
+
108
+ def _append_git_hash(base_version: str, git_hash: str | None) -> str:
109
+ if git_hash is None or "+" in base_version:
110
+ return base_version
111
+ return f"{base_version}+g{git_hash}"
112
+
113
+
114
+ def _resolve_base_version(dist) -> str:
115
+ if dist is not None:
116
+ return dist.version
117
+ return _read_version_from_pyproject() or DEFAULT_BASE_VERSION
118
+
119
+
120
+ def _resolve_version() -> str:
121
+ dist = _installed_distribution()
122
+ base_version = _resolve_base_version(dist)
123
+ git_hash = _read_git_hash_from_direct_url(dist) or _read_git_hash_from_checkout()
124
+ return _append_git_hash(base_version, git_hash)
125
+
126
+
127
+ __version__ = _resolve_version()