| import argparse | |
| from typing import IO | |
| from typing import NamedTuple | |
| from typing import Optional | |
| from typing import Sequence | |
| DEFAULT_PRAGMA = b'# -*- coding: utf-8 -*-' | |
| def has_coding(line: bytes) -> bool: | |
| if not line.strip(): | |
| return False | |
| return ( | |
| line.lstrip()[:1] == b'#' and ( | |
| b'unicode' in line or | |
| b'encoding' in line or | |
| b'coding:' in line or | |
| b'coding=' in line | |
| ) | |
| ) | |
| class ExpectedContents(NamedTuple): | |
| shebang: bytes | |
| rest: bytes | |
| # True: has exactly the coding pragma expected | |
| # False: missing coding pragma entirely | |
| # None: has a coding pragma, but it does not match | |
| pragma_status: Optional[bool] | |
| ending: bytes | |
| @property | |
| def has_any_pragma(self) -> bool: | |
| return self.pragma_status is not False | |
| def is_expected_pragma(self, remove: bool) -> bool: | |
| expected_pragma_status = not remove | |
| return self.pragma_status is expected_pragma_status | |
| def _get_expected_contents( | |
| first_line: bytes, | |
| second_line: bytes, | |
| rest: bytes, | |
| expected_pragma: bytes, | |
| ) -> ExpectedContents: | |
| ending = b'\r\n' if first_line.endswith(b'\r\n') else b'\n' | |
| if first_line.startswith(b'#!'): | |
| shebang = first_line | |
| potential_coding = second_line | |
| else: | |
| shebang = b'' | |
| potential_coding = first_line | |
| rest = second_line + rest | |
| if potential_coding.rstrip(b'\r\n') == expected_pragma: | |
| pragma_status: Optional[bool] = True | |
| elif has_coding(potential_coding): | |
| pragma_status = None | |
| else: | |
| pragma_status = False | |
| rest = potential_coding + rest | |
| return ExpectedContents( | |
| shebang=shebang, rest=rest, pragma_status=pragma_status, ending=ending, | |
| ) | |
| def fix_encoding_pragma( | |
| f: IO[bytes], | |
| remove: bool = False, | |
| expected_pragma: bytes = DEFAULT_PRAGMA, | |
| ) -> int: | |
| expected = _get_expected_contents( | |
| f.readline(), f.readline(), f.read(), expected_pragma, | |
| ) | |
| # Special cases for empty files | |
| if not expected.rest.strip(): | |
| # If a file only has a shebang or a coding pragma, remove it | |
| if expected.has_any_pragma or expected.shebang: | |
| f.seek(0) | |
| f.truncate() | |
| f.write(b'') | |
| return 1 | |
| else: | |
| return 0 | |
| if expected.is_expected_pragma(remove): | |
| return 0 | |
| # Otherwise, write out the new file | |
| f.seek(0) | |
| f.truncate() | |
| f.write(expected.shebang) | |
| if not remove: | |
| f.write(expected_pragma + expected.ending) | |
| f.write(expected.rest) | |
| return 1 | |
| def _normalize_pragma(pragma: str) -> bytes: | |
| return pragma.encode().rstrip() | |
| def main(argv: Optional[Sequence[str]] = None) -> int: | |
| parser = argparse.ArgumentParser( | |
| 'Fixes the encoding pragma of python files', | |
| ) | |
| parser.add_argument('filenames', nargs='*', help='Filenames to fix') | |
| parser.add_argument( | |
| '--pragma', default=DEFAULT_PRAGMA, type=_normalize_pragma, | |
| help=( | |
| f'The encoding pragma to use. ' | |
| f'Default: {DEFAULT_PRAGMA.decode()}' | |
| ), | |
| ) | |
| parser.add_argument( | |
| '--remove', action='store_true', | |
| help='Remove the encoding pragma (Useful in a python3-only codebase)', | |
| ) | |
| args = parser.parse_args(argv) | |
| retv = 0 | |
| if args.remove: | |
| fmt = 'Removed encoding pragma from {filename}' | |
| else: | |
| fmt = 'Added `{pragma}` to {filename}' | |
| for filename in args.filenames: | |
| with open(filename, 'r+b') as f: | |
| file_ret = fix_encoding_pragma( | |
| f, remove=args.remove, expected_pragma=args.pragma, | |
| ) | |
| retv |= file_ret | |
| if file_ret: | |
| print( | |
| fmt.format(pragma=args.pragma.decode(), filename=filename), | |
| ) | |
| return retv | |
| if __name__ == '__main__': | |
| exit(main()) |