pyOpenVBA 5.2.3__tar.gz → 5.2.4__tar.gz
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.
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/PKG-INFO +1 -1
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/pyproject.toml +1 -1
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/__init__.py +1 -1
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_designs.py +197 -33
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/database.py +38 -22
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/.gitignore +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/LICENSE.md +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/README.md +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/docs/architecture.md +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/docs/ms-ovba-implementation-guide_v2.md +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/docs/power_query.md +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/docs/research/access_complex/README.md +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/docs/research/access_designs/README.md +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/docs/research/access_macros/README.md +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/docs/research/access_write/README.md +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/docs/research/pcode/README.md +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/docs/roadmap.md +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/__main__.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_deflate.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_host.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_oforms_pages.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_oforms_records.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_ppt_container.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/__init__.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/designs/form.blob +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/designs/form.propdata +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/designs/report.blob +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/designs/report.propdata +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/__init__.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_alloc.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_btree.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_collation.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_compact.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_complex.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_datapage.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_ddl.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_facade.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_format.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_index.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_lval.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_macros.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_pages.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_props.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_queries.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_rows.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_schema.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_sql.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_storage.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_tdef.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_validate.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access/_vba.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/access_read.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/cfb.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/excel.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/exceptions.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/forms.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerpoint.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerquery/__init__.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerquery/_binary.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerquery/_files.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerquery/_mashup.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerquery/_metadata.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerquery/_opc.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerquery/_package.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerquery/_refresh.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerquery/_section.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerquery/_sheets.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/powerquery/workbook.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/vba.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/vba_pcode.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/word.py +0 -0
- {pyopenvba-5.2.3 → pyopenvba-5.2.4}/tests/fuzz_corpus/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyOpenVBA
|
|
3
|
-
Version: 5.2.
|
|
3
|
+
Version: 5.2.4
|
|
4
4
|
Summary: Read and write VBA macros and Excel Power Query in pure Python, no dependencies.
|
|
5
5
|
Project-URL: Homepage, https://github.com/WilliamSmithEdward/pyOpenVBA
|
|
6
6
|
Project-URL: Repository, https://github.com/WilliamSmithEdward/pyOpenVBA
|
|
@@ -31,8 +31,10 @@ answer it: a record's `code` is the property and a handful are named in
|
|
|
31
31
|
|
|
32
32
|
from __future__ import annotations
|
|
33
33
|
|
|
34
|
+
import re
|
|
34
35
|
import struct
|
|
35
|
-
|
|
36
|
+
import unicodedata
|
|
37
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
36
38
|
from dataclasses import dataclass, field
|
|
37
39
|
from pathlib import Path
|
|
38
40
|
from typing import NamedTuple
|
|
@@ -2645,7 +2647,9 @@ def remove_control(blob: bytes, name: str) -> bytes:
|
|
|
2645
2647
|
#: as the members VBA sees on the form's class: ``Me.Qty`` compiles, and
|
|
2646
2648
|
#: ``Qty_Click`` binds, only for a name listed here. After the magic come
|
|
2647
2649
|
#: a kind word, -1, the entry count and the design's CLSID; each entry is
|
|
2648
|
-
#: then ``<u32 type id><u32 ordinal><
|
|
2650
|
+
#: then ``<u32 type id><u32 ordinal><identifier>00<the design's name>00``,
|
|
2651
|
+
#: with the design's name left empty where the two are the same. An
|
|
2652
|
+
#: ActiveX control's 36-byte tail follows both.
|
|
2649
2653
|
#:
|
|
2650
2654
|
#: Access keeps the stream rather than rebuilding it (measured by editing
|
|
2651
2655
|
#: forms in Access and reading the stream back): a new member is appended
|
|
@@ -2752,15 +2756,66 @@ ACTIVEX_CONTROL = "CustomControl"
|
|
|
2752
2756
|
ACTIVEX_CODE = next(code for code, name in CONTROL_TYPES.items() if name == ACTIVEX_CONTROL)
|
|
2753
2757
|
TYPE_INFO_REPORT_IDS[ACTIVEX_CONTROL] = TYPE_INFO_IDS[ACTIVEX_CONTROL]
|
|
2754
2758
|
TYPE_INFO_ACTIVEX_TAIL = bytes(36)
|
|
2759
|
+
#: The page a TypeInfo stream is read in when the project's own does not
|
|
2760
|
+
#: fit its bytes. Western Access writes this one, and every stream
|
|
2761
|
+
#: measured here is in it.
|
|
2762
|
+
_FALLBACK_TYPE_INFO_ENCODING = "cp1252"
|
|
2763
|
+
|
|
2764
|
+
|
|
2765
|
+
def vba_identifier(name: str) -> str:
|
|
2766
|
+
"""The name VBA compiles against for a design object called `name`.
|
|
2767
|
+
|
|
2768
|
+
A control is rarely named as VBA would name it: the form wizard names
|
|
2769
|
+
one after its field, so `Order Date` is ordinary and `Me.Order_Date`
|
|
2770
|
+
is how code reaches it. Every ASCII character that is not a letter,
|
|
2771
|
+
a digit or an underscore becomes an underscore, one for one, and a
|
|
2772
|
+
result opening with a digit or an underscore takes `Ctl` in front. A
|
|
2773
|
+
character outside ASCII is kept. Measured over every pair in the
|
|
2774
|
+
fixture's streams: `Tax (VAT)` is `Tax__VAT_`, `2ndBox` is
|
|
2775
|
+
`Ctl2ndBox`, `_under` is `Ctl_under`.
|
|
2776
|
+
"""
|
|
2777
|
+
converted = re.sub(r"[^A-Za-z0-9_-]", "_", name)
|
|
2778
|
+
return "Ctl" + converted if re.match(r"[0-9_]", converted) else converted
|
|
2779
|
+
|
|
2780
|
+
|
|
2781
|
+
def code_page_holds(text: str, encoding: str) -> bool:
|
|
2782
|
+
"""Whether `encoding` holds every character of `text` exactly.
|
|
2783
|
+
|
|
2784
|
+
A name with a `?` in it names something else, so no best fit counts.
|
|
2785
|
+
That is how Access decides it: on a cp1252 machine a control named
|
|
2786
|
+
with an A-macron, a fullwidth A or a Greek omega is left out of its
|
|
2787
|
+
form's member list rather than listed as `A`, `A` or `O`.
|
|
2788
|
+
"""
|
|
2789
|
+
try:
|
|
2790
|
+
round_tripped = text.encode(encoding).decode(encoding)
|
|
2791
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
2792
|
+
return False
|
|
2793
|
+
# cp1258 stores a toned vowel as a base byte and a combining mark.
|
|
2794
|
+
return unicodedata.normalize("NFC", round_tripped) == unicodedata.normalize("NFC", text)
|
|
2755
2795
|
|
|
2756
2796
|
|
|
2757
2797
|
class TypeInfoEntry(NamedTuple):
|
|
2758
|
-
"""One member a TypeInfo stream lists.
|
|
2798
|
+
"""One member a TypeInfo stream lists.
|
|
2799
|
+
|
|
2800
|
+
`name` is the name the design gives the object and the designer
|
|
2801
|
+
shows; `identifier` is the name VBA compiles against, which is `name`
|
|
2802
|
+
itself wherever that is already an identifier. `stored` is the two
|
|
2803
|
+
names exactly as the stream held them, NULs included, so an entry
|
|
2804
|
+
carried forward writes back the bytes it was read from rather than
|
|
2805
|
+
going through a code page twice. A copy under another name, from a
|
|
2806
|
+
rename or a repair, carries no stored bytes and is encoded afresh.
|
|
2807
|
+
"""
|
|
2759
2808
|
|
|
2760
2809
|
ident: int
|
|
2761
2810
|
ordinal: int
|
|
2762
2811
|
name: str
|
|
2763
2812
|
tail: bytes = b""
|
|
2813
|
+
identifier: str = ""
|
|
2814
|
+
stored: bytes = b""
|
|
2815
|
+
|
|
2816
|
+
def named(self, name: str) -> "TypeInfoEntry":
|
|
2817
|
+
"""The same member under a new name, encoded afresh."""
|
|
2818
|
+
return self._replace(name=name, identifier=vba_identifier(name), stored=b"")
|
|
2764
2819
|
|
|
2765
2820
|
|
|
2766
2821
|
class TypeInfoMember(NamedTuple):
|
|
@@ -2770,23 +2825,23 @@ class TypeInfoMember(NamedTuple):
|
|
|
2770
2825
|
ident: int
|
|
2771
2826
|
tail: bytes = b""
|
|
2772
2827
|
|
|
2828
|
+
@property
|
|
2829
|
+
def identifier(self) -> str:
|
|
2830
|
+
return vba_identifier(self.name)
|
|
2773
2831
|
|
|
2774
|
-
|
|
2832
|
+
|
|
2833
|
+
def type_info_entries(stream: bytes, encoding: str = "cp1252") -> tuple[TypeInfoEntry, ...]:
|
|
2775
2834
|
"""The members a TypeInfo stream lists, in the order it lists them.
|
|
2776
2835
|
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
than substituting it: a control named in Cyrillic got no entry here
|
|
2787
|
-
at all, while the design blob kept its name in UTF-16. The writer
|
|
2788
|
-
below substitutes instead, which differs only for names cp1252
|
|
2789
|
-
cannot express.
|
|
2836
|
+
An entry holds **two** names: the identifier, a NUL, the design's
|
|
2837
|
+
name, a NUL, with the design's name left empty where the two are the
|
|
2838
|
+
same. That is why a plain name reads as ``Plain 00 00``, and why
|
|
2839
|
+
reading to the first double NUL walked into the next entry's type id
|
|
2840
|
+
for any name that is not already an identifier (GitHub issue #22).
|
|
2841
|
+
An ActiveX control's 36-byte tail follows both names.
|
|
2842
|
+
|
|
2843
|
+
The names are bytes in a code page, and `encoding` is the page to
|
|
2844
|
+
read them in; `type_info_code_page` picks it.
|
|
2790
2845
|
"""
|
|
2791
2846
|
if stream[:4] != TYPE_INFO_MAGIC or len(stream) < TYPE_INFO_ENTRIES_AT:
|
|
2792
2847
|
raise AccessError("this is not a TypeInfo stream")
|
|
@@ -2797,19 +2852,61 @@ def type_info_entries(stream: bytes) -> tuple[TypeInfoEntry, ...]:
|
|
|
2797
2852
|
if at + 8 > len(stream):
|
|
2798
2853
|
raise AccessError("this TypeInfo stream ends inside an entry")
|
|
2799
2854
|
ident, ordinal = struct.unpack_from("<II", stream, at)
|
|
2800
|
-
|
|
2801
|
-
if
|
|
2855
|
+
first = stream.find(b"\x00", at + 8)
|
|
2856
|
+
second = stream.find(b"\x00", first + 1) if first >= 0 else -1
|
|
2857
|
+
if first < 0 or second < 0:
|
|
2802
2858
|
raise AccessError("this TypeInfo stream ends inside a name")
|
|
2803
|
-
|
|
2804
|
-
|
|
2859
|
+
identifier = stream[at + 8 : first].decode(encoding, errors="replace")
|
|
2860
|
+
shown = stream[first + 1 : second].decode(encoding, errors="replace")
|
|
2861
|
+
stored = stream[at + 8 : second + 1]
|
|
2862
|
+
at = second + 1
|
|
2805
2863
|
tail = b""
|
|
2806
2864
|
if ident & 0xFF == ACTIVEX_CODE:
|
|
2807
2865
|
tail = stream[at : at + len(TYPE_INFO_ACTIVEX_TAIL)]
|
|
2808
2866
|
at += len(tail)
|
|
2809
|
-
out.append(
|
|
2867
|
+
out.append(
|
|
2868
|
+
TypeInfoEntry(ident, ordinal, shown or identifier, tail, identifier, stored)
|
|
2869
|
+
)
|
|
2810
2870
|
return tuple(out)
|
|
2811
2871
|
|
|
2812
2872
|
|
|
2873
|
+
def type_info_code_page(
|
|
2874
|
+
stream: bytes, design_names: Iterable[str], project_encoding: str
|
|
2875
|
+
) -> str:
|
|
2876
|
+
"""The page a stream's names are in, given the names its design has.
|
|
2877
|
+
|
|
2878
|
+
The project's page is tried first, because PROJECTCODEPAGE is the
|
|
2879
|
+
page of the machine that last saved the project, and this stream is
|
|
2880
|
+
written in the page of the machine Access ran on. The stream's own
|
|
2881
|
+
bytes overrule it: only an entry carrying a byte at or above 0x80
|
|
2882
|
+
says anything, and the fallback wins only where it matches more of
|
|
2883
|
+
the design's names.
|
|
2884
|
+
|
|
2885
|
+
An earlier note here claimed the stream is fixed at cp1252 because
|
|
2886
|
+
patching PROJECTCODEPAGE to 1251 changed nothing. That experiment
|
|
2887
|
+
was void: VBA went on reading the project as cp1252 and Access wrote
|
|
2888
|
+
1252 back over the patch on its next save, so it never tested what it
|
|
2889
|
+
appeared to (GitHub issue #23).
|
|
2890
|
+
"""
|
|
2891
|
+
if project_encoding == _FALLBACK_TYPE_INFO_ENCODING:
|
|
2892
|
+
return project_encoding
|
|
2893
|
+
names = set(design_names)
|
|
2894
|
+
|
|
2895
|
+
def fits(encoding: str) -> int:
|
|
2896
|
+
try:
|
|
2897
|
+
entries = type_info_entries(stream, encoding)
|
|
2898
|
+
except AccessError:
|
|
2899
|
+
return -1
|
|
2900
|
+
return sum(
|
|
2901
|
+
1
|
|
2902
|
+
for entry in entries
|
|
2903
|
+
if any(byte >= 0x80 for byte in entry.stored) and entry.name in names
|
|
2904
|
+
)
|
|
2905
|
+
|
|
2906
|
+
fallback = _FALLBACK_TYPE_INFO_ENCODING
|
|
2907
|
+
return fallback if fits(fallback) > fits(project_encoding) else project_encoding
|
|
2908
|
+
|
|
2909
|
+
|
|
2813
2910
|
def _holders(objects: tuple[DesignObject, ...]) -> dict[int, int]:
|
|
2814
2911
|
"""What holds each control -- its section, or the control it sits
|
|
2815
2912
|
inside -- as indexes into `objects`."""
|
|
@@ -2871,41 +2968,100 @@ def type_info_members(kind: str, objects: tuple[DesignObject, ...]) -> tuple[Typ
|
|
|
2871
2968
|
return (*sections, *controls)
|
|
2872
2969
|
|
|
2873
2970
|
|
|
2971
|
+
def _carried(entry: TypeInfoEntry) -> TypeInfoEntry:
|
|
2972
|
+
"""An entry as it is carried forward, repaired if it has to be.
|
|
2973
|
+
|
|
2974
|
+
The identifier Access stored is kept as it is. One that is not an
|
|
2975
|
+
identifier at all is not Access's: this module once read an entry as
|
|
2976
|
+
a single name and rewrote a member called `Order Date` as exactly
|
|
2977
|
+
that, which `Me.` cannot reach. That one is rebuilt from the design's
|
|
2978
|
+
name, and loses its stored bytes so it is written afresh.
|
|
2979
|
+
"""
|
|
2980
|
+
if entry.identifier and vba_identifier(entry.identifier) == entry.identifier:
|
|
2981
|
+
return entry
|
|
2982
|
+
return entry._replace(identifier=vba_identifier(entry.name), stored=b"")
|
|
2983
|
+
|
|
2984
|
+
|
|
2985
|
+
def _refuse_shared_identifiers(entries: Sequence[TypeInfoEntry]) -> None:
|
|
2986
|
+
"""VBA knows a member by its identifier alone, so two sharing one are
|
|
2987
|
+
a single name to the code behind the design. Access refuses the
|
|
2988
|
+
second control name as already in use, and so does this."""
|
|
2989
|
+
seen: dict[str, str] = {}
|
|
2990
|
+
for entry in entries:
|
|
2991
|
+
key = entry.identifier.lower()
|
|
2992
|
+
other = seen.get(key)
|
|
2993
|
+
if other is not None:
|
|
2994
|
+
raise AccessError(
|
|
2995
|
+
f"{entry.name!r} and {other!r} would both be {entry.identifier!r} to VBA, "
|
|
2996
|
+
"which knows a member by that name alone; Access refuses the second name "
|
|
2997
|
+
"as already in use"
|
|
2998
|
+
)
|
|
2999
|
+
seen[key] = entry.name
|
|
3000
|
+
|
|
3001
|
+
|
|
2874
3002
|
def type_info(
|
|
2875
3003
|
kind: str,
|
|
2876
3004
|
objects: tuple[DesignObject, ...],
|
|
2877
3005
|
existing: bytes,
|
|
2878
3006
|
*,
|
|
2879
3007
|
renamed: Mapping[str, str] | None = None,
|
|
3008
|
+
encoding: str = "cp1252",
|
|
2880
3009
|
) -> bytes:
|
|
2881
3010
|
"""The TypeInfo stream after the design changed, carried forward the
|
|
2882
3011
|
way Access carries it: members the design no longer has drop out, the
|
|
2883
3012
|
ones `renamed` maps (old name to new) move to the end keeping their
|
|
2884
3013
|
ordinals, and new members are appended with the ordinals that follow
|
|
2885
|
-
the highest present.
|
|
2886
|
-
|
|
3014
|
+
the highest present.
|
|
3015
|
+
|
|
3016
|
+
`encoding` is the project's code page, tried first and overruled by
|
|
3017
|
+
the stream's own bytes. A member whose name that page cannot hold is
|
|
3018
|
+
left out, as Access leaves it out, and takes no ordinal at all.
|
|
3019
|
+
"""
|
|
2887
3020
|
members = type_info_members(kind, objects)
|
|
2888
3021
|
wanted = {member.name for member in members}
|
|
2889
3022
|
renames = dict(renamed or {})
|
|
2890
|
-
|
|
3023
|
+
page = type_info_code_page(existing, wanted | set(renames), encoding)
|
|
3024
|
+
entries = type_info_entries(existing, page)
|
|
3025
|
+
kept = [_carried(e) for e in entries if e.name in wanted and e.name not in renames]
|
|
2891
3026
|
moved = [
|
|
2892
|
-
e.
|
|
3027
|
+
e.named(renames[e.name])
|
|
2893
3028
|
for e in entries
|
|
2894
3029
|
if e.name in renames and renames[e.name] in wanted
|
|
2895
3030
|
]
|
|
3031
|
+
moved = [e for e in moved if code_page_holds(e.name, page)]
|
|
2896
3032
|
present = {e.name for e in (*kept, *moved)}
|
|
2897
3033
|
ordinal = max((e.ordinal for e in (*kept, *moved)), default=-1) + 1
|
|
2898
3034
|
added: list[TypeInfoEntry] = []
|
|
2899
3035
|
for member in members:
|
|
2900
|
-
if member.name not
|
|
2901
|
-
|
|
2902
|
-
|
|
3036
|
+
if member.name in present or not code_page_holds(member.name, page):
|
|
3037
|
+
continue
|
|
3038
|
+
added.append(
|
|
3039
|
+
TypeInfoEntry(
|
|
3040
|
+
member.ident, ordinal, member.name, member.tail, member.identifier
|
|
3041
|
+
)
|
|
3042
|
+
)
|
|
3043
|
+
ordinal += 1
|
|
3044
|
+
following = (*kept, *moved, *added)
|
|
3045
|
+
_refuse_shared_identifiers(following)
|
|
2903
3046
|
clsid = existing[TYPE_INFO_CLSID_AT : TYPE_INFO_CLSID_AT + GUID_LENGTH]
|
|
2904
|
-
return pack_type_info(kind, clsid,
|
|
3047
|
+
return pack_type_info(kind, clsid, following, encoding=page)
|
|
3048
|
+
|
|
2905
3049
|
|
|
3050
|
+
def pack_type_info(
|
|
3051
|
+
kind: str,
|
|
3052
|
+
clsid: bytes,
|
|
3053
|
+
entries: Sequence[TypeInfoEntry],
|
|
3054
|
+
*,
|
|
3055
|
+
encoding: str = "cp1252",
|
|
3056
|
+
) -> bytes:
|
|
3057
|
+
"""A TypeInfo stream listing `entries` in that order.
|
|
2906
3058
|
|
|
2907
|
-
|
|
2908
|
-
|
|
3059
|
+
An entry that came from a stream writes back the bytes it was read
|
|
3060
|
+
from: a page can spell one character more than one way, so decoding
|
|
3061
|
+
and encoding again is not always what was there. One built here is
|
|
3062
|
+
encoded as the identifier, a NUL, the design's name, a NUL, with the
|
|
3063
|
+
design's name left out where the two are the same.
|
|
3064
|
+
"""
|
|
2909
3065
|
if kind not in TYPE_INFO_KIND:
|
|
2910
3066
|
raise AccessError(f"kind must be 'form' or 'report', not {kind!r}")
|
|
2911
3067
|
if len(clsid) != GUID_LENGTH:
|
|
@@ -2915,6 +3071,14 @@ def pack_type_info(kind: str, clsid: bytes, entries: Sequence[TypeInfoEntry]) ->
|
|
|
2915
3071
|
out += clsid
|
|
2916
3072
|
for entry in entries:
|
|
2917
3073
|
out += struct.pack("<II", entry.ident, entry.ordinal)
|
|
2918
|
-
|
|
3074
|
+
if entry.stored:
|
|
3075
|
+
out += entry.stored
|
|
3076
|
+
else:
|
|
3077
|
+
identifier = entry.identifier or vba_identifier(entry.name)
|
|
3078
|
+
out += identifier.encode(encoding) + bytes(1)
|
|
3079
|
+
if entry.name != identifier:
|
|
3080
|
+
out += entry.name.encode(encoding)
|
|
3081
|
+
out += bytes(1)
|
|
3082
|
+
out += entry.tail
|
|
2919
3083
|
return bytes(out)
|
|
2920
3084
|
|
|
@@ -2826,24 +2826,35 @@ class AccessDatabase:
|
|
|
2826
2826
|
and str(r["Name"]) == folder
|
|
2827
2827
|
)
|
|
2828
2828
|
storage = self.table(STORAGE_TABLE)
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
):
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2829
|
+
# Everything is computed before anything is written. The member
|
|
2830
|
+
# list can refuse the change -- two names that are one identifier
|
|
2831
|
+
# to VBA -- and a blob already written beside the old member list
|
|
2832
|
+
# would leave the database holding a design its class does not
|
|
2833
|
+
# describe (GitHub issue #23).
|
|
2834
|
+
def stream_row(name: str) -> tuple[RowId, bytes] | None:
|
|
2835
|
+
for rid, row in storage.rows_with_ids():
|
|
2836
|
+
payload = row.get("Lv")
|
|
2837
|
+
if (
|
|
2838
|
+
_as_int(row["ParentId"]) == folder_id
|
|
2839
|
+
and str(row["Name"]) == name
|
|
2840
|
+
and isinstance(payload, bytes)
|
|
2841
|
+
):
|
|
2842
|
+
return rid, payload
|
|
2843
|
+
return None
|
|
2844
|
+
|
|
2845
|
+
blob_row = stream_row("Blob")
|
|
2846
|
+
if blob_row is None: # pragma: no cover - a design without its own blob
|
|
2839
2847
|
raise AccessError(f"the {kind} {design!r} has no design blob")
|
|
2848
|
+
blob_rid, blob = blob_row
|
|
2849
|
+
updated = change(blob)
|
|
2850
|
+
rewritten = AccessDesign(found.name, kind, parse_design(updated)[1])
|
|
2851
|
+
|
|
2840
2852
|
# The TypeInfo stream lists the design's sections and controls as
|
|
2841
2853
|
# the members of the form's class, and follows every change to them
|
|
2842
2854
|
# (measured on forms Access built: Me.<control> compiles and the
|
|
2843
2855
|
# control's events bind only for a name it lists). A change that
|
|
2844
2856
|
# keeps every object where it was and alters a name is a rename,
|
|
2845
2857
|
# which Access carries differently from a removal and an addition.
|
|
2846
|
-
rewritten = self._design(kind, design)
|
|
2847
2858
|
renamed: dict[str, str] = {}
|
|
2848
2859
|
if len(found.objects) == len(rewritten.objects) and all(
|
|
2849
2860
|
(old.marker, old.type) == (new.marker, new.type)
|
|
@@ -2854,17 +2865,22 @@ class AccessDatabase:
|
|
|
2854
2865
|
for old, new in zip(found.objects, rewritten.objects)
|
|
2855
2866
|
if old.name and new.name and old.name != new.name
|
|
2856
2867
|
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
+
members = stream_row("TypeInfo")
|
|
2869
|
+
fresh = (
|
|
2870
|
+
type_info(
|
|
2871
|
+
kind,
|
|
2872
|
+
rewritten.objects,
|
|
2873
|
+
members[1],
|
|
2874
|
+
renamed=renamed,
|
|
2875
|
+
encoding=encoding_of(self._vba_dir()[1]),
|
|
2876
|
+
)
|
|
2877
|
+
if members is not None
|
|
2878
|
+
else b""
|
|
2879
|
+
)
|
|
2880
|
+
|
|
2881
|
+
storage.update_row(blob_rid, {"Lv": updated})
|
|
2882
|
+
if members is not None and fresh != members[1]:
|
|
2883
|
+
storage.update_row(members[0], {"Lv": fresh})
|
|
2868
2884
|
return rewritten
|
|
2869
2885
|
|
|
2870
2886
|
def set_control_property(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_database.accdb
RENAMED
|
File without changes
|
{pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb
RENAMED
|
File without changes
|
{pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_document.docm
RENAMED
|
File without changes
|
{pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam
RENAMED
|
File without changes
|
{pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm
RENAMED
|
File without changes
|
{pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb
RENAMED
|
File without changes
|
{pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm
RENAMED
|
File without changes
|
{pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx
RENAMED
|
File without changes
|
{pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb
RENAMED
|
File without changes
|
{pyopenvba-5.2.3 → pyopenvba-5.2.4}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|