Hi, As discussed previously[1], the backport.pl/nominate.pl script depend on the Perl-ism when-given which is deprecated and will be remove in the next version of Perl.
I've spent a big part of today hacking away on the backport.py scripts to build the foundation for a new interactive script. At this time, I can add new nominations using this code: [[[ import sys assert sys.version_info[0] == 3, "This script targets Python 3" import backport.status import backport.merger backport.merger.run_svn_quiet(['update']) sf = backport.status.StatusFile(open('./STATUS', encoding="UTF-8")) e = backport.status.StatusEntry(None) e.revisions = [77, 88, 99] e.logsummary = ["Add a new function"] e.depends = True e.depends_str = "Depends on fancy new feature" e.votes_str = " +1: jrandom\n" sf.insert(e, "Candidate changes") e = backport.status.StatusEntry(None) e.revisions = [102] e.logsummary = ["I think this is great"] e.votes_str = " +1: luser\n -1: jrandom (It breaks everything)\n" sf.insert(e, "Veto-blocked changes") print(str(sf)) ]]] Nominations will be inserted under the appropriate subheading (and if the subheading doesn't exist it will be added). Next step is to add the actual interactive features (argument parsing and what not). Feel free to take a look if you fancy some Python programming. I probably won't be able to put much time into this until next weekend. Cheers, Daniel [1] https://lists.apache.org/thread/1xd8xgvfz5s9pdy325p7fh9r3qybqs3p Den lör 1 mars 2025 kl 22:15 skrev <dsahlb...@apache.org>: > Author: dsahlberg > Date: Sat Mar 1 21:15:27 2025 > New Revision: 1924120 > > URL: http://svn.apache.org/viewvc?rev=1924120&view=rev > Log: > Parse additional subheaders and recreate the StatusEntry from its > attributes > instead of just outputting the original entry. > > This adds some normalization of the recreated entry: > * List of revisions is a single line. Parser can accept line breaks. > * The branch name is listed on a separate line after the Branch subheader > and without ^/subversion/branches/ path. Parser can accept branch name > on the same line and with full path. > * No spaces on the line after a status entry. Parser can accept any number > of > spaces. > > * tools/dist/backport/status.py > (StatusEntry): New attributes justification_str, depends_str and > notes_str > (StatusEntry.__init__): Parse the additional attributes and keep track of > some additional state required to recreate a faithful StatusEntry. > (StatusEntry.unparse): Remove obsolete comment (should have been moved to > __str__ in r1924119). > (StatusEntry.__str__): Create the StatusEntry from the individual > attributes. > (Test_StatusEntry.test___init__): Modify two unittest where the > normalization > caused failures. Also check the last line doesn't contain spaces. > > > Modified: > subversion/trunk/tools/dist/backport/status.py > > Modified: subversion/trunk/tools/dist/backport/status.py > URL: > http://svn.apache.org/viewvc/subversion/trunk/tools/dist/backport/status.py?rev=1924120&r1=1924119&r2=1924120&view=diff > > ============================================================================== > --- subversion/trunk/tools/dist/backport/status.py (original) > +++ subversion/trunk/tools/dist/backport/status.py Sat Mar 1 21:15:27 2025 > @@ -336,8 +336,11 @@ class StatusEntry: > branch - the backport branch's basename, or None. > revisions - the revisions to nominated, as iterable of int. > logsummary - the text before the justification, as an array of lines. > + justification_str - the justification, as an array of lines. An > unparsed string. > depends - true if a "Depends:" entry was found, False otherwise. > + depends_str - everything after the "Depends:" subheader. An unparsed > string. > accept - the value to pass to 'svn merge --accept=%s', or None. > + notes_str - everything after the "Notes:" subheader. An unparsed string. > votes_str - everything after the "Votes:" subheader. An unparsed > string. > """ > > @@ -348,10 +351,14 @@ class StatusEntry: > STATUS_FILE is the StatusFile object containing this entry, if any. > """ > self.branch = None > + self.branch_on_first_line = False > self.revisions = [] > self.logsummary = [] > + self.justification_str = None > self.depends = False > + self.depends_str = None > self.accept = None > + self.notes_str = None > self.votes_str = None > self.status_file = status_file > > @@ -366,8 +373,8 @@ class StatusEntry: > match = _re_entry_indentation.match(lines[0]) > if not match: > raise ParseException("Entry found with no ' * ' line") > - indentation = len(match.group(1)) > - lines = (line[indentation:] for line in lines) > + self.indentation = len(match.group(1)) > + lines = (line[self.indentation:] for line in lines) > lines = (line.rstrip() for line in lines) > > # Consume the generator. > @@ -378,6 +385,7 @@ class StatusEntry: > if match: > # Parse whichever group matched. > self.branch = self.parse_branch(match.group(1) or match.group(2)) > + self.branch_on_first_line = True > else: > while _re_revisions_line.match(lines[0]): > self.revisions.extend(map(int, > re.compile(r'(\d+)').findall(lines[0]))) > @@ -403,14 +411,26 @@ class StatusEntry: > else: > self.votes_str = None > > - # depends, branch, notes > + # depends, justification, branch, notes > while lines: > > if lines[0].strip().startswith('Depends:'): > self.depends = True > + self.depends_str = lines[0].strip()[8:].strip() > lines = lines[1:] > continue > > + if lines[0].strip().startswith('Justification:'): > + self.justification_str = lines[0].strip().split(':', 1)[1] + "\n" > + lines = lines[1:] > + > + # Consume the indented body of the "Justification" field. > + while lines and not lines[0][0].isalnum(): > + self.justification_str += lines[0] + "\n" > + lines = lines[1:] > + > + continue > + > if lines[0].strip().startswith('Branch:'): > maybe_value = lines[0].strip().split(':', 1)[1] > if maybe_value.strip(): > @@ -427,16 +447,16 @@ class StatusEntry: > continue > > if lines[0].strip().startswith('Notes:'): > - notes = lines[0].strip().split(':', 1)[1] + "\n" > + self.notes_str = lines[0].strip().split(':', 1)[1] + "\n" > lines = lines[1:] > > # Consume the indented body of the "Notes" field. > while lines and not lines[0][0].isalnum(): > - notes += lines[0] + "\n" > + self.notes_str += lines[0] + "\n" > lines = lines[1:] > > # Look for possible --accept directives. > - matches = re.compile(r'--accept[ =]([a-z-]+)').findall(notes) > + matches = re.compile(r'--accept[ > =]([a-z-]+)').findall(self.notes_str) > if len(matches) > 1: > raise ParseException("Too many --accept values at %s" % (self,)) > elif len(matches) == 1: > @@ -527,19 +547,42 @@ class StatusEntry: > > def unparse(self, stream): > "Write this entry to STREAM, an open file-like object." > - # For now, this is simple.. until we add interactive editing. > stream.write(self.__str__()) > > def __str__(self): > - s = self.raw > + indent = ''.ljust(self.indentation-2) > + s = indent + '* ' > + if (len(self.revisions) > 0): > + s += 'r' + ', r'.join(map(str, self.revisions)) + \ > + ('\n' + indent + ' ').join([""] + self.logsummary) + '\n' > + else: > + s += ('\n' + indent + ' ').join(self.logsummary) + '\n' > + if self.justification_str is not None: > + s += indent + ' Justification:' + \ > + ('\n' + indent + ' > ').join(self.justification_str.split("\n")[:-1]) + \ > + '\n' > + if self.branch is not None and not self.branch_on_first_line: > + s += indent + ' Branch:' + \ > + '\n' + indent + ' ' + self.branch + \ > + '\n' > + if self.depends: > + s += indent + ' Depends: ' + self.depends_str + '\n' > + if self.notes_str is not None: > + s += indent + ' Notes:' + \ > + ('\n' + indent + ' ').join(self.notes_str.split("\n")[:-1]) + \ > + '\n' > + if self.votes_str is not None: > + s += indent + ' Votes:' + \ > + ('\n' + indent + ' ').join([""] + > self.votes_str.split("\n")[:-1]) + "\n" > return s > > class Test_StatusEntry(unittest.TestCase): > def test___init__(self): > "Test the entry parser" > > - # All these entries actually have a "four spaces" line as their last > line, > - # but the parser doesn't care. > + # An entry may have spaces on its last line, the parser doesn't care. > + # However unparse() will not replicate this, causing a failure on > + # assertEqual(entry.unparse()) > > s = """\ > * r42, r43, > @@ -550,8 +593,17 @@ class Test_StatusEntry(unittest.TestCase > Votes: > +1: jrandom > """ > + # Revision list will be normalized to one line > + sExpected = """\ > + * r42, r43, r44 > + This is the logsummary. > + Branch: > + 1.8.x-rfourty-two > + Votes: > + +1: jrandom > +""" > entry = StatusEntry(s) > - self.assertEqual(entry.__str__(), s) > + self.assertEqual(entry.__str__(), sExpected) > self.assertEqual(entry.branch, "1.8.x-rfourty-two") > self.assertEqual(entry.revisions, [42, 43, 44]) > self.assertEqual(entry.logsummary, ["This is the logsummary."]) > @@ -574,7 +626,7 @@ class Test_StatusEntry(unittest.TestCase > Votes: > +1: jrandom > -1: jconstant > - """ > +""" > entry = StatusEntry(s) > self.assertEqual(entry.__str__(), s) > self.assertIsNone(entry.branch) > @@ -597,7 +649,7 @@ class Test_StatusEntry(unittest.TestCase > Votes: > +1: jrandom > -1 (see <message-id>): jconstant > - """ > +""" > entry = StatusEntry(s) > self.assertEqual(entry.__str__(), s) > self.assertEqual(entry.branch, "1.8.x-fixes") > @@ -611,8 +663,17 @@ class Test_StatusEntry(unittest.TestCase > Votes: > +1: jrandom > """ > + # Normalizes branch (without path) to separate line > + sExpected = """\ > + * r42 > + This is the logsummary. > + Branch: > + on-the-same-line > + Votes: > + +1: jrandom > +""" > entry = StatusEntry(s) > - self.assertEqual(entry.__str__(), s) > + self.assertEqual(entry.__str__(), sExpected) > self.assertEqual(entry.branch, "on-the-same-line") > self.assertEqual(entry.revisions, [42]) > > @@ -624,7 +685,7 @@ class Test_StatusEntry(unittest.TestCase > This is the logsummary. > Votes: > +1: jrandom > - """ > +""" > entry = StatusEntry(s) > self.assertEqual(entry.__str__(), s) > self.assertEqual(entry.branch, "1.8.x-fixes") > @@ -667,7 +728,7 @@ class Test_StatusEntry(unittest.TestCase > Fixes output that scripts depend on. > Votes: > +1: jrandom > - """ > +""" > entry = StatusEntry(s) > self.assertEqual(entry.__str__(), s) > self.assertEqual(entry.revisions, [42]) > > >