@@ -515,6 +515,15 @@ def analyze_all_modules(self, verbose: bool = True) -> Dict[str, Any]:
515515
516516 return results
517517
518+ def _is_file_path (self , path : str ) -> bool :
519+ """Check if a path looks like a file (has an extension) vs a directory."""
520+ basename = os .path .basename (path )
521+ return "." in basename and not basename .startswith ("." )
522+
523+ def _normalize_usernames (self , users : List [str ]) -> List [str ]:
524+ """Normalize usernames to have @ prefix."""
525+ return [f"@{ u } " if not u .startswith ("@" ) else u for u in users ]
526+
518527 def _merge_owners_with_overrides (
519528 self , module : str , computed_usernames : List [str ]
520529 ) -> List [str ]:
@@ -523,6 +532,9 @@ def _merge_owners_with_overrides(
523532 Override users are prepended to the list (indicating primary ownership),
524533 and duplicates are removed. The final list is limited to top_n_owners.
525534
535+ Note: File-level overrides are handled separately in generate_codeowners_file(),
536+ since there's no computed ownership for individual files.
537+
526538 Args:
527539 module: The module path (e.g., "flashinfer/fused_moe")
528540 computed_usernames: List of GitHub usernames from git history analysis
@@ -534,15 +546,17 @@ def _merge_owners_with_overrides(
534546 if not self .owner_overrides :
535547 return computed_usernames [: self .top_n_owners ]
536548
549+ # Skip file-level overrides (handled separately)
550+ if self ._is_file_path (module ):
551+ return computed_usernames [: self .top_n_owners ]
552+
537553 # Get override users for this module (without @ prefix in the config)
538554 override_users = self .owner_overrides .get (module , [])
539555 if not override_users :
540556 return computed_usernames [: self .top_n_owners ]
541557
542558 # Normalize override users to have @ prefix
543- override_usernames = [
544- f"@{ u } " if not u .startswith ("@" ) else u for u in override_users
545- ]
559+ override_usernames = self ._normalize_usernames (override_users )
546560
547561 # Build merged list: overrides first, then computed (excluding duplicates)
548562 merged = list (override_usernames )
@@ -565,6 +579,21 @@ def generate_codeowners_file(
565579 f .write ("# Manual overrides applied from overrides file\n " )
566580 f .write ("\n " )
567581
582+ # Write file-level overrides first (these are not merged with computed)
583+ if self .owner_overrides :
584+ file_overrides = [
585+ (path , users )
586+ for path , users in self .owner_overrides .items ()
587+ if self ._is_file_path (path )
588+ ]
589+ if file_overrides :
590+ f .write ("# File-level overrides\n " )
591+ for path , users in sorted (file_overrides ):
592+ usernames = self ._normalize_usernames (users )
593+ f .write (f"{ path } { ' ' .join (usernames )} \n " )
594+ f .write ("\n " )
595+
596+ # Write directory entries (computed + merged overrides)
568597 for module , data in results .items ():
569598 # Extract GitHub usernames from computed owners
570599 computed_usernames = []
0 commit comments