···11+import os
22+from urllib.parse import urlparse
33+from ward import fixture, test
44+from issurge.main import run
55+from pathlib import Path
66+from unittest.mock import Mock
77+from issurge.parser import Issue, subprocess
88+99+from issurge.utils import debugging, dry_running
1010+1111+1212+@fixture
1313+def setup():
1414+ Path("test_empty_issues").write_text("")
1515+ Path("test_some_issues").write_text(
1616+ """~common @common %common
1717+\tAn issue to submit
1818+Another ~issue to submit @me"""
1919+ )
2020+ subprocess.run = Mock()
2121+ Issue._get_remote_url = Mock(
2222+ return_value=urlparse("https://github.com/ewen-lbh/gh-api-playground")
2323+ )
2424+ yield
2525+ Path("test_empty_issues").unlink()
2626+ Path("test_some_issues").unlink()
2727+ del os.environ["ISSURGE_DEBUG"]
2828+ del os.environ["ISSURGE_DRY_RUN"]
2929+3030+3131+@fixture
3232+def default_opts():
3333+ yield {
3434+ "<submitter-args>": [],
3535+ "<file>": "test_empty_issues",
3636+ "--dry-run": False,
3737+ "--debug": False,
3838+ }
3939+4040+4141+@test("dry run is set when --dry-run is passed")
4242+def _(_=setup, opts=default_opts):
4343+ run(opts=opts | {"--dry-run": True})
4444+ assert dry_running()
4545+ assert not debugging()
4646+4747+4848+@test("debug is set when --debug is passed")
4949+def _(_=setup, opts=default_opts):
5050+ run(opts=opts | {"--debug": True})
5151+ assert debugging()
5252+ assert not dry_running()
5353+5454+5555+@test("dry run and debug are not set by default")
5656+def _(_=setup, opts=default_opts):
5757+ run(opts=opts)
5858+ assert not dry_running()
5959+ assert not debugging()
6060+6161+6262+@test("both dry run and debug are set when both are passed")
6363+def _(_=setup, opts=default_opts):
6464+ run(opts=opts | {"--dry-run": True, "--debug": True})
6565+ assert dry_running()
6666+ assert debugging()
6767+6868+6969+@test("issues are submitted when --dry-run is not passed, with github provider")
7070+def _(_=setup, opts=default_opts):
7171+ run(opts=opts | {"<file>": "test_some_issues"})
7272+ assert [call.args[0] for call in subprocess.run.mock_calls] == [
7373+ [
7474+ "gh",
7575+ "issue",
7676+ "new",
7777+ "-t",
7878+ "An issue to submit",
7979+ "-b",
8080+ "",
8181+ "-a",
8282+ "common",
8383+ "-l",
8484+ "common",
8585+ ],
8686+ [
8787+ "gh",
8888+ "issue",
8989+ "new",
9090+ "-t",
9191+ "Another issue to submit",
9292+ "-b",
9393+ "",
9494+ "-a",
9595+ "@me",
9696+ "-l",
9797+ "issue",
9898+ ],
9999+ ]
100100+101101+@test("issues are submitted when --dry-run is not passed, with gitlab provider")
102102+def _(_=setup, opts=default_opts):
103103+ Issue._get_remote_url = Mock(
104104+ return_value=urlparse("https://gitlab.com/ewen-lbh/gh-api-playground")
105105+ )
106106+ run(opts=opts | {"<file>": "test_some_issues"})
107107+ assert [call.args[0] for call in subprocess.run.mock_calls] == [
108108+ [
109109+ "glab",
110110+ "issue",
111111+ "new",
112112+ "-t",
113113+ "An issue to submit",
114114+ "-d",
115115+ "",
116116+ "-a",
117117+ "common",
118118+ "-l",
119119+ "common",
120120+ ],
121121+ [
122122+ "glab",
123123+ "issue",
124124+ "new",
125125+ "-t",
126126+ "Another issue to submit",
127127+ "-d",
128128+ "",
129129+ "-a",
130130+ "@me",
131131+ "-l",
132132+ "issue",
133133+ ],
134134+ ]
135135+136136+@test("issues are not submitted when --dry-run is passed")
137137+def _(_=setup, opts=default_opts):
138138+ run(opts=opts | {"<file>": "test_some_issues", "--dry-run": True})
139139+ assert len(subprocess.run.mock_calls) == 0
+13-7
issurge/parser.py
···42424343 @staticmethod
4444 def to_dict(to_parse: str) -> dict[str, Any]:
4545+ if not to_parse.strip():
4646+ return {}
4547 root = Node("root")
4648 root.add_children(
4749 [Node(line) for line in to_parse.splitlines() if line.strip()]
···213215 )
214216215217218218+def tree_to_text(tree: dict[str, Any], recursion_depth=0) -> str:
219219+ result = ""
220220+ for line, children in tree.items():
221221+ result += TAB * recursion_depth + line.strip() + NEWLINE
222222+ if children is not None:
223223+ result += tree_to_text(children, recursion_depth + 1)
224224+ return result
225225+226226+216227def parse_issue_fragment(
217228 issue_fragment: str,
218229 children: dict[str, Any],
···248259 if expecting_description:
249260 if children is None:
250261 raise ValueError(f"Expected a description after {issue_fragment!r}")
251251- current_description = ""
252252- for line, v in children.items():
253253- if v is not None:
254254- raise ValueError(
255255- "Description should not have indented lines at {line!r}"
256256- )
257257- current_description += f"{line.strip()}\n"
262262+ current_description = tree_to_text(children, 0)
258263259264 current_issue = Issue(
260265 title=current_title,
···289294290295def parse(raw: str) -> Iterable[Issue]:
291296 for item in Node.to_dict(raw).items():
297297+ debug(f"Processing {item!r}")
292298 for issue in parse_issue_fragment(*item, Issue("", "", set(), set(), "")):
293299 yield issue
+98-2
issurge/parser_test.py
···11-from ward import test
22-from .parser import Issue
11+from ward import test, raises
22+import os
33+from .parser import Issue, parse
44+import textwrap
3546for fragment, expected, description_expected in [
57 ("", Issue(), False),
···3638 actual, expecting_description = Issue.parse(fragment)
3739 assert expecting_description == description_expected
3840 assert actual == expected
4141+4242+4343+for lines, expected in [
4444+ ("", []),
4545+ ("A simple issue", [Issue(title="A simple issue")]),
4646+ ("~label @me", []),
4747+ (
4848+ """
4949+ @me some ~labels to ~organize issues ~bug
5050+ a %milestone to keep ~track of stuff
5151+ """,
5252+ [
5353+ Issue(
5454+ title="some labels to organize issues",
5555+ labels={"labels", "organize", "bug"},
5656+ assignees={"me"},
5757+ ),
5858+ Issue(
5959+ title="a milestone to keep track of stuff",
6060+ labels={"track"},
6161+ milestone="milestone",
6262+ ),
6363+ ],
6464+ ),
6565+ (
6666+ """
6767+ some stuff
6868+ \tinside: not processed
6969+ """,
7070+ [
7171+ Issue(title="some stuff"),
7272+ ],
7373+ ),
7474+ (
7575+ """
7676+ ~common-tag @someone
7777+ \tright there ~other-tag
7878+ \t//A comment
7979+8080+ \t@someone-else right %here
8181+ """,
8282+ [
8383+ Issue(
8484+ title="right there",
8585+ labels={"common-tag", "other-tag"},
8686+ assignees={"someone"},
8787+ ),
8888+ Issue(
8989+ title="right",
9090+ labels={"common-tag"},
9191+ assignees={"someone-else", "someone"},
9292+ milestone="here",
9393+ ),
9494+ ],
9595+ ),
9696+ (
9797+ """An ~issue with a description:
9898+\tThis is the %description of the issue:
9999+\t// This is *not* a comment
100100+\tIt has a
101101+\t- bullet list
102102+103103+\tAnd
104104+\t\tIndentation
105105+ """,
106106+ [
107107+ Issue(
108108+ title="An issue with a description",
109109+ labels={"issue"},
110110+ description="""This is the %description of the issue:
111111+// This is *not* a comment
112112+It has a
113113+- bullet list
114114+And
115115+\tIndentation
116116+""",
117117+ )
118118+ ],
119119+ ),
120120+]:
121121+122122+ @test(f"parse issues from {textwrap.dedent(lines)!r}")
123123+ def _(lines=lines, expected=expected):
124124+ assert list(parse(lines)) == expected
125125+126126+127127+@test("parse issue with missing description fails")
128128+def _():
129129+ with raises(ValueError) as exception:
130130+ list(parse("An ~issue with a description:\nNo description here"))
131131+ assert (
132132+ str(exception.raised)
133133+ == f"Expected a description after 'An ~issue with a description:'"
134134+ )
+38
issurge/utils_test.py
···11+import os, io, sys
22+from unittest.mock import Mock
33+from ward import test
44+from issurge.utils import debug, debugging, dry_running
55+import issurge.utils
66+77+88+@test("debugging is false by default")
99+def _():
1010+ assert not debugging()
1111+1212+1313+@test("debugging is true when ISSURGE_DEBUG is set")
1414+def _():
1515+ os.environ["ISSURGE_DEBUG"] = "1"
1616+ assert debugging()
1717+ del os.environ["ISSURGE_DEBUG"]
1818+1919+2020+@test("dry_running is false by default")
2121+def _():
2222+ assert not dry_running()
2323+2424+2525+@test("dry_running is true when ISSURGE_DRY_RUN is set")
2626+def _():
2727+ os.environ["ISSURGE_DRY_RUN"] = "1"
2828+ assert dry_running()
2929+ del os.environ["ISSURGE_DRY_RUN"]
3030+3131+3232+@test("debug only prints when debugging is true")
3333+def _():
3434+ issurge.utils.print = Mock()
3535+ os.environ["ISSURGE_DEBUG"] = "1"
3636+ debug("debug")
3737+ assert len(issurge.utils.print.mock_calls) == 1
3838+ assert issurge.utils.print.mock_calls[0].args[0] == "debug"