pyOpenVBA 5.2.0__tar.gz → 5.2.1__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.0 → pyopenvba-5.2.1}/PKG-INFO +3 -2
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/README.md +1 -1
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/docs/power_query.md +41 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/pyproject.toml +5 -1
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/__init__.py +1 -1
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerquery/_opc.py +25 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerquery/_sheets.py +75 -7
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerquery/workbook.py +15 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/.gitignore +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/LICENSE.md +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/docs/architecture.md +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/docs/ms-ovba-implementation-guide_v2.md +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/docs/research/access_complex/README.md +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/docs/research/access_designs/README.md +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/docs/research/access_macros/README.md +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/docs/research/access_write/README.md +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/docs/research/pcode/README.md +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/docs/roadmap.md +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/__main__.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_deflate.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_host.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_oforms_pages.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_oforms_records.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_ppt_container.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/__init__.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/designs/form.blob +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/designs/form.propdata +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/designs/report.blob +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/designs/report.propdata +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/__init__.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_alloc.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_btree.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_collation.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_compact.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_complex.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_datapage.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_ddl.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_designs.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_facade.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_format.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_index.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_lval.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_macros.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_pages.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_props.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_queries.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_rows.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_schema.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_sql.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_storage.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_tdef.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_validate.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/_vba.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access/database.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/access_read.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/cfb.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/excel.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/exceptions.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/forms.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerpoint.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerquery/__init__.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerquery/_binary.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerquery/_files.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerquery/_mashup.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerquery/_metadata.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerquery/_package.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerquery/_refresh.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/powerquery/_section.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/vba.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/vba_pcode.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/word.py +0 -0
- {pyopenvba-5.2.0 → pyopenvba-5.2.1}/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.1
|
|
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
|
|
@@ -29,6 +29,7 @@ Classifier: Typing :: Typed
|
|
|
29
29
|
Requires-Python: >=3.10
|
|
30
30
|
Provides-Extra: dev
|
|
31
31
|
Requires-Dist: build>=1.2; extra == 'dev'
|
|
32
|
+
Requires-Dist: openpyxl>=3.1; extra == 'dev'
|
|
32
33
|
Requires-Dist: pyright>=1.1.350; extra == 'dev'
|
|
33
34
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
34
35
|
Requires-Dist: twine>=5; extra == 'dev'
|
|
@@ -55,7 +56,7 @@ VBA, four hosts and one API:
|
|
|
55
56
|
* PowerPoint (`.pptm`, `.potm`, `.ppt`)
|
|
56
57
|
* Access (`.accdb`, `.mdb`)
|
|
57
58
|
|
|
58
|
-
Power Query, in any Excel package
|
|
59
|
+
Power Query, in any Excel package:
|
|
59
60
|
|
|
60
61
|
```python
|
|
61
62
|
from pyopenvba import PowerQueryWorkbook
|
|
@@ -19,7 +19,7 @@ VBA, four hosts and one API:
|
|
|
19
19
|
* PowerPoint (`.pptm`, `.potm`, `.ppt`)
|
|
20
20
|
* Access (`.accdb`, `.mdb`)
|
|
21
21
|
|
|
22
|
-
Power Query, in any Excel package
|
|
22
|
+
Power Query, in any Excel package:
|
|
23
23
|
|
|
24
24
|
```python
|
|
25
25
|
from pyopenvba import PowerQueryWorkbook
|
|
@@ -277,6 +277,47 @@ this file exists because nothing here is written on a guess.
|
|
|
277
277
|
|
|
278
278
|
---
|
|
279
279
|
|
|
280
|
+
## Workbooks another tool wrote
|
|
281
|
+
|
|
282
|
+
Excel is not the only writer of `.xlsx`, and the parts it produces are
|
|
283
|
+
one legal spelling among several. Three assumptions here came from
|
|
284
|
+
reading only Excel's output, and each one broke on openpyxl's:
|
|
285
|
+
|
|
286
|
+
* **Attribute order carries no meaning.** Excel opens a relationship with
|
|
287
|
+
`Id`, openpyxl closes with it. Matching in a fixed order found nothing,
|
|
288
|
+
so a sheet looked as though it had no part behind it and loading to it
|
|
289
|
+
failed outright.
|
|
290
|
+
* **An empty element may be written closed.** `<definedNames />` is the
|
|
291
|
+
same element as `<definedNames></definedNames>`. Appending a second
|
|
292
|
+
block beside it left two in the workbook, which Excel refuses.
|
|
293
|
+
* **A namespace prefix is declared where it is used.** Excel puts
|
|
294
|
+
`xmlns:r` on every worksheet; openpyxl puts it on a worksheet that
|
|
295
|
+
needs one, and a sheet with no table does not. Adding a `tablePart`
|
|
296
|
+
that used the prefix made the part not well formed.
|
|
297
|
+
|
|
298
|
+
Loading a query onto a sheet of a workbook openpyxl wrote works, and
|
|
299
|
+
Excel opens and refreshes the result.
|
|
300
|
+
|
|
301
|
+
**The other direction does not, and cannot be fixed here.** openpyxl
|
|
302
|
+
rebuilds the package from the parts it models and drops the rest, custom
|
|
303
|
+
XML included, so saving a workbook through it removes the Power Query
|
|
304
|
+
package and the queries with it. Nothing signals this: the file opens and
|
|
305
|
+
simply has no queries. Put pyOpenVBA last in the pipeline, or carry the
|
|
306
|
+
queries across with `pull_queries()` and `push_queries()`.
|
|
307
|
+
|
|
308
|
+
## `[trash]` parts
|
|
309
|
+
|
|
310
|
+
Excel's own file recovery leaves the parts it threw out under
|
|
311
|
+
`[trash]/NNNN.dat`, beside the real ones. An OPC part name cannot open a
|
|
312
|
+
segment with a bracket, and Excel holds itself to that when reading: the
|
|
313
|
+
same workbook opens before such an entry is added and fails to open at
|
|
314
|
+
all after, measured both ways.
|
|
315
|
+
|
|
316
|
+
The container preserves every entry as it arrived, which would hand back
|
|
317
|
+
a file that stays broken, so `save()` drops these and warns. Nothing in
|
|
318
|
+
the document depends on them: they carry no content type and no
|
|
319
|
+
relationship points at them.
|
|
320
|
+
|
|
280
321
|
## What Excel refuses
|
|
281
322
|
|
|
282
323
|
* A query name containing a dot. `Queries.Add` rejects it with
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyOpenVBA"
|
|
7
|
-
version = "5.2.
|
|
7
|
+
version = "5.2.1"
|
|
8
8
|
description = "Read and write VBA macros and Excel Power Query in pure Python, no dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -61,6 +61,10 @@ dev = [
|
|
|
61
61
|
"pyright>=1.1.350",
|
|
62
62
|
"build>=1.2",
|
|
63
63
|
"twine>=5",
|
|
64
|
+
# Test-time only, and the runtime keeps its promise of no dependencies.
|
|
65
|
+
# openpyxl writes the same parts a different legal way, which is what
|
|
66
|
+
# the interop tests are for.
|
|
67
|
+
"openpyxl>=3.1",
|
|
64
68
|
]
|
|
65
69
|
|
|
66
70
|
[project.urls]
|
|
@@ -29,6 +29,9 @@ _STORED = 0
|
|
|
29
29
|
_FLAGS = 0x0006
|
|
30
30
|
_MADE_BY = 45
|
|
31
31
|
_NEEDED = 20
|
|
32
|
+
#: Where Excel's file recovery parks the parts it threw out. Not a legal
|
|
33
|
+
#: OPC part name, and Excel will not open a package holding one.
|
|
34
|
+
_RESERVED = "[trash]/"
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
@dataclass
|
|
@@ -174,6 +177,28 @@ class OpcFile:
|
|
|
174
177
|
self.entries = kept
|
|
175
178
|
self.source = None
|
|
176
179
|
|
|
180
|
+
def drop_reserved(self) -> list[str]:
|
|
181
|
+
"""Take out entries whose names are not part names, and say which.
|
|
182
|
+
|
|
183
|
+
Excel's own file recovery leaves `[trash]/NNNN.dat` beside the
|
|
184
|
+
real parts. An OPC part name is built of segments that cannot
|
|
185
|
+
open with a bracket, and Excel holds itself to that when reading:
|
|
186
|
+
a workbook that opens cleanly stops opening at all once such an
|
|
187
|
+
entry is added, which is measured both ways in
|
|
188
|
+
`tests/test_powerquery_opc.py`.
|
|
189
|
+
|
|
190
|
+
Nothing in the document depends on them. They carry no content
|
|
191
|
+
type and no relationship points at them, so a package is repaired
|
|
192
|
+
by dropping them rather than damaged.
|
|
193
|
+
"""
|
|
194
|
+
doomed = [entry.name for entry in self.entries if entry.name.startswith(_RESERVED)]
|
|
195
|
+
if doomed:
|
|
196
|
+
self.entries = [
|
|
197
|
+
entry for entry in self.entries if not entry.name.startswith(_RESERVED)
|
|
198
|
+
]
|
|
199
|
+
self.source = None
|
|
200
|
+
return doomed
|
|
201
|
+
|
|
177
202
|
def serialize(self) -> bytes:
|
|
178
203
|
if self.source is not None:
|
|
179
204
|
return self.source
|
|
@@ -102,6 +102,38 @@ def _relationships(package: OpcFile, part: str) -> str:
|
|
|
102
102
|
)
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def _attributes(element: str) -> dict[str, str]:
|
|
106
|
+
"""The attributes of one element, in whatever order they were written.
|
|
107
|
+
|
|
108
|
+
XML gives attribute order no meaning, and writers differ: Excel opens
|
|
109
|
+
a relationship with ``Id``, openpyxl closes with it. Matching them in
|
|
110
|
+
a fixed order silently found nothing in the second case, which left a
|
|
111
|
+
sheet looking as though it had no part behind it.
|
|
112
|
+
"""
|
|
113
|
+
return dict(re.findall(r'([\w.:-]+)\s*=\s*"([^"]*)"', element))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _relationship_targets(rels: str) -> dict[str, str]:
|
|
117
|
+
"""``{relationship id: target}`` for one ``.rels`` part."""
|
|
118
|
+
out: dict[str, str] = {}
|
|
119
|
+
for element in re.findall(r"<Relationship\b[^>]*>", rels):
|
|
120
|
+
attributes = _attributes(element)
|
|
121
|
+
identifier, target = attributes.get("Id"), attributes.get("Target")
|
|
122
|
+
if identifier and target is not None:
|
|
123
|
+
out[identifier] = target
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _relationship_for(rels: str, target: str) -> str | None:
|
|
128
|
+
"""The id pointing at ``target``, comparing the part it names rather
|
|
129
|
+
than the spelling: a target may be relative or absolute."""
|
|
130
|
+
wanted = target.lstrip("/").removeprefix("../")
|
|
131
|
+
for identifier, found in _relationship_targets(rels).items():
|
|
132
|
+
if found.lstrip("/").removeprefix("../") == wanted:
|
|
133
|
+
return identifier
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
|
|
105
137
|
def add_relationship(package: OpcFile, part: str, kind: str, target: str) -> str:
|
|
106
138
|
raw = _relationships(package, part)
|
|
107
139
|
identifier = _next_relationship(raw)
|
|
@@ -142,8 +174,17 @@ def sheet_part(package: OpcFile, sheet: str | int) -> tuple[str, str]:
|
|
|
142
174
|
name of the sheet."""
|
|
143
175
|
workbook = package.read(_WORKBOOK).decode("utf-8")
|
|
144
176
|
rels = package.read(_WORKBOOK_RELS).decode("utf-8")
|
|
145
|
-
targets =
|
|
146
|
-
sheets
|
|
177
|
+
targets = _relationship_targets(rels)
|
|
178
|
+
sheets: list[tuple[str, str]] = []
|
|
179
|
+
for element in re.findall(r"<sheet\b[^>]*>", workbook):
|
|
180
|
+
attributes = _attributes(element)
|
|
181
|
+
# The relationship attribute carries a namespace prefix, and which
|
|
182
|
+
# prefix is the writer's to choose.
|
|
183
|
+
identifier = next(
|
|
184
|
+
(value for key, value in attributes.items() if key.endswith(":id")), None
|
|
185
|
+
)
|
|
186
|
+
if "name" in attributes and identifier:
|
|
187
|
+
sheets.append((attributes["name"], identifier))
|
|
147
188
|
if not sheets:
|
|
148
189
|
raise PowerQueryError("this workbook lists no worksheets")
|
|
149
190
|
if isinstance(sheet, int):
|
|
@@ -306,6 +347,7 @@ def _add_table_to_sheet(
|
|
|
306
347
|
for index, column in enumerate(columns)
|
|
307
348
|
)
|
|
308
349
|
raw = _write_header(raw, header, start, len(columns))
|
|
350
|
+
raw = _declare_relationship_namespace(raw)
|
|
309
351
|
if "<tableParts" in raw:
|
|
310
352
|
raw = re.sub(
|
|
311
353
|
r'<tableParts count="(\d+)">',
|
|
@@ -321,6 +363,28 @@ def _add_table_to_sheet(
|
|
|
321
363
|
package.write(part, raw.encode("utf-8"))
|
|
322
364
|
|
|
323
365
|
|
|
366
|
+
#: The namespace a `<tablePart r:id=...>` reference lives in.
|
|
367
|
+
_RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _declare_relationship_namespace(sheet: str) -> str:
|
|
371
|
+
"""Make sure the worksheet element declares the `r:` prefix.
|
|
372
|
+
|
|
373
|
+
Excel always writes it on the root element, so the reference added
|
|
374
|
+
below resolved. openpyxl writes the prefix only where it uses one,
|
|
375
|
+
and a worksheet with no table has no use for it, which left the
|
|
376
|
+
`r:id` added here pointing at a prefix nothing declared. That is not
|
|
377
|
+
well-formed XML, and Excel would not open the workbook at all.
|
|
378
|
+
"""
|
|
379
|
+
opening = re.search(r"<worksheet\b[^>]*>", sheet)
|
|
380
|
+
if opening is None or 'xmlns:r=' in opening.group(0):
|
|
381
|
+
return sheet
|
|
382
|
+
fixed = opening.group(0).replace(
|
|
383
|
+
"<worksheet", f'<worksheet xmlns:r="{_RELATIONSHIPS_NS}"', 1
|
|
384
|
+
)
|
|
385
|
+
return sheet.replace(opening.group(0), fixed, 1)
|
|
386
|
+
|
|
387
|
+
|
|
324
388
|
def _write_header(sheet: str, cells: str, start: CellRef, width: int) -> str:
|
|
325
389
|
"""Put the header cells on the sheet.
|
|
326
390
|
|
|
@@ -377,8 +441,14 @@ def _add_defined_name(package: OpcFile, sheet_name: str, number: int, reference:
|
|
|
377
441
|
f'<definedName name="ExternalData_{number}" localSheetId="0" hidden="1">'
|
|
378
442
|
f"{quoted}!{absolute}</definedName>"
|
|
379
443
|
)
|
|
444
|
+
empty = re.search(r"<definedNames\s*/>", raw)
|
|
380
445
|
if "<definedNames>" in raw:
|
|
381
446
|
raw = raw.replace("<definedNames>", "<definedNames>" + defined)
|
|
447
|
+
elif empty is not None:
|
|
448
|
+
# An empty element written closed, which openpyxl does and Excel
|
|
449
|
+
# does not. Appending a second block beside it puts two in the
|
|
450
|
+
# workbook, and Excel refuses that outright.
|
|
451
|
+
raw = raw.replace(empty.group(0), f"<definedNames>{defined}</definedNames>", 1)
|
|
382
452
|
elif "<calcPr" in raw:
|
|
383
453
|
raw = raw.replace("<calcPr", f"<definedNames>{defined}</definedNames><calcPr", 1)
|
|
384
454
|
else:
|
|
@@ -407,16 +477,14 @@ def unload_from_sheet(package: OpcFile, query: str) -> bool:
|
|
|
407
477
|
if not package.has(rels_part):
|
|
408
478
|
continue
|
|
409
479
|
rels = package.read(rels_part).decode("utf-8")
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
)
|
|
413
|
-
if found is None:
|
|
480
|
+
identifier = _relationship_for(rels, f"../tables/table{number}.xml")
|
|
481
|
+
if identifier is None:
|
|
414
482
|
continue
|
|
415
483
|
drop_relationship(package, rels_part, f"../tables/table{number}.xml")
|
|
416
484
|
if "<Relationship " not in package.read(rels_part).decode("utf-8"):
|
|
417
485
|
package.remove(rels_part)
|
|
418
486
|
sheet = package.read(part).decode("utf-8")
|
|
419
|
-
sheet = re.sub(rf'<tablePart
|
|
487
|
+
sheet = re.sub(rf'<tablePart\b[^>]*:id="{re.escape(identifier)}"[^>]*/>', "", sheet)
|
|
420
488
|
sheet = re.sub(
|
|
421
489
|
r'<tableParts count="(\d+)">',
|
|
422
490
|
lambda match: f'<tableParts count="{max(int(match.group(1)) - 1, 0)}">',
|
|
@@ -25,6 +25,7 @@ from __future__ import annotations
|
|
|
25
25
|
import base64
|
|
26
26
|
import re
|
|
27
27
|
import uuid
|
|
28
|
+
import warnings
|
|
28
29
|
from pathlib import Path
|
|
29
30
|
from typing import ClassVar
|
|
30
31
|
from xml.etree import ElementTree
|
|
@@ -564,6 +565,20 @@ class PowerQueryWorkbook:
|
|
|
564
565
|
|
|
565
566
|
def save(self, path: str | Path | None = None) -> Path:
|
|
566
567
|
out = Path(path) if path is not None else self.path
|
|
568
|
+
# A workbook Excel has recovered carries the parts it threw out
|
|
569
|
+
# under `[trash]`, and Excel will not open a package holding one.
|
|
570
|
+
# Preserving those faithfully would hand back a file that stays
|
|
571
|
+
# broken, so they go, and the caller is told.
|
|
572
|
+
dropped = self._opc.drop_reserved()
|
|
573
|
+
if dropped:
|
|
574
|
+
warnings.warn(
|
|
575
|
+
"dropped "
|
|
576
|
+
+ ", ".join(repr(name) for name in dropped)
|
|
577
|
+
+ ": Excel's file recovery leaves those behind and will not "
|
|
578
|
+
"open a workbook that carries them",
|
|
579
|
+
UserWarning,
|
|
580
|
+
stacklevel=2,
|
|
581
|
+
)
|
|
567
582
|
raw = self.to_bytes()
|
|
568
583
|
out.write_bytes(raw)
|
|
569
584
|
if path is None:
|
|
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.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_database.accdb
RENAMED
|
File without changes
|
{pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb
RENAMED
|
File without changes
|
{pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_document.docm
RENAMED
|
File without changes
|
{pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam
RENAMED
|
File without changes
|
{pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm
RENAMED
|
File without changes
|
{pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb
RENAMED
|
File without changes
|
{pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm
RENAMED
|
File without changes
|
{pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx
RENAMED
|
File without changes
|
{pyopenvba-5.2.0 → pyopenvba-5.2.1}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb
RENAMED
|
File without changes
|
{pyopenvba-5.2.0 → pyopenvba-5.2.1}/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
|