From 3499e03edafe515bfbfd4f4c3f61958783d52cf1 Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Tue, 20 Oct 2026 23:16:35 -0600 Subject: [PATCH 1/8] Measure the configured package mypy baseline on Python 3.8 --- .github/workflows/ci.yml | 3 + .../repair/task6-installer-mypy-baseline.md | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 agent-notes/repair/task6-installer-mypy-baseline.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac63cb4..1903d4d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,9 @@ jobs: - name: Check configured mypy target and API compatibility (Python 3.13) if: matrix.python == '3.8' run: uv run --isolated --python 3.13 --extra dev python checks/mypy_compatibility.py -v + - name: Establish configured package mypy baseline + if: matrix.python == '3.8' + run: uv run --extra dev python -m mypy --config-file pyproject.toml --package installer_builder - name: Build wheel and source distribution run: uv build - name: Require both package artifacts diff --git a/agent-notes/repair/task6-installer-mypy-baseline.md b/agent-notes/repair/task6-installer-mypy-baseline.md new file mode 100644 index 0000000..1291c2e --- /dev/null +++ b/agent-notes/repair/task6-installer-mypy-baseline.md @@ -0,0 +1,56 @@ +# Goal + +Measure the full configured installer_builder mypy baseline before changing +the dependency range in original same-org PR #4. Preserve package >=3.8.6, +mypy target 3.8 and every existing strict setting. + +# State + +Local-only repair/task6-installer-mypy-baseline starts at verified merged master +e59336b7b5acb88d76d829dc9b61ac6585ba189d. The workflow adds only three lines in +the existing Windows Python 3.8 leg. Current optional dev extra remains +mypy>=0.991,<1.0. No full package checker command has run locally. + +# Decisions and why + +- The new explicit baseline scope is `python -m mypy --config-file pyproject.toml + --package installer_builder`, invoked through `uv run --extra dev`. This is + newly wired coverage of all four production modules, not a previously + authoritative command or a tiny probe claimed to cover the whole package. +- Existing CI installs pytest through the dependency group, but does not select + the separate optional mypy extra. The added step selects that extra on the + existing 3.8 runtime, honoring the unchanged 3.8 target. +- Diagnostics fail normally. No continue-on-error, ignored diagnostic codes, + altered strict flags, target substitution, skip or mass annotation is added. + Existing source contains unannotated definitions; hosted CI must establish + actual diagnostics before deciding any bounded baseline repair. +- The existing Windows 3.8/3.14 test/build axes, check names, action pins, + permissions, checkout credentials policy, timeout and concurrency remain. + A failing baseline step prevents that leg's later build in the usual way; + it is not a green baseline and does not justify suppressing the failure. +- Any later compatibility probe and dependency narrowing stay separate. A probe + of config/target/API behavior is not full-package typecheck evidence. + +# Preservation + +backup/task6-installer-mypy-original-pr preserves original PR #4 head +62d473dd04ad154d3b4aade2b8723dc76ce73909. Canonical local master remains +099ceb30876aba2b853a8bb98815cba3bb19f3e9 with all untracked user environments, +history, lock and source files preserved. Existing branches/worktrees/backups +are untouched. No same-org PR branch or default branch was changed. + +# Verification + +No local full checker, tests/build or installer/signing action ran. Actionlint +1.7.12 passed with optional shellcheck/pyflakes disabled. Zizmor 1.31.1 offline +pedantic reported no findings. Complete diff inspection is the other local +check; no action pins or permissions changed. Actual +hosted baseline diagnostics remain pending coordinator publication of this +reviewable local candidate. No checker results/counts are inferred from source. + +# Next step + +Return the clean local commit/full diff for coordinator review and separate +draft publication authority so hosted CI measures the current configured +baseline. Keep the one-line original dependency proposal unchanged. No Claude, +blocked review route, QActivate, settings, remote write, merge or release. From 8178422df89c3aefdd31eaba17c777a86dc37d0d Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Tue, 20 Oct 2026 23:52:01 -0600 Subject: [PATCH 2/8] Record rebased mypy baseline handoff --- .../repair/task6-installer-mypy-baseline.md | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/agent-notes/repair/task6-installer-mypy-baseline.md b/agent-notes/repair/task6-installer-mypy-baseline.md index 1291c2e..d7b6fce 100644 --- a/agent-notes/repair/task6-installer-mypy-baseline.md +++ b/agent-notes/repair/task6-installer-mypy-baseline.md @@ -6,10 +6,13 @@ mypy target 3.8 and every existing strict setting. # State -Local-only repair/task6-installer-mypy-baseline starts at verified merged master -e59336b7b5acb88d76d829dc9b61ac6585ba189d. The workflow adds only three lines in -the existing Windows Python 3.8 leg. Current optional dev extra remains -mypy>=0.991,<1.0. No full package checker command has run locally. +repair/task6-installer-mypy-baseline is locally rebased onto verified merged +master 11ec654ac80ef0bd7423156e76a22aa1d8e3a453 using --no-update-refs. Draft +prerequisite #5 remains open and unmerged at published old head +748ddadfa400a596a12a03e32fc1923c99705dd5; this rebased successor is unpublished. +The workflow adds only three lines in the existing Windows Python 3.8 leg. +Current optional dev extra remains mypy>=0.991,<1.0. No full package checker +command has run locally. # Decisions and why @@ -24,6 +27,7 @@ mypy>=0.991,<1.0. No full package checker command has run locally. altered strict flags, target substitution, skip or mass annotation is added. Existing source contains unannotated definitions; hosted CI must establish actual diagnostics before deciding any bounded baseline repair. + The first hosted baseline has now established those diagnostics below. - The existing Windows 3.8/3.14 test/build axes, check names, action pins, permissions, checkout credentials policy, timeout and concurrency remain. A failing baseline step prevents that leg's later build in the usual way; @@ -36,21 +40,30 @@ mypy>=0.991,<1.0. No full package checker command has run locally. backup/task6-installer-mypy-original-pr preserves original PR #4 head 62d473dd04ad154d3b4aade2b8723dc76ce73909. Canonical local master remains 099ceb30876aba2b853a8bb98815cba3bb19f3e9 with all untracked user environments, -history, lock and source files preserved. Existing branches/worktrees/backups -are untouched. No same-org PR branch or default branch was changed. +history, lock and source files preserved. Other branches/worktrees/backups +are untouched. Named archive/task6-installer-mypy-baseline-before-11ec preserves +748ddadfa400a596a12a03e32fc1923c99705dd5 and +archive/task6-installer-mypy-compatibility-before-11ec preserves +e89bff687780f0aa2a426f2047cb4467a917f584. No remote PR branch or default branch +was changed by this local rebase. # Verification -No local full checker, tests/build or installer/signing action ran. Actionlint -1.7.12 passed with optional shellcheck/pyflakes disabled. Zizmor 1.31.1 offline -pedantic reported no findings. Complete diff inspection is the other local -check; no action pins or permissions changed. Actual -hosted baseline diagnostics remain pending coordinator publication of this -reviewable local candidate. No checker results/counts are inferred from source. +Historically, actionlint 1.7.12 passed with optional shellcheck/pyflakes disabled +and zizmor 1.31.1 offline pedantic reported no findings for the unchanged added +workflow step. Hosted run 39548810883 at old head 748ddadf failed the full +configured package check with 115 errors in four files using the existing <1.0 +tool; its Python 3.14 tests/build passed. That result is historical evidence, +not a result for this rebased head. The merged #6 base contains separate legacy +helper fixes and 71-test coverage; remaining baseline diagnostics need fresh +hosted measurement. No local full checker, tests/build or installer/signing +action ran, including during this rebase. Complete delta inspection confirms +the same three-line gate, unchanged strict flags, target and dependency range. # Next step -Return the clean local commit/full diff for coordinator review and separate -draft publication authority so hosted CI measures the current configured -baseline. Keep the one-line original dependency proposal unchanged. No Claude, -blocked review route, QActivate, settings, remote write, merge or release. +Return the clean rebased local commit/full diff for coordinator review before +guarded publication of draft #5 and fresh hosted baseline measurement. Keep +the one-line original dependency proposal unchanged. Draft #5 has not landed. +No Claude, blocked review route, QActivate, settings, remote write, merge or +release. From cde0f5c85b442e727583f06a6eeec352eaa0003a Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Wed, 21 Oct 2026 00:20:11 -0600 Subject: [PATCH 3/8] Record baseline rebase after mypy range compatibility merge --- .../repair/task6-installer-mypy-baseline.md | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/agent-notes/repair/task6-installer-mypy-baseline.md b/agent-notes/repair/task6-installer-mypy-baseline.md index d7b6fce..f9de6da 100644 --- a/agent-notes/repair/task6-installer-mypy-baseline.md +++ b/agent-notes/repair/task6-installer-mypy-baseline.md @@ -1,18 +1,20 @@ # Goal -Measure the full configured installer_builder mypy baseline before changing -the dependency range in original same-org PR #4. Preserve package >=3.8.6, -mypy target 3.8 and every existing strict setting. +Measure the full configured installer_builder mypy baseline after the bounded +range/target compatibility change landed separately in #7. Preserve package +>=3.8.6, mypy target 3.8 and every existing strict setting. # State repair/task6-installer-mypy-baseline is locally rebased onto verified merged -master 11ec654ac80ef0bd7423156e76a22aa1d8e3a453 using --no-update-refs. Draft -prerequisite #5 remains open and unmerged at published old head -748ddadfa400a596a12a03e32fc1923c99705dd5; this rebased successor is unpublished. -The workflow adds only three lines in the existing Windows Python 3.8 leg. -Current optional dev extra remains mypy>=0.991,<1.0. No full package checker -command has run locally. +master fad12d9a3eafa59fcd6033e54534c4eee3ed2fb7 using --no-update-refs. Draft +#5 remains open and unmerged at published old head +2f18a7922a3bec7ca31f9fadf633081403aad973; this rebased successor is unpublished. +The delta adds only the same three-line full-package gate in the existing +Windows Python 3.8 leg. The merged base now supplies mypy>=0.991,<1.15 and +the unchanged three-case target/API/config probes on tool Python 3.8 and 3.13. +The new full-package result with selected 1.14.1 remains pending hosted CI. +No full package checker command has run locally. # Decisions and why @@ -20,9 +22,10 @@ command has run locally. --package installer_builder`, invoked through `uv run --extra dev`. This is newly wired coverage of all four production modules, not a previously authoritative command or a tiny probe claimed to cover the whole package. -- Existing CI installs pytest through the dependency group, but does not select - the separate optional mypy extra. The added step selects that extra on the - existing 3.8 runtime, honoring the unchanged 3.8 target. +- Originally CI installed pytest through the dependency group without selecting + the optional mypy extra. The landed #7 now selects that extra for its bounded + compatibility probes. This separate full-package step uses the same extra on + the existing 3.8 runtime, honoring the unchanged 3.8 target. - Diagnostics fail normally. No continue-on-error, ignored diagnostic codes, altered strict flags, target substitution, skip or mass annotation is added. Existing source contains unannotated definitions; hosted CI must establish @@ -32,8 +35,11 @@ command has run locally. permissions, checkout credentials policy, timeout and concurrency remain. A failing baseline step prevents that leg's later build in the usual way; it is not a green baseline and does not justify suppressing the failure. -- Any later compatibility probe and dependency narrowing stay separate. A probe - of config/target/API behavior is not full-package typecheck evidence. +- The landed compatibility probes and dependency narrowing are separate source + intent, now inherited once each from actual master. Those green bounded + config/target/API probes are not full-package typecheck evidence. The rebase + resolved one adjacent-step workflow conflict by keeping both unchanged probes + followed by exactly one existing full-package command; no other hunk changed. # Preservation @@ -45,7 +51,11 @@ are untouched. Named archive/task6-installer-mypy-baseline-before-11ec preserves 748ddadfa400a596a12a03e32fc1923c99705dd5 and archive/task6-installer-mypy-compatibility-before-11ec preserves e89bff687780f0aa2a426f2047cb4467a917f584. No remote PR branch or default branch -was changed by this local rebase. +was changed by this local rebase. Named +archive/task6-installer-mypy-baseline-before-fad12 preserves published +2f18a7922a3bec7ca31f9fadf633081403aad973. The redundant stacked probe branch +stays unchanged at fd0103dc83f89e66c12b413e87d6fa724cbfbd91; it was not rebased +or forced through duplicate conflicts. Original PR #4 remains unchanged. # Verification @@ -53,17 +63,26 @@ Historically, actionlint 1.7.12 passed with optional shellcheck/pyflakes disable and zizmor 1.31.1 offline pedantic reported no findings for the unchanged added workflow step. Hosted run 39548810883 at old head 748ddadf failed the full configured package check with 115 errors in four files using the existing <1.0 -tool; its Python 3.14 tests/build passed. That result is historical evidence, -not a result for this rebased head. The merged #6 base contains separate legacy -helper fixes and 71-test coverage; remaining baseline diagnostics need fresh -hosted measurement. No local full checker, tests/build or installer/signing -action ran, including during this rebase. Complete delta inspection confirms -the same three-line gate, unchanged strict flags, target and dependency range. +tool; its Python 3.14 tests/build passed. The next published baseline 2f18a792 +had 112 diagnostics with mypy 0.991 in run 39552363115 after the three helper +diagnostics disappeared; both 71-test suites passed. These are historical +results under the old <1.0 range, not counts for this rebased head. Actual #7 +landed the narrower compatible <1.15 range and bounded probes separately. +The full-package result with 1.14.1 needs fresh hosted measurement; no green +baseline or new diagnostic count is inferred. No local full checker, +tests/build or installer/signing action ran, including during this rebase. +Complete delta inspection confirms the same three-line unsuppressed gate and +no delta to merged source, tests, manifest, probe assertions or strict flags. +After resolving the adjacent-step conflict, actionlint 1.7.12 again exited 0 +and offline pedantic zizmor 1.31.1 reported no findings. No local test or +checker execution was added to that static workflow verification. # Next step Return the clean rebased local commit/full diff for coordinator review before -guarded publication of draft #5 and fresh hosted baseline measurement. Keep -the one-line original dependency proposal unchanged. Draft #5 has not landed. +guarded publication of draft #5 and fresh hosted baseline measurement under the +landed range. Keep original #4 and the redundant stacked candidate unchanged. +Draft #5 remains unresolved and has not landed; no annotation migration or +suppression is proposed. No Claude, blocked review route, QActivate, settings, remote write, merge or release. From 631568a810a4318d09cd7b155ef8cda26ed46916 Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Wed, 21 Oct 2026 00:48:48 -0600 Subject: [PATCH 4/8] Record completed mypy baseline measurement --- .../repair/task6-installer-mypy-baseline.md | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/agent-notes/repair/task6-installer-mypy-baseline.md b/agent-notes/repair/task6-installer-mypy-baseline.md index f9de6da..f9ec938 100644 --- a/agent-notes/repair/task6-installer-mypy-baseline.md +++ b/agent-notes/repair/task6-installer-mypy-baseline.md @@ -6,14 +6,15 @@ range/target compatibility change landed separately in #7. Preserve package # State -repair/task6-installer-mypy-baseline is locally rebased onto verified merged -master fad12d9a3eafa59fcd6033e54534c4eee3ed2fb7 using --no-update-refs. Draft -#5 remains open and unmerged at published old head -2f18a7922a3bec7ca31f9fadf633081403aad973; this rebased successor is unpublished. +Published PR #5 remains open and unmerged, with full typing work remaining. +repair/task6-installer-mypy-baseline was genuinely rebased onto merged master +fad12d9a3eafa59fcd6033e54534c4eee3ed2fb7 using --no-update-refs. The last +recorded completed measurement is run 39555011151 at published source head +cde0f5c85b442e727583f06a6eeec352eaa0003a, detailed below. Live PR checks are +authoritative for later heads; that measurement is not a promised later result. The delta adds only the same three-line full-package gate in the existing Windows Python 3.8 leg. The merged base now supplies mypy>=0.991,<1.15 and the unchanged three-case target/API/config probes on tool Python 3.8 and 3.13. -The new full-package result with selected 1.14.1 remains pending hosted CI. No full package checker command has run locally. # Decisions and why @@ -56,6 +57,9 @@ archive/task6-installer-mypy-baseline-before-fad12 preserves published 2f18a7922a3bec7ca31f9fadf633081403aad973. The redundant stacked probe branch stays unchanged at fd0103dc83f89e66c12b413e87d6fa724cbfbd91; it was not rebased or forced through duplicate conflicts. Original PR #4 remains unchanged. +These earlier archive tips are historical. Named +archive/task6-installer-mypy-baseline-measurement-cde0f5c8 preserves the exact +cde0f5c85b442e727583f06a6eeec352eaa0003a measured tip before this note correction. # Verification @@ -66,11 +70,16 @@ configured package check with 115 errors in four files using the existing <1.0 tool; its Python 3.14 tests/build passed. The next published baseline 2f18a792 had 112 diagnostics with mypy 0.991 in run 39552363115 after the three helper diagnostics disappeared; both 71-test suites passed. These are historical -results under the old <1.0 range, not counts for this rebased head. Actual #7 +results under the old <1.0 range. Actual #7 landed the narrower compatible <1.15 range and bounded probes separately. -The full-package result with 1.14.1 needs fresh hosted measurement; no green -baseline or new diagnostic count is inferred. No local full checker, -tests/build or installer/signing action ran, including during this rebase. +The last recorded completed measurement, run 39555011151 at exact cde0f5c8, +selected mypy 1.14.1 and failed with 106 errors in four package files. Both +71-test suites passed, as did all three compatibility cases on tool Python 3.8 +and 3.13. The Python 3.8 build was skipped after the full-check failure; the +Python 3.14 build succeeded. This remains a red full-package baseline. Actual +review approved the workflow source hunk and requested only this stale-note +correction. No local full checker, tests/build or installer/signing action ran, +including for this note-only correction. Complete delta inspection confirms the same three-line unsuppressed gate and no delta to merged source, tests, manifest, probe assertions or strict flags. After resolving the adjacent-step conflict, actionlint 1.7.12 again exited 0 @@ -79,10 +88,10 @@ checker execution was added to that static workflow verification. # Next step -Return the clean rebased local commit/full diff for coordinator review before -guarded publication of draft #5 and fresh hosted baseline measurement under the -landed range. Keep original #4 and the redundant stacked candidate unchanged. -Draft #5 remains unresolved and has not landed; no annotation migration or -suppression is proposed. -No Claude, blocked review route, QActivate, settings, remote write, merge or -release. +Next engineering work is bounded genuine typing repair informed by the actual +diagnostics and source/callers, preserving configured checks and strict flags. +This note correction authorizes no broad annotation migration or suppression. +Keep original #4, the redundant stacked candidate and canonical work unchanged. +PR #5 remains open/unmerged; do not merge until the relevant full check is green +and source review is complete. Live checks govern later heads. No model, remote +write, source/config/test changes or merge occurred for this correction. From 76332cebdb1eaac1400427daf73b3b4bafd87b6c Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Wed, 21 Oct 2026 00:51:34 -0600 Subject: [PATCH 5/8] Specify recorded baseline diagnostic classes --- agent-notes/repair/task6-installer-mypy-baseline.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/agent-notes/repair/task6-installer-mypy-baseline.md b/agent-notes/repair/task6-installer-mypy-baseline.md index f9ec938..780e333 100644 --- a/agent-notes/repair/task6-installer-mypy-baseline.md +++ b/agent-notes/repair/task6-installer-mypy-baseline.md @@ -31,7 +31,8 @@ No full package checker command has run locally. altered strict flags, target substitution, skip or mass annotation is added. Existing source contains unannotated definitions; hosted CI must establish actual diagnostics before deciding any bounded baseline repair. - The first hosted baseline has now established those diagnostics below. + The latest recorded run 39555011151 at cde0f5c8 established the diagnostic + snapshot detailed in Verification below. - The existing Windows 3.8/3.14 test/build axes, check names, action pins, permissions, checkout credentials policy, timeout and concurrency remain. A failing baseline step prevents that leg's later build in the usual way; @@ -73,8 +74,10 @@ diagnostics disappeared; both 71-test suites passed. These are historical results under the old <1.0 range. Actual #7 landed the narrower compatible <1.15 range and bounded probes separately. The last recorded completed measurement, run 39555011151 at exact cde0f5c8, -selected mypy 1.14.1 and failed with 106 errors in four package files. Both -71-test suites passed, as did all three compatibility cases on tool Python 3.8 +selected mypy 1.14.1 and failed with 106 errors in four package files. +Diagnostic classes: 89 no-untyped-def, 6 var-annotated, 3 import-untyped, +3 import-not-found, 3 arg-type, 1 assignment and 1 operator. +Both 71-test suites passed, as did all three compatibility cases on tool Python 3.8 and 3.13. The Python 3.8 build was skipped after the full-check failure; the Python 3.14 build succeeded. This remains a red full-package baseline. Actual review approved the workflow source hunk and requested only this stale-note From 99229af562767fb850a0b924097d4f3681b291c5 Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Wed, 21 Oct 2026 01:34:58 -0600 Subject: [PATCH 6/8] Type installer builder package contracts without weakening checks --- .../repair/task6-installer-mypy-baseline.md | 87 ++++++-- installer_builder/__init__.py | 200 ++++++++++-------- installer_builder/innosetup.py | 147 +++++++------ installer_builder/new_inno_command.py | 57 +++-- installer_builder/signtool.py | 9 +- pyproject.toml | 1 + 6 files changed, 302 insertions(+), 199 deletions(-) diff --git a/agent-notes/repair/task6-installer-mypy-baseline.md b/agent-notes/repair/task6-installer-mypy-baseline.md index 780e333..a3db679 100644 --- a/agent-notes/repair/task6-installer-mypy-baseline.md +++ b/agent-notes/repair/task6-installer-mypy-baseline.md @@ -1,8 +1,8 @@ # Goal -Measure the full configured installer_builder mypy baseline after the bounded +Resolve the full configured installer_builder typing baseline after the bounded range/target compatibility change landed separately in #7. Preserve package ->=3.8.6, mypy target 3.8 and every existing strict setting. +>=3.8.6, mypy target 3.8, public behavior and every existing strict setting. # State @@ -12,10 +12,13 @@ fad12d9a3eafa59fcd6033e54534c4eee3ed2fb7 using --no-update-refs. The last recorded completed measurement is run 39555011151 at published source head cde0f5c85b442e727583f06a6eeec352eaa0003a, detailed below. Live PR checks are authoritative for later heads; that measurement is not a promised later result. -The delta adds only the same three-line full-package gate in the existing -Windows Python 3.8 leg. The merged base now supplies mypy>=0.991,<1.15 and -the unchanged three-case target/API/config probes on tool Python 3.8 and 3.13. -No full package checker command has run locally. +The baseline gate remains unchanged in the existing Windows Python 3.8 leg. +The merged base supplies mypy>=0.991,<1.15 and the unchanged three-case +target/API/config probes on tool Python 3.8 and 3.13. The authorized source +repair now annotates the four production modules and adds only the proven +Python 3.8-compatible types-setuptools development stub. The completed cde +measurement remains the last recorded result; no green result is inferred for +this source candidate. No full package checker command has run locally. # Decisions and why @@ -43,6 +46,47 @@ No full package checker command has run locally. resolved one adjacent-step workflow conflict by keeping both unchanged probes followed by exactly one existing full-package command; no other hunk changed. +# Typing repair decisions and actual boundaries + +- Annotate all 89 previously reported definitions and the six untyped container + sites with Python 3.8 typing forms: concrete string collections, data-file + tuples, generators, paths, optional lifecycle state and return types. Keep + implicit None fallthroughs explicit without adding new fallback values. +- Path aliases quote os.PathLike[str] so alias evaluation does not require + Python 3.9 runtime generics. Actual os/path-only input surfaces accept string + or PathLike paths; legacy chop/dist_dir operations still require strings. + Application.name is a read-only protocol property; srcname accepts modules, + dotted strings and the existing __file__ duck-typed input. InnoScript metadata + is object because it is inspected with dir/getattr and existing tests supply + SimpleNamespace, not only DistributionMetadata. +- Command dist_dir remains Optional during initialize_options. The narrow + casts at run/signing use the real distutils lifecycle: Distribution.run_command + calls ensure_finalized before run, and each finalize_options supplies dist. + Direct invalid calls retain their old failures; no new runtime guard is added. + Likewise build() sets build_start_time before both reporting paths and calls + build_installer's existing name validation before finalization. Casts express + those cross-method facts without changing initialization or validation. +- Keep Any only at actual external dynamic boundaries: extensible setuptools/ + py2exe keyword dictionaries, forwarded application **kwargs, public metadata + dir/getattr values and native registry values/defaults. Internal file lists, + command options, translations, script lines and generated metadata are typed. +- Optional Babel/pytz/certifi/enchant helpers consume only package __path__ or + __file__, not provider APIs. Use the existing importlib.import_module pattern + and stdlib module metadata types. Each real import still executes at the same + feature point and fails normally when absent; no provider or native Enchant + installation is added solely to satisfy a checker. File-backed provider path + casts retain the same invalid-metadata failures rather than inventing paths. +- Add types-setuptools==75.8.0.20250110 only to the existing optional dev extra. + Its official PyPI metadata requires >=3.8, has no dependencies and records + testing with mypy 1.14.1. It types actual setup/find_packages APIs; no runtime + dependency, mypy option, ignored module, coverage or workflow is weakened. + On supported Python >=3.8, import builtins directly under the existing alias + rather than probing the obsolete Python 2 __builtin__ module. +- No runtime defect was established in this repair. The small executable edits + are import spelling, identity casts, local variable separation for registry + root names/handles, and explicit existing None fallthroughs. No new signing, + build, compiler, native, data-generation or optional-feature policy is added. + # Preservation backup/task6-installer-mypy-original-pr preserves original PR #4 head @@ -61,6 +105,8 @@ or forced through duplicate conflicts. Original PR #4 remains unchanged. These earlier archive tips are historical. Named archive/task6-installer-mypy-baseline-measurement-cde0f5c8 preserves the exact cde0f5c85b442e727583f06a6eeec352eaa0003a measured tip before this note correction. +archive/task6-installer-mypy-baseline-before-typing preserves exact +76332cebdb1eaac1400427daf73b3b4bafd87b6c before these source annotations. # Verification @@ -83,18 +129,29 @@ Python 3.14 build succeeded. This remains a red full-package baseline. Actual review approved the workflow source hunk and requested only this stale-note correction. No local full checker, tests/build or installer/signing action ran, including for this note-only correction. -Complete delta inspection confirms the same three-line unsuppressed gate and -no delta to merged source, tests, manifest, probe assertions or strict flags. +At the recorded cde measurement, delta inspection confirmed the same three-line +unsuppressed gate without source/test/manifest/probe/strict-setting changes. After resolving the adjacent-step conflict, actionlint 1.7.12 again exited 0 and offline pedantic zizmor 1.31.1 reported no findings. No local test or checker execution was added to that static workflow verification. +For this source candidate, every existing production definition was inspected +and annotated, including all reported no-untyped-def/container sites. Known +import complaints are addressed through the real setuptools stub, supported +builtins import and module-path boundaries. Optional path/timing/platform +assignments are typed according to their actual lifecycles. This is an estimate +of addressed source sites, not a checker result or count prediction: newly +exposed precise-type diagnostics may need hosted feedback. No local checker, +tests/build, optional provider import or native action ran. No regression was +added merely to mirror annotations; existing 71 tests and both three-case +compatibility probes remain intact. The workflow is byte-for-byte unchanged. + # Next step -Next engineering work is bounded genuine typing repair informed by the actual -diagnostics and source/callers, preserving configured checks and strict flags. -This note correction authorizes no broad annotation migration or suppression. -Keep original #4, the redundant stacked candidate and canonical work unchanged. -PR #5 remains open/unmerged; do not merge until the relevant full check is green -and source review is complete. Live checks govern later heads. No model, remote -write, source/config/test changes or merge occurred for this correction. +Return this coherent source/manifest/note delta for coordinator review before +guarded publication and the first hosted measurement of these annotations. +Resolve any remaining accurate-type diagnostics under the unchanged full gate; +do not suppress, weaken flags or shrink coverage. Keep original #4, the redundant +stacked candidate and canonical work unchanged. PR #5 remains open/unmerged; +no merge until the relevant full check is green and source review is complete. +Live checks govern later heads. No model, remote write or merge ran here. diff --git a/installer_builder/__init__.py b/installer_builder/__init__.py index 376ea76..9bc5f6f 100644 --- a/installer_builder/__init__.py +++ b/installer_builder/__init__.py @@ -1,13 +1,10 @@ # * Encoding: UTF-8 -from __future__ import print_function +from __future__ import annotations, print_function import setuptools -try: - import __builtin__ -except ImportError: - import builtins as __builtin__ +import builtins as __builtin__ import collections import datetime import fnmatch @@ -20,6 +17,18 @@ import subprocess import sys import time +from typing import TYPE_CHECKING, Any, Callable, DefaultDict, Dict, Iterator, List, Mapping, Optional, Protocol, Set, Tuple, Type, Union, cast + +if TYPE_CHECKING: + from distutils.core import Command + +DataFiles = List[Tuple[str, List[str]]] +Path = Union[str, "os.PathLike[str]"] + + +class Application(Protocol): + @property + def name(self) -> str: ... is_windows = platform.system() == "Windows" is_mac = platform.system() == "Darwin" @@ -49,35 +58,35 @@ class InstallerBuilder(object): def __init__( self, - main_module=None, - name=None, - version=None, - url=None, - author=None, - author_email=None, - datafiles=None, - includes=None, - excludes=None, - dll_excludes=None, - compressed=False, - skip_archive=False, - bundle_level=3, - optimization_level=1, - extra_packages=None, - datafile_packages=None, - output_directory="release", - create_update=False, - postbuild_commands=None, - osx_frameworks=None, - extra_inno_script=None, - register_startup=False, - localized_packages=None, - has_translations=False, - certificate_file=None, - certificate_password=None, - extra_files_to_sign=None, - app_type="windows", - ): + main_module: Optional[Path] = None, + name: Optional[str] = None, + version: Optional[str] = None, + url: Optional[str] = None, + author: Optional[str] = None, + author_email: Optional[str] = None, + datafiles: Optional[DataFiles] = None, + includes: Optional[List[str]] = None, + excludes: Optional[List[str]] = None, + dll_excludes: Optional[List[str]] = None, + compressed: bool = False, + skip_archive: bool = False, + bundle_level: int = 3, + optimization_level: int = 1, + extra_packages: Optional[List[str]] = None, + datafile_packages: Optional[List[str]] = None, + output_directory: Path = "release", + create_update: bool = False, + postbuild_commands: Optional[Mapping[str, List[str]]] = None, + osx_frameworks: Optional[List[str]] = None, + extra_inno_script: Optional[str] = None, + register_startup: bool = False, + localized_packages: Optional[List[str]] = None, + has_translations: bool = False, + certificate_file: Optional[Path] = None, + certificate_password: Optional[str] = None, + extra_files_to_sign: Optional[List[Path]] = None, + app_type: str = "windows", + ) -> None: super(InstallerBuilder, self).__init__() self.main_module = main_module self.name = name @@ -114,13 +123,13 @@ def __init__( self.create_update = create_update if postbuild_commands is None: postbuild_commands = {} - self.postbuild_commands = collections.defaultdict(list) + self.postbuild_commands: DefaultDict[str, List[str]] = collections.defaultdict(list) self.postbuild_commands.update(postbuild_commands) if osx_frameworks is None: osx_frameworks = [] self.osx_frameworks = osx_frameworks self.extra_inno_script = extra_inno_script - self.build_start_time = None + self.build_start_time: Optional[float] = None self.register_startup = register_startup if localized_packages is None: localized_packages = [] @@ -135,14 +144,14 @@ def __init__( raise ValueError("Invalid app type") self.app_type = app_type - def get_version_specific_excludes(self): + def get_version_specific_excludes(self) -> List[str]: result = [] version = float("%d.%d" % (sys.version_info.major, sys.version_info.minor)) if version < 3.5: result.append("jinja2.asyncsupport") return result - def build(self, skip_finalize=False): + def build(self, skip_finalize: bool = False) -> None: self.build_start_time = time.time() self.prebuild_message() self.remove_previous_build() @@ -159,7 +168,7 @@ def build(self, skip_finalize=False): ) self.report_build_time() - def _installer_was_created(self): + def _installer_was_created(self) -> bool: """Check if an installer file was actually created by the build process""" try: installer_path = self.find_created_installer() @@ -168,14 +177,14 @@ def _installer_was_created(self): # If installer doesn't exist, no installer was created return False - def prebuild_message(self): + def prebuild_message(self) -> None: print("Installer builder version %s" % __version__) print( "Building %s installer for %s %s" % (platform.system(), self.name, self.version) ) - def remove_previous_build(self): + def remove_previous_build(self) -> None: print("Removing previous output directories") directories = self.build_dirs + [self.output_directory] for directory in directories: @@ -185,8 +194,8 @@ def remove_previous_build(self): shutil.rmtree(directory, ignore_errors=False) print("Deleted ", directory) - def find_datafiles(self): - datafiles = [] + def find_datafiles(self) -> DataFiles: + datafiles: DataFiles = [] for package in self.datafile_packages: pkg_datafile_function = DATAFILE_REGISTRY.get(package) if pkg_datafile_function is None: @@ -221,14 +230,14 @@ def find_datafiles(self): print("Total datafiles to be included: %d" % total_datafiles) return self.datafiles + datafiles - def find_application_language_data(self): + def find_application_language_data(self) -> Iterator[Tuple[str, List[str]]]: self._validate_translation_inventory( self.locale_dir, expected_domain=self.name ) for directory, filenames in self.find_locale_data(self.locale_dir): yield directory, filenames - def _validate_translation_inventory(self, locale_path, expected_domain=None): + def _validate_translation_inventory(self, locale_path: Path, expected_domain: Optional[str] = None) -> Tuple[str, ...]: application_mode = expected_domain is not None catalog_extensions = (".po", ".mo") if application_mode else (".mo",) catalogs = [] @@ -287,7 +296,7 @@ def _validate_translation_inventory(self, locale_path, expected_domain=None): % (catalog_directory, expected_domain, found_domain) ) - inventory = {} + inventory: Dict[Tuple[str, str], Set[str]] = {} for _, directory, stem, extension in catalogs: inventory.setdefault((directory, stem), set()).add(extension) @@ -321,17 +330,17 @@ def _validate_translation_inventory(self, locale_path, expected_domain=None): return tuple(sorted({stem for _, _, stem, extension in catalogs if extension == ".mo"})) - def find_babel_datafiles(self): - import babel + def find_babel_datafiles(self) -> Tuple[Tuple[str, List[str]], ...]: + babel = importlib.import_module("babel") return ( ( "locale-data", - glob.glob(os.path.join(babel.__path__[0], "locale-data", "*.*")), + glob.glob(os.path.join(cast(List[str], babel.__path__)[0], "locale-data", "*.*")), ), ) - def find_locale_data(self, locale_path): + def find_locale_data(self, locale_path: Path) -> Iterator[Tuple[str, List[str]]]: for dirpath, dirnames, filenames in os.walk(locale_path): files = [] for filename in filenames: @@ -344,12 +353,12 @@ def find_locale_data(self, locale_path): ) yield directory, files - def finalize_build(self): + def finalize_build(self) -> None: print("Finalizing build...") if platform.system() == "Darwin": self.remove_embedded_interpreter() self.shrink_mac_binaries() - self.lipo_file(os.path.join(self.get_app_path(), self.name)) + self.lipo_file(os.path.join(self.get_app_path(), cast(str, self.name))) self.create_dmg() # Only move output if installer was created @@ -362,35 +371,35 @@ def finalize_build(self): if self.create_update: self.create_update_archive() - def remove_embedded_interpreter(self): + def remove_embedded_interpreter(self) -> None: print("Replacing the embedded interpreter with a dumby file") interpreter_path = os.path.join(self.get_app_path(), "python") os.remove(interpreter_path) self.execute_command("touch %s" % interpreter_path) self.execute_command("chmod +x %s" % interpreter_path) - def get_app_path(self): + def get_app_path(self) -> Path: if platform.system() == "Darwin": return os.path.join( self.dist_dir, "%s.app" % self.name, "Contents", "MacOS" ) return self.dist_dir - def create_dmg(self): + def create_dmg(self) -> None: print("Creating .dmg disk image") self.execute_command( "hdiutil create -srcfolder dist/%s.app -size 150m dist/%s" % (self.name, self.installer_filename()) ) - def move_output(self): + def move_output(self) -> None: if not os.path.exists(self.output_directory): os.mkdir(self.output_directory) destination = os.path.join(self.output_directory, self.installer_filename()) os.rename(self.find_created_installer(), destination) print("Moved generated installer to %s" % destination) - def create_update_archive(self): + def create_update_archive(self) -> None: print("Generating update archive") name = "%s-%s-%s" % (self.name, self.version, platform.system()) root_dir = self.dist_dir @@ -404,7 +413,7 @@ def create_update_archive(self): os.rename(filename, destination) print("Generated update archive filename: %s" % destination) - def find_created_installer(self): + def find_created_installer(self) -> str: res = os.path.join("dist", self.installer_filename()) if not os.path.exists(res): res = os.path.join(self.output_directory, self.installer_filename()) @@ -412,7 +421,7 @@ def find_created_installer(self): raise RuntimeError("Installer %s does not exist" % res) return res - def installer_filename(self): + def installer_filename(self) -> str: if platform.system() == "Windows": return "%s-%s-setup.exe" % (self.name, self.version) elif platform.system() == "Darwin": @@ -421,7 +430,7 @@ def installer_filename(self): # Fallback for other systems return "%s-%s-installer" % (self.name, self.version) - def get_command_class(self): + def get_command_class(self) -> Optional[Type[Command]]: if platform.system() == "Windows": from .new_inno_command import NewInnoSetupCommand @@ -430,18 +439,19 @@ def get_command_class(self): import py2app.build_app return py2app.build_app.py2app + return None - def perform_postbuild_commands(self): + def perform_postbuild_commands(self) -> None: if not self.postbuild_commands[platform.system().lower()]: return print("Performing postbuild commands for platform %s" % platform.system()) for command in self.postbuild_commands[platform.system().lower()]: self.execute_command(command) - def execute_command(self, command): + def execute_command(self, command: str) -> None: subprocess.check_call([command], shell=True) - def report_build_statistics(self): + def report_build_statistics(self) -> None: try: installer_path = self.find_created_installer() print("Generated installer filename: %s" % installer_path) @@ -453,7 +463,7 @@ def report_build_statistics(self): print("Build completed - no installer created (py2exe/py2app only)") self.report_build_time() - def shrink_mac_binaries(self): + def shrink_mac_binaries(self) -> None: shrink_extensions = (".dylib", ".so") for basepath, dirs, files in os.walk(self.dist_dir): for file in files: @@ -461,16 +471,17 @@ def shrink_mac_binaries(self): path = os.path.join(basepath, file) self.lipo_file(path) - def lipo_file(self, filename): + def lipo_file(self, filename: Path) -> None: self.execute_command("lipo -thin i386 %s -output %s" % (filename, filename)) print("Lipoed file %s" % filename) - def report_build_time(self): - build_time = time.time() - self.build_start_time + def report_build_time(self) -> None: + # build() records this before either path that reports elapsed time. + build_time = time.time() - cast(float, self.build_start_time) td = datetime.timedelta(seconds=build_time) print("Build completed in ", format(td)) - def build_installer(self): + def build_installer(self) -> None: if None in (self.name, self.main_module): raise RuntimeError("Insufficient information provided to build") if ( @@ -481,7 +492,8 @@ def build_installer(self): self.certificate_password = os.environ.get( "CERTIFICATE_PASS" ) or getpass.getpass("Certificate password:") - setup_arguments = { + # setuptools/py2exe accept heterogeneous, extensible keyword payloads. + setup_arguments: Dict[str, Any] = { "name": self.name, "author": self.author, "author_email": self.author_email, @@ -566,15 +578,16 @@ def build_installer(self): return setuptools.setup(**setup_arguments) - def get_copyright(self): + def get_copyright(self) -> str: return "Copyright ©%d %s" % (datetime.date.today().year, self.author) class AppInstallerBuilder(InstallerBuilder): - def __init__(self, application=None, **kwargs): + def __init__(self, application: Optional[Application] = None, **kwargs: Any) -> None: self.application = application - new_kwargs = {} - new_kwargs["name"] = application.name + # Application providers and forwarded builder keyword options are dynamic. + new_kwargs: Dict[str, Any] = {} + new_kwargs["name"] = cast(Application, application).name new_kwargs["version"] = getattr(application, "version", None) new_kwargs["url"] = getattr(application, "website", None) new_kwargs["author"] = getattr(application, "author", None) @@ -593,16 +606,16 @@ def __init__(self, application=None, **kwargs): localized_packages = kwargs.get("localized_packages", []) config_spec = getattr(application, "config_spec", None) if config_spec is True: - config_spec = "%s.confspec" % application.name + config_spec = "%s.confspec" % cast(Application, application).name if config_spec is not None: datafiles.extend([("", [config_spec])]) - import babel + babel = importlib.import_module("babel") - datafiles.extend([("babel", [os.path.join(babel.__path__[0], "global.dat")])]) - from certifi import __file__ as cert_path + datafiles.extend([("babel", [os.path.join(cast(List[str], babel.__path__)[0], "global.dat")])]) + cert_path = importlib.import_module("certifi").__file__ datafiles.extend( - [("", [os.path.join(os.path.dirname(cert_path), "cacert.pem")])] + [("", [os.path.join(os.path.dirname(cast(str, cert_path)), "cacert.pem")])] ) kwargs["datafiles"] = datafiles if getattr(application, "output", False): @@ -636,7 +649,7 @@ def __init__(self, application=None, **kwargs): kwargs["extra_files_to_sign"] = files_to_sign new_kwargs.update(kwargs) if hasattr(application, "register_startup"): - new_kwargs["register_startup"] = application.register_startup + new_kwargs["register_startup"] = getattr(application, "register_startup") if hasattr(application, "debug_port") or hasattr(application, "debug_host"): if sys.version_info.major < 3: includes.append("SocketServer") # not picked up on Mac @@ -646,21 +659,22 @@ def __init__(self, application=None, **kwargs): super(AppInstallerBuilder, self).__init__(**new_kwargs) -def format_filesize(num): +def format_filesize(num: float) -> Optional[str]: for x in ["bytes", "KB", "MB", "GB", "TB"]: if num < 1024.0: return "%3.1f %s" % (num, x) num /= 1024.0 + return None -def standard_wx_excludes(): +def standard_wx_excludes() -> List[str]: return [ "wx.py", "wx.stc", ] -def sqlite_sqlalchemy_excludes(): +def sqlite_sqlalchemy_excludes() -> List[str]: return [ "sqlalchemy.testing", "sqlalchemy.dialects.postgresql", @@ -673,7 +687,7 @@ def sqlite_sqlalchemy_excludes(): ] -def app_framework_excludes(): +def app_framework_excludes() -> List[str]: return [ "watchdog", "yappi", @@ -683,7 +697,7 @@ def app_framework_excludes(): ] -def stdlib_excludes(pdb=True): +def stdlib_excludes(pdb: bool = True) -> List[str]: res = [ "doctest", "email.test", @@ -698,7 +712,7 @@ def stdlib_excludes(pdb=True): return res -def win32_excludes(): +def win32_excludes() -> List[str]: return [ "win32pipe", "win32wnet", @@ -706,7 +720,7 @@ def win32_excludes(): ] -def get_datafiles(directory="share", match="*", target_path=None): +def get_datafiles(directory: Path = "share", match: str = "*", target_path: Optional[str] = None) -> DataFiles: """builds list of data files to be included with data_files in setuptools A typical task in a setup.py file is to set the path and name of a list of data files to provide with the package. For instance files in share/data @@ -741,10 +755,10 @@ def get_datafiles(directory="share", match="*", target_path=None): return datafiles -def pytz_datafiles(): - import pytz +def pytz_datafiles() -> DataFiles: + pytz = importlib.import_module("pytz") - path = os.path.join(os.path.split(pytz.__file__)[0], "zoneinfo") + path = os.path.join(os.path.split(cast(str, pytz.__file__))[0], "zoneinfo") print("Collecting pytz datafiles from: %s" % path) files = get_datafiles(path, "*") index = path.index("zoneinfo") @@ -753,10 +767,10 @@ def pytz_datafiles(): return files -def enchant_datafiles(): - import enchant +def enchant_datafiles() -> DataFiles: + enchant = importlib.import_module("enchant") - enchant_path = os.path.split(enchant.__file__)[0] + enchant_path = os.path.split(cast(str, enchant.__file__))[0] print("Collecting enchant datafiles from: %s" % enchant_path) dll_files = get_datafiles(enchant_path, "*.dll") @@ -779,7 +793,7 @@ def enchant_datafiles(): return files -DATAFILE_REGISTRY = { +DATAFILE_REGISTRY: Dict[str, Callable[[], DataFiles]] = { "enchant": enchant_datafiles, "pytz": pytz_datafiles, } diff --git a/installer_builder/innosetup.py b/installer_builder/innosetup.py index 89142bd..3dec4ca 100644 --- a/installer_builder/innosetup.py +++ b/installer_builder/innosetup.py @@ -165,7 +165,7 @@ """ -from __future__ import absolute_import, print_function +from __future__ import absolute_import, annotations, print_function import distutils.command import distutils.core @@ -184,6 +184,15 @@ import shutil from xml.etree import ElementTree from zipfile import ZIP_DEFLATED, ZipFile +from types import ModuleType +from typing import Any, Dict, IO, Iterable, Iterator, List, Optional, Protocol, Tuple, Union, cast + +Path = Union[str, "os.PathLike[str]"] + + +class SourceModule(Protocol): + @property + def __file__(self) -> Optional[str]: ... import win32api # for read pe32 resource @@ -272,7 +281,7 @@ """ -def manifest(name, res_id=1): +def manifest(name: Optional[str], res_id: int = 1) -> Tuple[int, int, bytes]: """Create a manifest resource with the given name.""" data = MANIFEST_TEMPLATE % name return RT_MANIFEST, res_id, data.encode('utf-8') @@ -281,24 +290,25 @@ def manifest(name, res_id=1): README_EXT = ".html" -def load_manifest(handle): +def load_manifest(handle: int) -> Optional[str]: """get the first manifest string from HMODULE""" for restype in (RT_MANIFEST,): # win32api.EnumResourceTypes(handle) for name in win32api.EnumResourceNames(handle, restype): return win32api.LoadResource(handle, restype, name).decode("utf_8") + return None -def srcname(dottedname): +def srcname(dottedname: Union[str, ModuleType, SourceModule]) -> str: """get the source filename from importable name or module""" if hasattr(dottedname, "__file__"): - module = dottedname + module = cast(SourceModule, dottedname) else: - names = dottedname.split(".") + names = cast(str, dottedname).split(".") module = __import__(names.pop(0)) for name in names: module = getattr(module, name) - filename = module.__file__ + filename = cast(str, module.__file__) name, ext = os.path.splitext(filename) ext = ext.lower() @@ -315,22 +325,22 @@ def srcname(dottedname): raise ValueError(f"Source file not found for {dottedname}") -def srcnames(*args): +def srcnames(*args: Union[str, ModuleType, SourceModule]) -> List[str]: """get the source filename from importable name or module""" return [srcname(i) for i in args] -def modname(handle): +def modname(handle: int) -> str: """get module filename from HMODULE""" b = ctypes.create_unicode_buffer("", 1024) ctypes.windll.kernel32.GetModuleFileNameW(handle, b, 1024) return b.value -def findfiles(filenames, *conditions): +def findfiles(filenames: Iterable[str], *conditions: str) -> List[str]: """filter `filenames` by conditions""" - def check(filename): + def check(filename: str) -> Optional[bool]: filename = filename.lower() for i in conditions: i = i.lower() @@ -358,7 +368,7 @@ def check(filename): } -def getregvalue(path, default=None): +def getregvalue(path: str, default: Any = None) -> Any: """get registry value noname value @@ -369,11 +379,11 @@ def getregvalue(path, default=None): >>> getregvalue('HKEY_CLASSES_ROOT\\.py\\Content Type') ''text/plain """ - root, subkey = path.split("\\", 1) - if root.startswith("HKEY_"): - root = getattr(winreg, root) - elif root in hkshortnames: - root = hkshortnames[root] + root_name, subkey = path.split("\\", 1) + if root_name.startswith("HKEY_"): + root = getattr(winreg, root_name) + elif root_name in hkshortnames: + root = hkshortnames[root_name] else: root = winreg.HKEY_CURRENT_USER subkey = path @@ -395,7 +405,7 @@ class IssFile(io.TextIOWrapper): "Flags", ] - def __init__(self, filename, mode='w', encoding='utf-8'): + def __init__(self, filename: Path, mode: str = 'w', encoding: str = 'utf-8') -> None: # Open a buffered binary file and wrap it binary_file = open(filename, mode.replace('t', '') + 'b') super().__init__(binary_file, encoding=encoding) @@ -403,7 +413,7 @@ def __init__(self, filename, mode='w', encoding='utf-8'): if 'w' in mode and encoding.lower() == 'utf-8': self.write('\ufeff') # UTF-8 BOM - def issline(self, **kwargs): + def issline(self, **kwargs: object) -> None: args = [] for k, v in kwargs.items(): if k not in self.noescape: @@ -464,11 +474,11 @@ class InnoScript(object): ".dll", ".pyd", ) - iss_metadata = {} + iss_metadata: Dict[str, str] = {} - def __init__(self, dist_dir, metadata, inno_script, inno_setup_exe=None, - bundle_vcr=True, register_startup=False, zip_option=False, - extra_inno_script=None): + def __init__(self, dist_dir: str, metadata: object, inno_script: str, inno_setup_exe: Optional[Path] = None, + bundle_vcr: Union[bool, str] = True, register_startup: Union[bool, str] = False, zip_option: Union[bool, str] = False, + extra_inno_script: Optional[str] = None) -> None: """Initialize the InnoScript with the necessary parameters. Args: @@ -503,10 +513,10 @@ def __init__(self, dist_dir, metadata, inno_script, inno_setup_exe=None, # Scan the dist directory to find files self.created_files = self._scan_dist_dir() - def parse_iss(self, s): + def parse_iss(self, s: str) -> Iterator[Tuple[str, str, List[str]]]: firstline = "" sectionname = "" - lines = [] + lines: List[str] = [] for line in s.splitlines(): if line.startswith("[") and "]" in line: if lines: @@ -519,7 +529,7 @@ def parse_iss(self, s): if lines: yield firstline, sectionname, lines - def chop(self, filename, dirname=""): + def chop(self, filename: str, dirname: str = "") -> str: """get relative path""" if not dirname: dirname = self.dist_dir @@ -531,9 +541,9 @@ def chop(self, filename, dirname=""): # filename = os.path.basename(filename) return filename - def _scan_dist_dir(self): + def _scan_dist_dir(self) -> Dict[str, List[str]]: """Scan the dist directory to categorize files.""" - results = { + results: Dict[str, List[str]] = { 'executables': [], 'windows_exes': [], 'com_servers': [], @@ -572,7 +582,7 @@ def _scan_dist_dir(self): return results @property - def metadata(self): + def metadata(self) -> Dict[str, Any]: """Get metadata as a dictionary.""" metadata = {} for attr in dir(self._metadata): @@ -582,7 +592,7 @@ def metadata(self): return metadata @property - def appid(self): + def appid(self) -> str: """Generate a consistent AppID based on metadata.""" m = self.metadata if m["url"]: @@ -597,13 +607,13 @@ def appid(self): return f"{{{appid}}}" @property - def iss_consts(self): + def iss_consts(self) -> Dict[str, str]: """Get constants for the ISS file.""" metadata = self.metadata return dict((k, v % metadata) for k, v in self.consts_map.items()) @property - def innoexepath(self): + def innoexepath(self) -> Path: """Find the Inno Setup compiler executable.""" if self._inno_setup_exe and os.path.isfile(self._inno_setup_exe): return self._inno_setup_exe @@ -639,7 +649,7 @@ def innoexepath(self): return "ISCC.exe" @property - def msvcfiles(self): + def msvcfiles(self) -> List[str]: """Get MSVC runtime files if needed. Note: For modern Python (3.5+), this is usually not needed as py2exe @@ -680,11 +690,11 @@ def msvcfiles(self): return files_to_include - def handle_iss(self, lines, fp): + def handle_iss(self, lines: Iterable[str], fp: IO[str]) -> None: for line in lines: fp.write(line + "\n") - def handle_iss_setup(self, lines, fp): + def handle_iss_setup(self, lines: Iterable[str], fp: IO[str]) -> None: metadata = self.metadata iss_metadata = dict((k, v % metadata) for k, v in self.metadata_map.items()) @@ -750,7 +760,7 @@ def handle_iss_setup(self, lines, fp): fp.write("\n") - def handle_iss_files(self, lines, fp): + def handle_iss_files(self, lines: List[str], fp: IssFile) -> None: """Handle the [Files] section of the ISS file.""" files = [] excludes = [] @@ -792,7 +802,7 @@ def handle_iss_files(self, lines, fp): if filename.startswith(self.dist_dir): place = os.path.dirname(relname) - extraargs = {} + extraargs: Dict[str, object] = {} else: # isdir if filename.startswith(self.dist_dir): place = relname @@ -809,7 +819,7 @@ def handle_iss_files(self, lines, fp): self.handle_iss(lines, fp) - def _iter_bin_files(self, category, lines=[]): + def _iter_bin_files(self, category: str, lines: List[str] = []) -> Iterator[Tuple[str, str]]: """Iterate over binary files of a specific category.""" for filename in self.created_files.get(category, []): relname = self.chop(filename) @@ -817,7 +827,7 @@ def _iter_bin_files(self, category, lines=[]): continue yield filename, relname - def handle_iss_run(self, lines, fp): + def handle_iss_run(self, lines: List[str], fp: IssFile) -> None: """Handle the [Run] section of the ISS file.""" # Process COM servers for _, filename in self._iter_bin_files("com_servers", lines): @@ -861,7 +871,7 @@ def handle_iss_run(self, lines, fp): self.handle_iss(lines, fp) - def handle_iss_uninstallrun(self, lines, fp): + def handle_iss_uninstallrun(self, lines: List[str], fp: IssFile) -> None: """Handle the [UninstallRun] section of the ISS file.""" # Process COM servers for _, filename in self._iter_bin_files("com_servers", lines): @@ -905,7 +915,7 @@ def handle_iss_uninstallrun(self, lines, fp): self.handle_iss(lines, fp) - def handle_iss_icons(self, lines, fp): + def handle_iss_icons(self, lines: List[str], fp: IssFile) -> None: """Handle the [Icons] section of the ISS file.""" # Find the main executable to use for shortcuts main_exe = None @@ -944,7 +954,7 @@ def handle_iss_icons(self, lines, fp): self.handle_iss(lines, fp) - def handle_iss_tasks(self, lines, fp): + def handle_iss_tasks(self, lines: List[str], fp: IssFile) -> None: """Handle the [Tasks] section of the ISS file.""" fp.issline( Name="desktopicon", @@ -961,7 +971,7 @@ def handle_iss_tasks(self, lines, fp): self.handle_iss(lines, fp) - def handle_iss_registry(self, lines, fp): + def handle_iss_registry(self, lines: List[str], fp: IssFile) -> None: """Handle the [Registry] section of the ISS file.""" if self.register_startup: # Find the main executable @@ -986,7 +996,7 @@ def handle_iss_registry(self, lines, fp): self.handle_iss(lines, fp) - def handle_iss_languages(self, lines, fp): + def handle_iss_languages(self, lines: List[str], fp: IssFile) -> None: self.handle_iss(lines, fp) if lines: @@ -1003,11 +1013,11 @@ def handle_iss_languages(self, lines, fp): MessagesFile="compiler:%s" % filename, ) - def handle_iss_code(self, lines, fp): + def handle_iss_code(self, lines: List[str], fp: IssFile) -> None: self.handle_iss(lines, fp) fp.write(DEFAULT_CODES) - def create(self): + def create(self) -> None: """Create the Inno Setup script file.""" fp = IssFile(self.issfile, "wt") fp.write('; This file is created by py2exe InnoSetup extension.\n') @@ -1047,7 +1057,7 @@ def create(self): handler([], fp) fp.write("\n") - def compile_script(self): + def compile_script(self) -> str: """Compile the Inno Setup script into an installer.""" try: subprocess.check_call([self.innoexepath, self.issfile]) @@ -1075,14 +1085,14 @@ def compile_script(self): return setupfile @property - def setup_file_path(self): + def setup_file_path(self) -> str: return os.path.join( self.output_dir, self.iss_metadata.get("OutputBaseFilename", "setup") + ".exe", ) @property - def output_dir(self): + def output_dir(self) -> str: return self.iss_metadata.get( "OutputDir", os.path.join(os.path.dirname(self.issfile), "Output") ) @@ -1108,20 +1118,20 @@ class innosetup(distutils.core.Command): boolean_options = ["bundle_vcr", "zip", "register_startup"] - def initialize_options(self): + def initialize_options(self) -> None: """Initialize command options.""" - self.inno_setup_exe = None - self.inno_script = DEFAULT_ISS - self.extra_inno_script = None - self.certificate_file = None - self.certificate_password = None - self.extra_sign = [] - self.bundle_vcr = False # Default to False for modern Python - self.zip = False - self.register_startup = False - self.dist_dir = None + self.inno_setup_exe: Optional[Path] = None + self.inno_script: Optional[str] = DEFAULT_ISS + self.extra_inno_script: Optional[str] = None + self.certificate_file: Optional[Path] = None + self.certificate_password: Optional[str] = None + self.extra_sign: Union[str, List[Path]] = [] + self.bundle_vcr: Union[bool, str] = False # Default to False for modern Python + self.zip: Union[bool, str] = False + self.register_startup: Union[bool, str] = False + self.dist_dir: Optional[str] = None - def finalize_options(self): + def finalize_options(self) -> None: """Finalize command options.""" self.set_undefined_options('bdist', ('dist_dir', 'dist_dir')) if self.dist_dir is None: @@ -1147,7 +1157,7 @@ def finalize_options(self): if isinstance(self.extra_sign, str): self.extra_sign = [self.extra_sign] - def _find_inno_setup(self): + def _find_inno_setup(self) -> Optional[Path]: """Find the Inno Setup compiler.""" if self.inno_setup_exe and os.path.isfile(self.inno_setup_exe): return self.inno_setup_exe @@ -1181,7 +1191,7 @@ def _find_inno_setup(self): return None # Not found - def run(self): + def run(self) -> None: """Run the command: build the installer.""" # First, run py2exe to create the executable self.run_command('py2exe') @@ -1211,7 +1221,7 @@ def run(self): # Create the InnoScript instance script = InnoScript( - dist_dir=self.dist_dir, + dist_dir=cast(str, self.dist_dir), # finalized by distutils before run() metadata=self.distribution.metadata, inno_script=inno_script_content, inno_setup_exe=inno_exe_path, @@ -1235,10 +1245,11 @@ def run(self): if self.certificate_file: self.sign_executable(setup_file) - def sign_executables(self): + def sign_executables(self) -> None: """Sign all executables in the dist directory.""" # Find all executables in the dist directory - for root, _, files in os.walk(self.dist_dir): + dist_dir = cast(str, self.dist_dir) # finalized before run() + for root, _, files in os.walk(dist_dir): for file in files: if file.lower().endswith('.exe'): self.sign_executable(os.path.join(root, file)) @@ -1246,9 +1257,9 @@ def sign_executables(self): # Sign any extra files specified if self.extra_sign: for extra in self.extra_sign: - self.sign_executable(os.path.join(self.dist_dir, extra)) + self.sign_executable(os.path.join(dist_dir, extra)) - def sign_executable(self, exepath): + def sign_executable(self, exepath: Path) -> None: """Sign a single executable.""" if not os.path.exists(exepath): raise FileNotFoundError(f"File to sign not found: {exepath}") diff --git a/installer_builder/new_inno_command.py b/installer_builder/new_inno_command.py index b6a6a17..7ffb09d 100644 --- a/installer_builder/new_inno_command.py +++ b/installer_builder/new_inno_command.py @@ -1,16 +1,32 @@ +from __future__ import annotations + import distutils.core +from distutils.dist import Distribution import os import pathlib import platform +from types import ModuleType +from typing import TYPE_CHECKING, List, Optional, Protocol, Union, cast + +if TYPE_CHECKING: + import innosetup_builder + +Path = Union[str, "os.PathLike[str]"] + + +class InstallerCommand(Protocol): + distribution: Distribution + register_startup: bool # Only import Windows-specific modules on Windows if platform.system() == "Windows": - from . import signtool + from . import signtool as _signtool + signtool: Optional[ModuleType] = _signtool else: signtool = None -def create_installer_config(builder_instance, dist_dir): +def create_installer_config(builder_instance: InstallerCommand, dist_dir: Path) -> innosetup_builder.Installer: """Convert InstallerBuilder config to innosetup_builder.Installer""" import innosetup_builder @@ -53,20 +69,20 @@ class NewInnoSetupCommand(distutils.core.Command): ("dist-dir=", "d", "directory to put final built distributions in"), ] - def initialize_options(self): - self.extra_inno_script = None - self.certificate_file = None - self.certificate_password = None - self.extra_sign = [] + def initialize_options(self) -> None: + self.extra_inno_script: Optional[str] = None + self.certificate_file: Optional[Path] = None + self.certificate_password: Optional[str] = None + self.extra_sign: List[Path] = [] self.register_startup = False - self.dist_dir = None + self.dist_dir: Optional[Path] = None - def finalize_options(self): + def finalize_options(self) -> None: self.set_undefined_options('bdist', ('dist_dir', 'dist_dir')) if self.dist_dir is None: self.dist_dir = "dist" - def run(self): + def run(self) -> None: # Sign executables if requested if self.certificate_file: self._sign_executables() @@ -78,22 +94,25 @@ def run(self): if self.certificate_file: self._sign_installer() - def _create_installer(self): + def _create_installer(self) -> None: import innosetup_builder # Create installer config from distribution metadata - installer_config = create_installer_config(self, self.dist_dir) + # distutils finalizes options before run(); finalize_options supplies dist. + dist_dir = cast(Path, self.dist_dir) + installer_config = create_installer_config(self, dist_dir) # Create compiler and build installer innosetup_compiler = innosetup_builder.InnosetupCompiler() - innosetup_compiler.build(installer_config, self.dist_dir) + innosetup_compiler.build(installer_config, dist_dir) output_name = f"{installer_config.app_name}-{installer_config.app_version}-setup.exe" print(f"Created installer: {output_name}") - def _sign_executables(self): + def _sign_executables(self) -> None: """Sign all executables in dist directory""" - for root, _, files in os.walk(self.dist_dir): + dist_dir = cast(Path, self.dist_dir) # finalized before run() + for root, _, files in os.walk(dist_dir): for file in files: if file.lower().endswith('.exe'): self._sign_file(os.path.join(root, file)) @@ -101,15 +120,15 @@ def _sign_executables(self): # Sign extra files if specified if self.extra_sign: for extra in self.extra_sign: - self._sign_file(os.path.join(self.dist_dir, extra)) + self._sign_file(os.path.join(dist_dir, extra)) - def _sign_installer(self): + def _sign_installer(self) -> None: """Sign the created installer""" installer_name = f"{self.distribution.metadata.name}-{self.distribution.metadata.version}-setup.exe" - installer_path = os.path.join(self.dist_dir, installer_name) + installer_path = os.path.join(cast(Path, self.dist_dir), installer_name) self._sign_file(installer_path) - def _sign_file(self, filepath): + def _sign_file(self, filepath: Path) -> None: """Sign a single file""" if not os.path.exists(filepath): raise FileNotFoundError(f"File to sign not found: {filepath}") diff --git a/installer_builder/signtool.py b/installer_builder/signtool.py index b565d66..19dfa4a 100644 --- a/installer_builder/signtool.py +++ b/installer_builder/signtool.py @@ -1,10 +1,11 @@ -from __future__ import print_function +from __future__ import annotations, print_function import os import subprocess import sys import platform import logging import winreg +from typing import Optional, Union # Set up module logger logger = logging.getLogger(__name__) @@ -13,11 +14,11 @@ class SignToolNotFoundError(Exception): """Exception raised when signtool.exe cannot be found.""" - def __init__(self, message="Could not find signtool.exe. You may need to install the Windows SDK or Visual Studio."): + def __init__(self, message: str = "Could not find signtool.exe. You may need to install the Windows SDK or Visual Studio.") -> None: self.message = message super().__init__(self.message) -def find_signtool(): +def find_signtool() -> str: """ Find signtool.exe on the system, preferring architecture match and newest version. @@ -206,7 +207,7 @@ def find_signtool(): # Return the best match path return best_match[0] -def sign(filename, url='', description='', timestamp_server=DEFAULT_TIMESTAMP_SERVER, certificate_file='', certificate_password=''): +def sign(filename: Union[str, os.PathLike[str]], url: Optional[str] = '', description: str = '', timestamp_server: str = DEFAULT_TIMESTAMP_SERVER, certificate_file: Optional[Union[str, os.PathLike[str]]] = '', certificate_password: Optional[str] = '') -> int: """ Sign a Windows executable or DLL using signtool. diff --git a/pyproject.toml b/pyproject.toml index c4c3ac7..40189e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "mypy>=0.991,<1.15", + "types-setuptools==75.8.0.20250110", ] [dependency-groups] From bbf7a823aab11ea59728e580217d5c0f81d7aafb Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Wed, 21 Oct 2026 01:35:42 -0600 Subject: [PATCH 7/8] Distinguish independent dependency branch refresh in handoff --- agent-notes/repair/task6-installer-mypy-baseline.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/agent-notes/repair/task6-installer-mypy-baseline.md b/agent-notes/repair/task6-installer-mypy-baseline.md index a3db679..209fd9c 100644 --- a/agent-notes/repair/task6-installer-mypy-baseline.md +++ b/agent-notes/repair/task6-installer-mypy-baseline.md @@ -101,7 +101,12 @@ was changed by this local rebase. Named archive/task6-installer-mypy-baseline-before-fad12 preserves published 2f18a7922a3bec7ca31f9fadf633081403aad973. The redundant stacked probe branch stays unchanged at fd0103dc83f89e66c12b413e87d6fa724cbfbd91; it was not rebased -or forced through duplicate conflicts. Original PR #4 remains unchanged. +or forced through duplicate conflicts. The PR #4 branch was not modified by +this task. Dependabot independently refreshed it to +995f935b95fbe64004cb47fc779af9f13a589e3a with <2.5 after #7; coordinator +verified run 39554719205 fails two 3.13 probe cases with mypy 2.4. The original +62d473dd archive remains preserved; no result for that separate head is implied +by this #5 source repair. These earlier archive tips are historical. Named archive/task6-installer-mypy-baseline-measurement-cde0f5c8 preserves the exact cde0f5c85b442e727583f06a6eeec352eaa0003a measured tip before this note correction. From 7c6121ab07d5ecf1c3058f8193f1674500ea55ab Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Wed, 21 Oct 2026 01:42:12 -0600 Subject: [PATCH 8/8] Resolve remaining installer typing boundary diagnostics --- .../repair/task6-installer-mypy-baseline.md | 37 +++++++++++++++---- installer_builder/innosetup.py | 8 ++-- installer_builder/new_inno_command.py | 10 ++++- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/agent-notes/repair/task6-installer-mypy-baseline.md b/agent-notes/repair/task6-installer-mypy-baseline.md index 209fd9c..1f77834 100644 --- a/agent-notes/repair/task6-installer-mypy-baseline.md +++ b/agent-notes/repair/task6-installer-mypy-baseline.md @@ -9,15 +9,15 @@ range/target compatibility change landed separately in #7. Preserve package Published PR #5 remains open and unmerged, with full typing work remaining. repair/task6-installer-mypy-baseline was genuinely rebased onto merged master fad12d9a3eafa59fcd6033e54534c4eee3ed2fb7 using --no-update-refs. The last -recorded completed measurement is run 39555011151 at published source head -cde0f5c85b442e727583f06a6eeec352eaa0003a, detailed below. Live PR checks are +recorded completed measurement is run 39563222831 at published source head +bbf7a823aab11ea59728e580217d5c0f81d7aafb, detailed below. Live PR checks are authoritative for later heads; that measurement is not a promised later result. The baseline gate remains unchanged in the existing Windows Python 3.8 leg. The merged base supplies mypy>=0.991,<1.15 and the unchanged three-case target/API/config probes on tool Python 3.8 and 3.13. The authorized source repair now annotates the four production modules and adds only the proven -Python 3.8-compatible types-setuptools development stub. The completed cde -measurement remains the last recorded result; no green result is inferred for +Python 3.8-compatible types-setuptools development stub. The completed first +annotation measurement remains the last recorded result; no green result is inferred for this source candidate. No full package checker command has run locally. # Decisions and why @@ -34,7 +34,7 @@ this source candidate. No full package checker command has run locally. altered strict flags, target substitution, skip or mass annotation is added. Existing source contains unannotated definitions; hosted CI must establish actual diagnostics before deciding any bounded baseline repair. - The latest recorded run 39555011151 at cde0f5c8 established the diagnostic + The latest recorded run 39563222831 at bbf7a823 established the diagnostic snapshot detailed in Verification below. - The existing Windows 3.8/3.14 test/build axes, check names, action pins, permissions, checkout credentials policy, timeout and concurrency remain. @@ -86,6 +86,16 @@ this source candidate. No full package checker command has run locally. are import spelling, identity casts, local variable separation for registry root names/handles, and explicit existing None fallthroughs. No new signing, build, compiler, native, data-generation or optional-feature policy is added. +- First hosted annotation feedback left six errors. Remove the now-proven + redundant dottedname cast and spell the three existing bare returns as None. + DistributionMetadata name/version are genuinely nullable; do not cast away + that state or use getters that invent UNKNOWN/0.0.0. At the final join boundary + explicitly narrow both captured values to str, retaining TypeError for invalid + values with join's existing sequence-item/type-name message. Valid string + metadata comes from the existing Distribution producer in test_new_inno.py; + the type checks preserve the accepted input set instead of asserting that + every Distribution always has valid metadata. This exposes the existing + string-join contract without a new runtime feature or a suppression. # Preservation @@ -112,6 +122,8 @@ archive/task6-installer-mypy-baseline-measurement-cde0f5c8 preserves the exact cde0f5c85b442e727583f06a6eeec352eaa0003a measured tip before this note correction. archive/task6-installer-mypy-baseline-before-typing preserves exact 76332cebdb1eaac1400427daf73b3b4bafd87b6c before these source annotations. +archive/task6-installer-mypy-baseline-first-typing-bbf7a82 preserves exact +bbf7a823aab11ea59728e580217d5c0f81d7aafb before the focused six-error follow-up. # Verification @@ -124,7 +136,7 @@ had 112 diagnostics with mypy 0.991 in run 39552363115 after the three helper diagnostics disappeared; both 71-test suites passed. These are historical results under the old <1.0 range. Actual #7 landed the narrower compatible <1.15 range and bounded probes separately. -The last recorded completed measurement, run 39555011151 at exact cde0f5c8, +The historical pre-annotation measurement, run 39555011151 at exact cde0f5c8, selected mypy 1.14.1 and failed with 106 errors in four package files. Diagnostic classes: 89 no-untyped-def, 6 var-annotated, 3 import-untyped, 3 import-not-found, 3 arg-type, 1 assignment and 1 operator. @@ -151,10 +163,19 @@ tests/build, optional provider import or native action ran. No regression was added merely to mirror annotations; existing 71 tests and both three-case compatibility probes remain intact. The workflow is byte-for-byte unchanged. +Last recorded completed source measurement on 2026-10-21: run 39563222831 at +exact bbf7a823aab11ea59728e580217d5c0f81d7aafb. Both 71-test suites passed; +all three probe cases passed with mypy 1.14.1 on Python 3.8.20 and 3.13.16. +The Python 3.14 build passed; the full checker remained red with six errors in +two files (two list-item, one redundant-cast, three return-value). Python 3.8 +build was skipped after that failure. The focused follow-up addresses those +exact six source sites; its result must come from the next hosted run. No +local checker, tests/build, provider import, signing or native action ran. + # Next step -Return this coherent source/manifest/note delta for coordinator review before -guarded publication and the first hosted measurement of these annotations. +Return this focused two-source-file/note follow-up for coordinator review +before guarded publication and the next hosted full-package measurement. Resolve any remaining accurate-type diagnostics under the unchanged full gate; do not suppress, weaken flags or shrink coverage. Keep original #4, the redundant stacked candidate and canonical work unchanged. PR #5 remains open/unmerged; diff --git a/installer_builder/innosetup.py b/installer_builder/innosetup.py index 3dec4ca..d99d5ea 100644 --- a/installer_builder/innosetup.py +++ b/installer_builder/innosetup.py @@ -303,7 +303,7 @@ def srcname(dottedname: Union[str, ModuleType, SourceModule]) -> str: if hasattr(dottedname, "__file__"): module = cast(SourceModule, dottedname) else: - names = cast(str, dottedname).split(".") + names = dottedname.split(".") module = __import__(names.pop(0)) for name in names: module = getattr(module, name) @@ -346,13 +346,13 @@ def check(filename: str) -> Optional[bool]: i = i.lower() if i.startswith("."): # compare ext if os.path.splitext(filename)[1] != i: - return + return None elif i.count(".") == 1: # compare basename if os.path.basename(filename) != i: - return + return None else: # contains if i not in os.path.basename(filename): - return + return None return True return [i for i in filenames if check(i)] diff --git a/installer_builder/new_inno_command.py b/installer_builder/new_inno_command.py index 7ffb09d..3aa7894 100644 --- a/installer_builder/new_inno_command.py +++ b/installer_builder/new_inno_command.py @@ -45,9 +45,15 @@ def create_installer_config(builder_instance: InstallerCommand, dist_dir: Path) installer.run_at_startup = builder_instance.register_startup # Set output filename + app_name, app_version = installer.app_name, installer.app_version + # The existing join requires strings; retain its TypeError for invalid metadata. + if not isinstance(app_name, str): + raise TypeError(f"sequence item 0: expected str instance, {type(app_name).__name__} found") + if not isinstance(app_version, str): + raise TypeError(f"sequence item 1: expected str instance, {type(app_version).__name__} found") installer_filename = "-".join([ - installer.app_name, - installer.app_version, + app_name, + app_version, 'setup' ]) installer.output_base_filename = installer_filename