Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 1 | from __future__ import annotations |
| 2 | |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 3 | import argparse |
Max Rozentsveyg | 7ebd420 | 2020-05-16 23:58:27 -0400 | [diff] [blame] | 4 | import re |
Anthony Sottile | 030bfac | 2019-01-31 19:19:10 -0800 | [diff] [blame] | 5 | from typing import IO |
Anthony Sottile | 030bfac | 2019-01-31 19:19:10 -0800 | [diff] [blame] | 6 | from typing import Sequence |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 7 | |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 8 | |
Daniel Gallagher | 844d983 | 2017-06-25 10:14:58 -0700 | [diff] [blame] | 9 | PASS = 0 |
| 10 | FAIL = 1 |
| 11 | |
| 12 | |
Anthony Sottile | f5c42a0 | 2020-02-05 11:10:42 -0800 | [diff] [blame] | 13 | class Requirement: |
Max Rozentsveyg | 7ebd420 | 2020-05-16 23:58:27 -0400 | [diff] [blame] | 14 | UNTIL_COMPARISON = re.compile(b'={2,3}|!=|~=|>=?|<=?') |
| 15 | UNTIL_SEP = re.compile(rb'[^;\s]+') |
| 16 | |
Anthony Sottile | f5c42a0 | 2020-02-05 11:10:42 -0800 | [diff] [blame] | 17 | def __init__(self) -> None: |
Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 18 | self.value: bytes | None = None |
| 19 | self.comments: list[bytes] = [] |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 20 | |
Vinay Karanam | c58ae08 | 2016-07-03 04:10:20 +0530 | [diff] [blame] | 21 | @property |
Anthony Sottile | f5c42a0 | 2020-02-05 11:10:42 -0800 | [diff] [blame] | 22 | def name(self) -> bytes: |
Anthony Sottile | 030bfac | 2019-01-31 19:19:10 -0800 | [diff] [blame] | 23 | assert self.value is not None, self.value |
Max Rozentsveyg | 7ebd420 | 2020-05-16 23:58:27 -0400 | [diff] [blame] | 24 | name = self.value.lower() |
Vinay Karanam | 189e33e | 2019-11-14 02:22:07 +0530 | [diff] [blame] | 25 | for egg in (b'#egg=', b'&egg='): |
| 26 | if egg in self.value: |
Max Rozentsveyg | 7ebd420 | 2020-05-16 23:58:27 -0400 | [diff] [blame] | 27 | return name.partition(egg)[-1] |
Vinay Karanam | c58ae08 | 2016-07-03 04:10:20 +0530 | [diff] [blame] | 28 | |
Max Rozentsveyg | 7ebd420 | 2020-05-16 23:58:27 -0400 | [diff] [blame] | 29 | m = self.UNTIL_SEP.match(name) |
| 30 | assert m is not None |
| 31 | |
| 32 | name = m.group() |
| 33 | m = self.UNTIL_COMPARISON.search(name) |
| 34 | if not m: |
| 35 | return name |
| 36 | |
| 37 | return name[:m.start()] |
Vinay Karanam | c58ae08 | 2016-07-03 04:10:20 +0530 | [diff] [blame] | 38 | |
Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 39 | def __lt__(self, requirement: Requirement) -> bool: |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 40 | # \n means top of file comment, so always return True, |
| 41 | # otherwise just do a string comparison with value. |
Anthony Sottile | 030bfac | 2019-01-31 19:19:10 -0800 | [diff] [blame] | 42 | assert self.value is not None, self.value |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 43 | if self.value == b'\n': |
| 44 | return True |
| 45 | elif requirement.value == b'\n': |
| 46 | return False |
| 47 | else: |
Vinay Karanam | c58ae08 | 2016-07-03 04:10:20 +0530 | [diff] [blame] | 48 | return self.name < requirement.name |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 49 | |
Aniket Bhatnagar | bbcd31e | 2020-05-07 21:02:12 +0100 | [diff] [blame] | 50 | def is_complete(self) -> bool: |
| 51 | return ( |
| 52 | self.value is not None and |
| 53 | not self.value.rstrip(b'\r\n').endswith(b'\\') |
| 54 | ) |
| 55 | |
| 56 | def append_value(self, value: bytes) -> None: |
| 57 | if self.value is not None: |
| 58 | self.value += value |
| 59 | else: |
| 60 | self.value = value |
| 61 | |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 62 | |
Anthony Sottile | f5c42a0 | 2020-02-05 11:10:42 -0800 | [diff] [blame] | 63 | def fix_requirements(f: IO[bytes]) -> int: |
Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 64 | requirements: list[Requirement] = [] |
Barak Y. Reif | e4cfaa6 | 2019-09-28 22:16:20 +0300 | [diff] [blame] | 65 | before = list(f) |
Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 66 | after: list[bytes] = [] |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 67 | |
Barak Y. Reif | d4b544d | 2019-09-28 21:59:41 +0300 | [diff] [blame] | 68 | before_string = b''.join(before) |
| 69 | |
Barak Y. Reif | da2ea3f | 2019-09-28 21:40:09 +0300 | [diff] [blame] | 70 | # adds new line in case one is missing |
| 71 | # AND a change to the requirements file is needed regardless: |
| 72 | if before and not before[-1].endswith(b'\n'): |
| 73 | before[-1] += b'\n' |
| 74 | |
Daniel Gallagher | 7ccfa05 | 2017-06-23 17:19:21 -0700 | [diff] [blame] | 75 | # If the file is empty (i.e. only whitespace/newlines) exit early |
| 76 | if before_string.strip() == b'': |
Daniel Gallagher | 844d983 | 2017-06-25 10:14:58 -0700 | [diff] [blame] | 77 | return PASS |
Daniel Gallagher | 7ccfa05 | 2017-06-23 17:19:21 -0700 | [diff] [blame] | 78 | |
| 79 | for line in before: |
| 80 | # If the most recent requirement object has a value, then it's |
| 81 | # time to start building the next requirement object. |
Anthony Sottile | 030bfac | 2019-01-31 19:19:10 -0800 | [diff] [blame] | 82 | |
Aniket Bhatnagar | bbcd31e | 2020-05-07 21:02:12 +0100 | [diff] [blame] | 83 | if not len(requirements) or requirements[-1].is_complete(): |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 84 | requirements.append(Requirement()) |
| 85 | |
| 86 | requirement = requirements[-1] |
| 87 | |
Daniel Gallagher | 7ccfa05 | 2017-06-23 17:19:21 -0700 | [diff] [blame] | 88 | # If we see a newline before any requirements, then this is a |
| 89 | # top of file comment. |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 90 | if len(requirements) == 1 and line.strip() == b'': |
Anthony Sottile | 4575652 | 2019-02-11 19:56:15 -0800 | [diff] [blame] | 91 | if ( |
| 92 | len(requirement.comments) and |
| 93 | requirement.comments[0].startswith(b'#') |
| 94 | ): |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 95 | requirement.value = b'\n' |
| 96 | else: |
| 97 | requirement.comments.append(line) |
Viacheslav Greshilov | 28b2c8e | 2021-01-17 21:15:05 +0200 | [diff] [blame] | 98 | elif line.lstrip().startswith(b'#') or line.strip() == b'': |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 99 | requirement.comments.append(line) |
| 100 | else: |
Aniket Bhatnagar | bbcd31e | 2020-05-07 21:02:12 +0100 | [diff] [blame] | 101 | requirement.append_value(line) |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 102 | |
Anthony Sottile | 86691ed | 2017-10-09 10:59:17 -0700 | [diff] [blame] | 103 | # if a file ends in a comment, preserve it at the end |
| 104 | if requirements[-1].value is None: |
| 105 | rest = requirements.pop().comments |
| 106 | else: |
| 107 | rest = [] |
| 108 | |
Michał Sochoń | 9e28aaf | 2018-03-26 00:02:23 +0200 | [diff] [blame] | 109 | # find and remove pkg-resources==0.0.0 |
| 110 | # which is automatically added by broken pip package under Debian |
Michał Sochoń | b0d44c7 | 2018-03-26 00:17:13 +0200 | [diff] [blame] | 111 | requirements = [ |
Michał Sochoń | 980fc9b | 2018-03-26 00:24:36 +0200 | [diff] [blame] | 112 | req for req in requirements |
| 113 | if req.value != b'pkg-resources==0.0.0\n' |
Michał Sochoń | b0d44c7 | 2018-03-26 00:17:13 +0200 | [diff] [blame] | 114 | ] |
Michał Sochoń | 1d6ad0d | 2018-03-25 23:28:04 +0200 | [diff] [blame] | 115 | |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 116 | for requirement in sorted(requirements): |
Daniel Gallagher | 844d983 | 2017-06-25 10:14:58 -0700 | [diff] [blame] | 117 | after.extend(requirement.comments) |
Anthony Sottile | 030bfac | 2019-01-31 19:19:10 -0800 | [diff] [blame] | 118 | assert requirement.value, requirement.value |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 119 | after.append(requirement.value) |
Anthony Sottile | 86691ed | 2017-10-09 10:59:17 -0700 | [diff] [blame] | 120 | after.extend(rest) |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 121 | |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 122 | after_string = b''.join(after) |
| 123 | |
Barak Y. Reif | d4b544d | 2019-09-28 21:59:41 +0300 | [diff] [blame] | 124 | if before_string == after_string: |
Daniel Gallagher | 844d983 | 2017-06-25 10:14:58 -0700 | [diff] [blame] | 125 | return PASS |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 126 | else: |
| 127 | f.seek(0) |
| 128 | f.write(after_string) |
| 129 | f.truncate() |
Daniel Gallagher | 844d983 | 2017-06-25 10:14:58 -0700 | [diff] [blame] | 130 | return FAIL |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 131 | |
| 132 | |
Anthony Sottile | 8f61529 | 2022-01-15 19:24:05 -0500 | [diff] [blame] | 133 | def main(argv: Sequence[str] | None = None) -> int: |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 134 | parser = argparse.ArgumentParser() |
| 135 | parser.add_argument('filenames', nargs='*', help='Filenames to fix') |
| 136 | args = parser.parse_args(argv) |
| 137 | |
Daniel Gallagher | 844d983 | 2017-06-25 10:14:58 -0700 | [diff] [blame] | 138 | retv = PASS |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 139 | |
| 140 | for arg in args.filenames: |
Anthony Sottile | 2f1d2bb | 2015-01-04 11:08:53 -0800 | [diff] [blame] | 141 | with open(arg, 'rb+') as file_obj: |
Cameron Paul | f06dfda | 2015-01-12 13:03:11 -0600 | [diff] [blame] | 142 | ret_for_file = fix_requirements(file_obj) |
| 143 | |
| 144 | if ret_for_file: |
Anthony Sottile | f5c42a0 | 2020-02-05 11:10:42 -0800 | [diff] [blame] | 145 | print(f'Sorting {arg}') |
Cameron Paul | f06dfda | 2015-01-12 13:03:11 -0600 | [diff] [blame] | 146 | |
| 147 | retv |= ret_for_file |
Cameron Paul | b83ea59 | 2014-12-16 12:22:37 -0800 | [diff] [blame] | 148 | |
| 149 | return retv |
Anthony Sottile | 030bfac | 2019-01-31 19:19:10 -0800 | [diff] [blame] | 150 | |
| 151 | |
| 152 | if __name__ == '__main__': |
Anthony Sottile | 39ab2ed | 2021-10-23 13:23:50 -0400 | [diff] [blame] | 153 | raise SystemExit(main()) |