We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Export version from pyodide JS module
## 🚀 Feature

In `pyodide.d.ts` I see `declare let version: string;` but it's not exported. It'd be great if it was exported so that I can do this:

```ts
import {version} from "pyodide";
```

### Motivation

I have [some code](https://github.com/alexmojaki/pyodide-worker-runner/blob/e7dd3d0ee1dff457bf9d6104944477840a83e5a7/lib/index.ts#L16) that roughly looks like this:

```ts
import {loadPyodide, PyodideInterface} from "pyodide";

const version = "0.21.1";
const indexURL = `https://cdn.jsdelivr.net/pyodide/v${version}/full/`;
const pyodide: PyodideInterface = await loadPyodide({indexURL});
if (pyodide.version !== version) {
  throw new Error(
    `loadPyodide loaded version ${pyodide.version} instead of ${version}`,
  );
}
```

I'd like to import `version` instead of setting it manually, so that it always automatically matches whatever version of pyodide is installed.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `tools/bump_version.py`
Content:
```
1 #!/usr/bin/env python3
2 
3 import argparse
4 import difflib
5 import functools
6 import itertools
7 import pathlib
8 import re
9 from ast import Str
10 from collections import namedtuple
11 from collections.abc import Callable
12 
13 CORE_VERSION_REGEX = r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)"
14 
15 PYTHON_VERSION_REGEX = CORE_VERSION_REGEX + (
16     r"((?P<pre>a|b|rc)(?P<preversion>\d+))?" r"(\.(?P<dev>dev)(?P<devversion>\d+))?"
17 )
18 
19 JS_VERSION_REGEX = CORE_VERSION_REGEX + (
20     r"(\-(?P<pre>alpha|beta|rc)\.(?P<preversion>\d+))?"
21     r"(\-(?P<dev>dev)\.(?P<devversion>\d+))?"
22 )
23 
24 
25 def build_version_pattern(pattern):
26     return re.compile(
27         pattern.format(
28             python_version=f"(?P<version>{PYTHON_VERSION_REGEX})",
29             js_version=f"(?P<version>{JS_VERSION_REGEX})",
30         )
31     )
32 
33 
34 ROOT = pathlib.Path(__file__).resolve().parent.parent
35 Target = namedtuple("target", ("file", "pattern", "prerelease"))
36 PYTHON_TARGETS = [
37     Target(
38         file=ROOT / "src/py/pyodide/__init__.py",
39         pattern=build_version_pattern('__version__ = "{python_version}"'),
40         prerelease=True,
41     ),
42     Target(
43         file=ROOT / "src/py/setup.cfg",
44         pattern=build_version_pattern("version = {python_version}"),
45         prerelease=True,
46     ),
47     Target(
48         ROOT / "pyodide-build/pyodide_build/__init__.py",
49         pattern=build_version_pattern('__version__ = "{python_version}"'),
50         prerelease=True,
51     ),
52     Target(
53         ROOT / "docs/conf.py",
54         build_version_pattern('pyodide_version = "{python_version}"'),
55         prerelease=True,
56     ),
57     Target(
58         ROOT / "run_docker",
59         build_version_pattern('PYODIDE_PREBUILT_IMAGE_TAG="{python_version}"'),
60         prerelease=False,
61     ),
62     Target(
63         ROOT / "docs/project/about.md",
64         build_version_pattern(r"version\s*=\s*{{{python_version}}}"),
65         prerelease=False,
66     ),
67 ]
68 
69 JS_TARGETS = [
70     Target(
71         ROOT / "src/js/package.json",
72         build_version_pattern(r'"pyodide",\s*"version": "{js_version}"'),
73         prerelease=True,
74     ),
75     Target(
76         ROOT / "src/js/package-lock.json",
77         build_version_pattern(r'"pyodide",\s*"version": "{js_version}"'),
78         prerelease=True,
79     ),
80 ]
81 
82 
83 @functools.lru_cache
84 def python_version_to_js_version(version: str) -> Str:
85     """
86     Convert Python version name to JS version name
87     These two are different in prerelease or dev versions.
88     e.g. 1.2.3a0 <==> 1.2.3-alpha.0
89          4.5.6.dev2 <==> 4.5.6-dev.2
90     """
91     match = re.match(PYTHON_VERSION_REGEX, version)
92     matches = match.groupdict()
93 
94     prerelease = matches["pre"] is not None
95     devrelease = matches["dev"] is not None
96 
97     if prerelease and devrelease:
98         raise ValueError("Cannot have both prerelease and devrelease")
99     elif prerelease:
100         matches["pre"] = matches["pre"].replace("a", "alpha").replace("b", "beta")
101         return "{major}.{minor}.{patch}-{pre}.{preversion}".format(**matches)
102     elif devrelease:
103         return "{major}.{minor}.{patch}-{dev}.{devversion}".format(**matches)
104     else:
105         return "{major}.{minor}.{patch}".format(**matches)
106 
107 
108 @functools.lru_cache
109 def is_core_version(version: str) -> bool:
110     match = re.fullmatch(CORE_VERSION_REGEX, version)
111     if match is None:
112         return False
113 
114     return True
115 
116 
117 def parse_current_version(target: Target) -> str:
118     """Parse current version"""
119     content = target.file.read_text()
120     match = target.pattern.search(content)
121 
122     if match is None:
123         raise ValueError(f"Unabled to detect version string: {target.file}")
124 
125     return match.groupdict()["version"]
126 
127 
128 def generate_updated_content(
129     target: Target, current_version: str, new_version: str
130 ) -> Callable:
131     file = target.file
132     pattern = target.pattern
133     content = file.read_text()
134 
135     if current_version == new_version:
136         return None
137 
138     # Some files only required to be bumped on core version release.
139     # For example, we don't deploy prebuilt docker images for dev release.
140     if not target.prerelease:
141         if not is_core_version(new_version):
142             print(f"[*] {file}: Skipped (not targeting a core version)")
143             return None
144 
145     new_content = content
146     startpos = 0
147     while match := pattern.search(new_content, pos=startpos):
148         version = match.groupdict()["version"]
149         if version == current_version:
150             start, end = match.span()
151             new_span = new_content[start:end].replace(current_version, new_version)
152             new_content = new_content[:start] + new_span + new_content[end:]
153             startpos = end
154         elif version == new_version:
155             break
156         else:
157             raise ValueError(
158                 f"'{file}' contains invalid version: expected '{current_version}' but found '{version}'"
159             )
160 
161     show_diff(content, new_content, file)
162 
163     return new_content
164 
165 
166 def show_diff(before: str, after: str, file: pathlib.Path):
167     diffs = list(
168         difflib.unified_diff(
169             before.splitlines(keepends=True), after.splitlines(keepends=True), n=0
170         )
171     )[2:]
172     print(f"[*] Diff of '{file}':\n")
173     print("".join(diffs))
174 
175 
176 def parse_args():
177     parser = argparse.ArgumentParser("Bump version strings in the Pyodide repository")
178     parser.add_argument("--new-version", help="New version")
179     parser.add_argument(
180         "--dry-run", action="store_true", help="Don't actually write anything"
181     )
182 
183     return parser.parse_args()
184 
185 
186 def main():
187     args = parse_args()
188 
189     if args.new_version is None:
190         new_version = input("New version (e.g. 0.22.0, 0.22.0a0, 0.22.0.dev0): ")
191     else:
192         new_version = args.new_version
193 
194     if re.fullmatch(PYTHON_VERSION_REGEX, new_version) is None:
195         raise ValueError(f"Invalid new version: {new_version}")
196 
197     new_version_py = new_version
198     new_version_js = python_version_to_js_version(new_version)
199 
200     # We want to update files in all-or-nothing strategy,
201     # so we keep the queue of update functions
202     update_queue = []
203 
204     targets = itertools.chain(
205         zip(PYTHON_TARGETS, [new_version_py] * len(PYTHON_TARGETS)),
206         zip(JS_TARGETS, [new_version_js] * len(JS_TARGETS)),
207     )
208     for target, new_version in targets:
209         current_version = parse_current_version(target)
210         new_content = generate_updated_content(target, current_version, new_version)
211         if new_content is not None:
212             update_queue.append((target, new_content))
213 
214     if args.dry_run:
215         return
216 
217     for target, content in update_queue:
218         target.file.write_text(content)
219 
220 
221 if __name__ == "__main__":
222     main()
223
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/tools/bump_version.py b/tools/bump_version.py
--- a/tools/bump_version.py
+++ b/tools/bump_version.py
@@ -64,6 +64,16 @@
         build_version_pattern(r"version\s*=\s*{{{python_version}}}"),
         prerelease=False,
     ),
+    Target(
+        ROOT / "src/js/version.ts",
+        build_version_pattern('version: string = "{python_version}"'),
+        prerelease=True,
+    ),
+    Target(
+        ROOT / "src/core/pre.js",
+        build_version_pattern('API.version = "{python_version}"'),
+        prerelease=True,
+    ),
 ]
 
 JS_TARGETS = [

{"golden_diff": "diff --git a/tools/bump_version.py b/tools/bump_version.py\n--- a/tools/bump_version.py\n+++ b/tools/bump_version.py\n@@ -64,6 +64,16 @@\n         build_version_pattern(r\"version\\s*=\\s*{{{python_version}}}\"),\n         prerelease=False,\n     ),\n+    Target(\n+        ROOT / \"src/js/version.ts\",\n+        build_version_pattern('version: string = \"{python_version}\"'),\n+        prerelease=True,\n+    ),\n+    Target(\n+        ROOT / \"src/core/pre.js\",\n+        build_version_pattern('API.version = \"{python_version}\"'),\n+        prerelease=True,\n+    ),\n ]\n \n JS_TARGETS = [\n", "issue": "Export version from pyodide JS module\n## \ud83d\ude80 Feature\r\n\r\nIn `pyodide.d.ts` I see `declare let version: string;` but it's not exported. It'd be great if it was exported so that I can do this:\r\n\r\n```ts\r\nimport {version} from \"pyodide\";\r\n```\r\n\r\n### Motivation\r\n\r\nI have [some code](https://github.com/alexmojaki/pyodide-worker-runner/blob/e7dd3d0ee1dff457bf9d6104944477840a83e5a7/lib/index.ts#L16) that roughly looks like this:\r\n\r\n```ts\r\nimport {loadPyodide, PyodideInterface} from \"pyodide\";\r\n\r\nconst version = \"0.21.1\";\r\nconst indexURL = `https://cdn.jsdelivr.net/pyodide/v${version}/full/`;\r\nconst pyodide: PyodideInterface = await loadPyodide({indexURL});\r\nif (pyodide.version !== version) {\r\n  throw new Error(\r\n    `loadPyodide loaded version ${pyodide.version} instead of ${version}`,\r\n  );\r\n}\r\n```\r\n\r\nI'd like to import `version` instead of setting it manually, so that it always automatically matches whatever version of pyodide is installed.\n", "before_files": [{"content": "#!/usr/bin/env python3\n\nimport argparse\nimport difflib\nimport functools\nimport itertools\nimport pathlib\nimport re\nfrom ast import Str\nfrom collections import namedtuple\nfrom collections.abc import Callable\n\nCORE_VERSION_REGEX = r\"(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)\"\n\nPYTHON_VERSION_REGEX = CORE_VERSION_REGEX + (\n    r\"((?P<pre>a|b|rc)(?P<preversion>\\d+))?\" r\"(\\.(?P<dev>dev)(?P<devversion>\\d+))?\"\n)\n\nJS_VERSION_REGEX = CORE_VERSION_REGEX + (\n    r\"(\\-(?P<pre>alpha|beta|rc)\\.(?P<preversion>\\d+))?\"\n    r\"(\\-(?P<dev>dev)\\.(?P<devversion>\\d+))?\"\n)\n\n\ndef build_version_pattern(pattern):\n    return re.compile(\n        pattern.format(\n            python_version=f\"(?P<version>{PYTHON_VERSION_REGEX})\",\n            js_version=f\"(?P<version>{JS_VERSION_REGEX})\",\n        )\n    )\n\n\nROOT = pathlib.Path(__file__).resolve().parent.parent\nTarget = namedtuple(\"target\", (\"file\", \"pattern\", \"prerelease\"))\nPYTHON_TARGETS = [\n    Target(\n        file=ROOT / \"src/py/pyodide/__init__.py\",\n        pattern=build_version_pattern('__version__ = \"{python_version}\"'),\n        prerelease=True,\n    ),\n    Target(\n        file=ROOT / \"src/py/setup.cfg\",\n        pattern=build_version_pattern(\"version = {python_version}\"),\n        prerelease=True,\n    ),\n    Target(\n        ROOT / \"pyodide-build/pyodide_build/__init__.py\",\n        pattern=build_version_pattern('__version__ = \"{python_version}\"'),\n        prerelease=True,\n    ),\n    Target(\n        ROOT / \"docs/conf.py\",\n        build_version_pattern('pyodide_version = \"{python_version}\"'),\n        prerelease=True,\n    ),\n    Target(\n        ROOT / \"run_docker\",\n        build_version_pattern('PYODIDE_PREBUILT_IMAGE_TAG=\"{python_version}\"'),\n        prerelease=False,\n    ),\n    Target(\n        ROOT / \"docs/project/about.md\",\n        build_version_pattern(r\"version\\s*=\\s*{{{python_version}}}\"),\n        prerelease=False,\n    ),\n]\n\nJS_TARGETS = [\n    Target(\n        ROOT / \"src/js/package.json\",\n        build_version_pattern(r'\"pyodide\",\\s*\"version\": \"{js_version}\"'),\n        prerelease=True,\n    ),\n    Target(\n        ROOT / \"src/js/package-lock.json\",\n        build_version_pattern(r'\"pyodide\",\\s*\"version\": \"{js_version}\"'),\n        prerelease=True,\n    ),\n]\n\n\n@functools.lru_cache\ndef python_version_to_js_version(version: str) -> Str:\n    \"\"\"\n    Convert Python version name to JS version name\n    These two are different in prerelease or dev versions.\n    e.g. 1.2.3a0 <==> 1.2.3-alpha.0\n         4.5.6.dev2 <==> 4.5.6-dev.2\n    \"\"\"\n    match = re.match(PYTHON_VERSION_REGEX, version)\n    matches = match.groupdict()\n\n    prerelease = matches[\"pre\"] is not None\n    devrelease = matches[\"dev\"] is not None\n\n    if prerelease and devrelease:\n        raise ValueError(\"Cannot have both prerelease and devrelease\")\n    elif prerelease:\n        matches[\"pre\"] = matches[\"pre\"].replace(\"a\", \"alpha\").replace(\"b\", \"beta\")\n        return \"{major}.{minor}.{patch}-{pre}.{preversion}\".format(**matches)\n    elif devrelease:\n        return \"{major}.{minor}.{patch}-{dev}.{devversion}\".format(**matches)\n    else:\n        return \"{major}.{minor}.{patch}\".format(**matches)\n\n\n@functools.lru_cache\ndef is_core_version(version: str) -> bool:\n    match = re.fullmatch(CORE_VERSION_REGEX, version)\n    if match is None:\n        return False\n\n    return True\n\n\ndef parse_current_version(target: Target) -> str:\n    \"\"\"Parse current version\"\"\"\n    content = target.file.read_text()\n    match = target.pattern.search(content)\n\n    if match is None:\n        raise ValueError(f\"Unabled to detect version string: {target.file}\")\n\n    return match.groupdict()[\"version\"]\n\n\ndef generate_updated_content(\n    target: Target, current_version: str, new_version: str\n) -> Callable:\n    file = target.file\n    pattern = target.pattern\n    content = file.read_text()\n\n    if current_version == new_version:\n        return None\n\n    # Some files only required to be bumped on core version release.\n    # For example, we don't deploy prebuilt docker images for dev release.\n    if not target.prerelease:\n        if not is_core_version(new_version):\n            print(f\"[*] {file}: Skipped (not targeting a core version)\")\n            return None\n\n    new_content = content\n    startpos = 0\n    while match := pattern.search(new_content, pos=startpos):\n        version = match.groupdict()[\"version\"]\n        if version == current_version:\n            start, end = match.span()\n            new_span = new_content[start:end].replace(current_version, new_version)\n            new_content = new_content[:start] + new_span + new_content[end:]\n            startpos = end\n        elif version == new_version:\n            break\n        else:\n            raise ValueError(\n                f\"'{file}' contains invalid version: expected '{current_version}' but found '{version}'\"\n            )\n\n    show_diff(content, new_content, file)\n\n    return new_content\n\n\ndef show_diff(before: str, after: str, file: pathlib.Path):\n    diffs = list(\n        difflib.unified_diff(\n            before.splitlines(keepends=True), after.splitlines(keepends=True), n=0\n        )\n    )[2:]\n    print(f\"[*] Diff of '{file}':\\n\")\n    print(\"\".join(diffs))\n\n\ndef parse_args():\n    parser = argparse.ArgumentParser(\"Bump version strings in the Pyodide repository\")\n    parser.add_argument(\"--new-version\", help=\"New version\")\n    parser.add_argument(\n        \"--dry-run\", action=\"store_true\", help=\"Don't actually write anything\"\n    )\n\n    return parser.parse_args()\n\n\ndef main():\n    args = parse_args()\n\n    if args.new_version is None:\n        new_version = input(\"New version (e.g. 0.22.0, 0.22.0a0, 0.22.0.dev0): \")\n    else:\n        new_version = args.new_version\n\n    if re.fullmatch(PYTHON_VERSION_REGEX, new_version) is None:\n        raise ValueError(f\"Invalid new version: {new_version}\")\n\n    new_version_py = new_version\n    new_version_js = python_version_to_js_version(new_version)\n\n    # We want to update files in all-or-nothing strategy,\n    # so we keep the queue of update functions\n    update_queue = []\n\n    targets = itertools.chain(\n        zip(PYTHON_TARGETS, [new_version_py] * len(PYTHON_TARGETS)),\n        zip(JS_TARGETS, [new_version_js] * len(JS_TARGETS)),\n    )\n    for target, new_version in targets:\n        current_version = parse_current_version(target)\n        new_content = generate_updated_content(target, current_version, new_version)\n        if new_content is not None:\n            update_queue.append((target, new_content))\n\n    if args.dry_run:\n        return\n\n    for target, content in update_queue:\n        target.file.write_text(content)\n\n\nif __name__ == \"__main__\":\n    main()\n", "path": "tools/bump_version.py"}], "after_files": [{"content": "#!/usr/bin/env python3\n\nimport argparse\nimport difflib\nimport functools\nimport itertools\nimport pathlib\nimport re\nfrom ast import Str\nfrom collections import namedtuple\nfrom collections.abc import Callable\n\nCORE_VERSION_REGEX = r\"(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)\"\n\nPYTHON_VERSION_REGEX = CORE_VERSION_REGEX + (\n    r\"((?P<pre>a|b|rc)(?P<preversion>\\d+))?\" r\"(\\.(?P<dev>dev)(?P<devversion>\\d+))?\"\n)\n\nJS_VERSION_REGEX = CORE_VERSION_REGEX + (\n    r\"(\\-(?P<pre>alpha|beta|rc)\\.(?P<preversion>\\d+))?\"\n    r\"(\\-(?P<dev>dev)\\.(?P<devversion>\\d+))?\"\n)\n\n\ndef build_version_pattern(pattern):\n    return re.compile(\n        pattern.format(\n            python_version=f\"(?P<version>{PYTHON_VERSION_REGEX})\",\n            js_version=f\"(?P<version>{JS_VERSION_REGEX})\",\n        )\n    )\n\n\nROOT = pathlib.Path(__file__).resolve().parent.parent\nTarget = namedtuple(\"target\", (\"file\", \"pattern\", \"prerelease\"))\nPYTHON_TARGETS = [\n    Target(\n        file=ROOT / \"src/py/pyodide/__init__.py\",\n        pattern=build_version_pattern('__version__ = \"{python_version}\"'),\n        prerelease=True,\n    ),\n    Target(\n        file=ROOT / \"src/py/setup.cfg\",\n        pattern=build_version_pattern(\"version = {python_version}\"),\n        prerelease=True,\n    ),\n    Target(\n        ROOT / \"pyodide-build/pyodide_build/__init__.py\",\n        pattern=build_version_pattern('__version__ = \"{python_version}\"'),\n        prerelease=True,\n    ),\n    Target(\n        ROOT / \"docs/conf.py\",\n        build_version_pattern('pyodide_version = \"{python_version}\"'),\n        prerelease=True,\n    ),\n    Target(\n        ROOT / \"run_docker\",\n        build_version_pattern('PYODIDE_PREBUILT_IMAGE_TAG=\"{python_version}\"'),\n        prerelease=False,\n    ),\n    Target(\n        ROOT / \"docs/project/about.md\",\n        build_version_pattern(r\"version\\s*=\\s*{{{python_version}}}\"),\n        prerelease=False,\n    ),\n    Target(\n        ROOT / \"src/js/version.ts\",\n        build_version_pattern('version: string = \"{python_version}\"'),\n        prerelease=True,\n    ),\n    Target(\n        ROOT / \"src/core/pre.js\",\n        build_version_pattern('API.version = \"{python_version}\"'),\n        prerelease=True,\n    ),\n]\n\nJS_TARGETS = [\n    Target(\n        ROOT / \"src/js/package.json\",\n        build_version_pattern(r'\"pyodide\",\\s*\"version\": \"{js_version}\"'),\n        prerelease=True,\n    ),\n    Target(\n        ROOT / \"src/js/package-lock.json\",\n        build_version_pattern(r'\"pyodide\",\\s*\"version\": \"{js_version}\"'),\n        prerelease=True,\n    ),\n]\n\n\n@functools.lru_cache\ndef python_version_to_js_version(version: str) -> Str:\n    \"\"\"\n    Convert Python version name to JS version name\n    These two are different in prerelease or dev versions.\n    e.g. 1.2.3a0 <==> 1.2.3-alpha.0\n         4.5.6.dev2 <==> 4.5.6-dev.2\n    \"\"\"\n    match = re.match(PYTHON_VERSION_REGEX, version)\n    matches = match.groupdict()\n\n    prerelease = matches[\"pre\"] is not None\n    devrelease = matches[\"dev\"] is not None\n\n    if prerelease and devrelease:\n        raise ValueError(\"Cannot have both prerelease and devrelease\")\n    elif prerelease:\n        matches[\"pre\"] = matches[\"pre\"].replace(\"a\", \"alpha\").replace(\"b\", \"beta\")\n        return \"{major}.{minor}.{patch}-{pre}.{preversion}\".format(**matches)\n    elif devrelease:\n        return \"{major}.{minor}.{patch}-{dev}.{devversion}\".format(**matches)\n    else:\n        return \"{major}.{minor}.{patch}\".format(**matches)\n\n\n@functools.lru_cache\ndef is_core_version(version: str) -> bool:\n    match = re.fullmatch(CORE_VERSION_REGEX, version)\n    if match is None:\n        return False\n\n    return True\n\n\ndef parse_current_version(target: Target) -> str:\n    \"\"\"Parse current version\"\"\"\n    content = target.file.read_text()\n    match = target.pattern.search(content)\n\n    if match is None:\n        raise ValueError(f\"Unabled to detect version string: {target.file}\")\n\n    return match.groupdict()[\"version\"]\n\n\ndef generate_updated_content(\n    target: Target, current_version: str, new_version: str\n) -> Callable:\n    file = target.file\n    pattern = target.pattern\n    content = file.read_text()\n\n    if current_version == new_version:\n        return None\n\n    # Some files only required to be bumped on core version release.\n    # For example, we don't deploy prebuilt docker images for dev release.\n    if not target.prerelease:\n        if not is_core_version(new_version):\n            print(f\"[*] {file}: Skipped (not targeting a core version)\")\n            return None\n\n    new_content = content\n    startpos = 0\n    while match := pattern.search(new_content, pos=startpos):\n        version = match.groupdict()[\"version\"]\n        if version == current_version:\n            start, end = match.span()\n            new_span = new_content[start:end].replace(current_version, new_version)\n            new_content = new_content[:start] + new_span + new_content[end:]\n            startpos = end\n        elif version == new_version:\n            break\n        else:\n            raise ValueError(\n                f\"'{file}' contains invalid version: expected '{current_version}' but found '{version}'\"\n            )\n\n    show_diff(content, new_content, file)\n\n    return new_content\n\n\ndef show_diff(before: str, after: str, file: pathlib.Path):\n    diffs = list(\n        difflib.unified_diff(\n            before.splitlines(keepends=True), after.splitlines(keepends=True), n=0\n        )\n    )[2:]\n    print(f\"[*] Diff of '{file}':\\n\")\n    print(\"\".join(diffs))\n\n\ndef parse_args():\n    parser = argparse.ArgumentParser(\"Bump version strings in the Pyodide repository\")\n    parser.add_argument(\"--new-version\", help=\"New version\")\n    parser.add_argument(\n        \"--dry-run\", action=\"store_true\", help=\"Don't actually write anything\"\n    )\n\n    return parser.parse_args()\n\n\ndef main():\n    args = parse_args()\n\n    if args.new_version is None:\n        new_version = input(\"New version (e.g. 0.22.0, 0.22.0a0, 0.22.0.dev0): \")\n    else:\n        new_version = args.new_version\n\n    if re.fullmatch(PYTHON_VERSION_REGEX, new_version) is None:\n        raise ValueError(f\"Invalid new version: {new_version}\")\n\n    new_version_py = new_version\n    new_version_js = python_version_to_js_version(new_version)\n\n    # We want to update files in all-or-nothing strategy,\n    # so we keep the queue of update functions\n    update_queue = []\n\n    targets = itertools.chain(\n        zip(PYTHON_TARGETS, [new_version_py] * len(PYTHON_TARGETS)),\n        zip(JS_TARGETS, [new_version_js] * len(JS_TARGETS)),\n    )\n    for target, new_version in targets:\n        current_version = parse_current_version(target)\n        new_content = generate_updated_content(target, current_version, new_version)\n        if new_content is not None:\n            update_queue.append((target, new_content))\n\n    if args.dry_run:\n        return\n\n    for target, content in update_queue:\n        target.file.write_text(content)\n\n\nif __name__ == \"__main__\":\n    main()\n", "path": "tools/bump_version.py"}]}