Repository: huggingface/transformers
Pull request: model-linter: Added rule 10

# What does this PR do?

This adds rule 10:  

```
Direct config definitions must use @strict(accept_kwargs=True).
```

Changed files:
filename: tests/repo_utils/test_check_modeling_structure.py
patch: @@ -249,10 +249,63 @@ def test_trf009_ignores_modular_files(self):
         trf009 = [v for v in violations if v.rule_id == cms.TRF009]
         self.assertEqual(trf009, [])
 
+    # --- TRF010: strict config decorator ---
+
+    def test_trf010_allows_direct_config_with_strict(self):
+        source = """
+from huggingface_hub.dataclasses import strict
+
+@strict(accept_kwargs=True)
+class FooConfig(PretrainedConfig):
+    pass
+"""
+        file_path = Path("src/transformers/models/foo/configuration_foo.py")
+        violations = cms.analyze_file(file_path, source, enabled_rules={cms.TRF010})
+        trf010 = [v for v in violations if v.rule_id == cms.TRF010]
+        self.assertEqual(trf010, [])
+
+    def test_trf010_flags_missing_strict_on_direct_config(self):
+        source = """
+class FooConfig(PretrainedConfig):
+    pass
+"""
+        file_path = Path("src/transformers/models/foo/configuration_foo.py")
+        violations = cms.analyze_file(file_path, source, enabled_rules={cms.TRF010})
+        trf010 = [v for v in violations if v.rule_id == cms.TRF010]
+        self.assertEqual(len(trf010), 1)
+        self.assertIn("missing @strict(accept_kwargs=True)", trf010[0].message)
+
+    def test_trf010_ignores_non_direct_config_alias_wrappers(self):
+        source = """
+from huggingface_hub.dataclasses import strict
+
+@strict(accept_kwargs=True)
+class FooConfig(PretrainedConfig):
+    pass
+
+class FooCompatConfig(FooConfig):
+    pass
+"""
+        file_path = Path("src/transformers/models/foo/configuration_foo.py")
+        violations = cms.analyze_file(file_path, source, enabled_rules={cms.TRF010})
+        trf010 = [v for v in violations if v.rule_id == cms.TRF010]
+        self.assertEqual(trf010, [])
+
     # --- Utility tests ---
 
+    def test_analyze_file_allows_subscripted_class_bases(self):
+        source = """
+from collections import OrderedDict
+
+class _LazyConfigMapping(OrderedDict[str, str]):
+    pass
+"""
+        file_path = Path("src/transformers/models/auto/configuration_auto.py")
+        violations = cms.analyze_file(file_path, source)
+        self.assertEqual(violations, [])
+
     @patch("check_modeling_structure.subprocess.run")
-    def test_get_changed_modeling_files_filters_non_model_files(self, mock_run):
+    def test_get_changed_modeling_files_includes_configuration_files(self, mock_run):
         mock_run.side_effect = [
             subprocess.CompletedProcess(
                 args=["git", "diff"],
@@ -275,6 +328,7 @@ def test_get_changed_modeling_files_filters_non_model_files(self, mock_run):
             {
                 Path("src/transformers/models/foo/modeling_foo.py"),
                 Path("src/transformers/models/foo/modular_foo.py"),
+                Path("src/transformers/models/foo/configuration_foo.py"),
             },
         )
 

filename: utils/check_modeling_structure.py
patch: @@ -13,7 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """
-Lint modeling and modular files under ``src/transformers/models`` for structural conventions.
+Lint modeling, modular, and configuration files under ``src/transformers/models`` for structural conventions.
 
 How rule registration works
 ---------------------------
@@ -38,9 +38,9 @@
 
 CLI usage
 ---------
-- ``python utils/check_modeling_structure.py``: check all modeling and modular files.
+- ``python utils/check_modeling_structure.py``: check all modeling, modular, and configuration files.
 - ``python utils/check_modeling_structure.py --changed-only --base-ref origin/main``: only check files changed
-  against a git base ref, plus local staged, unstaged, and untracked modeling files.
+  against a git base ref, plus local staged, unstaged, and untracked model source files.
 - ``python utils/check_modeling_structure.py --list-rules``: print all available TRF rules and their default state.
 - ``python utils/check_modeling_structure.py --rule TRF001``: show the detailed documentation for one rule from the
   TOML file.
@@ -70,7 +70,7 @@
 
 
 MODELS_ROOT = Path("src/transformers/models")
-MODELING_PATTERNS = ("modeling_*.py", "modular_*.py")
+MODELING_PATTERNS = ("modeling_*.py", "modular_*.py", "configuration_*.py")
 RULE_SPECS_PATH = Path(__file__).with_name("check_modeling_structure_rules.toml")
 
 
@@ -267,7 +267,16 @@ def check_post_init(node: ast.AST, violations: list[Violation], file_path: Path)
     very important as we need to do some processing there.
     """
     # Check if it's a PreTrainedModel class definition
-    if isinstance(node, ast.ClassDef) and any(full_name(parent).endswith("PreTrainedModel") for parent in node.bases):
+    if isinstance(node, ast.ClassDef):
+        base_names = []
+        for parent in node.bases:
+            try:
+                base_names.append(full_name(parent))
+            except ValueError:
+                continue
+        if not any(base_name.endswith("PreTrainedModel") for base_name in base_names):
+            return violations
+
         for sub_node in node.body:
             # Check that we are in __init__
             if isinstance(sub_node, ast.FunctionDef) and sub_node.name == "__init__":
@@ -336,6 +345,35 @@ def _class_methods(class_node: ast.ClassDef) -> dict[str, ast.FunctionDef]:
     return {item.name: item for item in class_node.body if isinstance(item, ast.FunctionDef)}
 
 
+def _is_direct_pretrained_config_subclass(class_node: ast.ClassDef) -> bool:
+    for base in class_node.bases:
+        try:
+            if _simple_name(full_name(base)) in {"PreTrainedConfig", "PretrainedConfig"}:
+                return True
+        except ValueError:
+            continue
+    return False
+
+
+def _has_strict_accept_kwargs_decorator(class_node: ast.ClassDef) -> bool:
+    for decorator in class_node.decorator_list:
+        if not isinstance(decorator, ast.Call):
+            continue
+        try:
+            decorator_name = _simple_name(full_name(decorator.func))
+        except ValueError:
+            continue
+        if decorator_name != "strict" or decorator.args:
+            continue
+
+        for keyword in decorator.keywords:
+            if keyword.arg != "accept_kwargs":
+                continue
+            return isinstance(keyword.value, ast.Constant) and keyword.value.value is True
+
+    return False
+
+
 def trf001_check_config_class_consistency(
     tree: ast.Module, file_path: Path, source_lines: list[str], violations: list[Violation], rule_id: str
 ) -> list[Violation]:
@@ -748,8 +786,43 @@ def trf008_check_doc_decorator_usage(
     return violations
 
 
+def trf010_check_config_strict_decorator(
+    tree: ast.Module, file_path: Path, source_lines: list[str], violations: list[Violation], rule_id: str
+) -> list[Violation]:
+    if not file_path.name.startswith(("configuration_", "modular_")):
+        return violations
+
+    for node in tree.body:
+        if not isinstance(node, ast.ClassDef):
+            continue
+        if not _is_direct_pretrained_config_subclass(node):
+            continue
+        if _has_rule_suppression(source_lines, rule_id, node.lineno):
+            continue
+        if _has_strict_accept_kwargs_decorator(node):
+            continue
+
+        violations.append(
+            Violation(
+                file_path=file_path,
+                line_number=node.lineno,
+                rule_id=rule_id,
+                message=(
+                    f"{rule_id}: {node.name} directly inherits PreTrainedConfig but is missing "
+                    "@strict(accept_kwargs=True)."
+                ),
+            )
+        )
+
+    return violations
+
+
 def _is_modeling_candidate(path: Path) -> bool:
-    return path.suffix == ".py" and path.name.startswith(("modeling_", "modular_")) and MODELS_ROOT in path.parents
+    return (
+        path.suffix == ".py"
+        and path.name.startswith(("modeling_", "modular_", "configuration_"))
+        and MODELS_ROOT in path.parents
+    )
 
 
 def _git_name_only(command: list[str]) -> list[str]:

filename: utils/check_modeling_structure_rules.toml
patch: @@ -157,3 +157,21 @@ good_example = '''
 # Keep implementation local in this modeling file.
 # If code is reused, copy it with an appropriate # Copied from ... statement.
 '''
+
+[rules.TRF010]
+description = "Direct config definitions must use @strict(accept_kwargs=True)."
+default_enabled = true
+allowlist_models = ["nemotron_h", "vibevoice_asr"]
+
+[rules.TRF010.explanation]
+what_it_does = "Checks direct PreTrainedConfig/PretrainedConfig subclasses in configuration_*.py and modular_*.py for an explicit @strict(accept_kwargs=True) decorator."
+why_bad = "Without strict, new config classes miss the repo's runtime type-validation contract and drift from the dataclass-based config standard."
+bad_example = '''
+class FooConfig(PreTrainedConfig):
+    ...
+'''
+good_example = '''
+@strict(accept_kwargs=True)
+class FooConfig(PreTrainedConfig):
+    ...
+'''