Skip to content

Commit 9b38096

Browse files
committed
Merge branch 'record-elements-as-we-go'
2 parents 6ba11b6 + d7fa217 commit 9b38096

File tree

2 files changed

+112
-14
lines changed

2 files changed

+112
-14
lines changed

scripts/element_tracking.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import contextlib
2+
import json
3+
from pathlib import Path
4+
from types import TracebackType
5+
from typing import (
6+
Callable,
7+
Dict,
8+
Iterator,
9+
MutableMapping,
10+
Optional,
11+
Set,
12+
Type,
13+
)
14+
15+
16+
class ElementsCache:
17+
"""
18+
Load and cache the tasks that have been created in a file.
19+
20+
Use like:
21+
```
22+
with ElementsCache(Path('cache.json')) as elements:
23+
elements['key'] = 14
24+
"""
25+
26+
def __init__(self, cache_path: Path) -> None:
27+
self.cache_path = cache_path
28+
self.elements: Dict[str, int] = {}
29+
30+
try:
31+
self.elements = json.loads(cache_path.read_text())
32+
except IOError:
33+
pass
34+
35+
def __enter__(self) -> MutableMapping[str, int]:
36+
return self.elements
37+
38+
def __exit__(
39+
self,
40+
exc_type: Optional[Type[BaseException]],
41+
exc_val: Optional[BaseException],
42+
exc_tb: Optional[TracebackType],
43+
) -> None:
44+
self.cache_path.write_text(json.dumps(self.elements, sort_keys=True, indent=2))
45+
46+
47+
class ElementsInProgress:
48+
"""
49+
Keep track of the tasks as they're being imported.
50+
51+
This checks for cyclic dependencies and provides a way to consume existing
52+
mappings of tasks that are known to be equivalent to those in this repo.
53+
"""
54+
55+
def __init__(self, known_elements: Optional[MutableMapping[str, int]]) -> None:
56+
self.known_elements = known_elements or {}
57+
self._current: Set[str] = set()
58+
59+
def get(self, key: str) -> Optional[int]:
60+
if key in self._current:
61+
raise RuntimeError(f"Cyclic dependency on {key}")
62+
63+
return self.known_elements.get(key)
64+
65+
@contextlib.contextmanager
66+
def process(self, key: str) -> Iterator[Callable[[int], None]]:
67+
if key in self._current:
68+
raise RuntimeError(
69+
f"Re-entrant processing not supported (attempted {key})",
70+
)
71+
72+
def done(x: int) -> None:
73+
self.known_elements[key] = x
74+
75+
self._current.add(key)
76+
try:
77+
yield done
78+
finally:
79+
self._current.remove(key)

scripts/import.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
#!/usr/bin/env python3
22

33
import argparse
4+
import contextlib
45
from pathlib import Path
5-
from typing import TYPE_CHECKING, Callable, Dict, Union
6+
from typing import TYPE_CHECKING, Callable, MutableMapping, Optional
67

78
import yaml
9+
from element_tracking import ElementsCache, ElementsInProgress
810
from import_backends import FakeTracBackend, GitHubBackend, RealTracBackend
911
from ticket_type import Ticket
1012

@@ -79,7 +81,13 @@ def process(element_name: str, *, year: str, handle_dep: Callable[[str], int]) -
7981
return ticket
8082

8183

82-
def add(element: str, backend: 'Backend', year: str) -> int:
84+
def add(
85+
element: str,
86+
backend: 'Backend',
87+
year: str,
88+
*,
89+
known_elements: Optional[MutableMapping[str, int]] = None,
90+
) -> int:
8391
"""
8492
Add 'element' into the task tracker, along with all its dependencies.
8593
@@ -92,22 +100,19 @@ def add(element: str, backend: 'Backend', year: str) -> int:
92100
dependencies which have already been imported at the point they are
93101
depended upon by a new parent.
94102
"""
95-
CYCLE = object()
96-
elements: Dict[str, Union[int, object]] = {}
103+
elements = ElementsInProgress(known_elements)
97104

98105
def _add(element: str) -> int:
99-
if element in elements:
100-
previous = elements[element]
101-
if previous is CYCLE:
102-
raise RuntimeError(f"cyclic dependency on {element}")
103-
assert isinstance(previous, int)
106+
previous = elements.get(element)
107+
if previous:
104108
return previous
105-
else:
106-
elements[element] = CYCLE
109+
110+
with elements.process(element) as record_id:
107111
generated = process(element, year=year, handle_dep=_add)
108112
ticket_id = backend.submit(generated)
109-
elements[element] = ticket_id
110-
return ticket_id
113+
record_id(ticket_id)
114+
115+
return ticket_id
111116

112117
return _add(element)
113118

@@ -119,6 +124,12 @@ def parse_args() -> argparse.Namespace:
119124
'year',
120125
help="SR year to generate for (specify as just the number part)",
121126
)
127+
parser.add_argument(
128+
'--cache',
129+
help="Path to a JSON file in which to load/store mapping from existing tasks.",
130+
type=Path,
131+
default=None,
132+
)
122133

123134
backends_group = parser.add_mutually_exclusive_group()
124135
backends_group.add_argument(
@@ -143,7 +154,15 @@ def main(arguments: argparse.Namespace) -> None:
143154
else:
144155
backend = FakeTracBackend()
145156

146-
add(arguments.base, backend, arguments.year)
157+
with contextlib.ExitStack() as stack:
158+
if arguments.cache:
159+
elements: MutableMapping[str, int] = stack.enter_context(
160+
ElementsCache(arguments.cache),
161+
)
162+
else:
163+
elements = {}
164+
165+
add(arguments.base, backend, arguments.year, known_elements=elements)
147166

148167

149168
if __name__ == '__main__':

0 commit comments

Comments
 (0)