blob: 58843940275cdd10758c02d941bd68d52b2be2b8 [file] [log] [blame]
Anthony Sottile8f615292022-01-15 19:24:05 -05001from __future__ import annotations
2
Cameron Paulb83ea592014-12-16 12:22:37 -08003import argparse
Max Rozentsveyg7ebd4202020-05-16 23:58:27 -04004import re
Anthony Sottile030bfac2019-01-31 19:19:10 -08005from typing import IO
Anthony Sottile030bfac2019-01-31 19:19:10 -08006from typing import Sequence
Cameron Paulb83ea592014-12-16 12:22:37 -08007
Cameron Paulb83ea592014-12-16 12:22:37 -08008
Daniel Gallagher844d9832017-06-25 10:14:58 -07009PASS = 0
10FAIL = 1
11
12
Anthony Sottilef5c42a02020-02-05 11:10:42 -080013class Requirement:
Max Rozentsveyg7ebd4202020-05-16 23:58:27 -040014 UNTIL_COMPARISON = re.compile(b'={2,3}|!=|~=|>=?|<=?')
15 UNTIL_SEP = re.compile(rb'[^;\s]+')
16
Anthony Sottilef5c42a02020-02-05 11:10:42 -080017 def __init__(self) -> None:
Anthony Sottile8f615292022-01-15 19:24:05 -050018 self.value: bytes | None = None
19 self.comments: list[bytes] = []
Cameron Paulb83ea592014-12-16 12:22:37 -080020
Vinay Karanamc58ae082016-07-03 04:10:20 +053021 @property
Anthony Sottilef5c42a02020-02-05 11:10:42 -080022 def name(self) -> bytes:
Anthony Sottile030bfac2019-01-31 19:19:10 -080023 assert self.value is not None, self.value
Max Rozentsveyg7ebd4202020-05-16 23:58:27 -040024 name = self.value.lower()
Vinay Karanam189e33e2019-11-14 02:22:07 +053025 for egg in (b'#egg=', b'&egg='):
26 if egg in self.value:
Max Rozentsveyg7ebd4202020-05-16 23:58:27 -040027 return name.partition(egg)[-1]
Vinay Karanamc58ae082016-07-03 04:10:20 +053028
Max Rozentsveyg7ebd4202020-05-16 23:58:27 -040029 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 Karanamc58ae082016-07-03 04:10:20 +053038
Anthony Sottile8f615292022-01-15 19:24:05 -050039 def __lt__(self, requirement: Requirement) -> bool:
Cameron Paulb83ea592014-12-16 12:22:37 -080040 # \n means top of file comment, so always return True,
41 # otherwise just do a string comparison with value.
Anthony Sottile030bfac2019-01-31 19:19:10 -080042 assert self.value is not None, self.value
Cameron Paulb83ea592014-12-16 12:22:37 -080043 if self.value == b'\n':
44 return True
45 elif requirement.value == b'\n':
46 return False
47 else:
Vinay Karanamc58ae082016-07-03 04:10:20 +053048 return self.name < requirement.name
Cameron Paulb83ea592014-12-16 12:22:37 -080049
Aniket Bhatnagarbbcd31e2020-05-07 21:02:12 +010050 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 Paulb83ea592014-12-16 12:22:37 -080062
Anthony Sottilef5c42a02020-02-05 11:10:42 -080063def fix_requirements(f: IO[bytes]) -> int:
Anthony Sottile8f615292022-01-15 19:24:05 -050064 requirements: list[Requirement] = []
Barak Y. Reife4cfaa62019-09-28 22:16:20 +030065 before = list(f)
Anthony Sottile8f615292022-01-15 19:24:05 -050066 after: list[bytes] = []
Cameron Paulb83ea592014-12-16 12:22:37 -080067
Barak Y. Reifd4b544d2019-09-28 21:59:41 +030068 before_string = b''.join(before)
69
Barak Y. Reifda2ea3f2019-09-28 21:40:09 +030070 # 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 Gallagher7ccfa052017-06-23 17:19:21 -070075 # If the file is empty (i.e. only whitespace/newlines) exit early
76 if before_string.strip() == b'':
Daniel Gallagher844d9832017-06-25 10:14:58 -070077 return PASS
Daniel Gallagher7ccfa052017-06-23 17:19:21 -070078
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 Sottile030bfac2019-01-31 19:19:10 -080082
Aniket Bhatnagarbbcd31e2020-05-07 21:02:12 +010083 if not len(requirements) or requirements[-1].is_complete():
Cameron Paulb83ea592014-12-16 12:22:37 -080084 requirements.append(Requirement())
85
86 requirement = requirements[-1]
87
Daniel Gallagher7ccfa052017-06-23 17:19:21 -070088 # If we see a newline before any requirements, then this is a
89 # top of file comment.
Cameron Paulb83ea592014-12-16 12:22:37 -080090 if len(requirements) == 1 and line.strip() == b'':
Anthony Sottile45756522019-02-11 19:56:15 -080091 if (
92 len(requirement.comments) and
93 requirement.comments[0].startswith(b'#')
94 ):
Cameron Paulb83ea592014-12-16 12:22:37 -080095 requirement.value = b'\n'
96 else:
97 requirement.comments.append(line)
Viacheslav Greshilov28b2c8e2021-01-17 21:15:05 +020098 elif line.lstrip().startswith(b'#') or line.strip() == b'':
Cameron Paulb83ea592014-12-16 12:22:37 -080099 requirement.comments.append(line)
100 else:
Aniket Bhatnagarbbcd31e2020-05-07 21:02:12 +0100101 requirement.append_value(line)
Cameron Paulb83ea592014-12-16 12:22:37 -0800102
Anthony Sottile86691ed2017-10-09 10:59:17 -0700103 # 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ń9e28aaf2018-03-26 00:02:23 +0200109 # find and remove pkg-resources==0.0.0
110 # which is automatically added by broken pip package under Debian
Michał Sochońb0d44c72018-03-26 00:17:13 +0200111 requirements = [
Michał Sochoń980fc9b2018-03-26 00:24:36 +0200112 req for req in requirements
113 if req.value != b'pkg-resources==0.0.0\n'
Michał Sochońb0d44c72018-03-26 00:17:13 +0200114 ]
Michał Sochoń1d6ad0d2018-03-25 23:28:04 +0200115
Cameron Paulb83ea592014-12-16 12:22:37 -0800116 for requirement in sorted(requirements):
Daniel Gallagher844d9832017-06-25 10:14:58 -0700117 after.extend(requirement.comments)
Anthony Sottile030bfac2019-01-31 19:19:10 -0800118 assert requirement.value, requirement.value
Cameron Paulb83ea592014-12-16 12:22:37 -0800119 after.append(requirement.value)
Anthony Sottile86691ed2017-10-09 10:59:17 -0700120 after.extend(rest)
Cameron Paulb83ea592014-12-16 12:22:37 -0800121
Cameron Paulb83ea592014-12-16 12:22:37 -0800122 after_string = b''.join(after)
123
Barak Y. Reifd4b544d2019-09-28 21:59:41 +0300124 if before_string == after_string:
Daniel Gallagher844d9832017-06-25 10:14:58 -0700125 return PASS
Cameron Paulb83ea592014-12-16 12:22:37 -0800126 else:
127 f.seek(0)
128 f.write(after_string)
129 f.truncate()
Daniel Gallagher844d9832017-06-25 10:14:58 -0700130 return FAIL
Cameron Paulb83ea592014-12-16 12:22:37 -0800131
132
Anthony Sottile8f615292022-01-15 19:24:05 -0500133def main(argv: Sequence[str] | None = None) -> int:
Cameron Paulb83ea592014-12-16 12:22:37 -0800134 parser = argparse.ArgumentParser()
135 parser.add_argument('filenames', nargs='*', help='Filenames to fix')
136 args = parser.parse_args(argv)
137
Daniel Gallagher844d9832017-06-25 10:14:58 -0700138 retv = PASS
Cameron Paulb83ea592014-12-16 12:22:37 -0800139
140 for arg in args.filenames:
Anthony Sottile2f1d2bb2015-01-04 11:08:53 -0800141 with open(arg, 'rb+') as file_obj:
Cameron Paulf06dfda2015-01-12 13:03:11 -0600142 ret_for_file = fix_requirements(file_obj)
143
144 if ret_for_file:
Anthony Sottilef5c42a02020-02-05 11:10:42 -0800145 print(f'Sorting {arg}')
Cameron Paulf06dfda2015-01-12 13:03:11 -0600146
147 retv |= ret_for_file
Cameron Paulb83ea592014-12-16 12:22:37 -0800148
149 return retv
Anthony Sottile030bfac2019-01-31 19:19:10 -0800150
151
152if __name__ == '__main__':
Anthony Sottile39ab2ed2021-10-23 13:23:50 -0400153 raise SystemExit(main())