Skip to content

Commit 9ab0285

Browse files
committed
tools: add releases.json TypedDict wrapper
Avoid duplicated code for reading/writing releases.json, and ensure the type checker has information about its contents.
1 parent 5edd7cb commit 9ab0285

File tree

6 files changed

+54
-59
lines changed

6 files changed

+54
-59
lines changed

tools/create_release.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import json
2727

2828
from pathlib import Path
29-
from utils import is_ci, is_debianlike, read_wrap, write_wrap
29+
from utils import Releases, is_ci, is_debianlike, read_wrap, write_wrap
3030

3131
class CreateRelease:
3232
def __init__(self, repo: T.Optional[str], token: T.Optional[str], tag: str):
@@ -155,8 +155,7 @@ def create_source_fallback(self) -> None:
155155
self.wrap_section['source_fallback_url'] = f'https://github.com/mesonbuild/wrapdb/releases/download/{self.tag}/{filename.name}'
156156

157157
def run(repo: T.Optional[str], token: T.Optional[str]) -> None:
158-
with open('releases.json', 'r') as f:
159-
releases = json.load(f)
158+
releases = Releases.load()
160159
stdout = subprocess.check_output(['git', 'tag'])
161160
tags = [t.strip() for t in stdout.decode().splitlines()]
162161
for name, info in releases.items():

tools/format.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,15 @@
1515
# limitations under the License.
1616

1717
from __future__ import annotations
18-
import json
1918
from pathlib import Path
2019
import subprocess
2120

22-
from utils import format_meson, read_wrap
21+
from utils import Releases, format_meson, read_wrap
2322

2423
FORMAT_FILES = {'meson.build', 'meson_options.txt', 'meson.options'}
2524

2625
def main() -> None:
27-
with open('releases.json', 'r', encoding='utf-8') as f:
28-
releases = json.load(f)
29-
26+
releases = Releases.load()
3027
tags = set(subprocess.check_output(['git', 'tag', '--merged'], text=True).splitlines())
3128

3229
files = []

tools/internalize_sources.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,14 @@
1717
from __future__ import annotations
1818
from argparse import ArgumentParser
1919
from hashlib import sha256
20-
import json
2120
from pathlib import Path
2221
import subprocess
2322

24-
from utils import read_wrap, write_wrap, wrap_path
23+
from utils import Releases, read_wrap, write_wrap, wrap_path
2524

2625
class Internalizer:
2726
def __init__(self, all=False):
28-
with open('releases.json') as fh:
29-
releases = json.load(fh)
27+
releases = Releases.load()
3028
tags = set(
3129
subprocess.check_output(['git', 'tag'], text=True).splitlines()
3230
)

tools/sanity_checks.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import shutil
3030

3131
from pathlib import Path
32-
from utils import Version, ci_group, is_ci, is_alpinelike, is_debianlike, is_macos, is_windows, is_msys, read_wrap, FormattingError, format_meson
32+
from utils import Releases, Version, ci_group, is_ci, is_alpinelike, is_debianlike, is_macos, is_windows, is_msys, read_wrap, FormattingError, format_meson
3333

3434
PERMITTED_FILES = {'generator.sh', 'meson.build', 'meson_options.txt', 'meson.options', 'LICENSE.build'}
3535
PER_PROJECT_PERMITTED_FILES: dict[str, set[str]] = {
@@ -181,19 +181,13 @@ class CiConfigProject(T.TypedDict, total=False):
181181
skip_tests: bool
182182

183183

184-
class ReleasesProject(T.TypedDict, total=False):
185-
dependency_names: list[str]
186-
program_names: list[str]
187-
versions: T.Required[list[str]]
188-
189-
190184
class TestReleases(unittest.TestCase):
191185
# requires casts for special keys e.g. broken_*
192186
ci_config: dict[str, CiConfigProject]
193187
fatal_warnings: bool
194188
annotate_context: bool
195189
skip_build: bool
196-
releases: dict[str, ReleasesProject]
190+
releases: Releases
197191
skip: list[str]
198192
tags: set[str]
199193
timeout_multiplier: float
@@ -216,14 +210,12 @@ def setUpClass(cls):
216210
print(f'Ignoring unreachable tags: {stdout.decode().splitlines()}')
217211

218212
try:
219-
fn = 'releases.json'
220-
with open(fn, 'r', encoding='utf-8') as f:
221-
cls.releases = json.load(f)
213+
cls.releases = Releases.load()
222214
fn = 'ci_config.json'
223215
with open(fn, 'r', encoding='utf-8') as f:
224216
cls.ci_config = json.load(f)
225-
except json.decoder.JSONDecodeError:
226-
raise RuntimeError(f'file {fn} is malformed')
217+
except json.decoder.JSONDecodeError as ex:
218+
raise RuntimeError('metadata is malformed') from ex
227219

228220
system = platform.system().lower()
229221
cls.skip = T.cast(T.List[str], cls.ci_config[f'broken_{system}'])

tools/utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
# limitations under the License.
1414

1515
from __future__ import annotations
16+
import abc
1617
import configparser
1718
from contextlib import contextmanager
1819
import functools
1920
import io
21+
import json
2022
import operator
2123
import os
2224
from pathlib import Path
@@ -106,6 +108,32 @@ def __cmp(self, other: 'Version', comparator: T.Callable[[T.Any, T.Any], bool])
106108
# versions are equal, so compare revisions
107109
return comparator(self._r, other._r)
108110

111+
class _JSONFile(abc.ABC):
112+
FILENAME: str
113+
114+
def __init__(self, _: T.Any): ...
115+
116+
@classmethod
117+
def load(cls) -> T.Self:
118+
with open(cls.FILENAME, encoding='utf-8') as f:
119+
return cls(json.load(f))
120+
121+
def encode(self) -> str:
122+
return json.dumps(self, indent=2, sort_keys=True) + '\n'
123+
124+
def save(self) -> None:
125+
with open(f'{self.FILENAME}.new', 'w', encoding='utf-8') as f:
126+
f.write(self.encode())
127+
os.rename(f'{self.FILENAME}.new', self.FILENAME)
128+
129+
class ProjectReleases(T.TypedDict):
130+
dependency_names: T.NotRequired[list[str]]
131+
program_names: T.NotRequired[list[str]]
132+
versions: list[str]
133+
134+
class Releases(dict[str, ProjectReleases], _JSONFile):
135+
FILENAME = 'releases.json'
136+
109137
def wrap_path(name: str) -> Path:
110138
return Path('subprojects', f'{name}.wrap')
111139

tools/versions.py

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
import requests
3232

33-
from utils import read_wrap, wrap_path
33+
from utils import Releases, read_wrap, wrap_path
3434

3535
WRAP_URL_TEMPLATE = (
3636
'https://github.com/mesonbuild/wrapdb/blob/master/subprojects/{0}.wrap'
@@ -60,12 +60,6 @@ class AnityaPackage(T.TypedDict):
6060
version: str
6161

6262

63-
class WrapInfo(T.TypedDict):
64-
versions: list[str]
65-
dependency_names: list[str]
66-
program_names: list[str]
67-
68-
6963
@cache
7064
def get_upstream_versions() -> dict[str, str]:
7165
'''Query Anitya and return a dict: wrap_name -> upstream_version.'''
@@ -107,32 +101,27 @@ def sub(name: str, old: str, new: str) -> None:
107101

108102

109103
@cache
110-
def get_releases(commit=None) -> dict[str, WrapInfo]:
111-
'''Parse and return releases.json for the specified commit or the working
112-
tree.'''
113-
if commit is not None:
114-
data = subprocess.check_output(
115-
['git', 'cat-file', 'blob', f'{commit}:releases.json'], text=True
116-
)
117-
else:
118-
with open('releases.json') as f:
119-
data = f.read()
120-
return json.loads(data)
104+
def get_commit_releases(commit: str) -> Releases:
105+
'''Parse and return releases.json for the specified commit.'''
106+
data = subprocess.check_output(
107+
['git', 'cat-file', 'blob', f'{commit}:releases.json'], text=True
108+
)
109+
return Releases(json.loads(data))
121110

122111

123112
def get_wrap_versions() -> dict[str, str]:
124113
'''Return a dict: wrap_name -> wrapdb_version.'''
125114
return {
126115
name: info['versions'][0].split('-')[0]
127-
for name, info in get_releases().items()
116+
for name, info in Releases.load().items()
128117
if name not in DEPRECATED_WRAPS
129118
}
130119

131120

132121
def get_port_wraps() -> set[str]:
133122
'''Return the names of wraps that have a patch directory.'''
134123
ports = set()
135-
for name, info in get_releases().items():
124+
for name, info in Releases.load().items():
136125
wrap = read_wrap(name)
137126
if wrap.has_option('wrap-file', 'patch_directory'):
138127
ports.add(name)
@@ -190,17 +179,9 @@ def update_wrap(name: str, old_ver: str, new_ver: str) -> None:
190179
f.write(''.join(lines))
191180

192181

193-
def write_releases(releases: dict[str, WrapInfo]) -> None:
194-
'''Write modified releases.json.'''
195-
with open('releases.json.new', 'w') as f:
196-
json.dump(releases, f, indent=2, sort_keys=True)
197-
f.write('\n')
198-
os.rename('releases.json.new', 'releases.json')
199-
200-
201182
def update_revisions(args: Namespace) -> None:
202183
# run queries
203-
releases = get_releases()
184+
releases = Releases.load()
204185
cur_vers = get_wrap_versions()
205186

206187
# decide what to update
@@ -217,7 +198,7 @@ def update_revisions(args: Namespace) -> None:
217198
print(f'Updating {name} revision...')
218199
cur_rev = int(releases[name]['versions'][0].split('-')[1])
219200
releases[name]['versions'].insert(0, f'{cur_vers[name]}-{cur_rev + 1}')
220-
write_releases(releases)
201+
releases.save()
221202

222203

223204
def do_autoupdate(args: Namespace) -> None:
@@ -226,7 +207,7 @@ def do_autoupdate(args: Namespace) -> None:
226207
return update_revisions(args)
227208

228209
# run queries
229-
releases = get_releases()
210+
releases = Releases.load()
230211
cur_vers = get_wrap_versions()
231212
upstream_vers = get_upstream_versions()
232213
ports = get_port_wraps()
@@ -257,7 +238,7 @@ def do_autoupdate(args: Namespace) -> None:
257238
print(f'Updating {name}...')
258239
update_wrap(name, cur_ver, upstream_ver)
259240
releases[name]['versions'].insert(0, f'{upstream_ver}-1')
260-
write_releases(releases)
241+
releases.save()
261242
except Exception as e:
262243
print(e, file=sys.stderr)
263244
failures += 1
@@ -266,8 +247,8 @@ def do_autoupdate(args: Namespace) -> None:
266247

267248

268249
def do_commit(args: Namespace) -> None:
269-
old_releases = get_releases('HEAD')
270-
new_releases = get_releases()
250+
old_releases = get_commit_releases('HEAD')
251+
new_releases = Releases.load()
271252

272253
# we don't validate any invariants checked by sanity_checks.py
273254
changed_wraps = [

0 commit comments

Comments
 (0)