Skip to content

Commit cdcbda9

Browse files
committed
use parsed version tuple everywhere
avoids re-parsing the specified version multiple times
1 parent b0fd434 commit cdcbda9

File tree

5 files changed

+65
-47
lines changed

5 files changed

+65
-47
lines changed

clang_tools/install.py

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,31 @@
1010
import shutil
1111
import subprocess
1212
import sys
13-
from typing import Optional
14-
from . import release_tag
13+
from typing import Optional, cast
1514

16-
from . import install_os, RESET_COLOR, suffix, YELLOW
17-
from .util import download_file, verify_sha512, get_sha_checksum, parse_version
15+
from . import release_tag, install_os, RESET_COLOR, suffix, YELLOW
16+
from .util import (
17+
download_file,
18+
verify_sha512,
19+
get_sha_checksum,
20+
parse_version,
21+
VERSION_TUPLE,
22+
)
1823

1924

2025
#: This pattern is designed to match only the major version number.
2126
RE_PARSE_VERSION = re.compile(rb"version\s([\d\.]+)", re.MULTILINE)
2227

2328

24-
def is_installed(tool_name: str, version: str) -> Optional[Path]:
29+
def is_installed(tool_name: str, version: VERSION_TUPLE) -> Optional[Path]:
2530
"""Detect if the specified tool is installed.
2631
2732
:param tool_name: The name of the specified tool.
2833
:param version: The specific version to expect.
2934
3035
:returns: The path to the detected tool (if found), otherwise `None`.
3136
"""
32-
version_tuple = parse_version(version)
33-
ver_major = version_tuple[0]
37+
ver_major = version[0]
3438
exe_name = (
3539
f"{tool_name}" + (f"-{ver_major}" if install_os != "windows" else "") + suffix
3640
)
@@ -44,24 +48,28 @@ def is_installed(tool_name: str, version: str) -> Optional[Path]:
4448
except (FileNotFoundError, subprocess.CalledProcessError):
4549
return None # tool is not installed
4650
ver_num = RE_PARSE_VERSION.search(result.stdout)
51+
assert ver_num is not None, "Failed to parse version from tool output"
52+
ver_match = cast(bytes, ver_num.groups(0)[0]).decode(encoding="utf-8")
4753
print(
4854
f"Found a installed version of {tool_name}:",
49-
ver_num.groups(0)[0].decode(encoding="utf-8"),
55+
ver_match,
5056
end=" ",
5157
)
52-
path = shutil.which(exe_name) # find the installed binary
53-
if path is None:
58+
exe_path = shutil.which(exe_name) # find the installed binary
59+
if exe_path is None:
5460
print() # print end-of-line
5561
return None # failed to locate the binary
56-
path = Path(path).resolve()
62+
path = Path(exe_path).resolve()
5763
print("at", str(path))
58-
ver_num = ver_num.groups(0)[0].decode(encoding="utf-8").split(".")
59-
if ver_num is None or ver_num[0] != str(ver_major):
64+
ver_tuple = ver_match.split(".")
65+
if ver_tuple is None or ver_tuple[0] != str(ver_major):
6066
return None # version is unknown or not the desired major release
6167
return path
6268

6369

64-
def clang_tools_binary_url(tool: str, version: str, tag: str = release_tag) -> str:
70+
def clang_tools_binary_url(
71+
tool: str, version: VERSION_TUPLE, tag: str = release_tag
72+
) -> str:
6573
"""Assemble the URL to the binary.
6674
6775
:param tool: The name of the tool to download.
@@ -74,12 +82,12 @@ def clang_tools_binary_url(tool: str, version: str, tag: str = release_tag) -> s
7482
"https://github.com/cpp-linter/clang-tools-static-binaries/releases/download/"
7583
+ tag
7684
)
77-
download_url = f"{base_url}/{tool}-{version}_{install_os}-amd64{suffix}"
85+
download_url = f"{base_url}/{tool}-{version[0]}_{install_os}-amd64{suffix}"
7886
return download_url.replace(" ", "")
7987

8088

8189
def install_tool(
82-
tool_name: str, version: str, directory: str, no_progress_bar: bool
90+
tool_name: str, version: VERSION_TUPLE, directory: str, no_progress_bar: bool
8391
) -> bool:
8492
"""An abstract function that can install either clang-tidy or clang-format.
8593
@@ -91,21 +99,21 @@ def install_tool(
9199
:returns: `True` if the binary had to be downloaded and installed.
92100
`False` if the binary was not downloaded but is installed in ``directory``.
93101
"""
94-
destination = Path(directory, f"{tool_name}-{version}{suffix}")
102+
destination = Path(directory, f"{tool_name}-{version[0]}{suffix}")
95103
bin_url = clang_tools_binary_url(tool_name, version)
96104
if destination.exists():
97-
print(f"{tool_name}-{version}", "already installed...")
105+
print(f"{tool_name}-{version[0]}", "already installed...")
98106
print(" checking SHA512...", end=" ")
99107
if verify_sha512(get_sha_checksum(bin_url), destination.read_bytes()):
100108
print("valid")
101109
return False
102110
print("invalid")
103111
uninstall_tool(tool_name, version, directory)
104-
print("Downloading", tool_name, f"(version {version})")
112+
print("Downloading", tool_name, f"(version {version[0]})")
105113
bin_name = str(PurePath(bin_url).stem)
106114
if download_file(bin_url, bin_name, no_progress_bar) is None:
107115
raise OSError(f"Failed to download {bin_name} from {bin_url}")
108-
move_and_chmod_bin(bin_name, f"{tool_name}-{version}{suffix}", directory)
116+
move_and_chmod_bin(bin_name, f"{tool_name}-{version[0]}{suffix}", directory)
109117
if not verify_sha512(get_sha_checksum(bin_url), destination.read_bytes()):
110118
raise ValueError(
111119
f"File was corrupted during download from {bin_url}"
@@ -154,10 +162,10 @@ def move_and_chmod_bin(old_bin_name: str, new_bin_name: str, install_dir: str) -
154162

155163
def create_sym_link(
156164
tool_name: str,
157-
version: str,
165+
version: VERSION_TUPLE,
158166
install_dir: str,
159167
overwrite: bool = False,
160-
target: Path = None,
168+
target: Optional[Path] = None,
161169
) -> bool:
162170
"""Create a symlink to the installed binary that
163171
doesn't have the version number appended.
@@ -178,7 +186,7 @@ def create_sym_link(
178186
link_root_path.mkdir(parents=True)
179187
link = link_root_path / (tool_name + suffix)
180188
if target is None:
181-
target = link_root_path / f"{tool_name}-{version}{suffix}"
189+
target = link_root_path / f"{tool_name}-{version[0]}{suffix}"
182190
if link.exists():
183191
if not link.is_symlink():
184192
print(
@@ -212,15 +220,15 @@ def create_sym_link(
212220
return False
213221

214222

215-
def uninstall_tool(tool_name: str, version: str, directory: str):
223+
def uninstall_tool(tool_name: str, version: VERSION_TUPLE, directory: str):
216224
"""Remove a specified tool of a given version.
217225
218226
:param tool_name: The name of the clang tool to uninstall.
219227
:param version: The version of the clang-tools to remove.
220228
:param directory: The directory from which to remove the
221229
installed clang-tools.
222230
"""
223-
tool_path = Path(directory, f"{tool_name}-{version}{suffix}")
231+
tool_path = Path(directory, f"{tool_name}-{version[0]}{suffix}")
224232
if tool_path.exists():
225233
print("Removing", tool_path.name, "from", str(tool_path.parent))
226234
tool_path.unlink()
@@ -241,12 +249,17 @@ def uninstall_clang_tools(version: str, directory: str):
241249
"""
242250
install_dir = install_dir_name(directory)
243251
print(f"Uninstalling version {version} from {str(install_dir)}")
252+
version_tuple = parse_version(version)
244253
for tool in ("clang-format", "clang-tidy"):
245-
uninstall_tool(tool, version, install_dir)
254+
uninstall_tool(tool, version_tuple, install_dir)
246255

247256

248257
def install_clang_tools(
249-
version: str, tools: str, directory: str, overwrite: bool, no_progress_bar: bool
258+
version: VERSION_TUPLE,
259+
tools: str,
260+
directory: str,
261+
overwrite: bool,
262+
no_progress_bar: bool,
250263
) -> None:
251264
"""Wraps functions used to individually install tools.
252265
@@ -258,7 +271,7 @@ def install_clang_tools(
258271
:param no_progress_bar: A flag used to disable the downloads' progress bar.
259272
"""
260273
install_dir = install_dir_name(directory)
261-
if install_dir.rstrip(os.sep) not in os.environ.get("PATH"):
274+
if install_dir.rstrip(os.sep) not in os.environ.get("PATH", ""):
262275
print(
263276
f"{YELLOW}{install_dir}",
264277
f"directory is not in your environment variable PATH.{RESET_COLOR}",

clang_tools/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ def main():
6868
if args.uninstall:
6969
uninstall_clang_tools(args.uninstall, args.directory)
7070
elif args.install:
71-
if parse_version(args.install) != (0, 0, 0):
71+
version = parse_version(args.install)
72+
if version != (0, 0, 0):
7273
install_clang_tools(
73-
args.install,
74+
version,
7475
args.tool,
7576
args.directory,
7677
args.overwrite,

clang_tools/util.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from urllib.error import HTTPError
1313
from http.client import HTTPResponse
1414

15+
VERSION_TUPLE = Tuple[int, int, int]
16+
1517

1618
def check_install_os() -> str:
1719
"""Identify this Operating System.
@@ -82,7 +84,6 @@ def get_sha_checksum(binary_url: str) -> str:
8284
with urllib.request.urlopen(
8385
binary_url.replace(".exe", "") + ".sha512sum"
8486
) as response:
85-
response: HTTPResponse
8687
return response.read(response.length).decode(encoding="utf-8")
8788

8889

@@ -101,7 +102,7 @@ def verify_sha512(checksum: str, exe: bytes) -> bool:
101102
return checksum == hashlib.sha512(exe).hexdigest()
102103

103104

104-
def parse_version(version: str) -> Tuple[int]:
105+
def parse_version(version: str) -> VERSION_TUPLE:
105106
"""Parse the given version string into a semantic specification.
106107
107108
:param version: The version specification as a string.
@@ -114,7 +115,7 @@ def parse_version(version: str) -> Tuple[int]:
114115
# append minor and patch version numbers if not specified
115116
version_tuple += ["0"] * (3 - len(version_tuple))
116117
try:
117-
return tuple([int(x) for x in version_tuple])
118+
return tuple([int(x) for x in version_tuple]) # type: ignore[return-value]
118119
except ValueError:
119120
assert Path(version).exists(), "specified version is not a semantic or a path"
120121
return (0, 0, 0)

tests/test_install.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@
1212
is_installed,
1313
uninstall_clang_tools,
1414
)
15+
from clang_tools.util import VERSION_TUPLE
1516

1617

17-
@pytest.mark.parametrize("version", [str(v) for v in range(7, 17)] + ["12.0.1"])
18+
@pytest.mark.parametrize("version", [(v, 0, 0) for v in range(7, 17)] + [(12, 0, 1)])
1819
@pytest.mark.parametrize(
1920
"tool_name",
2021
["clang-format", "clang-tidy", "clang-query", "clang-apply-replacements"],
2122
)
22-
def test_clang_tools_binary_url(tool_name: str, version: str):
23+
def test_clang_tools_binary_url(tool_name: str, version: VERSION_TUPLE):
2324
"""Test `clang_tools_binary_url()`"""
2425
url = clang_tools_binary_url(tool_name, version)
25-
assert f"{tool_name}-{version}_{install_os}-amd64" in url
26+
assert f"{tool_name}-{version[0]}_{install_os}-amd64" in url
2627

2728

2829
@pytest.mark.parametrize("directory", ["", "."])
@@ -35,10 +36,10 @@ def test_dir_name(monkeypatch: pytest.MonkeyPatch, directory: str):
3536

3637
def test_create_symlink(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
3738
"""Test creation of symlink."""
38-
tool_name, version = ("clang-tool", "1")
39+
tool_name, version = ("clang-tool", (1, 0, 0))
3940
monkeypatch.chdir(str(tmp_path))
4041
# use a test tar file and rename it to "clang-tool-1" (+ OS suffix)
41-
test_target = tmp_path / f"{tool_name}-{version}{suffix}"
42+
test_target = tmp_path / f"{tool_name}-{version[0]}{suffix}"
4243
test_target.write_bytes(b"some binary data")
4344

4445
# create the symlink
@@ -54,8 +55,10 @@ def test_create_symlink(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
5455
assert not create_sym_link(tool_name, version, str(tmp_path), True)
5556

5657

57-
@pytest.mark.parametrize("version", ["12"])
58-
def test_install_tools(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, version: str):
58+
@pytest.mark.parametrize("version", [(12, 0, 0)])
59+
def test_install_tools(
60+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, version: VERSION_TUPLE
61+
):
5962
"""Test install tools to a temp directory."""
6063
monkeypatch.chdir(tmp_path)
6164
tool_name = "clang-format"
@@ -64,14 +67,14 @@ def test_install_tools(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, version:
6467
# invoking again should return False
6568
assert not install_tool(tool_name, version, str(tmp_path), False)
6669
# uninstall the tool deliberately
67-
uninstall_clang_tools(version, str(tmp_path))
70+
uninstall_clang_tools(".".join([str(x) for x in version]), str(tmp_path))
6871
assert f"{tool_name}-{version}{suffix}" not in [
6972
fd.name for fd in tmp_path.iterdir()
7073
]
7174

7275

73-
@pytest.mark.parametrize("version", ["0"])
74-
def test_is_installed(version: str):
76+
@pytest.mark.parametrize("version", [(0, 0, 0)])
77+
def test_is_installed(version: VERSION_TUPLE):
7578
"""Test if installed version matches specified ``version``"""
7679
tool_path = is_installed("clang-format", version=version)
7780
assert tool_path is None
@@ -84,9 +87,9 @@ def test_path_warning(capsys: pytest.CaptureFixture):
8487
2. indicates a failure to download a tool
8588
"""
8689
try:
87-
install_clang_tools("x", "x", ".", False, False)
90+
install_clang_tools((0, 0, 0), "x", ".", False, False)
8891
except OSError as exc:
89-
if install_dir_name(".") not in os.environ.get("PATH"): # pragma: no cover
92+
if install_dir_name(".") not in os.environ.get("PATH", ""): # pragma: no cover
9093
# this warning does not happen in an activated venv
9194
result = capsys.readouterr()
9295
assert "directory is not in your environment variable PATH" in result.out

tests/test_util.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def test_check_install_os():
2525
def test_download_file(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, tag: str):
2626
"""Test that deliberately fails to download a file."""
2727
monkeypatch.chdir(str(tmp_path))
28-
url = clang_tools_binary_url("clang-format", "12", tag=tag)
28+
url = clang_tools_binary_url("clang-format", (12, 0, 0), tag=tag)
2929
file_name = download_file(url, "file.tar.gz", True)
3030
assert file_name is not None
3131

@@ -37,7 +37,7 @@ def test_get_sha(monkeypatch: pytest.MonkeyPatch):
3737
expected = Path(f"clang-format-12_{install_os}-amd64.sha512sum").read_text(
3838
encoding="utf-8"
3939
)
40-
url = clang_tools_binary_url("clang-format", "12", tag=release_tag)
40+
url = clang_tools_binary_url("clang-format", (12, 0, 0), tag=release_tag)
4141
assert get_sha_checksum(url) == expected
4242

4343

0 commit comments

Comments
 (0)