Extend bulk issue creator (#80423)

It can now be used to create feature requests, not just deprecation bug reports.
pull/78730/head
Matt Clay 2 years ago committed by GitHub
parent fc5c0aadc9
commit 163f297d7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -8,11 +8,14 @@ import abc
import argparse import argparse
import dataclasses import dataclasses
import os import os
import pathlib
import re import re
import subprocess import subprocess
import sys import sys
import typing as t import typing as t
import yaml
try: try:
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import argcomplete import argcomplete
@ -31,19 +34,83 @@ class Issue:
summary: str summary: str
body: str body: str
project: str project: str
labels: list[str] | None = None
def create(self) -> str: def create(self) -> str:
cmd = ['gh', 'issue', 'create', '--title', self.title, '--body', self.body, '--project', self.project] cmd = ['gh', 'issue', 'create', '--title', self.title, '--body', self.body, '--project', self.project]
if self.labels:
for label in self.labels:
cmd.extend(('--label', label))
process = subprocess.run(cmd, capture_output=True, check=True) process = subprocess.run(cmd, capture_output=True, check=True)
url = process.stdout.decode().strip() url = process.stdout.decode().strip()
return url return url
@dataclasses.dataclass(frozen=True)
class Feature:
title: str
summary: str
component: str
labels: list[str] | None = None
@staticmethod
def from_dict(data: dict[str, t.Any]) -> Feature:
title = data.get('title')
summary = data.get('summary')
component = data.get('component')
labels = data.get('labels')
if not isinstance(title, str):
raise RuntimeError(f'`title` is not `str`: {title}')
if not isinstance(summary, str):
raise RuntimeError(f'`summary` is not `str`: {summary}')
if not isinstance(component, str):
raise RuntimeError(f'`component` is not `str`: {component}')
if not isinstance(labels, list) or not all(isinstance(item, str) for item in labels):
raise RuntimeError(f'`labels` is not `list[str]`: {labels}')
return Feature(
title=title,
summary=summary,
component=component,
labels=labels,
)
def create_issue(self, project: str) -> Issue:
body = f'''
### Summary
{self.summary}
### Issue Type
Feature Idea
### Component Name
`{self.component}`
'''
return Issue(
title=self.title,
summary=self.summary,
body=body.strip(),
project=project,
labels=self.labels,
)
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class BugReport: class BugReport:
title: str title: str
summary: str summary: str
component: str component: str
labels: list[str] | None = None
def create_issue(self, project: str) -> Issue: def create_issue(self, project: str) -> Issue:
body = f''' body = f'''
@ -89,6 +156,7 @@ N/A
summary=self.summary, summary=self.summary,
body=body.strip(), body=body.strip(),
project=project, project=project,
labels=self.labels,
) )
@ -174,14 +242,49 @@ TEST_OPTIONS = {
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class Args: class Args:
tests: list[str]
create: bool create: bool
verbose: bool verbose: bool
def run(self) -> None:
raise NotImplementedError()
@dataclasses.dataclass(frozen=True)
class DeprecationArgs(Args):
tests: list[str]
def run(self) -> None:
deprecated_command(self)
@dataclasses.dataclass(frozen=True)
class FeatureArgs(Args):
source: pathlib.Path
def run(self) -> None:
feature_command(self)
def parse_args() -> Args: def parse_args() -> Args:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
create_common_arguments(parser)
subparser = parser.add_subparsers(required=True)
create_deprecation_parser(subparser)
create_feature_parser(subparser)
args = invoke_parser(parser)
return args
def create_deprecation_parser(subparser) -> None:
parser: argparse.ArgumentParser = subparser.add_parser('deprecation')
parser.set_defaults(type=DeprecationArgs)
parser.set_defaults(command=deprecated_command)
parser.add_argument( parser.add_argument(
'--test', '--test',
dest='tests', dest='tests',
@ -190,6 +293,25 @@ def parse_args() -> Args:
help='sanity test name', help='sanity test name',
) )
create_common_arguments(parser)
def create_feature_parser(subparser) -> None:
parser: argparse.ArgumentParser = subparser.add_parser('feature')
parser.set_defaults(type=FeatureArgs)
parser.set_defaults(command=feature_command)
parser.add_argument(
'--source',
type=pathlib.Path,
default=pathlib.Path('issues.yml'),
help='YAML file containing issue details (default: %(default)s)',
)
create_common_arguments(parser)
def create_common_arguments(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
'--create', '--create',
action='store_true', action='store_true',
@ -203,20 +325,20 @@ def parse_args() -> Args:
help='verbose output', help='verbose output',
) )
def invoke_parser(parser: argparse.ArgumentParser) -> Args:
if argcomplete: if argcomplete:
argcomplete.autocomplete(parser) argcomplete.autocomplete(parser)
parsed_args = parser.parse_args() parsed_args = parser.parse_args()
if not parsed_args.tests:
parsed_args.tests = list(TEST_OPTIONS)
kvp = {} kvp = {}
args_type = parsed_args.type
for field in dataclasses.fields(Args): for field in dataclasses.fields(args_type):
kvp[field.name] = getattr(parsed_args, field.name) kvp[field.name] = getattr(parsed_args, field.name)
args = Args(**kvp) args = args_type(**kvp)
return args return args
@ -238,7 +360,7 @@ def run_sanity_test(test_name: str) -> list[str]:
return messages return messages
def create_issues(test_type: t.Type[Deprecation], messages: list[str]) -> list[Issue]: def create_issues_from_deprecation_messages(test_type: t.Type[Deprecation], messages: list[str]) -> list[Issue]:
deprecations = [test_type.parse(message) for message in messages] deprecations = [test_type.parse(message) for message in messages]
bug_reports = [deprecation.create_bug_report() for deprecation in deprecations] bug_reports = [deprecation.create_bug_report() for deprecation in deprecations]
issues = [bug_report.create_issue(PROJECT) for bug_report in bug_reports] issues = [bug_report.create_issue(PROJECT) for bug_report in bug_reports]
@ -251,14 +373,47 @@ def info(message: str) -> None:
def main() -> None: def main() -> None:
args = parse_args() args = parse_args()
args.run()
def deprecated_command(args: DeprecationArgs) -> None:
issues: list[Issue] = [] issues: list[Issue] = []
for test in args.tests: for test in args.tests or list(TEST_OPTIONS):
test_type = TEST_OPTIONS[test] test_type = TEST_OPTIONS[test]
info(f'Running "{test}" sanity test...') info(f'Running "{test}" sanity test...')
messages = run_sanity_test(test) messages = run_sanity_test(test)
issues.extend(create_issues(test_type, messages)) issues.extend(create_issues_from_deprecation_messages(test_type, messages))
create_issues(args, issues)
def feature_command(args: FeatureArgs) -> None:
with args.source.open() as source_file:
source = yaml.safe_load(source_file)
default: dict[str, t.Any] = source.get('default', {})
features: list[dict[str, t.Any]] = source.get('features', [])
if not isinstance(default, dict):
raise RuntimeError('`default` must be `dict[str, ...]`')
if not isinstance(features, list):
raise RuntimeError('`features` must be `list[dict[str, ...]]`')
issues: list[Issue] = []
for feature in features:
data = default.copy()
data.update(feature)
feature = Feature.from_dict(data)
issues.append(feature.create_issue(PROJECT))
create_issues(args, issues)
def create_issues(args: Args, issues: list[Issue]) -> None:
if not issues: if not issues:
info('No issues found.') info('No issues found.')
return return

@ -49,7 +49,7 @@ def assemble_files_to_ship(complete_file_list):
'hacking/cgroup_perf_recap_graph.py', 'hacking/cgroup_perf_recap_graph.py',
'hacking/create_deprecated_issues.py', 'hacking/create_deprecated_issues.py',
'hacking/deprecated_issue_template.md', 'hacking/deprecated_issue_template.md',
'hacking/create_deprecation_bug_reports.py', 'hacking/create-bulk-issues.py',
'hacking/fix_test_syntax.py', 'hacking/fix_test_syntax.py',
'hacking/get_library.py', 'hacking/get_library.py',
'hacking/metadata-tool.py', 'hacking/metadata-tool.py',

Loading…
Cancel
Save