athena-python-pptx 0.1.74__tar.gz → 0.1.75__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 (83) hide show
  1. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/CHANGELOG.md +54 -0
  2. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/PKG-INFO +1 -1
  3. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/__init__.py +1 -1
  4. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/client.py +17 -0
  5. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/shapes/__init__.py +7 -1
  6. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/text/__init__.py +25 -1
  7. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/typing.py +21 -0
  8. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/units.py +20 -0
  9. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pyproject.toml +1 -1
  10. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/.gitignore +0 -0
  11. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/API_PARITY_REPORT.md +0 -0
  12. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/CLAUDE.md +0 -0
  13. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/DEV-GUIDE.md +0 -0
  14. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/PARITY_QUESTIONS.md +0 -0
  15. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/PUBLISHING.md +0 -0
  16. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/README.md +0 -0
  17. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/docs/API_PARITY_EXCEPTIONS.md +0 -0
  18. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/docs/athena-api.json +0 -0
  19. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/docs/athena-api.md +0 -0
  20. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/_ptc.py +0 -0
  21. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/action.py +0 -0
  22. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/batching.py +0 -0
  23. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/__init__.py +0 -0
  24. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/axis.py +0 -0
  25. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/category.py +0 -0
  26. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/chart.py +0 -0
  27. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/data.py +0 -0
  28. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/datalabel.py +0 -0
  29. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/legend.py +0 -0
  30. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/marker.py +0 -0
  31. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/plot.py +0 -0
  32. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/point.py +0 -0
  33. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/series.py +0 -0
  34. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/chart/xlsx.py +0 -0
  35. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/commands.py +0 -0
  36. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/decorators.py +0 -0
  37. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/dml/__init__.py +0 -0
  38. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/dml/chtfmt.py +0 -0
  39. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/dml/color.py +0 -0
  40. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/dml/effect.py +0 -0
  41. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/dml/fill.py +0 -0
  42. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/dml/line.py +0 -0
  43. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/docgen.py +0 -0
  44. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/enum/__init__.py +0 -0
  45. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/enum/action.py +0 -0
  46. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/enum/chart.py +0 -0
  47. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/enum/dml.py +0 -0
  48. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/enum/lang.py +0 -0
  49. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/enum/shapes.py +0 -0
  50. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/enum/text.py +0 -0
  51. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/errors.py +0 -0
  52. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/exc.py +0 -0
  53. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/media.py +0 -0
  54. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/package.py +0 -0
  55. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/parts/__init__.py +0 -0
  56. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/parts/_base.py +0 -0
  57. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/parts/chart.py +0 -0
  58. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/parts/coreprops.py +0 -0
  59. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/parts/embeddedpackage.py +0 -0
  60. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/parts/image.py +0 -0
  61. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/parts/media.py +0 -0
  62. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/parts/presentation.py +0 -0
  63. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/parts/slide.py +0 -0
  64. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/presentation.py +0 -0
  65. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/shapes/autoshape.py +0 -0
  66. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/shapes/base.py +0 -0
  67. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/shapes/connector.py +0 -0
  68. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/shapes/freeform.py +0 -0
  69. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/shapes/graphfrm.py +0 -0
  70. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/shapes/group.py +0 -0
  71. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/shapes/picture.py +0 -0
  72. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/shapes/placeholder.py +0 -0
  73. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/shapes/shapetree.py +0 -0
  74. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/shared.py +0 -0
  75. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/slide.py +0 -0
  76. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/slides.py +0 -0
  77. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/spec.py +0 -0
  78. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/table.py +0 -0
  79. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/text/fonts.py +0 -0
  80. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/text/layout.py +0 -0
  81. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/text/text.py +0 -0
  82. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/types.py +0 -0
  83. {athena_python_pptx-0.1.74 → athena_python_pptx-0.1.75}/pptx/util.py +0 -0
@@ -2,6 +2,60 @@
2
2
 
3
3
  All notable changes to `athena-python-pptx` are documented in this file.
4
4
 
5
+ ## 0.1.75
6
+
7
+ **Critical bug fix — invisible text from `font.size = Pt(X)`.**
8
+
9
+ A user surfaced that agent-generated decks rendered as blank
10
+ rectangles in the Olympus editor. Investigation traced the symptom to
11
+ `fontSizePt: 355600` (= 28 × 12700) landing on the wire instead of
12
+ `fontSizePt: 28` — making the renderer treat text as 355,600-point
13
+ glyphs and rendering them effectively invisible.
14
+
15
+ ### Root cause
16
+
17
+ `Length.__truediv__` returns a `Length` subclass — e.g.
18
+ `Pt(28) / EMU_PER_PT` returns a `Pt` instance with raw int value `28`
19
+ (meaning 28 EMU, semantically "28 points worth"). The SDK's font-size
20
+ serializer relies on this to convert EMU → points:
21
+
22
+ ```python
23
+ style["fontSizePt"] = self._size / EMU_PER_PT # Pt(355600) / 12700
24
+ ```
25
+
26
+ The trouble: `Command.to_dict()` calls `dataclasses.asdict()` which
27
+ **deep-copies all nested values**. `copy.deepcopy(Pt(28))` reconstructs
28
+ via `Pt.__new__(28)`, and `Pt.__new__` interprets its argument as
29
+ *points* (not EMU) and multiplies by 12700. So a Pt instance whose
30
+ internal int was 28 EMU came back out as 28 × 12700 = 355600 EMU.
31
+
32
+ By the time the JSON went over the wire, `fontSizePt` was 355600.
33
+ Server stored it, renderer rendered text at 355,600pt → invisible.
34
+
35
+ ### Fix
36
+
37
+ Override `Length.__reduce_ex__` so `copy.deepcopy` (and `pickle`)
38
+ reconstruct via the new module-level `_length_reconstruct`, which uses
39
+ `Length._from_emu` (preserves raw EMU value). Three lines in
40
+ `pptx/units.py`; behaviour-preserving for every existing call site.
41
+
42
+ ### Regression test
43
+
44
+ New `test_length_round_trips_through_deepcopy` in `tests/test_units.py`
45
+ asserts that `copy.deepcopy(Pt(28))` and `asdict({...Pt(28) / 12700...})`
46
+ both preserve the raw EMU value. Would have caught this bug on day one.
47
+
48
+ ### Impact
49
+
50
+ Every agent-authored deck where the agent set `run.font.size = Pt(X)`
51
+ through the SDK was silently shipping `fontSizePt: X * 12700` to the
52
+ server. Existing decks may need to be regenerated by the agent or
53
+ manually corrected. Future agent runs against v0.1.75+ will render
54
+ text correctly.
55
+
56
+ The new SDK was published to PyPI as 0.1.75 and the Daytona
57
+ `presentation-exec` snapshot bumped to v38.
58
+
5
59
  ## 0.1.74
6
60
 
7
61
  **True drop-in import-path parity.** A behavioural-fidelity audit found
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-pptx
3
- Version: 0.1.74
3
+ Version: 0.1.75
4
4
  Summary: Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration
5
5
  Project-URL: Homepage, https://github.com/pptx-studio/python-sdk
6
6
  Project-URL: Documentation, https://docs.pptx-studio.com/sdk/python
@@ -127,7 +127,7 @@ def flush_all() -> None:
127
127
  _active_buffers[:] = alive
128
128
 
129
129
 
130
- __version__ = "0.1.74"
130
+ __version__ = "0.1.75"
131
131
 
132
132
  __all__ = [
133
133
  # Main entry point
@@ -542,6 +542,7 @@ class Client:
542
542
  MasterSnapshot,
543
543
  PlaceholderSnapshot,
544
544
  SlideSnapshot,
545
+ TextFramePropertiesSnapshot,
545
546
  ThemeHierarchySnapshot,
546
547
  Transform,
547
548
  )
@@ -577,6 +578,21 @@ class Client:
577
578
  local_transform_override=placeholder_data.get("localTransformOverride"),
578
579
  )
579
580
 
581
+ # Server stores margins under `marginLeftEmu` after any SDK update
582
+ # and under `marginLeft` immediately after ingest. Read both.
583
+ tfp_data = elem_data.get("textFrameProperties")
584
+ text_frame_properties: Optional[TextFramePropertiesSnapshot] = None
585
+ if isinstance(tfp_data, dict):
586
+ text_frame_properties = TextFramePropertiesSnapshot(
587
+ word_wrap=tfp_data.get("wordWrap"),
588
+ auto_size=tfp_data.get("autoSize"),
589
+ vertical_anchor=tfp_data.get("verticalAnchor"),
590
+ margin_left=tfp_data.get("marginLeftEmu", tfp_data.get("marginLeft")),
591
+ margin_right=tfp_data.get("marginRightEmu", tfp_data.get("marginRight")),
592
+ margin_top=tfp_data.get("marginTopEmu", tfp_data.get("marginTop")),
593
+ margin_bottom=tfp_data.get("marginBottomEmu", tfp_data.get("marginBottom")),
594
+ )
595
+
580
596
  elements[elem_id] = ElementSnapshot(
581
597
  id=elem_data["id"],
582
598
  type=elem_data["type"],
@@ -596,6 +612,7 @@ class Client:
596
612
  source=elem_data.get("source"),
597
613
  table_data=elem_data.get("tableData"),
598
614
  asset_id=elem_data.get("assetId"),
615
+ text_frame_properties=text_frame_properties,
599
616
  )
600
617
 
601
618
  theme_hierarchy_raw = data.get("themeHierarchy")
@@ -26,7 +26,7 @@ from ..chart.category import Categories
26
26
  from ..dml.color import RGBColor, ColorFormat
27
27
  from ..errors import UnsupportedFeatureError
28
28
  from ..text import TextFrame
29
- from ..typing import ElementSnapshot, ShapeId, Transform, PlaceholderSnapshot, PP_PLACEHOLDER
29
+ from ..typing import ElementSnapshot, ShapeId, Transform, PlaceholderSnapshot, PP_PLACEHOLDER, TextFramePropertiesSnapshot
30
30
  from ..units import Emu, Length, ensure_emu
31
31
 
32
32
  if TYPE_CHECKING:
@@ -2516,6 +2516,7 @@ class Shape:
2516
2516
  properties: Optional[dict[str, Any]] = None,
2517
2517
  placeholder: Optional[PlaceholderSnapshot] = None,
2518
2518
  source: Optional[str] = None,
2519
+ text_frame_properties: Optional[TextFramePropertiesSnapshot] = None,
2519
2520
  ):
2520
2521
  self._shape_id = shape_id
2521
2522
  self._slide = slide
@@ -2536,6 +2537,7 @@ class Shape:
2536
2537
  buffer=buffer,
2537
2538
  preview_text=preview_text,
2538
2539
  rich_content=rich_content,
2540
+ text_frame_properties=text_frame_properties,
2539
2541
  )
2540
2542
 
2541
2543
  # Known misspelled/non-existent attributes with specific fix guidance
@@ -5159,6 +5161,7 @@ class Shapes:
5159
5161
  properties=elem.properties,
5160
5162
  placeholder=elem.placeholder,
5161
5163
  source=elem.source,
5164
+ text_frame_properties=elem.text_frame_properties,
5162
5165
  )
5163
5166
  elif elem.type == "connector":
5164
5167
  shape = Connector(
@@ -5170,6 +5173,7 @@ class Shapes:
5170
5173
  properties=elem.properties,
5171
5174
  placeholder=elem.placeholder,
5172
5175
  source=elem.source,
5176
+ text_frame_properties=elem.text_frame_properties,
5173
5177
  )
5174
5178
  elif elem.type == "group":
5175
5179
  shape = GroupShape(
@@ -5181,6 +5185,7 @@ class Shapes:
5181
5185
  properties=elem.properties,
5182
5186
  placeholder=elem.placeholder,
5183
5187
  source=elem.source,
5188
+ text_frame_properties=elem.text_frame_properties,
5184
5189
  )
5185
5190
  else:
5186
5191
  shape = Shape(
@@ -5193,6 +5198,7 @@ class Shapes:
5193
5198
  properties=elem.properties,
5194
5199
  placeholder=elem.placeholder,
5195
5200
  source=elem.source,
5201
+ text_frame_properties=elem.text_frame_properties,
5196
5202
  )
5197
5203
  self._shapes.append(shape)
5198
5204
  self._shapes_by_id[elem.id] = shape
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Iterator, Optional, Union
10
10
  from ..commands import SetText, SetRunStyle, SetParagraphStyle, SetTextFrameProperties, FitText
11
11
  from ..dml.color import RGBColor, ColorFormat
12
12
  from ..errors import UnsupportedFeatureError
13
- from ..typing import ShapeId, TextStyle
13
+ from ..typing import ShapeId, TextStyle, TextFramePropertiesSnapshot
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from ..batching import CommandBuffer
@@ -1328,12 +1328,36 @@ class TextFrame:
1328
1328
  buffer: Optional[CommandBuffer],
1329
1329
  preview_text: Optional[str] = None,
1330
1330
  rich_content: Optional[dict[str, Any]] = None,
1331
+ text_frame_properties: Optional[TextFramePropertiesSnapshot] = None,
1331
1332
  ):
1332
1333
  self._shape_id = shape_id
1333
1334
  self._buffer = buffer
1334
1335
  self._preview_text = preview_text or ""
1335
1336
  self._paragraphs: list[Paragraph] = []
1336
1337
 
1338
+ # Seed internal text-frame-properties state from the source snapshot so
1339
+ # subsequent setter emits (which always rebroadcast the full state)
1340
+ # preserve the props the user hasn't touched. Without this, setting
1341
+ # `text_frame.margin_left = 0` would emit a SetTextFrameProperties
1342
+ # command without `autoSize`, the server's existing autoSize would be
1343
+ # the only thing keeping the box auto-fit — and it isn't there for
1344
+ # shapes created via AddTextBox, so the box renders without auto-fit.
1345
+ if text_frame_properties is not None:
1346
+ if text_frame_properties.word_wrap is not None:
1347
+ self._word_wrap = text_frame_properties.word_wrap
1348
+ if text_frame_properties.auto_size is not None:
1349
+ self._auto_size = text_frame_properties.auto_size
1350
+ if text_frame_properties.vertical_anchor is not None:
1351
+ self._vertical_anchor = text_frame_properties.vertical_anchor
1352
+ if text_frame_properties.margin_left is not None:
1353
+ self._margin_left = text_frame_properties.margin_left
1354
+ if text_frame_properties.margin_right is not None:
1355
+ self._margin_right = text_frame_properties.margin_right
1356
+ if text_frame_properties.margin_top is not None:
1357
+ self._margin_top = text_frame_properties.margin_top
1358
+ if text_frame_properties.margin_bottom is not None:
1359
+ self._margin_bottom = text_frame_properties.margin_bottom
1360
+
1337
1361
  # Initialize paragraphs from rich content or plain text
1338
1362
  if rich_content and rich_content.get("paragraphs"):
1339
1363
  for i, para_data in enumerate(rich_content["paragraphs"]):
@@ -162,6 +162,26 @@ class PlaceholderSnapshot:
162
162
  local_transform_override: Optional[bool] = None
163
163
 
164
164
 
165
+ @dataclass
166
+ class TextFramePropertiesSnapshot:
167
+ """Snapshot of a text frame's structural properties (auto-size, margins, etc.).
168
+
169
+ Carries the source PPTX's <a:bodyPr> state through `from_url` so the SDK's
170
+ TextFrame can populate its internal `_auto_size`/`_word_wrap`/`_margin_*`
171
+ fields. Without this, a user setter (`text_frame.margin_left = 0`) emits a
172
+ SetTextFrameProperties command that omits the un-touched props, the server
173
+ keeps whatever it has, and round-trip is lossy.
174
+ """
175
+
176
+ word_wrap: Optional[bool] = None
177
+ auto_size: Optional[Literal["none", "shape_to_fit_text", "text_to_fit_shape"]] = None
178
+ vertical_anchor: Optional[Literal["top", "middle", "bottom"]] = None
179
+ margin_left: Optional[int] = None
180
+ margin_right: Optional[int] = None
181
+ margin_top: Optional[int] = None
182
+ margin_bottom: Optional[int] = None
183
+
184
+
165
185
  @dataclass
166
186
  class ElementSnapshot:
167
187
  """Snapshot of an element's state."""
@@ -176,6 +196,7 @@ class ElementSnapshot:
176
196
  source: Optional[Literal["ingested", "sdk", "layout", "master"]] = None
177
197
  table_data: Optional[dict[str, Any]] = None
178
198
  asset_id: Optional[str] = None
199
+ text_frame_properties: Optional[TextFramePropertiesSnapshot] = None
179
200
 
180
201
 
181
202
  @dataclass
@@ -31,6 +31,17 @@ EMU_PER_CENTIPOINT = 127
31
31
  EMU_PER_PX = 9525 # At 96 DPI
32
32
 
33
33
 
34
+ def _length_reconstruct(cls: type, emu_value: int): # noqa: ANN201
35
+ """Top-level reconstructor for :class:`Length` subclasses.
36
+
37
+ Used by :meth:`Length.__reduce_ex__` so that ``copy.deepcopy`` (and
38
+ ``pickle``) round-trip a :class:`Length` subclass without going
39
+ through the unit-converting ``__new__``. Must be a module-level
40
+ function so pickle can locate it by qualified name.
41
+ """
42
+ return cls._from_emu(emu_value)
43
+
44
+
34
45
  class Length(int):
35
46
  """
36
47
  Length expressed in English Metric Units (EMU) — base unit class.
@@ -45,6 +56,15 @@ class Length(int):
45
56
  def __new__(cls, value: Union[int, float, "Length"]) -> "Length":
46
57
  return super().__new__(cls, int(round(value)))
47
58
 
59
+ # ``copy.deepcopy`` (used by ``dataclasses.asdict``) reconstructs ints
60
+ # by invoking ``Type(int_value)``. For our subclasses that means
61
+ # ``Pt(355600)`` etc., which interpret the value as user-units and
62
+ # multiply by the unit's EMU factor → ``355600 → 4,516,120,000``.
63
+ # Override the pickle/reduce protocol so deep-copy round-trips via
64
+ # ``_from_emu`` (treats the value as raw EMU), preserving identity.
65
+ def __reduce_ex__(self, protocol: int): # type: ignore[override]
66
+ return (_length_reconstruct, (type(self), int(self)))
67
+
48
68
  @property
49
69
  def inches(self) -> float:
50
70
  """Return value in inches."""
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "athena-python-pptx"
7
- version = "0.1.74"
7
+ version = "0.1.75"
8
8
  description = "Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration"
9
9
  readme = "README.md"
10
10
  license = "MIT"