this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

♻️ Refactor into a parse function in a separate file

+229 -203
+11 -203
issurge/main.py
··· 11 11 --debug Print debug information 12 12 """ 13 13 import json 14 + import os 14 15 from collections.abc import Iterable 16 + from pathlib import Path 15 17 from subprocess import run 16 - import subprocess 17 - from rich import print 18 - from pathlib import Path 19 - from typing import Any, Generator, NamedTuple, TypeAlias, TypeVar 18 + from typing import Generator, TypeAlias, TypeVar 19 + 20 20 from docopt import docopt 21 + from rich import print 22 + 23 + from issurge.parser import parse 24 + from issurge.utils import debug 25 + 21 26 22 27 def run(): 23 28 opts = docopt(__doc__) 24 - TAB = "\t" 25 - 26 - def debug(*args, **kwargs): 27 - if opts["--debug"]: 28 - print(*args, **kwargs) 29 + os.environ["ISSURGE_DEBUG"] = "1" if opts["--debug"] else "" 29 30 30 31 debug(f"Running with options: {opts}") 31 - 32 - class Node: 33 - def __init__(self, indented_line): 34 - self.children = [] 35 - self.level = len(indented_line) - len(indented_line.lstrip()) 36 - self.text = indented_line.strip() 37 - 38 - def add_children(self, nodes): 39 - childlevel = nodes[0].level 40 - while nodes: 41 - node = nodes.pop(0) 42 - if node.level == childlevel: # add node as a child 43 - self.children.append(node) 44 - elif ( 45 - node.level > childlevel 46 - ): # add nodes as grandchildren of the last child 47 - nodes.insert(0, node) 48 - self.children[-1].add_children(nodes) 49 - elif node.level <= self.level: # this node is a sibling, no more children 50 - nodes.insert(0, node) 51 - return 52 - 53 - def as_dict(self): 54 - if len(self.children) > 1: 55 - child_dicts = {} 56 - for node in self.children: 57 - child_dicts |= node.as_dict() 58 - return {self.text: child_dicts} 59 - elif len(self.children) == 1: 60 - return {self.text: self.children[0].as_dict()} 61 - else: 62 - return {self.text: None} 63 - 64 - 65 - def indented_to_dict(to_parse: str) -> dict[str, Any]: 66 - root = Node("root") 67 - root.add_children([Node(line) for line in to_parse.splitlines() if line.strip()]) 68 - return root.as_dict()["root"] 69 - 70 - 71 - class Issue(NamedTuple): 72 - title: str 73 - description: str 74 - labels: set[str] 75 - assignees: set[str] 76 - milestone: str 77 - 78 - def __str__(self) -> str: 79 - result = f"[white]{self.title[:30]}[/white]" or "[red]<No title>[/red]" 80 - if len(self.title) > 30: 81 - result += " [white dim](...)[/white dim]" 82 - if self.labels: 83 - result += ( 84 - f" [yellow]{' '.join(['~' + l for l in self.labels][:4])}[/yellow]" 85 - ) 86 - if len(self.labels) > 4: 87 - result += " [yellow dim]~...[/yellow dim]" 88 - if self.milestone: 89 - result += f" [purple]%{self.milestone}[/purple]" 90 - if self.assignees: 91 - result += f" [cyan]{' '.join(['@' + a for a in self.assignees])}[/cyan]" 92 - if self.description: 93 - result += " [white][...][/white]" 94 - return result 95 - 96 - def submit(self): 97 - command = ["glab", "issue", "new"] 98 - if self.title: 99 - command += ["-t", self.title] 100 - command += ["-d", self.description or ""] 101 - for a in self.assignees: 102 - command += ["-a", a if a != "me" else "@me"] 103 - for l in self.labels: 104 - command += ["-l", l] 105 - if self.milestone: 106 - command += ["-m", self.milestone] 107 - command.extend(opts["<glab-args>"]) 108 - if opts['--dry-run'] or opts['--debug']: 109 - print( 110 - f"{'Would run' if opts['--dry-run'] else 'Running'} [white bold]{subprocess.list2cmdline(command)}[/]" 111 - ) 112 - if not opts["--dry-run"]: 113 - subprocess.run(command) 114 - 115 - # The boolean is true if the issue expects a description (ending ':') 116 - @classmethod 117 - def parse(cls, raw: str) -> tuple["Issue", bool]: 118 - raw = raw.strip() 119 - expects_description = False 120 - if raw.endswith(":"): 121 - expects_description = True 122 - raw = raw[:-1].strip() 123 - 124 - title = "" 125 - description = "" 126 - labels = set() 127 - assignees = set() 128 - milestone = "" 129 - for word in raw.split(" "): 130 - if word.startswith("~"): 131 - labels.add(word[1:]) 132 - elif word.startswith("%"): 133 - milestone = word[1:] 134 - elif word.startswith("@"): 135 - assignees.add(word[1:]) 136 - else: 137 - title += f" {word}" 138 - 139 - return ( 140 - cls( 141 - title=title.strip(), 142 - description=description, 143 - labels=labels, 144 - assignees=assignees, 145 - milestone=milestone, 146 - ), 147 - expects_description, 148 - ) 149 - 150 - 151 - issues: list[Issue] = [] 152 - 153 - 154 - def dict_to_issue( 155 - issue_fragment: str, 156 - children: dict[str, Any], 157 - current_issue: Issue, 158 - recursion_depth=0, 159 - ) -> list[Issue]: 160 - log = lambda *args, **kwargs: debug( 161 - f"[white]{issue_fragment[:50]: <50}[/white]\t{TAB*recursion_depth}", 162 - *args, 163 - **kwargs, 164 - ) 165 - 166 - if issue_fragment.strip().startswith("//"): 167 - log(f"[yellow bold]Skipping comment[/]") 168 - return [] 169 - current_title = current_issue.title 170 - current_description = current_issue.description 171 - current_labels = set(current_issue.labels) 172 - current_assignees = set(current_issue.assignees) 173 - current_milestone = current_issue.milestone 174 - 175 - parsed, expecting_description = Issue.parse(issue_fragment) 176 - if expecting_description: 177 - log(f"[white dim]{parsed} expects a description[/]") 178 - 179 - current_title = parsed.title 180 - current_labels |= parsed.labels 181 - current_assignees |= parsed.assignees 182 - current_milestone = parsed.milestone 183 - if expecting_description: 184 - if children is None: 185 - raise ValueError(f"Expected a description after {issue_fragment!r}") 186 - current_description = "" 187 - for line, v in children.items(): 188 - if v is not None: 189 - raise ValueError( 190 - "Description should not have indented lines at {line!r}" 191 - ) 192 - current_description += f"{line.strip()}\n" 193 - 194 - current_issue = Issue( 195 - title=current_title, 196 - description=current_description, 197 - labels=current_labels, 198 - assignees=current_assignees, 199 - milestone=current_milestone, 200 - ) 201 - 202 - if current_issue.title: 203 - log(f"Made {current_issue!s}") 204 - return [current_issue] 205 - 206 - if not expecting_description and children is not None: 207 - result = [] 208 - log(f"Making children from {current_issue!s}") 209 - for child, grandchildren in children.items(): 210 - result.extend( 211 - dict_to_issue(child, grandchildren, current_issue, recursion_depth + 1) 212 - ) 213 - return result 214 - 215 - log(f"[red bold]Issue {issue_fragment!r} has no title and no children[/red bold]") 216 - return [] 217 - 218 - 219 - for item in indented_to_dict(Path(opts["<file>"]).read_text()).items(): 220 - issue = dict_to_issue(*item, Issue("", "", set(), set(), "")) 221 - issues.extend(issue) 222 - 223 32 print("Submitting issues...") 224 - for issue in issues: 225 - # debug(issue) 33 + for issue in parse(Path(opts["<file>"]).read_text()): 226 34 issue.submit()
+208
issurge/parser.py
··· 1 + import subprocess 2 + from typing import Any, Iterable 3 + from urllib.parse import urlparse 4 + from rich import NamedTuple, print 5 + 6 + from issurge.utils import TAB, debug 7 + 8 + 9 + class Node: 10 + def __init__(self, indented_line): 11 + self.children = [] 12 + self.level = len(indented_line) - len(indented_line.lstrip()) 13 + self.text = indented_line.strip() 14 + 15 + def add_children(self, nodes): 16 + childlevel = nodes[0].level 17 + while nodes: 18 + node = nodes.pop(0) 19 + if node.level == childlevel: # add node as a child 20 + self.children.append(node) 21 + elif ( 22 + node.level > childlevel 23 + ): # add nodes as grandchildren of the last child 24 + nodes.insert(0, node) 25 + self.children[-1].add_children(nodes) 26 + elif node.level <= self.level: # this node is a sibling, no more children 27 + nodes.insert(0, node) 28 + return 29 + 30 + def as_dict(self) -> dict[str, Any]: 31 + if len(self.children) > 1: 32 + child_dicts = {} 33 + for node in self.children: 34 + child_dicts |= node.as_dict() 35 + return {self.text: child_dicts} 36 + elif len(self.children) == 1: 37 + return {self.text: self.children[0].as_dict()} 38 + else: 39 + return {self.text: None} 40 + 41 + @staticmethod 42 + def to_dict(to_parse: str) -> dict[str, Any]: 43 + root = Node("root") 44 + root.add_children( 45 + [Node(line) for line in to_parse.splitlines() if line.strip()] 46 + ) 47 + return root.as_dict()["root"] 48 + 49 + 50 + class Issue(NamedTuple): 51 + _cli_options: dict[str, Any] 52 + title: str 53 + description: str 54 + labels: set[str] 55 + assignees: set[str] 56 + milestone: str 57 + 58 + def __str__(self) -> str: 59 + result = f"[white]{self.title[:30]}[/white]" or "[red]<No title>[/red]" 60 + if len(self.title) > 30: 61 + result += " [white dim](...)[/white dim]" 62 + if self.labels: 63 + result += ( 64 + f" [yellow]{' '.join(['~' + l for l in self.labels][:4])}[/yellow]" 65 + ) 66 + if len(self.labels) > 4: 67 + result += " [yellow dim]~...[/yellow dim]" 68 + if self.milestone: 69 + result += f" [purple]%{self.milestone}[/purple]" 70 + if self.assignees: 71 + result += f" [cyan]{' '.join(['@' + a for a in self.assignees])}[/cyan]" 72 + if self.description: 73 + result += " [white][...][/white]" 74 + return result 75 + 76 + def submit(self): 77 + command = ["glab", "issue", "new"] 78 + if self.title: 79 + command += ["-t", self.title] 80 + command += ["-d", self.description or ""] 81 + for a in self.assignees: 82 + command += ["-a", a if a != "me" else "@me"] 83 + for l in self.labels: 84 + command += ["-l", l] 85 + if self.milestone: 86 + command += ["-m", self.milestone] 87 + command.extend(self._cli_options["<glab-args>"]) 88 + if self._cli_options["--dry-run"] or self._cli_options["--debug"]: 89 + print( 90 + f"{'Would run' if self._cli_options['--dry-run'] else 'Running'} [white bold]{subprocess.list2cmdline(command)}[/]" 91 + ) 92 + if not self._cli_options["--dry-run"]: 93 + subprocess.run(command) 94 + 95 + # The boolean is true if the issue expects a description (ending ':') 96 + @classmethod 97 + def parse(cls, raw: str) -> tuple["Issue", bool]: 98 + raw = raw.strip() 99 + expects_description = False 100 + if raw.endswith(":"): 101 + expects_description = True 102 + raw = raw[:-1].strip() 103 + 104 + title = "" 105 + description = "" 106 + labels = set() 107 + assignees = set() 108 + milestone = "" 109 + for word in raw.split(" "): 110 + if word.startswith("~"): 111 + labels.add(word[1:]) 112 + elif word.startswith("%"): 113 + milestone = word[1:] 114 + elif word.startswith("@"): 115 + assignees.add(word[1:]) 116 + else: 117 + title += f" {word}" 118 + 119 + return ( 120 + cls( 121 + title=title.strip(), 122 + description=description, 123 + labels=labels, 124 + assignees=assignees, 125 + milestone=milestone, 126 + ), 127 + expects_description, 128 + ) 129 + 130 + 131 + def parse_issue_fragment( 132 + issue_fragment: str, 133 + children: dict[str, Any], 134 + current_issue: Issue, 135 + recursion_depth=0, 136 + cli_options: dict[str, Any] | None = None, 137 + ) -> list[Issue]: 138 + if not cli_options: 139 + cli_options = {} 140 + log = lambda *args, **kwargs: debug( 141 + f"[white]{issue_fragment[:50]: <50}[/white]\t{TAB*recursion_depth}", 142 + *args, 143 + **kwargs, 144 + ) 145 + 146 + if issue_fragment.strip().startswith("//"): 147 + log(f"[yellow bold]Skipping comment[/]") 148 + return [] 149 + current_title = current_issue.title 150 + current_description = current_issue.description 151 + current_labels = set(current_issue.labels) 152 + current_assignees = set(current_issue.assignees) 153 + current_milestone = current_issue.milestone 154 + 155 + parsed, expecting_description = Issue.parse(issue_fragment) 156 + if expecting_description: 157 + log(f"[white dim]{parsed} expects a description[/]") 158 + 159 + current_title = parsed.title 160 + current_labels |= parsed.labels 161 + current_assignees |= parsed.assignees 162 + current_milestone = parsed.milestone 163 + if expecting_description: 164 + if children is None: 165 + raise ValueError(f"Expected a description after {issue_fragment!r}") 166 + current_description = "" 167 + for line, v in children.items(): 168 + if v is not None: 169 + raise ValueError( 170 + "Description should not have indented lines at {line!r}" 171 + ) 172 + current_description += f"{line.strip()}\n" 173 + 174 + current_issue = Issue( 175 + title=current_title, 176 + description=current_description, 177 + labels=current_labels, 178 + assignees=current_assignees, 179 + milestone=current_milestone, 180 + _cli_options=cli_options, 181 + ) 182 + 183 + if current_issue.title: 184 + log(f"Made {current_issue!s}") 185 + return [current_issue] 186 + 187 + if not expecting_description and children is not None: 188 + result = [] 189 + log(f"Making children from {current_issue!s}") 190 + for child, grandchildren in children.items(): 191 + result.extend( 192 + parse_issue_fragment( 193 + child, 194 + grandchildren, 195 + current_issue, 196 + recursion_depth + 1, 197 + cli_options, 198 + ) 199 + ) 200 + return result 201 + 202 + log(f"[red bold]Issue {issue_fragment!r} has no title and no children[/red bold]") 203 + return [] 204 + 205 + 206 + def parse(raw: str) -> Iterable[Issue]: 207 + for item in Node.to_dict(raw).items(): 208 + yield parse_issue_fragment(*item, Issue("", "", set(), set(), ""))
+10
issurge/utils.py
··· 1 + from rich import print 2 + import os 3 + 4 + 5 + def debug(*args, **kwargs): 6 + if os.environ.get("ISSURGE_DEBUG"): 7 + print(*args, **kwargs) 8 + 9 + 10 + TAB = "\t"