python-msilib 0.7.0__cp315-cp315-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
msilib/__init__.py ADDED
@@ -0,0 +1,937 @@
1
+ """Read and write Microsoft Installer files."""
2
+
3
+ # Copyright (C) 2005 Martin v. Löwis
4
+ # Licensed to PSF under a Contributor Agreement.
5
+ from __future__ import annotations
6
+
7
+ import contextlib
8
+ import fnmatch
9
+ import os
10
+ import platform
11
+ import re
12
+ import string
13
+ from importlib import metadata
14
+ from tempfile import mkstemp
15
+ from typing import TYPE_CHECKING, Any, Final
16
+
17
+ from msilib._msi import (
18
+ MSICOLINFO_NAMES,
19
+ MSICOLINFO_TYPES,
20
+ MSIDBOPEN_CREATE,
21
+ MSIDBOPEN_CREATEDIRECT,
22
+ MSIDBOPEN_DIRECT,
23
+ MSIDBOPEN_PATCHFILE,
24
+ MSIDBOPEN_READONLY,
25
+ MSIDBOPEN_TRANSACT,
26
+ MSIMODIFY_ASSIGN,
27
+ MSIMODIFY_DELETE,
28
+ MSIMODIFY_INSERT,
29
+ MSIMODIFY_INSERT_TEMPORARY,
30
+ MSIMODIFY_MERGE,
31
+ MSIMODIFY_REFRESH,
32
+ MSIMODIFY_REPLACE,
33
+ MSIMODIFY_SEEK,
34
+ MSIMODIFY_UPDATE,
35
+ MSIMODIFY_VALIDATE,
36
+ MSIMODIFY_VALIDATE_DELETE,
37
+ MSIMODIFY_VALIDATE_FIELD,
38
+ MSIMODIFY_VALIDATE_NEW,
39
+ PID_APPNAME,
40
+ PID_AUTHOR,
41
+ PID_CHARCOUNT,
42
+ PID_CODEPAGE,
43
+ PID_COMMENTS,
44
+ PID_CREATE_DTM,
45
+ PID_KEYWORDS,
46
+ PID_LASTAUTHOR,
47
+ PID_LASTPRINTED,
48
+ PID_LASTSAVE_DTM,
49
+ PID_PAGECOUNT,
50
+ PID_REVNUMBER,
51
+ PID_SECURITY,
52
+ PID_SUBJECT,
53
+ PID_TEMPLATE,
54
+ PID_TITLE,
55
+ PID_WORDCOUNT,
56
+ CreateRecord,
57
+ FCICreate,
58
+ MSIError,
59
+ OpenDatabase,
60
+ UuidCreate,
61
+ )
62
+
63
+ if TYPE_CHECKING:
64
+ from collections.abc import Container, Iterable
65
+ from types import ModuleType
66
+
67
+ from ._msi import _Database
68
+ from .sequence import _SequenceType
69
+
70
+ __version__ = metadata.version("python-msilib")
71
+
72
+ __all__ = [
73
+ "CAB",
74
+ "MSICOLINFO_NAMES",
75
+ "MSICOLINFO_TYPES",
76
+ "MSIDBOPEN_CREATE",
77
+ "MSIDBOPEN_CREATEDIRECT",
78
+ "MSIDBOPEN_DIRECT",
79
+ "MSIDBOPEN_PATCHFILE",
80
+ "MSIDBOPEN_READONLY",
81
+ "MSIDBOPEN_TRANSACT",
82
+ "MSIMODIFY_ASSIGN",
83
+ "MSIMODIFY_DELETE",
84
+ "MSIMODIFY_INSERT",
85
+ "MSIMODIFY_INSERT_TEMPORARY",
86
+ "MSIMODIFY_MERGE",
87
+ "MSIMODIFY_REFRESH",
88
+ "MSIMODIFY_REPLACE",
89
+ "MSIMODIFY_SEEK",
90
+ "MSIMODIFY_UPDATE",
91
+ "MSIMODIFY_VALIDATE",
92
+ "MSIMODIFY_VALIDATE_DELETE",
93
+ "MSIMODIFY_VALIDATE_FIELD",
94
+ "MSIMODIFY_VALIDATE_NEW",
95
+ "PID_APPNAME",
96
+ "PID_AUTHOR",
97
+ "PID_CHARCOUNT",
98
+ "PID_CODEPAGE",
99
+ "PID_COMMENTS",
100
+ "PID_CREATE_DTM",
101
+ "PID_KEYWORDS",
102
+ "PID_LASTAUTHOR",
103
+ "PID_LASTPRINTED",
104
+ "PID_LASTSAVE_DTM",
105
+ "PID_PAGECOUNT",
106
+ "PID_REVNUMBER",
107
+ "PID_SECURITY",
108
+ "PID_SUBJECT",
109
+ "PID_TEMPLATE",
110
+ "PID_TITLE",
111
+ "PID_WORDCOUNT",
112
+ "Binary",
113
+ "Control",
114
+ "CreateRecord",
115
+ "Dialog",
116
+ "Directory",
117
+ "FCICreate",
118
+ "Feature",
119
+ "MSIError",
120
+ "OpenDatabase",
121
+ "RadioButtonGroup",
122
+ "Table",
123
+ "UuidCreate",
124
+ "add_data",
125
+ "add_stream",
126
+ "add_tables",
127
+ "change_sequence",
128
+ "datasizemask",
129
+ "gen_uuid",
130
+ "init_database",
131
+ "knownbits",
132
+ "make_id",
133
+ "type_binary",
134
+ "type_key",
135
+ "type_localizable",
136
+ "type_long",
137
+ "type_nullable",
138
+ "type_short",
139
+ "type_string",
140
+ "type_valid",
141
+ "typemask",
142
+ ]
143
+
144
+
145
+ # Should work in windows, but also in mingw, cygwin, ...
146
+ AMD64: Final[bool] = platform.machine() in ("x64", "x86_64", "AMD64")
147
+ ARM64: Final[bool] = platform.machine() in ("aarch64", "arm64", "ARM64")
148
+
149
+ # Keep msilib.Win64 around to preserve backwards compatibility.
150
+ Win64: Final[bool] = AMD64
151
+
152
+ # Partially taken from Wine
153
+ datasizemask: Final = 0x00FF
154
+ type_valid: Final = 0x0100
155
+ type_localizable: Final = 0x0200
156
+
157
+ typemask: Final = 0x0C00
158
+ type_long: Final = 0x0000
159
+ type_short: Final = 0x0400
160
+ type_string: Final = 0x0C00
161
+ type_binary: Final = 0x0800
162
+
163
+ type_nullable: Final = 0x1000
164
+ type_key: Final = 0x2000
165
+ # XXX temporary, localizable?
166
+ knownbits: Final = (
167
+ datasizemask
168
+ | type_valid
169
+ | type_localizable
170
+ | typemask
171
+ | type_nullable
172
+ | type_key
173
+ )
174
+
175
+
176
+ class Table:
177
+ name: str
178
+ fields: list[tuple[int, str, int]]
179
+
180
+ def __init__(self, name: str) -> None:
181
+ self.name = name
182
+ self.fields = []
183
+
184
+ def add_field(self, index: int, name: str, type: int) -> None:
185
+ self.fields.append((index, name, type))
186
+
187
+ def sql(self) -> str:
188
+ fields = []
189
+ keys = []
190
+ self.fields.sort()
191
+ # fields: list[str] = [""] * len(self.fields)
192
+ for index, name, ftype in self.fields:
193
+ index - 1
194
+ unk = ftype & ~knownbits
195
+ if unk:
196
+ print(f"{self.name}.{name} unknown bits {unk:x}")
197
+ size = ftype & datasizemask
198
+ dtype = ftype & typemask
199
+ if dtype == type_string:
200
+ tname = f"CHAR({size})" if size else "CHAR"
201
+ elif dtype == type_short:
202
+ assert size == 2
203
+ tname = "SHORT"
204
+ elif dtype == type_long:
205
+ assert size == 4
206
+ tname = "LONG"
207
+ elif dtype == type_binary:
208
+ assert size == 0
209
+ tname = "OBJECT"
210
+ else:
211
+ tname = "unknown"
212
+ print(f"{self.name}.{name} unknown integer type {size}")
213
+ flags = "" if ftype & type_nullable else " NOT NULL"
214
+ if ftype & type_localizable:
215
+ flags += " LOCALIZABLE"
216
+ # fields[idx] = f"`{name}` {tname}{flags}"
217
+ fields.append(f"`{name}` {tname}{flags}")
218
+ if ftype & type_key:
219
+ keys.append(f"`{name}`")
220
+ fields = ", ".join(fields)
221
+ keys = ", ".join(keys)
222
+ return f"CREATE TABLE {self.name} ({fields} PRIMARY KEY {keys})"
223
+
224
+ def create(self, db: _Database) -> None:
225
+ v = db.OpenView(self.sql())
226
+ v.Execute(None)
227
+ v.Close()
228
+
229
+
230
+ def change_sequence(
231
+ seq: _SequenceType,
232
+ action: str,
233
+ seqno: int | None = None,
234
+ cond: str | None = None,
235
+ ) -> None:
236
+ """Change the sequence number of an action in a sequence list."""
237
+ for i in range(len(seq)):
238
+ if seq[i][0] == action:
239
+ if cond is None:
240
+ cond = seq[i][1]
241
+ if seqno is None:
242
+ seqno = seq[i][2]
243
+ seq[i] = (action, cond, seqno)
244
+ return
245
+ msg = "Action not found in sequence"
246
+ raise ValueError(msg)
247
+
248
+
249
+ def add_data(
250
+ db: _Database, table: str, values: Iterable[tuple[Any, ...]]
251
+ ) -> None:
252
+ sql = "SELECT * FROM `{table}`"
253
+ v = db.OpenView(sql.format(table=table))
254
+ count = v.GetColumnInfo(MSICOLINFO_NAMES).GetFieldCount()
255
+ r = CreateRecord(count)
256
+ for value in values:
257
+ assert len(value) == count, value
258
+ for i in range(count):
259
+ field = value[i]
260
+ if isinstance(field, int):
261
+ r.SetInteger(i + 1, field)
262
+ elif isinstance(field, str):
263
+ r.SetString(i + 1, field)
264
+ elif field is None:
265
+ pass
266
+ elif isinstance(field, Binary):
267
+ r.SetStream(i + 1, field.name)
268
+ else:
269
+ msg = f"Unsupported type {field.__class__.__name__}"
270
+ raise TypeError(msg)
271
+ try:
272
+ v.Modify(MSIMODIFY_INSERT, r)
273
+ except Exception: # noqa: BLE001
274
+ msg = f"Could not insert {values!r} into {table}"
275
+ raise MSIError(msg) from None
276
+
277
+ r.ClearData()
278
+ v.Close()
279
+
280
+
281
+ def add_stream(db: _Database, name: str, path: str) -> None:
282
+ sql = "INSERT INTO _Streams (Name, Data) VALUES ('{name}', ?)"
283
+ v = db.OpenView(sql.format(name=name))
284
+ r = CreateRecord(1)
285
+ r.SetStream(1, path)
286
+ v.Execute(r)
287
+ v.Close()
288
+
289
+
290
+ def init_database(
291
+ name: str,
292
+ schema: ModuleType,
293
+ ProductName: str,
294
+ ProductCode: str,
295
+ ProductVersion: str,
296
+ Manufacturer: str,
297
+ ) -> _Database:
298
+ with contextlib.suppress(OSError):
299
+ os.unlink(name)
300
+ ProductCode = ProductCode.upper()
301
+ # Create the database
302
+ db = OpenDatabase(name, MSIDBOPEN_CREATE)
303
+ # Create the tables
304
+ for t in schema.tables:
305
+ t.create(db)
306
+ # Fill the validation table
307
+ add_data(db, "_Validation", schema._Validation_records) # noqa: SLF001
308
+ # Initialize the summary information, allowing at most 20 properties
309
+ si = db.GetSummaryInformation(20)
310
+ si.SetProperty(PID_TITLE, "Installation Database")
311
+ si.SetProperty(PID_SUBJECT, ProductName)
312
+ si.SetProperty(PID_AUTHOR, Manufacturer)
313
+ # https://learn.microsoft.com/en-us/windows/win32/msi/template-summary
314
+ if AMD64:
315
+ si.SetProperty(PID_TEMPLATE, "x64;1033")
316
+ elif ARM64:
317
+ si.SetProperty(PID_TEMPLATE, "Arm64;1033")
318
+ else:
319
+ si.SetProperty(PID_TEMPLATE, "Intel;1033")
320
+ si.SetProperty(PID_REVNUMBER, gen_uuid())
321
+ # https://learn.microsoft.com/en-us/windows/win32/msi/word-count-summary
322
+ # 2 = long file names, compressed, original media
323
+ si.SetProperty(PID_WORDCOUNT, 2)
324
+ # https://learn.microsoft.com/en-us/windows/win32/msi/page-count-summary
325
+ # https://learn.microsoft.com/en-us/windows/win32/msi/using-64-bit-windows-installer-packages
326
+ if ARM64:
327
+ si.SetProperty(PID_PAGECOUNT, 500) # minimum of Windows Installer 5.0
328
+ else:
329
+ si.SetProperty(PID_PAGECOUNT, 200) # minimum of Windows Installer 2.0
330
+ si.SetProperty(PID_APPNAME, "Python MSI Library")
331
+ # XXX more properties
332
+ si.Persist()
333
+ add_data(
334
+ db,
335
+ "Property",
336
+ [
337
+ ("ProductName", ProductName),
338
+ ("ProductCode", ProductCode),
339
+ ("ProductVersion", ProductVersion),
340
+ ("Manufacturer", Manufacturer),
341
+ ("ProductLanguage", "1033"),
342
+ ],
343
+ )
344
+ db.Commit()
345
+ return db
346
+
347
+
348
+ def add_tables(db: _Database, module: ModuleType) -> None:
349
+ for table in module.tables:
350
+ add_data(db, table, getattr(module, table))
351
+
352
+
353
+ def make_id(value: str) -> str:
354
+ identifier_chars = string.ascii_letters + string.digits + "._"
355
+ value = "".join([c if c in identifier_chars else "_" for c in value])
356
+ if value[0] in (string.digits + "."):
357
+ value = "_" + value
358
+ assert re.match("^[A-Za-z_][A-Za-z0-9_.]*$", value), "FILE" + value
359
+ return value
360
+
361
+
362
+ def gen_uuid() -> str:
363
+ return "{" + UuidCreate().upper() + "}"
364
+
365
+
366
+ class CAB:
367
+ name: str
368
+ files: list[tuple[str, str]]
369
+ filenames: set[str]
370
+ index: int
371
+
372
+ def __init__(self, name: str) -> None:
373
+ self.name = name
374
+ self.files = []
375
+ self.filenames = set()
376
+ self.index = 0
377
+
378
+ def gen_id(self, file: str) -> str:
379
+ logical = _logical = make_id(file)
380
+ pos = 1
381
+ while logical in self.filenames:
382
+ logical = f"{_logical}.{pos}"
383
+ pos += 1
384
+ self.filenames.add(logical)
385
+ return logical
386
+
387
+ def append(
388
+ self, full: str, file: str, logical: str | None
389
+ ) -> tuple[int, str] | None:
390
+ if os.path.isdir(full):
391
+ return None
392
+ if not logical:
393
+ logical = self.gen_id(file)
394
+ self.index += 1
395
+ self.files.append((full, logical))
396
+ return self.index, logical
397
+
398
+ def commit(self, db: _Database) -> None:
399
+ fd, filename = mkstemp()
400
+ os.close(fd)
401
+ FCICreate(filename, self.files)
402
+ add_data(
403
+ db, "Media", [(1, self.index, None, "#" + self.name, None, None)]
404
+ )
405
+ add_stream(db, self.name, filename)
406
+ os.unlink(filename)
407
+ db.Commit()
408
+
409
+
410
+ _directories: set[str] = set()
411
+
412
+
413
+ class Directory:
414
+ db: _Database
415
+ cab: CAB
416
+ basedir: Directory | None
417
+ physical: str
418
+ logical: str
419
+ component: str | None
420
+ short_names: set[str]
421
+ ids: set[str]
422
+ keyfiles: dict[str, str]
423
+ componentflags: int | None
424
+ absolute: str
425
+
426
+ def __init__(
427
+ self,
428
+ db: _Database,
429
+ cab: CAB,
430
+ basedir: Directory | None,
431
+ physical: str,
432
+ _logical: str,
433
+ default: str,
434
+ componentflags: int | None = None,
435
+ ) -> None:
436
+ """Create a new directory in the Directory table.
437
+
438
+ There is a current component at each point in time for the directory,
439
+ which is either explicitly created through start_component, or
440
+ implicitly when files are added for the first time. Files are added
441
+ into the current component, and into the cab file. To create a
442
+ directory, a base directory object needs to be specified (can be None),
443
+ the path to the physical directory, and a logical directory name.
444
+ Default specifies the DefaultDir slot in the directory table.
445
+ componentflags specifies the default flags that new components get.
446
+ """
447
+ index = 1
448
+ _logical = make_id(_logical)
449
+ logical = _logical
450
+ while logical in _directories:
451
+ logical = f"{_logical}{index}"
452
+ index += 1
453
+ _directories.add(logical)
454
+ self.db = db
455
+ self.cab = cab
456
+ self.basedir = basedir
457
+ self.physical = physical
458
+ self.logical = logical
459
+ self.component = None
460
+ self.short_names = set()
461
+ self.ids = set()
462
+ self.keyfiles = {}
463
+ self.componentflags = componentflags
464
+ if basedir:
465
+ self.absolute = os.path.join(basedir.absolute, physical)
466
+ blogical = basedir.logical
467
+ else:
468
+ self.absolute = physical
469
+ blogical = None
470
+ add_data(db, "Directory", [(logical, blogical, default)])
471
+
472
+ def start_component(
473
+ self,
474
+ component: str | None = None,
475
+ feature: Feature | None = None,
476
+ flags: int | None = None,
477
+ keyfile: str | None = None,
478
+ uuid: str | None = None,
479
+ ) -> None:
480
+ """Add an entry to the Component table.
481
+
482
+ And make this component the current for this directory. If no component
483
+ name is given, the directory name is used. If no feature is given, the
484
+ current feature is used. If no flags are given, the directory's default
485
+ flags are used. If no keyfile is given, the KeyPath is left null in the
486
+ Component table.
487
+ """
488
+ if flags is None:
489
+ flags = self.componentflags or 0
490
+ uuid = gen_uuid() if uuid is None else uuid.upper()
491
+ if component is None:
492
+ component = self.logical
493
+ self.component = component
494
+ # https://learn.microsoft.com/en-us/windows/win32/msi/component-table
495
+ if AMD64 or ARM64:
496
+ flags |= 256 # msidbComponentAttributes64bit
497
+ if keyfile:
498
+ keyid = self.cab.gen_id(keyfile)
499
+ self.keyfiles[keyfile] = keyid
500
+ else:
501
+ keyid = None
502
+ add_data(
503
+ self.db,
504
+ "Component",
505
+ [(component, uuid, self.logical, flags, None, keyid)],
506
+ )
507
+ if feature is None:
508
+ feature = current_feature
509
+ add_data(self.db, "FeatureComponents", [(feature.id, component)])
510
+
511
+ def make_short(self, file: str) -> str:
512
+ oldfile = file
513
+ file = file.replace("+", "_")
514
+ file = "".join(c for c in file if c not in r' "/\[]:;=,')
515
+ parts = file.split(".")
516
+ if len(parts) > 1:
517
+ prefix = "".join(parts[:-1]).upper()
518
+ suffix = parts[-1].upper()
519
+ if not prefix:
520
+ prefix = suffix
521
+ suffix = None
522
+ else:
523
+ prefix = file.upper()
524
+ suffix = None
525
+ if (
526
+ len(parts) < 3
527
+ and len(prefix) <= 8
528
+ and file == oldfile
529
+ and (not suffix or len(suffix) <= 3)
530
+ ):
531
+ newfile = f"{prefix}.{suffix}" if suffix else prefix
532
+ else:
533
+ newfile = None
534
+ if newfile is None or newfile in self.short_names:
535
+ prefix = prefix[:6]
536
+ if suffix:
537
+ suffix = suffix[:3]
538
+ pos = 1
539
+ while 1:
540
+ if suffix:
541
+ newfile = f"{prefix}~{pos}.{suffix}"
542
+ else:
543
+ newfile = f"{prefix}~{pos}"
544
+ if newfile not in self.short_names:
545
+ break
546
+ pos += 1
547
+ assert pos < 10000
548
+ if pos in (10, 100, 1000):
549
+ prefix = prefix[:-1]
550
+ self.short_names.add(newfile)
551
+ # restrictions on short names
552
+ assert not re.search(r'[\?|><:/*"+,;=\[\]]', newfile)
553
+ return newfile
554
+
555
+ def add_file(
556
+ self,
557
+ file: str,
558
+ src: str | None = None,
559
+ version: str | None = None,
560
+ language: str | None = None,
561
+ ) -> str | None:
562
+ """Add a file to the current component of the directory.
563
+
564
+ Starting a new one if there is no current component. By default, the
565
+ file name in the source and the file table will be identical. If the
566
+ src file is specified, it is interpreted relative to the current
567
+ directory. Optionally, a version and a language can be specified for
568
+ the entry in the File table.
569
+ """
570
+ if not self.component:
571
+ self.start_component(self.logical, current_feature, 0)
572
+ if not src:
573
+ # Allow relative paths for file if src is not specified
574
+ src = file
575
+ file = os.path.basename(file)
576
+ absolute = os.path.join(self.absolute, src)
577
+ # restrictions on long names
578
+ assert not re.search(r'[\?|><:/*]"', file)
579
+ logical = self.keyfiles.get(file, None)
580
+ ok = self.cab.append(absolute, file, logical)
581
+ if ok is None:
582
+ return None
583
+ sequence, logical = ok
584
+ assert logical not in self.ids
585
+ self.ids.add(logical)
586
+ short = self.make_short(file)
587
+ full = f"{short}|{file}"
588
+ filesize = os.stat(absolute).st_size
589
+ # constants.msidbFileAttributesVital
590
+ # Compressed omitted, since it is the database default
591
+ # could add r/o, system, hidden
592
+ attributes = 512
593
+ add_data(
594
+ self.db,
595
+ "File",
596
+ [
597
+ (
598
+ logical,
599
+ self.component,
600
+ full,
601
+ filesize,
602
+ version,
603
+ language,
604
+ attributes,
605
+ sequence,
606
+ )
607
+ ],
608
+ )
609
+ # if not version:
610
+ # # Add hash if the file is not versioned
611
+ # filehash = FileHash(absolute, 0)
612
+ # add_data(self.db, "MsiFileHash",
613
+ # [(logical, 0, filehash.IntegerData(1),
614
+ # filehash.IntegerData(2), filehash.IntegerData(3),
615
+ # filehash.IntegerData(4))])
616
+ # Automatically remove .pyc files on uninstall (2)
617
+ # XXX: adding so many RemoveFile entries makes installer unbelievably
618
+ # slow. So instead, we have to use wildcard remove entries
619
+ if file.endswith(".py"):
620
+ add_data(
621
+ self.db,
622
+ "RemoveFile",
623
+ [
624
+ (
625
+ logical + "c",
626
+ self.component,
627
+ f"{short}C|{file}c",
628
+ self.logical,
629
+ 2,
630
+ ),
631
+ (
632
+ logical + "o",
633
+ self.component,
634
+ f"{short}O|{file}o",
635
+ self.logical,
636
+ 2,
637
+ ),
638
+ ],
639
+ )
640
+ return logical
641
+
642
+ def glob(
643
+ self, pattern: str, exclude: Container[str] | None = None
644
+ ) -> list[str]:
645
+ """Add a list of files to the current component.
646
+
647
+ Add a list of files to the current component as specified in the
648
+ glob pattern. Individual files can be excluded in the exclude list.
649
+ """
650
+ try:
651
+ files = os.listdir(self.absolute)
652
+ except OSError:
653
+ return []
654
+ if pattern[:1] != ".":
655
+ files = (f for f in files if f[0] != ".")
656
+ files = fnmatch.filter(files, pattern)
657
+ for f in files:
658
+ if exclude and f in exclude:
659
+ continue
660
+ self.add_file(f)
661
+ return files
662
+
663
+ def remove_pyc(self) -> None:
664
+ """Remove .pyc files on uninstall."""
665
+ if not self.component:
666
+ self.component = self.logical
667
+ add_data(
668
+ self.db,
669
+ "RemoveFile",
670
+ [(self.component + "c", self.component, "*.pyc", self.logical, 2)],
671
+ )
672
+
673
+
674
+ class Binary:
675
+ name: str
676
+
677
+ def __init__(self, fname: str) -> None:
678
+ self.name = fname
679
+
680
+ def __repr__(self) -> str:
681
+ return f'msilib.Binary(os.path.join(dirname,"{self.name}"))'
682
+
683
+
684
+ current_feature: Feature
685
+
686
+
687
+ class Feature:
688
+ id: str
689
+
690
+ def __init__(
691
+ self,
692
+ db: _Database,
693
+ id: str,
694
+ title: str,
695
+ desc: str,
696
+ display: int,
697
+ level: int = 1,
698
+ parent: Feature | None = None,
699
+ directory: str | None = None,
700
+ attributes: int = 0,
701
+ ) -> None:
702
+ self.id = id
703
+ pid = parent.id if parent else None
704
+ add_data(
705
+ db,
706
+ "Feature",
707
+ [(id, pid, title, desc, display, level, directory, attributes)],
708
+ )
709
+
710
+ def set_current(self) -> None:
711
+ global current_feature # noqa: PLW0603
712
+ current_feature = self
713
+
714
+
715
+ class Control:
716
+ dlg: Dialog
717
+ name: str
718
+
719
+ def __init__(self, dlg: Dialog, name: str) -> None:
720
+ self.dlg = dlg
721
+ self.name = name
722
+
723
+ def event(
724
+ self,
725
+ event: str,
726
+ argument: str,
727
+ condition: str = "1",
728
+ ordering: int | None = None,
729
+ ) -> None:
730
+ add_data(
731
+ self.dlg.db,
732
+ "ControlEvent",
733
+ [(self.dlg.name, self.name, event, argument, condition, ordering)],
734
+ )
735
+
736
+ def mapping(self, event: str, attribute: str) -> None:
737
+ add_data(
738
+ self.dlg.db,
739
+ "EventMapping",
740
+ [(self.dlg.name, self.name, event, attribute)],
741
+ )
742
+
743
+ def condition(self, action: str, condition: str) -> None:
744
+ add_data(
745
+ self.dlg.db,
746
+ "ControlCondition",
747
+ [(self.dlg.name, self.name, action, condition)],
748
+ )
749
+
750
+
751
+ class RadioButtonGroup(Control):
752
+ property: str
753
+ index: int
754
+
755
+ def __init__(self, dlg: Dialog, name: str, property: str) -> None:
756
+ self.dlg = dlg
757
+ self.name = name
758
+ self.property = property
759
+ self.index = 1
760
+
761
+ def add(
762
+ self,
763
+ name: str,
764
+ x: int,
765
+ y: int,
766
+ w: int,
767
+ h: int,
768
+ text: str,
769
+ value: str | None = None,
770
+ ) -> None:
771
+ if value is None:
772
+ value = name
773
+ add_data(
774
+ self.dlg.db,
775
+ "RadioButton",
776
+ [(self.property, self.index, value, x, y, w, h, text, None)],
777
+ )
778
+ self.index += 1
779
+
780
+
781
+ class Dialog:
782
+ db: _Database
783
+ name: str
784
+ x: int
785
+ y: int
786
+ w: int
787
+ h: int
788
+
789
+ def __init__(
790
+ self,
791
+ db: _Database,
792
+ name: str,
793
+ x: int,
794
+ y: int,
795
+ w: int,
796
+ h: int,
797
+ attr: int,
798
+ title: str,
799
+ first: str,
800
+ default: str | None,
801
+ cancel: str | None,
802
+ ) -> None:
803
+ self.db = db
804
+ self.name = name
805
+ self.x, self.y, self.w, self.h = x, y, w, h
806
+ add_data(
807
+ db,
808
+ "Dialog",
809
+ [(name, x, y, w, h, attr, title, first, default, cancel)],
810
+ )
811
+
812
+ def control(
813
+ self,
814
+ name: str,
815
+ type: str,
816
+ x: int,
817
+ y: int,
818
+ w: int,
819
+ h: int,
820
+ attr: int,
821
+ prop: str | None,
822
+ text: str | None,
823
+ next: str | None,
824
+ help: str | None,
825
+ ) -> Control:
826
+ add_data(
827
+ self.db,
828
+ "Control",
829
+ [
830
+ (
831
+ self.name,
832
+ name,
833
+ type,
834
+ x,
835
+ y,
836
+ w,
837
+ h,
838
+ attr,
839
+ prop,
840
+ text,
841
+ next,
842
+ help,
843
+ )
844
+ ],
845
+ )
846
+ return Control(self, name)
847
+
848
+ def text(
849
+ self,
850
+ name: str,
851
+ x: int,
852
+ y: int,
853
+ w: int,
854
+ h: int,
855
+ attr: int,
856
+ text: str | None,
857
+ ) -> Control:
858
+ return self.control(
859
+ name, "Text", x, y, w, h, attr, None, text, None, None
860
+ )
861
+
862
+ def bitmap(
863
+ self, name: str, x: int, y: int, w: int, h: int, text: str | None
864
+ ) -> Control:
865
+ return self.control(
866
+ name, "Bitmap", x, y, w, h, 1, None, text, None, None
867
+ )
868
+
869
+ def line(self, name: str, x: int, y: int, w: int, h: int) -> Control:
870
+ return self.control(
871
+ name, "Line", x, y, w, h, 1, None, None, None, None
872
+ )
873
+
874
+ def pushbutton(
875
+ self,
876
+ name: str,
877
+ x: int,
878
+ y: int,
879
+ w: int,
880
+ h: int,
881
+ attr: int,
882
+ text: str | None,
883
+ next: str | None,
884
+ ) -> Control:
885
+ return self.control(
886
+ name, "PushButton", x, y, w, h, attr, None, text, next, None
887
+ )
888
+
889
+ def radiogroup(
890
+ self,
891
+ name: str,
892
+ x: int,
893
+ y: int,
894
+ w: int,
895
+ h: int,
896
+ attr: int,
897
+ prop: str,
898
+ text: str | None,
899
+ next: str | None,
900
+ ) -> Control:
901
+ add_data(
902
+ self.db,
903
+ "Control",
904
+ [
905
+ (
906
+ self.name,
907
+ name,
908
+ "RadioButtonGroup",
909
+ x,
910
+ y,
911
+ w,
912
+ h,
913
+ attr,
914
+ prop,
915
+ text,
916
+ next,
917
+ None,
918
+ )
919
+ ],
920
+ )
921
+ return RadioButtonGroup(self, name, prop)
922
+
923
+ def checkbox(
924
+ self,
925
+ name: str,
926
+ x: int,
927
+ y: int,
928
+ w: int,
929
+ h: int,
930
+ attr: int,
931
+ prop: str | None,
932
+ text: str | None,
933
+ next: str | None,
934
+ ) -> Control:
935
+ return self.control(
936
+ name, "CheckBox", x, y, w, h, attr, prop, text, next, None
937
+ )