Skip to content

Commit 53015d7

Browse files
chore: type check using 'ty' (#120)
1 parent 4b3359a commit 53015d7

9 files changed

Lines changed: 90 additions & 40 deletions

File tree

.github/workflows/ci.yml

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,54 @@ jobs:
2020
with:
2121
egress-policy: audit
2222

23-
- name: Fetch only the required files for testing
24-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
23+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2524

2625
- uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
26+
with:
27+
extra-args: "--all-files --skip=ty-check"
2728

2829
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
2930
if: always()
3031

32+
type_check:
33+
runs-on: ${{ matrix.os }}-${{ matrix.os_version }}
34+
strategy:
35+
fail-fast: false
36+
matrix:
37+
os: [windows]
38+
os_version: [latest]
39+
python-version: ["3.13", "3.14"]
40+
defaults:
41+
run:
42+
shell: bash
43+
env:
44+
UV_NO_PROGRESS: true
45+
steps:
46+
- name: Harden the runner (Audit all outbound calls)
47+
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1
48+
with:
49+
egress-policy: audit
50+
51+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
52+
53+
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
54+
with:
55+
activate-environment: true
56+
cache-dependency-glob: "pyproject.toml"
57+
enable-cache: true
58+
59+
- name: Install dependencies
60+
run: ci/install-tools.sh --dev --tests
61+
62+
- name: Type check
63+
run: ty check
64+
3165
build_wheel:
3266
name: Build wheels
3367
uses: ./.github/workflows/build.yml
3468
needs:
3569
- pre_commit
70+
- type_check
3671

3772
testpypi:
3873
name: Publish package to TestPyPI

.pre-commit-config.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ repos:
2323
hooks:
2424
- id: validate-pyproject
2525
additional_dependencies:
26-
- validate-pyproject-schema-store[all]>=2026.05.18
26+
- validate-pyproject-schema-store[all]>=2026.05.24
2727

2828
- repo: https://github.com/astral-sh/ruff-pre-commit
2929
rev: 0c7b6c989466a93942def1f84baf36ddfcd60c83 # frozen: v0.15.14
@@ -58,6 +58,16 @@ repos:
5858
hooks:
5959
- id: shellcheck
6060

61+
- repo: local
62+
hooks:
63+
- id: ty-check
64+
name: typecheck
65+
description: Type check python files with ty
66+
entry: ty check
67+
language: system
68+
types: [python]
69+
pass_filenames: false
70+
6171
- repo: local
6272
hooks:
6373
- id: pythoncapi

ci/install-tools.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ elif which python &>/dev/null; then
2323
PY_PLATFORM=$(python -c "import sysconfig; print(sysconfig.get_platform(), end='')")
2424
IS_WINDOWS=$([[ $PY_PLATFORM == win* ]] && echo "1")
2525
IS_UV="1"
26+
python ci/requirements.py
2627
else
2728
echo "error: Python is required."
2829
exit 1
@@ -187,7 +188,7 @@ if [ "$INSTALL_DEV" == "1" ]; then
187188
# prek has no dependencies (doesn't bloat the installed packages)
188189
if [ "$name" == "prek" ] && [ "$IS_CONDA" == "1" ]; then
189190
$CONDA_EXE install -c conda-forge "$name" -S -q -y
190-
elif [ "$name" == "prek" ] && [ "$IS_UV" == "1" ]; then
191+
elif [ "$name" != "cibuildwheel" ] && [ "$IS_UV" == "1" ]; then
191192
uv pip install --upgrade "$name"
192193
else
193194
filename=$INSTALL_DIR/$name

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Documentation = "https://docs.python.org/3.12/library/msilib.html"
4848
dev = [
4949
"cibuildwheel>=3.4.1",
5050
"prek>=0.4.0,<0.5.0",
51+
"ty>=0.0.39",
5152
]
5253
tests = [
5354
"coverage>=7.13.0",

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
cibuildwheel>=3.4.1
22
prek>=0.4.0,<0.5.0
3+
ty>=0.0.39

src/msilib/__init__.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@
6161
)
6262

6363
if TYPE_CHECKING:
64-
from collections.abc import Container, Iterable, Sequence
64+
from collections.abc import Container, Iterable
6565
from types import ModuleType
6666

6767
from ._msi import _Database
68+
from .sequence import _SequenceType
6869

6970
__version__ = metadata.version("python-msilib")
7071

@@ -187,9 +188,9 @@ def sql(self) -> str:
187188
fields = []
188189
keys = []
189190
self.fields.sort()
190-
fields = [None] * len(self.fields)
191+
# fields: list[str] = [""] * len(self.fields)
191192
for index, name, ftype in self.fields:
192-
idx = index - 1
193+
index - 1
193194
unk = ftype & ~knownbits
194195
if unk:
195196
print(f"{self.name}.{name} unknown bits {unk:x}")
@@ -212,7 +213,8 @@ def sql(self) -> str:
212213
flags = "" if ftype & type_nullable else " NOT NULL"
213214
if ftype & type_localizable:
214215
flags += " LOCALIZABLE"
215-
fields[idx] = f"`{name}` {tname}{flags}"
216+
# fields[idx] = f"`{name}` {tname}{flags}"
217+
fields.append(f"`{name}` {tname}{flags}")
216218
if ftype & type_key:
217219
keys.append(f"`{name}`")
218220
fields = ", ".join(fields)
@@ -225,22 +227,18 @@ def create(self, db: _Database) -> None:
225227
v.Close()
226228

227229

228-
class _Unspecified:
229-
pass
230-
231-
232230
def change_sequence(
233-
seq: Sequence[tuple[str, str | None, int]],
231+
seq: _SequenceType,
234232
action: str,
235-
seqno: int | type[_Unspecified] = _Unspecified,
236-
cond: str | type[_Unspecified] = _Unspecified,
233+
seqno: int | None = None,
234+
cond: str | None = None,
237235
) -> None:
238236
"""Change the sequence number of an action in a sequence list."""
239237
for i in range(len(seq)):
240238
if seq[i][0] == action:
241-
if cond is _Unspecified:
239+
if cond is None:
242240
cond = seq[i][1]
243-
if seqno is _Unspecified:
241+
if seqno is None:
244242
seqno = seq[i][2]
245243
seq[i] = (action, cond, seqno)
246244
return
@@ -387,7 +385,7 @@ def gen_id(self, file: str) -> str:
387385
return logical
388386

389387
def append(
390-
self, full: str, file: str, logical: str
388+
self, full: str, file: str, logical: str | None
391389
) -> tuple[int, str] | None:
392390
if os.path.isdir(full):
393391
return None
@@ -415,7 +413,7 @@ def commit(self, db: _Database) -> None:
415413
class Directory:
416414
db: _Database
417415
cab: CAB
418-
basedir: str
416+
basedir: Directory
419417
physical: str
420418
logical: str
421419
component: str | None
@@ -429,7 +427,7 @@ def __init__(
429427
self,
430428
db: _Database,
431429
cab: CAB,
432-
basedir: str,
430+
basedir: Directory,
433431
physical: str,
434432
_logical: str,
435433
default: str,
@@ -485,7 +483,7 @@ def start_component(
485483
no keyfile is given, the KeyPath is left null in the Component table.
486484
"""
487485
if flags is None:
488-
flags = self.componentflags
486+
flags = self.componentflags or 0
489487
uuid = gen_uuid() if uuid is None else uuid.upper()
490488
if component is None:
491489
component = self.logical
@@ -527,37 +525,37 @@ def make_short(self, file: str) -> str:
527525
and file == oldfile
528526
and (not suffix or len(suffix) <= 3)
529527
):
530-
file = f"{prefix}.{suffix}" if suffix else prefix
528+
newfile = f"{prefix}.{suffix}" if suffix else prefix
531529
else:
532-
file = None
533-
if file is None or file in self.short_names:
530+
newfile = None
531+
if newfile is None or newfile in self.short_names:
534532
prefix = prefix[:6]
535533
if suffix:
536534
suffix = suffix[:3]
537535
pos = 1
538536
while 1:
539537
if suffix:
540-
file = f"{prefix}~{pos}.{suffix}"
538+
newfile = f"{prefix}~{pos}.{suffix}"
541539
else:
542-
file = f"{prefix}~{pos}"
543-
if file not in self.short_names:
540+
newfile = f"{prefix}~{pos}"
541+
if newfile not in self.short_names:
544542
break
545543
pos += 1
546544
assert pos < 10000
547545
if pos in (10, 100, 1000):
548546
prefix = prefix[:-1]
549-
self.short_names.add(file)
547+
self.short_names.add(newfile)
550548
# restrictions on short names
551-
assert not re.search(r'[\?|><:/*"+,;=\[\]]', file)
552-
return file
549+
assert not re.search(r'[\?|><:/*"+,;=\[\]]', newfile)
550+
return newfile
553551

554552
def add_file(
555553
self,
556554
file: str,
557555
src: str | None = None,
558556
version: str | None = None,
559557
language: str | None = None,
560-
) -> str:
558+
) -> str | None:
561559
"""Add a file to the current component of the directory, starting a new
562560
one if there is no current component. By default, the file name in the
563561
source and the file table will be identical. If the src file is
@@ -575,7 +573,10 @@ def add_file(
575573
# restrictions on long names
576574
assert not re.search(r'[\?|><:/*]"', file)
577575
logical = self.keyfiles.get(file, None)
578-
sequence, logical = self.cab.append(absolute, file, logical)
576+
ok = self.cab.append(absolute, file, logical)
577+
if ok is None:
578+
return None
579+
sequence, logical = ok
579580
assert logical not in self.ids
580581
self.ids.add(logical)
581582
short = self.make_short(file)
@@ -655,6 +656,8 @@ def glob(
655656

656657
def remove_pyc(self) -> None:
657658
"""Remove .pyc files on uninstall."""
659+
if not self.component:
660+
self.component = self.logical
658661
add_data(
659662
self.db,
660663
"RemoveFile",
@@ -691,12 +694,11 @@ def __init__(
691694
attributes: int = 0,
692695
) -> None:
693696
self.id = id
694-
if parent:
695-
parent = parent.id
697+
pid = parent.id if parent else None
696698
add_data(
697699
db,
698700
"Feature",
699-
[(id, parent, title, desc, display, level, directory, attributes)],
701+
[(id, pid, title, desc, display, level, directory, attributes)],
700702
)
701703

702704
def set_current(self) -> None:
@@ -886,7 +888,7 @@ def radiogroup(
886888
w: int,
887889
h: int,
888890
attr: int,
889-
prop: str | None,
891+
prop: str,
890892
text: str | None,
891893
next: str | None,
892894
) -> Control:

src/msilib/_msi.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ static FNFCIGETOPENINFO(cb_getopeninfo)
236236
_msi.FCICreate
237237
cabname: str
238238
the name of the CAB file
239-
files: object
239+
files: list[tuple[str, str]]
240240
a list of tuples, each containing the name of the file on disk,
241241
and the name of the file inside the CAB file
242242
/

src/msilib/_msi.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class _Record:
5353
__init__: None # type: ignore[assignment]
5454

5555
def UuidCreate() -> str: ...
56-
def FCICreate(cabname: str, files: list[str], /) -> None: ...
56+
def FCICreate(cabname: str, files: list[tuple[str, str]], /) -> None: ...
5757
def OpenDatabase(path: str, persist: int, /) -> _Database: ...
5858
def CreateRecord(count: int, /) -> _Record: ...
5959

tests/test_msilib.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ def test_summaryinfo_getproperty_issue1104(db: _Database) -> None:
7373
title = sum_info.GetProperty(msilib.PID_TITLE)
7474
assert title == b"a" * 1001
7575
finally:
76-
db = None
77-
sum_info = None
76+
del db
77+
del sum_info
7878

7979

8080
def test_database_open_failed() -> None:

0 commit comments

Comments
 (0)