pyOpenVBA 5.1.2__tar.gz → 5.1.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.1.2 → pyopenvba-5.1.3}/PKG-INFO +1 -1
  2. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/pyproject.toml +1 -1
  3. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/__init__.py +1 -1
  4. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_index.py +6 -3
  5. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_rows.py +30 -3
  6. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/database.py +28 -11
  7. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/.gitignore +0 -0
  8. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/LICENSE.md +0 -0
  9. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/README.md +0 -0
  10. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/docs/architecture.md +0 -0
  11. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/docs/ms-ovba-implementation-guide_v2.md +0 -0
  12. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/docs/power_query.md +0 -0
  13. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/docs/research/access_complex/README.md +0 -0
  14. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/docs/research/access_designs/README.md +0 -0
  15. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/docs/research/access_macros/README.md +0 -0
  16. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/docs/research/access_write/README.md +0 -0
  17. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/docs/research/pcode/README.md +0 -0
  18. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/docs/roadmap.md +0 -0
  19. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/__main__.py +0 -0
  20. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_deflate.py +0 -0
  21. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_host.py +0 -0
  22. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_oforms_pages.py +0 -0
  23. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_oforms_records.py +0 -0
  24. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_ppt_container.py +0 -0
  25. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/__init__.py +0 -0
  26. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
  27. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/blank_files/blank_database_module.accdb +0 -0
  28. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
  29. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/blank_files/blank_excel_addin.xlam +0 -0
  30. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
  31. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
  32. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
  33. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsx +0 -0
  34. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/blank_files/engine_skeleton.accdb +0 -0
  35. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/blank_files/engine_skeleton.mdb +0 -0
  36. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/designs/form.blob +0 -0
  37. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/designs/form.lvprop +0 -0
  38. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/designs/form.propdata +0 -0
  39. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/designs/form.prototypes +0 -0
  40. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/designs/form.typeinfo +0 -0
  41. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/designs/report.blob +0 -0
  42. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/designs/report.lvprop +0 -0
  43. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/designs/report.propdata +0 -0
  44. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/designs/report.prototypes +0 -0
  45. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/_templates/designs/report.typeinfo +0 -0
  46. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/__init__.py +0 -0
  47. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_alloc.py +0 -0
  48. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_btree.py +0 -0
  49. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_collation.py +0 -0
  50. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_collation_general_legacy.py +0 -0
  51. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_compact.py +0 -0
  52. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_complex.py +0 -0
  53. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_datapage.py +0 -0
  54. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_ddl.py +0 -0
  55. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_designs.py +0 -0
  56. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_facade.py +0 -0
  57. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_format.py +0 -0
  58. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_lval.py +0 -0
  59. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_macros.py +0 -0
  60. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_pages.py +0 -0
  61. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_props.py +0 -0
  62. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_queries.py +0 -0
  63. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_schema.py +0 -0
  64. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_sql.py +0 -0
  65. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_storage.py +0 -0
  66. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_tdef.py +0 -0
  67. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_validate.py +0 -0
  68. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access/_vba.py +0 -0
  69. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/access_read.py +0 -0
  70. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/cfb.py +0 -0
  71. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/excel.py +0 -0
  72. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/exceptions.py +0 -0
  73. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/forms.py +0 -0
  74. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerpoint.py +0 -0
  75. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerquery/__init__.py +0 -0
  76. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerquery/_binary.py +0 -0
  77. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerquery/_files.py +0 -0
  78. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerquery/_mashup.py +0 -0
  79. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerquery/_metadata.py +0 -0
  80. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerquery/_opc.py +0 -0
  81. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerquery/_package.py +0 -0
  82. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerquery/_refresh.py +0 -0
  83. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerquery/_section.py +0 -0
  84. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerquery/_sheets.py +0 -0
  85. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/powerquery/workbook.py +0 -0
  86. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/vba.py +0 -0
  87. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/vba_pcode.py +0 -0
  88. {pyopenvba-5.1.2 → pyopenvba-5.1.3}/src/pyopenvba/word.py +0 -0
  89. {pyopenvba-5.1.2 → pyopenvba-5.1.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.1.2
3
+ Version: 5.1.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyOpenVBA"
7
- version = "5.1.2"
7
+ version = "5.1.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.1.2"
294
+ __version__ = "5.1.3"
@@ -58,7 +58,7 @@ from pyopenvba.access._pages import (
58
58
  PAGE_SIZE,
59
59
  PageStore,
60
60
  )
61
- from pyopenvba.access._rows import decode_datetime
61
+ from pyopenvba.access._rows import decode_datetime, scaled_decimal, scaled_int
62
62
  from pyopenvba.access._tdef import (
63
63
  TYPE_BIGINT,
64
64
  TYPE_BINARY,
@@ -463,7 +463,10 @@ def _encode_value(column: ColumnDef, value: object) -> bytes:
463
463
  if code == TYPE_NUMERIC:
464
464
  if not isinstance(value, (int, Decimal)):
465
465
  raise AccessError(f"column {column.name!r}: {value!r} is not a Decimal")
466
- scaled = int(Decimal(value).scaleb(column.scale))
466
+ # Through ``scaled_int``, so the key rounds a fractional value the
467
+ # way the row encoder does and carries a 29-digit one without the
468
+ # default context rounding it (GitHub issue #20).
469
+ scaled = scaled_int(Decimal(value), column.scale)
467
470
  magnitude = abs(scaled).to_bytes(16, "big")
468
471
  return b"\xff" + magnitude if scaled >= 0 else b"\x00" + _invert(magnitude)
469
472
  if code == TYPE_GUID:
@@ -510,5 +513,5 @@ def _decode_fixed(column: ColumnDef, raw: bytes) -> object:
510
513
  value = -Decimal(int.from_bytes(_invert(magnitude), "big"))
511
514
  else:
512
515
  raise AccessError(f"column {column.name!r}: Decimal key sign byte {sign:#04x}")
513
- return value.scaleb(-column.scale)
516
+ return scaled_decimal(value, -column.scale)
514
517
  raise AccessError(f"column {column.name!r}: no fixed key codec for {column.type_name}")
@@ -23,7 +23,7 @@ import datetime as _dt
23
23
  import struct
24
24
  import uuid
25
25
  from dataclasses import dataclass
26
- from decimal import ROUND_HALF_EVEN, Decimal
26
+ from decimal import ROUND_HALF_EVEN, Decimal, localcontext
27
27
 
28
28
  from pyopenvba.access_read import AccessError
29
29
  from pyopenvba.access._tdef import (
@@ -203,6 +203,33 @@ def encode_datetime(value: _dt.datetime) -> bytes:
203
203
  return struct.pack("<d", number)
204
204
 
205
205
 
206
+ #: A Decimal column is a 16-byte magnitude, so a value carries up to 39
207
+ #: digits, while Python's default arithmetic context rounds at 28. Every
208
+ #: Decimal conversion here and in the index key codec runs at this
209
+ #: precision instead: scaling ``2**96 - 1`` through the default context
210
+ #: returns it five too high, which puts a key out of step with the row it
211
+ #: points at (GitHub issue #20).
212
+ DECIMAL_PREC = 64
213
+
214
+
215
+ def scaled_decimal(value: Decimal | int, scale: int) -> Decimal:
216
+ """``value`` shifted by ``10 ** scale``, with no context rounding."""
217
+ with localcontext() as ctx:
218
+ ctx.prec = DECIMAL_PREC
219
+ return Decimal(value).scaleb(scale)
220
+
221
+
222
+ def scaled_int(value: Decimal, scale: int) -> int:
223
+ """``value`` shifted by ``10 ** scale`` and rounded to an integer.
224
+
225
+ Half-even, as Access rounds, and at a precision no Decimal a column
226
+ can hold will overflow.
227
+ """
228
+ with localcontext() as ctx:
229
+ ctx.prec = DECIMAL_PREC
230
+ return int(Decimal(value).scaleb(scale).to_integral_value())
231
+
232
+
206
233
  def decode_numeric(raw: bytes, scale: int) -> Decimal:
207
234
  if len(raw) != 17:
208
235
  raise AccessError(f"Decimal value is {len(raw)} bytes, not 17")
@@ -211,12 +238,12 @@ def decode_numeric(raw: bytes, scale: int) -> Decimal:
211
238
  magnitude = 0
212
239
  for word in words:
213
240
  magnitude = (magnitude << 32) | word
214
- value = Decimal(magnitude).scaleb(-scale)
241
+ value = scaled_decimal(magnitude, -scale)
215
242
  return -value if negative else value
216
243
 
217
244
 
218
245
  def encode_numeric(value: Decimal, scale: int) -> bytes:
219
- scaled = int((value.copy_abs() * (Decimal(10) ** scale)).to_integral_value())
246
+ scaled = scaled_int(value.copy_abs(), scale)
220
247
  if scaled >= 1 << 128:
221
248
  raise AccessError("Decimal value does not fit 16 bytes")
222
249
  words = [(scaled >> shift) & 0xFFFFFFFF for shift in (96, 64, 32, 0)]
@@ -4156,24 +4156,41 @@ class AccessDatabase:
4156
4156
  def delete_module(self, name: str) -> None:
4157
4157
  """Remove a module and every structure it occupies."""
4158
4158
  module = self.module(name)
4159
- index = [m.name for m in self.modules()].index(module.name)
4160
4159
  encoding = encoding_of(self._vba_dir()[1])
4161
4160
  storage = self.table(STORAGE_TABLE)
4162
4161
  modules_id, project_id, streams_id = self._vba_storage_ids()
4163
4162
 
4164
- # A module's storage folder is linked to it by position, not by
4165
- # name: deleting the second of three modules, Access dropped the
4166
- # second folder and left the others named as they were.
4167
- folders = sorted(
4163
+ # The container's own DirData names each module's storage folder,
4164
+ # the way `_delete_design` reads it. This used to take the folder
4165
+ # by position among `modules()`, which is dir-stream order and
4166
+ # includes the module behind a form or report. Those have no
4167
+ # folder here at all, so the two lists were different lengths and
4168
+ # every module after a document one sat a place high: deleting one
4169
+ # took another's folder, or ran off the end (GitHub issue #19).
4170
+ listing = next(
4168
4171
  (
4169
- (rid, row)
4170
- for rid, row in storage.rows_with_ids()
4171
- if _as_int(row["ParentId"]) == modules_id and _as_int(row["Type"]) == 1
4172
+ payload
4173
+ for _rid, row in storage.rows_with_ids()
4174
+ if _as_int(row["ParentId"]) == modules_id
4175
+ and str(row["Name"]) == DIR_DATA
4176
+ and isinstance(payload := row.get("Lv"), bytes)
4172
4177
  ),
4173
- key=lambda pair: _as_int(pair[1]["Id"]),
4178
+ None,
4179
+ )
4180
+ entries = dict(dir_data_entries(listing)) if listing is not None else {}
4181
+ if module.name not in entries:
4182
+ raise AccessError(
4183
+ f"{module.name!r} is the code behind a form or report, which has no "
4184
+ "storage folder of its own; delete the design instead"
4185
+ )
4186
+ folder_name = entries[module.name]
4187
+ folder_rid, folder_id = next(
4188
+ (rid, _as_int(row["Id"]))
4189
+ for rid, row in storage.rows_with_ids()
4190
+ if _as_int(row["ParentId"]) == modules_id
4191
+ and _as_int(row["Type"]) == TYPE_FOLDER
4192
+ and str(row["Name"]) == folder_name
4174
4193
  )
4175
- folder_rid, folder_row = folders[index]
4176
- folder_name, folder_id = str(folder_row["Name"]), _as_int(folder_row["Id"])
4177
4194
  doomed = [folder_rid] + [
4178
4195
  rid for rid, row in storage.rows_with_ids() if _as_int(row["ParentId"]) == folder_id
4179
4196
  ]
File without changes
File without changes
File without changes
File without changes
File without changes