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.
Files changed (89) hide show
  1. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/PKG-INFO +1 -1
  2. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/power_query.md +18 -6
  3. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/pyproject.toml +1 -1
  4. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/__init__.py +1 -1
  5. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_sheets.py +60 -6
  6. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/.gitignore +0 -0
  7. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/LICENSE.md +0 -0
  8. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/README.md +0 -0
  9. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/architecture.md +0 -0
  10. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/ms-ovba-implementation-guide_v2.md +0 -0
  11. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/research/access_complex/README.md +0 -0
  12. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/research/access_designs/README.md +0 -0
  13. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/research/access_macros/README.md +0 -0
  14. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/research/access_write/README.md +0 -0
  15. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/research/pcode/README.md +0 -0
  16. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/docs/roadmap.md +0 -0
  17. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/__main__.py +0 -0
  18. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_deflate.py +0 -0
  19. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_host.py +0 -0
  20. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_oforms_pages.py +0 -0
  21. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_oforms_records.py +0 -0
  22. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_ppt_container.py +0 -0
  23. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/__init__.py +0 -0
  24. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  25. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
  26. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  27. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
  28. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  29. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  30. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  31. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
  32. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
  33. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
  34. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/form.blob +0 -0
  35. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
  36. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/form.propdata +0 -0
  37. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
  38. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
  39. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/report.blob +0 -0
  40. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
  41. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/report.propdata +0 -0
  42. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
  43. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
  44. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/__init__.py +0 -0
  45. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_alloc.py +0 -0
  46. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_btree.py +0 -0
  47. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_collation.py +0 -0
  48. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
  49. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_compact.py +0 -0
  50. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_complex.py +0 -0
  51. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_datapage.py +0 -0
  52. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_ddl.py +0 -0
  53. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_designs.py +0 -0
  54. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_facade.py +0 -0
  55. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_format.py +0 -0
  56. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_index.py +0 -0
  57. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_lval.py +0 -0
  58. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_macros.py +0 -0
  59. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_pages.py +0 -0
  60. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_props.py +0 -0
  61. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_queries.py +0 -0
  62. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_rows.py +0 -0
  63. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_schema.py +0 -0
  64. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_sql.py +0 -0
  65. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_storage.py +0 -0
  66. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_tdef.py +0 -0
  67. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_validate.py +0 -0
  68. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/_vba.py +0 -0
  69. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access/database.py +0 -0
  70. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/access_read.py +0 -0
  71. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/cfb.py +0 -0
  72. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/excel.py +0 -0
  73. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/exceptions.py +0 -0
  74. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/forms.py +0 -0
  75. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerpoint.py +0 -0
  76. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/__init__.py +0 -0
  77. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_binary.py +0 -0
  78. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_files.py +0 -0
  79. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_mashup.py +0 -0
  80. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_metadata.py +0 -0
  81. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_opc.py +0 -0
  82. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_package.py +0 -0
  83. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_refresh.py +0 -0
  84. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/_section.py +0 -0
  85. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/powerquery/workbook.py +0 -0
  86. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/vba.py +0 -0
  87. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/vba_pcode.py +0 -0
  88. {pyopenvba-5.2.2 → pyopenvba-5.2.3}/src/pyopenvba/word.py +0 -0
  89. {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.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 a rule of its own. **`localSheetId` is the zero-based
225
- position of the sheet the name belongs to** among the workbook's sheets,
226
- not a constant, and it has to agree with the sheet the reference names.
227
- Written as 0 it was right only while the table landed on the first sheet;
228
- anywhere else Excel refuses to open the workbook, which is invisible in a
229
- one-sheet file and so went unnoticed.
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyOpenVBA"
7
- version = "5.2.2"
7
+ version = "5.2.3"
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" }
@@ -291,4 +291,4 @@ def push_power_query(
291
291
  """
292
292
  return _push_queries(src_dir, workbook, out=out, encoding=encoding, remove_missing=remove_missing)
293
293
 
294
- __version__ = "5.2.2"
294
+ __version__ = "5.2.3"
@@ -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 &amp; 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 dict(re.findall(r'([\w.:-]+)\s*=\s*"([^"]*)"', element))
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
- match = re.search(r'<dimension ref="([^"]*)"/>', sheet)
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 match.group(1).split(":")]
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
- return sheet.replace(match.group(0), f'<dimension ref="{first}:{last}"/>')
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"{quoted}!{absolute}</definedName>"
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