@@ -397,7 +397,7 @@ def show_file_lines(file_path: Path, num_lines: int = 10) -> str:
397397
398398
399399def process_file (file_path : Path , mode : str , authors : str , show_diff : bool = False , debug : bool = False ,
400- require_shebang : Optional [bool ] = None , require_encoding : bool = True ) -> Optional [Dict [str , Any ]]:
400+ require_shebang : Optional [bool ] = None , require_encoding : bool = True ) -> Optional [Dict [str , Any ]]:
401401 """Check a single file and optionally fix its header.
402402
403403 Args:
@@ -476,6 +476,8 @@ def process_file(file_path: Path, mode: str, authors: str, show_diff: bool = Fal
476476 location_match = re .search (r"^Location: \./(.*)$" , docstring_node , re .MULTILINE )
477477 if not location_match :
478478 issues .append ("Missing 'Location' line" )
479+ elif location_match .group (1 ) != relative_path_str :
480+ issues .append (f"Incorrect 'Location' line: expected './{ relative_path_str } ', found './{ location_match .group (1 )} '" )
479481
480482 if f"Copyright { COPYRIGHT_YEAR } " not in docstring_node :
481483 issues .append ("Missing 'Copyright' line" )
@@ -489,7 +491,8 @@ def process_file(file_path: Path, mode: str, authors: str, show_diff: bool = Fal
489491 if not issues :
490492 return None
491493
492- if mode in ["fix-all" , "fix" , "interactive" ]:
494+ # Generate new source code for diff preview or actual fixing
495+ if mode in ["fix-all" , "fix" , "interactive" ] or show_diff :
493496 # Extract the raw docstring from source
494497 if module_body and isinstance (module_body [0 ], ast .Expr ):
495498 docstring_expr_node = module_body [0 ]
@@ -500,38 +503,68 @@ def process_file(file_path: Path, mode: str, authors: str, show_diff: bool = Fal
500503 quotes = '"""' if raw_docstring .startswith ('"""' ) else "'''"
501504 inner_content = raw_docstring .strip (quotes )
502505
503- # Extract existing header fields and body
506+ # Extract existing header fields
504507 existing_header_fields = extract_header_info (source_code , inner_content )
505508
506- # Find where the header ends and body begins
509+ # Split docstring into lines for analysis
507510 docstring_lines = inner_content .strip ().splitlines ()
508- header_end_idx = 0
511+
512+ # Separate the docstring into header and content parts
513+ content_lines = []
514+ in_header_section = False
509515
510516 for i , line in enumerate (docstring_lines ):
511- if any (line .strip ().startswith (field + ":" ) for field in HEADER_FIELDS ):
512- header_end_idx = i + 1
513- elif header_end_idx > 0 and line .strip ():
514- # Found first non-header content
517+ line_stripped = line .strip ()
518+
519+ # Check if this line is a header field
520+ is_header_field = (any (line_stripped .startswith (field + ":" ) for field in HEADER_FIELDS ) or
521+ line_stripped .startswith ("Copyright" ))
522+
523+ if is_header_field :
524+ in_header_section = True
525+ elif in_header_section and not line_stripped :
526+ # Empty line might separate header from content - continue checking
527+ continue
528+ elif in_header_section and line_stripped and not is_header_field :
529+ # Found content after header section - this and everything after is content
530+ content_lines .extend (docstring_lines [i :])
515531 break
516-
517- # Extract body content
518- docstring_body_lines = docstring_lines [header_end_idx :]
519- if docstring_body_lines and not docstring_body_lines [0 ].strip ():
520- docstring_body_lines = docstring_body_lines [1 :]
532+ elif not in_header_section and line_stripped :
533+ # Content before any header section (like module descriptions)
534+ # Look ahead to see if there are headers following
535+ has_headers_following = any (
536+ any (future_line .strip ().startswith (field + ":" ) for field in HEADER_FIELDS ) or
537+ future_line .strip ().startswith ("Copyright" )
538+ for future_line in docstring_lines [i + 1 :]
539+ )
540+ if has_headers_following :
541+ # This is content, headers follow later
542+ content_lines .append (line )
543+ else :
544+ # No headers following, this is regular content
545+ content_lines .extend (docstring_lines [i :])
546+ break
521547
522548 # Build new header
523549 new_header_lines = []
524- new_header_lines .append (existing_header_fields .get ("Location" ) or f"Location: ./{ relative_path_str } " )
550+ # Always use correct location path
551+ new_header_lines .append (f"Location: ./{ relative_path_str } " )
525552 new_header_lines .append (existing_header_fields .get ("Copyright" ) or f"Copyright { COPYRIGHT_YEAR } " )
526553 new_header_lines .append (existing_header_fields .get ("SPDX-License-Identifier" ) or f"SPDX-License-Identifier: { LICENSE } " )
527- new_header_lines .append (f"Authors: { authors } " )
554+ # Preserve existing Authors field if it exists, otherwise use the provided authors
555+ new_header_lines .append (existing_header_fields .get ("Authors" ) or f"Authors: { authors } " )
528556
529- # Reconstruct docstring
557+ # Reconstruct docstring with preserved content
530558 new_inner_content = "\n " .join (new_header_lines )
531- if docstring_body_lines :
532- new_inner_content += "\n \n " + "\n " .join (docstring_body_lines ).strip ()
559+ if content_lines :
560+ content_str = "\n " .join (content_lines )
561+ new_inner_content += "\n \n " + content_str
562+
563+ # Ensure proper ending with newline before closing quotes
564+ if not new_inner_content .endswith ('\n ' ):
565+ new_inner_content += '\n '
533566
534- new_docstring = f"{ quotes } { new_inner_content . strip () } { quotes } "
567+ new_docstring = f"{ quotes } { new_inner_content } { quotes } "
535568
536569 # Prepare source with appropriate headers
537570 header_lines = []
@@ -562,7 +595,8 @@ def process_file(file_path: Path, mode: str, authors: str, show_diff: bool = Fal
562595 # No docstring found
563596 issues .append ("No docstring found" )
564597
565- if mode in ["fix-all" , "fix" , "interactive" ]:
598+ # Generate new source code for diff preview or actual fixing
599+ if mode in ["fix-all" , "fix" , "interactive" ] or show_diff :
566600 # Create new header
567601 new_header = get_header_template (
568602 relative_path_str ,
@@ -683,15 +717,15 @@ def parse_arguments(argv: Optional[List[str]] = None) -> argparse.Namespace:
683717 # Header configuration options
684718 header_group = parser .add_argument_group ("header configuration" )
685719 header_group .add_argument ("--require-shebang" , choices = ["always" , "never" , "auto" ], default = "auto" ,
686- help = "Require shebang line: 'always', 'never', or 'auto' (only for executable files). Default: auto" )
720+ help = "Require shebang line: 'always', 'never', or 'auto' (only for executable files). Default: auto" )
687721 header_group .add_argument ("--require-encoding" , action = "store_true" , default = True ,
688- help = "Require encoding line. Default: True" )
722+ help = "Require encoding line. Default: True" )
689723 header_group .add_argument ("--no-encoding" , action = "store_false" , dest = "require_encoding" ,
690- help = "Don't require encoding line." )
724+ help = "Don't require encoding line." )
691725 header_group .add_argument ("--copyright-year" , type = int , default = COPYRIGHT_YEAR ,
692- help = f"Copyright year to use. Default: { COPYRIGHT_YEAR } " )
726+ help = f"Copyright year to use. Default: { COPYRIGHT_YEAR } " )
693727 header_group .add_argument ("--license" , type = str , default = LICENSE ,
694- help = f"License identifier to use. Default: { LICENSE } " )
728+ help = f"License identifier to use. Default: { LICENSE } " )
695729
696730 return parser .parse_args (argv )
697731
@@ -830,7 +864,7 @@ def print_results(issues_found: List[Dict[str, Any]], mode: str, modified_count:
830864 # Show debug info if available
831865 if "debug" in issue_info :
832866 debug = issue_info ["debug" ]
833- print (f " Debug info:" , file = sys .stderr )
867+ print (" Debug info:" , file = sys .stderr )
834868 print (f" Executable: { debug ['executable' ]} " , file = sys .stderr )
835869 print (f" Has shebang: { debug ['has_shebang' ]} " , file = sys .stderr )
836870 print (f" Has encoding: { debug ['has_encoding' ]} " , file = sys .stderr )
0 commit comments