pyOpenVBA 5.2.2__tar.gz → 5.2.3__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.2 → pyopenvba-5.2.3}/PKG-INFO +1 -1
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/power_query.md +18 -6
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/pyproject.toml +1 -1
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/__init__.py +1 -1
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_sheets.py +60 -6
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/.gitignore +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/LICENSE.md +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/README.md +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/architecture.md +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/ms-ovba-implementation-guide_v2.md +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/research/access_complex/README.md +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/research/access_designs/README.md +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/research/access_macros/README.md +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/research/access_write/README.md +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/research/pcode/README.md +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/roadmap.md +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/__main__.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_deflate.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_host.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_oforms_pages.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_oforms_records.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_ppt_container.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/__init__.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/form.blob +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/form.propdata +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/report.blob +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/report.propdata +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/__init__.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_alloc.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_btree.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_collation.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_compact.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_complex.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_datapage.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_ddl.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_designs.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_facade.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_format.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_index.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_lval.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_macros.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_pages.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_props.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_queries.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_rows.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_schema.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_sql.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_storage.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_tdef.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_validate.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_vba.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/database.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access_read.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/cfb.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/excel.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/exceptions.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/forms.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerpoint.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/__init__.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_binary.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_files.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_mashup.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_metadata.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_opc.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_package.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_refresh.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_section.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/workbook.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/vba.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/vba_pcode.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/word.py +0 -0
- {pyopenvba-5.2.2 → pyopenvba-5.2.3}/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.3
|
|
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
|
|
@@ -221,12 +221,24 @@ wrong: **a connections part holding no connections is one Excel refuses**,
|
|
|
221
221
|
so removing the last connection removes the part, its content type and
|
|
222
222
|
its relationship as well.
|
|
223
223
|
|
|
224
|
-
The hidden name has
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
224
|
+
The hidden name has three rules of its own, and each was learned the
|
|
225
|
+
hard way:
|
|
226
|
+
|
|
227
|
+
* **`localSheetId` is the zero-based position of the sheet the name
|
|
228
|
+
belongs to** among the workbook's sheets, not a constant, and it has to
|
|
229
|
+
agree with the sheet the reference names. Written as 0 it was right
|
|
230
|
+
only while the table landed on the first sheet; anywhere else Excel
|
|
231
|
+
refuses to open the workbook, which a one-sheet file cannot show.
|
|
232
|
+
* **A sheet name inside a reference is quoted unless it is a plain
|
|
233
|
+
identifier**, and an apostrophe within it is doubled: `It's` is spelled
|
|
234
|
+
`'It''s'`. A single apostrophe closes the quoting early and the
|
|
235
|
+
reference names something else. A name opening with a digit is quoted
|
|
236
|
+
too, since unquoted it reads as part of a cell reference.
|
|
237
|
+
* **A sheet name is stored XML-escaped**, so `A & B` sits in the file as
|
|
238
|
+
`A & B`. Reading the stored form as if it were the name meant that
|
|
239
|
+
asking for the sheet by the name it actually has found nothing.
|
|
240
|
+
Attribute values are decoded on the way in and escaped again on the way
|
|
241
|
+
out.
|
|
230
242
|
|
|
231
243
|
### Refresh control
|
|
232
244
|
|
|
@@ -84,6 +84,28 @@ def _escape(value: str) -> str:
|
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
_ENTITIES = {"amp": "&", "lt": "<", "gt": ">", "quot": '"', "apos": "'"}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _unescape(value: str) -> str:
|
|
91
|
+
"""The characters an XML attribute's stored text stands for.
|
|
92
|
+
|
|
93
|
+
A sheet named ``A & B`` is stored as ``A & B``, and reading the
|
|
94
|
+
stored form as if it were the name meant that asking for the sheet by
|
|
95
|
+
the name it actually has found nothing.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def replace(match: re.Match[str]) -> str:
|
|
99
|
+
body = match.group(1)
|
|
100
|
+
if body[:2] in {"#x", "#X"}:
|
|
101
|
+
return chr(int(body[2:], 16))
|
|
102
|
+
if body.startswith("#"):
|
|
103
|
+
return chr(int(body[1:]))
|
|
104
|
+
return _ENTITIES.get(body, match.group(0))
|
|
105
|
+
|
|
106
|
+
return re.sub(r"&(#[0-9]+|#[xX][0-9a-fA-F]+|[A-Za-z]+);", replace, value)
|
|
107
|
+
|
|
108
|
+
|
|
87
109
|
def _next_relationship(rels: str) -> str:
|
|
88
110
|
used = {int(number) for number in re.findall(r'Id="rId(\d+)"', rels)}
|
|
89
111
|
index = 1
|
|
@@ -109,8 +131,15 @@ def _attributes(element: str) -> dict[str, str]:
|
|
|
109
131
|
a relationship with ``Id``, openpyxl closes with it. Matching them in
|
|
110
132
|
a fixed order silently found nothing in the second case, which left a
|
|
111
133
|
sheet looking as though it had no part behind it.
|
|
134
|
+
|
|
135
|
+
Values come back as the characters they stand for, not as stored, so
|
|
136
|
+
a caller comparing one against a name a user typed compares like with
|
|
137
|
+
like.
|
|
112
138
|
"""
|
|
113
|
-
return
|
|
139
|
+
return {
|
|
140
|
+
key: _unescape(value)
|
|
141
|
+
for key, value in re.findall(r'([\w.:-]+)\s*=\s*"([^"]*)"', element)
|
|
142
|
+
}
|
|
114
143
|
|
|
115
144
|
|
|
116
145
|
def _relationship_targets(rels: str) -> dict[str, str]:
|
|
@@ -441,16 +470,42 @@ def _write_header(sheet: str, cells: str, start: CellRef, width: int) -> str:
|
|
|
441
470
|
|
|
442
471
|
|
|
443
472
|
def _widen_dimension(sheet: str, reference: str) -> str:
|
|
444
|
-
|
|
473
|
+
"""Grow the sheet's declared extent to cover the range just written.
|
|
474
|
+
|
|
475
|
+
The element is matched by name rather than by its exact spelling: it
|
|
476
|
+
may carry other attributes, and a writer is free to put a space
|
|
477
|
+
before the closing slash, which openpyxl does. Insisting on Excel's
|
|
478
|
+
spelling left the extent saying the sheet ended where it did before
|
|
479
|
+
the table was written.
|
|
480
|
+
"""
|
|
481
|
+
match = re.search(r"<dimension\b[^>]*/>", sheet)
|
|
445
482
|
if match is None:
|
|
446
483
|
return sheet
|
|
484
|
+
current = _attributes(match.group(0)).get("ref")
|
|
485
|
+
if current is None:
|
|
486
|
+
return sheet
|
|
447
487
|
wanted = [CellRef.parse(part) for part in reference.split(":")]
|
|
448
|
-
have = [CellRef.parse(part) for part in
|
|
488
|
+
have = [CellRef.parse(part) for part in current.split(":")]
|
|
449
489
|
if len(have) == 1:
|
|
450
490
|
have = [have[0], have[0]]
|
|
451
491
|
first = CellRef(min(have[0].column, wanted[0].column), min(have[0].row, wanted[0].row))
|
|
452
492
|
last = CellRef(max(have[1].column, wanted[1].column), max(have[1].row, wanted[1].row))
|
|
453
|
-
|
|
493
|
+
widened = match.group(0).replace(f'ref="{current}"', f'ref="{first}:{last}"', 1)
|
|
494
|
+
return sheet.replace(match.group(0), widened, 1)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def quote_sheet(name: str) -> str:
|
|
498
|
+
"""A sheet's name as a formula reference spells it.
|
|
499
|
+
|
|
500
|
+
Anything but a plain identifier is wrapped in apostrophes, and an
|
|
501
|
+
apostrophe inside the name is doubled, because a single one would
|
|
502
|
+
close the quoting early and leave a reference to something else. A
|
|
503
|
+
name opening with a digit is quoted too: unquoted it reads as part of
|
|
504
|
+
a cell reference.
|
|
505
|
+
"""
|
|
506
|
+
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
|
|
507
|
+
return name
|
|
508
|
+
return "'" + name.replace("'", "''") + "'"
|
|
454
509
|
|
|
455
510
|
|
|
456
511
|
def _add_defined_name(package: OpcFile, sheet_name: str, number: int, reference: str) -> None:
|
|
@@ -467,10 +522,9 @@ def _add_defined_name(package: OpcFile, sheet_name: str, number: int, reference:
|
|
|
467
522
|
first, last = reference.split(":")
|
|
468
523
|
absolute = f"${column_letter(CellRef.parse(first).column)}${CellRef.parse(first).row}"
|
|
469
524
|
absolute += f":${column_letter(CellRef.parse(last).column)}${CellRef.parse(last).row}"
|
|
470
|
-
quoted = f"'{sheet_name}'" if re.search(r"[^A-Za-z0-9_]", sheet_name) else sheet_name
|
|
471
525
|
defined = (
|
|
472
526
|
f'<definedName name="ExternalData_{number}" localSheetId="{local}" hidden="1">'
|
|
473
|
-
f"{
|
|
527
|
+
f"{_escape(quote_sheet(sheet_name))}!{absolute}</definedName>"
|
|
474
528
|
)
|
|
475
529
|
empty = re.search(r"<definedNames\s*/>", raw)
|
|
476
530
|
if "<definedNames>" in raw:
|
|
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.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_database.accdb
RENAMED
|
File without changes
|
{pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb
RENAMED
|
File without changes
|
{pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_document.docm
RENAMED
|
File without changes
|
{pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam
RENAMED
|
File without changes
|
{pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm
RENAMED
|
File without changes
|
{pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb
RENAMED
|
File without changes
|
{pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm
RENAMED
|
File without changes
|
{pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx
RENAMED
|
File without changes
|
{pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb
RENAMED
|
File without changes
|
{pyopenvba-5.2.2 → pyopenvba-5.2.3}/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
|
|
File without changes
|