[libcxx-commits] [libcxx] f117f0a - [libc++] Add a script to synchronize status-tracking CSVs with Github issues (#101704)
via libcxx-commits
libcxx-commits at lists.llvm.org
Tue Aug 13 06:57:56 PDT 2024
Author: Louis Dionne
Date: 2024-08-13T09:57:52-04:00
New Revision: f117f0a746177bdc8b5f658e6da84cd02909aff1
URL: https://github.com/llvm/llvm-project/commit/f117f0a746177bdc8b5f658e6da84cd02909aff1
DIFF: https://github.com/llvm/llvm-project/commit/f117f0a746177bdc8b5f658e6da84cd02909aff1.diff
LOG: [libc++] Add a script to synchronize status-tracking CSVs with Github issues (#101704)
This script can be run manually to synchronize the CSV files that we use
to track Standards Conformance with the Github issues that track our
implementation of LWG issues and papers.
Added:
libcxx/utils/synchronize_csv_status_files.py
Modified:
Removed:
################################################################################
diff --git a/libcxx/utils/synchronize_csv_status_files.py b/libcxx/utils/synchronize_csv_status_files.py
new file mode 100755
index 00000000000000..b44b02f5304c0a
--- /dev/null
+++ b/libcxx/utils/synchronize_csv_status_files.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python3
+# ===----------------------------------------------------------------------===##
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+# ===----------------------------------------------------------------------===##
+
+from typing import List, Dict, Tuple, Optional
+import csv
+import itertools
+import json
+import os
+import pathlib
+import re
+import subprocess
+
+# Number of the 'Libc++ Standards Conformance' project on Github
+LIBCXX_CONFORMANCE_PROJECT = '31'
+
+class PaperInfo:
+ paper_number: str
+ """
+ Identifier for the paper or the LWG issue. This must be something like 'PnnnnRx', 'Nxxxxx' or 'LWGxxxxx'.
+ """
+
+ paper_name: str
+ """
+ Plain text string representing the name of the paper.
+ """
+
+ meeting: Optional[str]
+ """
+ Plain text string representing the meeting at which the paper/issue was voted.
+ """
+
+ status: Optional[str]
+ """
+ Status of the paper/issue. This must be '|Complete|', '|Nothing To Do|', '|In Progress|',
+ '|Partial|' or 'Resolved by <something>'.
+ """
+
+ first_released_version: Optional[str]
+ """
+ First version of LLVM in which this paper/issue was resolved.
+ """
+
+ labels: Optional[List[str]]
+ """
+ List of labels to associate to the issue in the status-tracking table. Supported labels are
+ 'format', 'ranges', 'spaceship', 'flat_containers', 'concurrency TS' and 'DR'.
+ """
+
+ original: Optional[object]
+ """
+ Object from which this PaperInfo originated. This is used to track the CSV row or Github issue that
+ was used to generate this PaperInfo and is useful for error reporting purposes.
+ """
+
+ def __init__(self, paper_number: str, paper_name: str,
+ meeting: Optional[str] = None,
+ status: Optional[str] = None,
+ first_released_version: Optional[str] = None,
+ labels: Optional[List[str]] = None,
+ original: Optional[object] = None):
+ self.paper_number = paper_number
+ self.paper_name = paper_name
+ self.meeting = meeting
+ self.status = status
+ self.first_released_version = first_released_version
+ self.labels = labels
+ self.original = original
+
+ def for_printing(self) -> Tuple[str, str, str, str, str, str]:
+ return (
+ f'`{self.paper_number} <https://wg21.link/{self.paper_number}>`__',
+ self.paper_name,
+ self.meeting if self.meeting is not None else '',
+ self.status if self.status is not None else '',
+ self.first_released_version if self.first_released_version is not None else '',
+ ' '.join(f'|{label}|' for label in self.labels) if self.labels is not None else '',
+ )
+
+ def __repr__(self) -> str:
+ return repr(self.original) if self.original is not None else repr(self.for_printing())
+
+ def is_implemented(self) -> bool:
+ if self.status is None:
+ return False
+ if re.search(r'(in progress|partial)', self.status.lower()):
+ return False
+ return True
+
+ @staticmethod
+ def from_csv_row(row: Tuple[str, str, str, str, str, str]):# -> PaperInfo:
+ """
+ Given a row from one of our status-tracking CSV files, create a PaperInfo object representing that row.
+ """
+ # Extract the paper number from the first column
+ match = re.search(r"((P[0-9R]+)|(LWG[0-9]+)|(N[0-9]+))\s+", row[0])
+ if match is None:
+ raise RuntimeError(f"Can't parse paper/issue number out of row: {row}")
+
+ return PaperInfo(
+ paper_number=match.group(1),
+ paper_name=row[1],
+ meeting=row[2] or None,
+ status=row[3] or None,
+ first_released_version=row[4] or None,
+ labels=[l.strip('|') for l in row[5].split(' ') if l] or None,
+ original=row,
+ )
+
+ @staticmethod
+ def from_github_issue(issue: Dict):# -> PaperInfo:
+ """
+ Create a PaperInfo object from the Github issue information obtained from querying a Github Project.
+ """
+ # Extract the paper number from the issue title
+ match = re.search(r"((P[0-9R]+)|(LWG[0-9]+)|(N[0-9]+)):", issue['title'])
+ if match is None:
+ raise RuntimeError(f"Issue doesn't have a title that we know how to parse: {issue}")
+ paper = match.group(1)
+
+ # Figure out the status of the paper according to the Github project information.
+ #
+ # Sadly, we can't make a finer-grained distiction about *how* the issue
+ # was closed (such as Nothing To Do or similar).
+ status = '|Complete|' if 'status' in issue and issue['status'] == 'Done' else None
+
+ # Handle labels
+ valid_labels = ('format', 'ranges', 'spaceship', 'flat_containers', 'concurrency TS', 'DR')
+ labels = [label for label in issue['labels'] if label in valid_labels]
+
+ return PaperInfo(
+ paper_number=paper,
+ paper_name=issue['title'],
+ meeting=issue.get('meeting Voted', None),
+ status=status,
+ first_released_version=None, # TODO
+ labels=labels if labels else None,
+ original=issue,
+ )
+
+def load_csv(file: pathlib.Path) -> List[Tuple]:
+ rows = []
+ with open(file, newline='') as f:
+ reader = csv.reader(f, delimiter=',')
+ for row in reader:
+ rows.append(row)
+ return rows
+
+def write_csv(output: pathlib.Path, rows: List[Tuple]):
+ with open(output, 'w', newline='') as f:
+ writer = csv.writer(f, quoting=csv.QUOTE_ALL, lineterminator='\n')
+ for row in rows:
+ writer.writerow(row)
+
+def sync_csv(rows: List[Tuple], from_github: List[PaperInfo]) -> List[Tuple]:
+ """
+ Given a list of CSV rows representing an existing status file and a list of PaperInfos representing
+ up-to-date (but potentially incomplete) tracking information from Github, this function returns the
+ new CSV rows synchronized with the up-to-date information.
+
+ Note that this only tracks changes from 'not implemented' issues to 'implemented'. If an up-to-date
+ PaperInfo reports that a paper is not implemented but the existing CSV rows report it as implemented,
+ it is an error (i.e. the result is not a CSV row where the paper is *not* implemented).
+ """
+ results = [rows[0]] # Start with the header
+ for row in rows[1:]: # Skip the header
+ # If the row contains empty entries, this is a "separator row" between meetings.
+ # Preserve it as-is.
+ if row[0] == "":
+ results.append(row)
+ continue
+
+ paper = PaperInfo.from_csv_row(row)
+
+ # If the row is already implemented, basically keep it unchanged but also validate that we're not
+ # out-of-sync with any still-open Github issue tracking the same paper.
+ if paper.is_implemented():
+ dangling = [gh for gh in from_github if gh.paper_number == paper.paper_number and not gh.is_implemented()]
+ if dangling:
+ raise RuntimeError(f"We found the following open tracking issues for a row which is already marked as implemented:\nrow: {row}\ntracking issues: {dangling}")
+ results.append(paper.for_printing())
+ else:
+ # Find any Github issues tracking this paper
+ tracking = [gh for gh in from_github if paper.paper_number == gh.paper_number]
+
+ # If there is no tracking issue for that row in the CSV, this is an error since we're
+ # missing a Github issue.
+ if not tracking:
+ raise RuntimeError(f"Can't find any Github issue for CSV row which isn't marked as done yet: {row}")
+
+ # If there's more than one tracking issue, something is weird too.
+ if len(tracking) > 1:
+ raise RuntimeError(f"Found a row with more than one tracking issue: {row}\ntracked by: {tracking}")
+
+ # If the issue is closed, synchronize the row based on the Github issue. Otherwise, use the
+ # existing CSV row as-is.
+ results.append(tracking[0].for_printing() if tracking[0].is_implemented() else row)
+
+ return results
+
+CSV_FILES_TO_SYNC = [
+ 'Cxx14Issues.csv',
+ 'Cxx17Issues.csv',
+ 'Cxx17Papers.csv',
+ 'Cxx20Issues.csv',
+ 'Cxx20Papers.csv',
+ # TODO: The Github issues are not created yet.
+ # 'Cxx23Issues.csv',
+ # 'Cxx23Papers.csv',
+ # 'Cxx2cIssues.csv',
+ # 'Cxx2cPapers.csv',
+]
+
+def main():
+ libcxx_root = pathlib.Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+ # Extract the list of PaperInfos from issues we're tracking on Github.
+ print("Loading all issues from Github")
+ gh_command_line = ['gh', 'project', 'item-list', LIBCXX_CONFORMANCE_PROJECT, '--owner', 'llvm', '--format', 'json', '--limit', '9999999']
+ project_info = json.loads(subprocess.check_output(gh_command_line))
+ from_github = [PaperInfo.from_github_issue(i) for i in project_info['items']]
+
+ for filename in CSV_FILES_TO_SYNC:
+ print(f"Synchronizing {filename} with Github issues")
+ file = libcxx_root / 'docs' / 'Status' / filename
+ csv = load_csv(file)
+ synced = sync_csv(csv, from_github)
+ write_csv(file, synced)
+
+if __name__ == '__main__':
+ main()
More information about the libcxx-commits
mailing list