pdfdancer-client-python 0.2.28__tar.gz → 0.2.29__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 (76) hide show
  1. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/PKG-INFO +1 -1
  2. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/pyproject.toml +1 -1
  3. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer/types.py +52 -13
  4. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer_client_python.egg-info/PKG-INFO +1 -1
  5. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_text_line_edit.py +75 -79
  6. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/.claude/commands/discuss.md +0 -0
  7. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/.flake8 +0 -0
  8. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/.github/workflows/ci.yml +0 -0
  9. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/.github/workflows/daily-tests.yml +0 -0
  10. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/.gitignore +0 -0
  11. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/CLAUDE.md +0 -0
  12. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/LICENSE +0 -0
  13. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/NOTICE +0 -0
  14. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/README.md +0 -0
  15. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/TODO.md +0 -0
  16. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/check.py +0 -0
  17. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/docs/openapi.yml +0 -0
  18. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/media/logo-orange-512h.webp +0 -0
  19. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/media/logo-orange-60h.webp +0 -0
  20. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/release.py +0 -0
  21. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/setup.cfg +0 -0
  22. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer/__init__.py +0 -0
  23. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer/exceptions.py +0 -0
  24. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer/fingerprint.py +0 -0
  25. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer/image_builder.py +0 -0
  26. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer/models.py +0 -0
  27. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer/page_builder.py +0 -0
  28. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer/paragraph_builder.py +0 -0
  29. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer/path_builder.py +0 -0
  30. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer/pdfdancer_v1.py +0 -0
  31. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer/text_line_builder.py +0 -0
  32. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer_client_python.egg-info/SOURCES.txt +0 -0
  33. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer_client_python.egg-info/dependency_links.txt +0 -0
  34. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer_client_python.egg-info/requires.txt +0 -0
  35. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/src/pdfdancer_client_python.egg-info/top_level.txt +0 -0
  36. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/test.sh +0 -0
  37. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/__init__.py +0 -0
  38. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/conftest.py +0 -0
  39. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/__init__.py +0 -0
  40. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/pdf_assertions.py +0 -0
  41. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_acroform.py +0 -0
  42. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_bezier_builder.py +0 -0
  43. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_context_manager.py +0 -0
  44. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_form_x_objects.py +0 -0
  45. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_image.py +0 -0
  46. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_line.py +0 -0
  47. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_line_builder.py +0 -0
  48. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_new_pdf.py +0 -0
  49. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_page.py +0 -0
  50. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_paragraph.py +0 -0
  51. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_path.py +0 -0
  52. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_path_builder.py +0 -0
  53. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_path_builder_rectangle.py +0 -0
  54. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_path_comprehensive.py +0 -0
  55. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_pdfdancer.py +0 -0
  56. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_positioning.py +0 -0
  57. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_rectangle_builder.py +0 -0
  58. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_singular_selection.py +0 -0
  59. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/e2e/test_snapshot.py +0 -0
  60. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/fixtures/DancingScript-Regular.ttf +0 -0
  61. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/fixtures/Empty.pdf +0 -0
  62. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/fixtures/JetBrainsMono-Regular.ttf +0 -0
  63. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/fixtures/Showcase.pdf +0 -0
  64. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/fixtures/basic-paths.pdf +0 -0
  65. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/fixtures/form-xobject-example.pdf +0 -0
  66. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/fixtures/logo-80.png +0 -0
  67. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/fixtures/mixed-form-types.pdf +0 -0
  68. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/test_anonymous_token.py +0 -0
  69. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/test_fingerprint.py +0 -0
  70. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/test_models.py +0 -0
  71. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/test_openapi_compliance.py +0 -0
  72. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/test_path_models.py +0 -0
  73. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/test_pdf_object_equality.py +0 -0
  74. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/test_rate_limit.py +0 -0
  75. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/tests/test_standard_fonts.py +0 -0
  76. {pdfdancer_client_python-0.2.28 → pdfdancer_client_python-0.2.29}/update-api-spec.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.2.28
3
+ Version: 0.2.29
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pdfdancer-client-python"
7
- version = "0.2.28"
7
+ version = "0.2.29"
8
8
  description = "Python client for PDFDancer API"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -160,14 +160,14 @@ class BaseTextEdit:
160
160
 
161
161
  class TextLineEdit(BaseTextEdit):
162
162
  def apply(self) -> bool:
163
- # Text lines don't support line spacing - ignore if set
163
+ # Line spacing is NOT supported for text lines - fail hard
164
164
  if self._line_spacing is not None:
165
- print(
166
- "WARNING: Line spacing is not supported for text lines and will be ignored",
167
- file=sys.stderr,
165
+ raise UnsupportedOperation(
166
+ "Line spacing changes are not supported for individual text lines. "
167
+ "Line spacing can only be modified on paragraphs, not individual text lines."
168
168
  )
169
169
 
170
- # Simple text-only change
170
+ # If only text changed (no font, color, or position), use simple text modification
171
171
  only_text_changed = (
172
172
  self._new_text is not None
173
173
  and self._font_name is None
@@ -185,7 +185,7 @@ class TextLineEdit(BaseTextEdit):
185
185
  print(f"WARNING: {result.warning}", file=sys.stderr)
186
186
  return result
187
187
 
188
- # Position-only change (move operation)
188
+ # If only position changed (move operation)
189
189
  only_move = (
190
190
  self._position is not None
191
191
  and self._new_text is None
@@ -216,15 +216,54 @@ class TextLineEdit(BaseTextEdit):
216
216
  result = self._target_obj._client._move(self._object_ref, position)
217
217
  return result
218
218
 
219
- # Text lines with font/color styling changes are not supported by the PDF API
220
- # Text lines are components of paragraphs and can only have their text modified
221
- # or be moved, but not have their styling changed independently
222
- raise UnsupportedOperation(
223
- "Text lines can only have their text content modified or be moved. "
224
- "Font and color changes are not supported for individual text lines. "
225
- "To modify text styling, please modify the parent paragraph instead."
219
+ # For font/color changes or combined operations, use TextLineBuilder
220
+ # This ensures proper handling of font/color fallbacks just like ParagraphEditSession
221
+ from .text_line_builder import TextLineBuilder
222
+
223
+ builder = TextLineBuilder.from_object_ref(
224
+ self._target_obj._client, self._object_ref
226
225
  )
227
226
 
227
+ # Apply modifications to builder
228
+ # IMPORTANT: Always explicitly set text to ensure it's preserved
229
+ if self._new_text is not None:
230
+ builder.text(self._new_text)
231
+ elif hasattr(self._object_ref, "text") and self._object_ref.text:
232
+ # Preserve original text when only changing font/color/position
233
+ builder.text(self._object_ref.text)
234
+
235
+ # IMPORTANT: Always explicitly set font to ensure it's preserved
236
+ if self._font_name is not None and self._font_size is not None:
237
+ builder.font(self._font_name, self._font_size)
238
+ elif hasattr(self._object_ref, "font_name") and hasattr(self._object_ref, "font_size"):
239
+ if self._object_ref.font_name and self._object_ref.font_size:
240
+ # Preserve original font when only changing color/position
241
+ builder.font(self._object_ref.font_name, self._object_ref.font_size)
242
+
243
+ if self._color is not None:
244
+ builder.color(self._color)
245
+ if self._position is not None:
246
+ x = self._position.x()
247
+ y = self._position.y()
248
+ if x is None or y is None:
249
+ raise ValidationException("Position must have x and y coordinates")
250
+ page_index = (
251
+ self._object_ref.position.page_index
252
+ if self._object_ref.position
253
+ else None
254
+ )
255
+ if page_index is None:
256
+ raise ValidationException(
257
+ "Text line position must include a page index"
258
+ )
259
+ builder.at(page_index, x, y)
260
+
261
+ # Use builder's modify method which handles all the complexity
262
+ result = builder.modify(self._object_ref)
263
+ if result.warning:
264
+ print(f"WARNING: {result.warning}", file=sys.stderr)
265
+ return result
266
+
228
267
 
229
268
  class ParagraphObject(PDFObjectBase):
230
269
  """Represents a paragraph text block inside a PDF page."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pdfdancer-client-python
3
- Version: 0.2.28
3
+ Version: 0.2.29
4
4
  Summary: Python client for PDFDancer API
5
5
  Author-email: "The Famous Cat Ltd." <hi@thefamouscat.com>
6
6
  License:
@@ -1,7 +1,7 @@
1
1
  import pytest
2
-
3
2
  from pdfdancer import Color
4
3
  from pdfdancer.pdfdancer_v1 import PDFDancer
4
+
5
5
  from tests.e2e import _require_env_and_fixture
6
6
  from tests.e2e.pdf_assertions import PDFAssertions
7
7
 
@@ -15,8 +15,8 @@ def test_text_line_edit_text_only():
15
15
  text_lines = pdf.page(0).select_text_lines_starting_with(
16
16
  "This is regular Sans text showing alignment and styles."
17
17
  )
18
- if not text_lines:
19
- pytest.skip("Required text line not found in test PDF")
18
+
19
+ assert len(text_lines) >= 1
20
20
 
21
21
  text_line = text_lines[0]
22
22
 
@@ -34,49 +34,45 @@ def test_text_line_edit_text_only():
34
34
 
35
35
 
36
36
  def test_text_line_edit_font_only():
37
- """Test that text line font-only changes raise UnsupportedOperation"""
37
+ """Test text line font-only changes"""
38
38
  base_url, token, pdf_path = _require_env_and_fixture("Showcase.pdf")
39
39
 
40
40
  with PDFDancer.open(pdf_path, token=token, base_url=base_url) as pdf:
41
41
  text_lines = pdf.page(0).select_text_lines_starting_with(
42
42
  "This is regular Sans text showing alignment and styles."
43
43
  )
44
- if not text_lines:
45
- pytest.skip("Required text line not found in test PDF")
44
+ assert len(text_lines) >= 1
46
45
 
47
46
  text_line = text_lines[0]
47
+ original_text = text_line.text
48
48
 
49
- # Font changes on text lines should raise UnsupportedOperation
50
- from pdfdancer.types import UnsupportedOperation
49
+ # Font changes on text lines should work
50
+ with text_line.edit() as editor:
51
+ editor.font("Helvetica", 28)
51
52
 
52
- with pytest.raises(
53
- UnsupportedOperation, match="Font and color changes are not supported"
54
- ):
55
- with text_line.edit() as editor:
56
- editor.font("Helvetica", 28)
53
+ # Verify the text still exists (font changed but text preserved)
54
+ PDFAssertions(pdf).assert_textline_exists(original_text)
57
55
 
58
56
 
59
57
  def test_text_line_edit_color_only():
60
- """Test that text line color-only changes raise UnsupportedOperation"""
58
+ """Test text line color-only changes"""
61
59
  base_url, token, pdf_path = _require_env_and_fixture("Showcase.pdf")
62
60
 
63
61
  with PDFDancer.open(pdf_path, token=token, base_url=base_url) as pdf:
64
62
  text_lines = pdf.page(0).select_text_lines_starting_with(
65
63
  "This is regular Sans text showing alignment and styles."
66
64
  )
67
- if not text_lines:
68
- pytest.skip("Required text line not found in test PDF")
65
+ assert len(text_lines) >= 1
69
66
 
70
67
  text_line = text_lines[0]
68
+ original_text = text_line.text
71
69
 
72
- # Color changes on text lines should raise UnsupportedOperation
73
- from pdfdancer.types import UnsupportedOperation
70
+ # Color changes on text lines should work
71
+ with text_line.edit() as editor:
72
+ editor.color(Color(0, 255, 0))
74
73
 
75
- with pytest.raises(
76
- UnsupportedOperation, match="Font and color changes are not supported"
77
- ):
78
- with text_line.edit() as editor:
79
- editor.color(Color(0, 255, 0))
74
+ # Verify the text still exists (color changed but text preserved)
75
+ PDFAssertions(pdf).assert_textline_exists(original_text)
80
76
 
81
77
 
82
78
  def test_text_line_edit_move_only():
@@ -87,8 +83,7 @@ def test_text_line_edit_move_only():
87
83
  text_lines = pdf.page(0).select_text_lines_starting_with(
88
84
  "This is regular Sans text showing alignment and styles."
89
85
  )
90
- if not text_lines:
91
- pytest.skip("Required text line not found in test PDF")
86
+ assert len(text_lines) >= 1
92
87
 
93
88
  text_line = text_lines[0]
94
89
 
@@ -108,100 +103,105 @@ def test_text_line_edit_move_only():
108
103
 
109
104
 
110
105
  def test_text_line_edit_text_and_font():
111
- """Test that text+font changes raise UnsupportedOperation"""
106
+ """Test text+font changes work together"""
112
107
  base_url, token, pdf_path = _require_env_and_fixture("Showcase.pdf")
113
108
 
114
109
  with PDFDancer.open(pdf_path, token=token, base_url=base_url) as pdf:
115
110
  text_lines = pdf.page(0).select_text_lines_starting_with(
116
111
  "This is regular Sans text showing alignment and styles."
117
112
  )
118
- if not text_lines:
119
- pytest.skip("Required text line not found in test PDF")
113
+ assert len(text_lines) >= 1
120
114
 
121
115
  text_line = text_lines[0]
122
116
 
123
- # Text + Font changes should raise UnsupportedOperation
124
- from pdfdancer.types import UnsupportedOperation
117
+ # Text + Font changes should work
118
+ with text_line.edit() as editor:
119
+ editor.replace("New Text Here")
120
+ editor.font("Helvetica", 16)
125
121
 
126
- with pytest.raises(
127
- UnsupportedOperation, match="Font and color changes are not supported"
128
- ):
129
- with text_line.edit() as editor:
130
- editor.replace("New Text Here")
131
- editor.font("Helvetica", 16)
122
+ # Verify the new text exists
123
+ (
124
+ PDFAssertions(pdf)
125
+ .assert_textline_exists("New Text Here")
126
+ .assert_textline_does_not_exist(
127
+ "This is regular Sans text showing alignment and styles."
128
+ )
129
+ )
132
130
 
133
131
 
134
132
  def test_text_line_edit_all_properties():
135
- """Test that combined property changes raise UnsupportedOperation"""
133
+ """Test that combined property changes work (except line spacing)"""
136
134
  base_url, token, pdf_path = _require_env_and_fixture("Showcase.pdf")
137
135
 
138
136
  with PDFDancer.open(pdf_path, token=token, base_url=base_url) as pdf:
139
137
  text_lines = pdf.page(0).select_text_lines_starting_with(
140
138
  "This is regular Sans text showing alignment and styles."
141
139
  )
142
- if not text_lines:
143
- pytest.skip("Required text line not found in test PDF")
140
+ assert len(text_lines) >= 1
144
141
 
145
142
  text_line = text_lines[0]
146
143
 
147
- # Combined changes including font/color should raise UnsupportedOperation
148
- from pdfdancer.types import UnsupportedOperation
144
+ # Combined changes including font/color/position should work
145
+ with text_line.edit() as editor:
146
+ editor.replace("Fully Modified")
147
+ editor.font("Helvetica", 18)
148
+ editor.color(Color(255, 0, 0))
149
+ editor.move_to(100, 200)
149
150
 
150
- with pytest.raises(
151
- UnsupportedOperation, match="Font and color changes are not supported"
152
- ):
153
- with text_line.edit() as editor:
154
- editor.replace("Fully Modified")
155
- editor.font("Helvetica", 18)
156
- editor.color(Color(255, 0, 0))
157
- editor.move_to(100, 200)
151
+ # Verify the modified text exists
152
+ PDFAssertions(pdf).assert_textline_exists("Fully Modified")
158
153
 
159
154
 
160
- def test_text_line_edit_line_spacing_ignored():
161
- """Test that line spacing is ignored for text lines with a warning"""
155
+ def test_text_line_edit_line_spacing_fails():
156
+ """Test that line spacing changes fail hard for text lines"""
162
157
  base_url, token, pdf_path = _require_env_and_fixture("Showcase.pdf")
163
158
 
164
159
  with PDFDancer.open(pdf_path, token=token, base_url=base_url) as pdf:
165
160
  text_lines = pdf.page(0).select_text_lines_starting_with(
166
161
  "This is regular Sans text showing alignment and styles."
167
162
  )
168
- if not text_lines:
169
- pytest.skip("Required text line not found in test PDF")
163
+ assert len(text_lines) >= 1
170
164
 
171
165
  text_line = text_lines[0]
172
166
 
173
- # Line spacing should be ignored with a warning
174
- with text_line.edit() as editor:
175
- editor.line_spacing(2.0) # This should be ignored
176
- editor.replace("Text with ignored spacing")
167
+ # Line spacing should raise UnsupportedOperation
168
+ from pdfdancer.types import UnsupportedOperation
177
169
 
178
- # Verify the text was changed
179
- PDFAssertions(pdf).assert_textline_exists("Text with ignored spacing")
170
+ with pytest.raises(
171
+ UnsupportedOperation,
172
+ match="Line spacing changes are not supported for individual text lines",
173
+ ):
174
+ with text_line.edit() as editor:
175
+ editor.line_spacing(2.0)
176
+ editor.replace("Text with spacing")
180
177
 
181
178
 
182
179
  def test_text_line_edit_chaining():
183
- """Test that chained font/color changes raise UnsupportedOperation"""
180
+ """Test that chained font/color changes work"""
184
181
  base_url, token, pdf_path = _require_env_and_fixture("Showcase.pdf")
185
182
 
186
183
  with PDFDancer.open(pdf_path, token=token, base_url=base_url) as pdf:
187
184
  text_lines = pdf.page(0).select_text_lines_starting_with(
188
185
  "This is regular Sans text showing alignment and styles."
189
186
  )
190
- if not text_lines:
191
- pytest.skip("Required text line not found in test PDF")
187
+ assert len(text_lines) >= 1
192
188
 
193
189
  text_line = text_lines[0]
194
190
 
195
- # Chained font/color changes should raise UnsupportedOperation
196
- from pdfdancer.types import UnsupportedOperation
191
+ # Chained font/color changes should work
192
+ with text_line.edit() as editor:
193
+ editor.replace("Chained Edits").font("Helvetica", 15).color(
194
+ Color(128, 128, 128)
195
+ )
197
196
 
198
- with pytest.raises(
199
- UnsupportedOperation, match="Font and color changes are not supported"
200
- ):
201
- with text_line.edit() as editor:
202
- editor.replace("Chained Edits").font("Helvetica", 15).color(
203
- Color(128, 128, 128)
204
- )
197
+ # Verify the new text exists
198
+ (
199
+ PDFAssertions(pdf)
200
+ .assert_textline_exists("Chained Edits")
201
+ .assert_textline_does_not_exist(
202
+ "This is regular Sans text showing alignment and styles."
203
+ )
204
+ )
205
205
 
206
206
 
207
207
  def test_text_line_edit_with_exception_no_apply():
@@ -212,8 +212,7 @@ def test_text_line_edit_with_exception_no_apply():
212
212
  text_lines = pdf.page(0).select_text_lines_starting_with(
213
213
  "This is regular Sans text showing alignment and styles."
214
214
  )
215
- if not text_lines:
216
- pytest.skip("Required text line not found in test PDF")
215
+ assert len(text_lines) >= 1
217
216
 
218
217
  text_line = text_lines[0]
219
218
 
@@ -238,8 +237,7 @@ def test_text_line_edit_multiple_sequential():
238
237
  text_lines = pdf.page(0).select_text_lines_starting_with(
239
238
  "This is regular Sans text showing alignment and styles."
240
239
  )
241
- if not text_lines:
242
- pytest.skip("Required text line not found in test PDF")
240
+ assert len(text_lines) >= 1
243
241
 
244
242
  text_line = text_lines[0]
245
243
  with text_line.edit() as editor:
@@ -270,8 +268,7 @@ def test_text_line_edit_vs_manual_apply():
270
268
  text_lines = pdf1.page(0).select_text_lines_starting_with(
271
269
  "This is regular Sans text showing alignment and styles."
272
270
  )
273
- if not text_lines:
274
- pytest.skip("Required text line not found in test PDF")
271
+ assert len(text_lines) >= 1
275
272
 
276
273
  text_line = text_lines[0]
277
274
 
@@ -285,8 +282,7 @@ def test_text_line_edit_vs_manual_apply():
285
282
  text_lines = pdf2.page(0).select_text_lines_starting_with(
286
283
  "This is regular Sans text showing alignment and styles."
287
284
  )
288
- if not text_lines:
289
- pytest.skip("Required text line not found in test PDF")
285
+ assert len(text_lines) >= 1
290
286
 
291
287
  text_line = text_lines[0]
292
288