-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathtest_parser.py
More file actions
374 lines (268 loc) · 9.52 KB
/
Copy pathtest_parser.py
File metadata and controls
374 lines (268 loc) · 9.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
"""
Tests for the screenplay parser.
"""
import pytest
from screenpy import ScreenplayParser
from screenpy.models import LocationType, SegmentType, ShotType
from screenpy.parser.shot_parser import ShotHeadingParser
from screenpy.parser.time_parser import TimeParser
class TestShotHeadingParser:
"""Test shot heading parsing."""
def setup_method(self):
self.parser = ShotHeadingParser()
def test_master_heading_basic(self):
"""Test basic master heading parsing."""
text = "INT. CENTRAL PARK - DAY"
result = self.parser.parse(text)
assert result is not None
assert result.location_type == LocationType.INTERIOR
assert result.locations == ["CENTRAL PARK"]
assert result.time_of_day == "DAY"
assert result.is_master
def test_master_heading_complex(self):
"""Test complex master heading with multiple locations."""
text = "EXT. WHITE HOUSE - SOUTH LAWN - SUNSET"
result = self.parser.parse(text)
assert result is not None
assert result.location_type == LocationType.EXTERIOR
assert result.locations == ["WHITE HOUSE", "SOUTH LAWN"]
assert result.time_of_day == "SUNSET"
def test_shot_heading_with_shot_type(self):
"""Test heading with shot type."""
text = "INT. OFFICE - CLOSE ON JOHN - DAY"
result = self.parser.parse(text)
assert result is not None
assert result.location_type == LocationType.INTERIOR
assert result.locations == ["OFFICE"]
assert result.shot_type == ShotType.CLOSE
assert result.subject == "JOHN"
assert result.time_of_day == "DAY"
def test_shot_only_heading(self):
"""Test shot-only heading (non-master)."""
text = "WIDE SHOT - RACETRACK"
result = self.parser.parse(text)
assert result is not None
assert result.location_type == LocationType.NONE
assert result.shot_type == ShotType.WIDE
assert result.subject == "RACETRACK"
assert not result.is_master
def test_pov_shot(self):
"""Test POV shot parsing."""
text = "JOHN'S POV"
result = self.parser.parse(text)
assert result is not None
assert result.shot_type == ShotType.POV
assert not result.is_master
def test_tracking_shot(self):
"""Test tracking shot parsing."""
text = "TRACKING SHOT - THROUGH THE FOREST"
result = self.parser.parse(text)
assert result is not None
assert result.shot_type == ShotType.TRACKING
assert result.subject == "THROUGH THE FOREST"
def test_time_only_heading(self):
"""Test heading with only time."""
text = "MIDNIGHT"
result = self.parser.parse(text)
assert result is not None
assert result.time_of_day == "MIDNIGHT"
def test_int_ext_heading(self):
"""Test INT./EXT. heading."""
text = "INT./EXT. CAR - MOVING - DAY"
result = self.parser.parse(text)
assert result is not None
assert result.location_type == LocationType.INT_EXT
assert result.locations == ["CAR", "MOVING"]
assert result.time_of_day == "DAY"
class TestTimeParser:
"""Test time expression parsing."""
def setup_method(self):
self.parser = TimeParser()
def test_standard_times(self):
"""Test standard time of day expressions."""
times = ["DAY", "NIGHT", "MORNING", "AFTERNOON", "DUSK", "DAWN"]
for time in times:
assert self.parser.is_time_expression(time)
def test_clock_times(self):
"""Test clock time expressions."""
times = ["3 AM", "10:30 PM", "12:00", "6 P.M."]
for time in times:
assert self.parser.is_time_expression(time)
def test_relative_times(self):
"""Test relative time expressions."""
times = ["LATER", "MOMENTS LATER", "CONTINUOUS", "SAME TIME"]
for time in times:
assert self.parser.is_time_expression(time)
def test_holidays(self):
"""Test holiday detection."""
holidays = ["CHRISTMAS EVE", "NEW YEAR'S DAY", "THANKSGIVING"]
for holiday in holidays:
assert self.parser.is_time_expression(holiday)
def test_dates(self):
"""Test date detection."""
dates = ["MARCH 15", "DECEMBER 25TH", "JULY 4, 1776"]
for date in dates:
assert self.parser.is_time_expression(date)
def test_not_time(self):
"""Test non-time expressions."""
not_times = ["OFFICE", "JOHN", "TABLE", "DOOR"]
for text in not_times:
assert not self.parser.is_time_expression(text)
def test_extract_components(self):
"""Test component extraction."""
text = "CHRISTMAS EVE, 1999 - MIDNIGHT"
components = self.parser.extract_time_components(text)
assert components["holiday"] == "CHRISTMAS EVE"
assert components["year"] == "1999"
assert components["time_of_day"] == "MIDNIGHT"
class TestScreenplayParser:
"""Test full screenplay parsing."""
def setup_method(self):
self.parser = ScreenplayParser()
def test_parse_simple_screenplay(self):
"""Test parsing a simple screenplay."""
text = """
FADE IN:
INT. OFFICE - DAY
John sits at his desk.
JOHN
Hello, world!
MARY
(surprised)
What did you say?
CUT TO:
EXT. STREET - NIGHT
They walk together.
FADE OUT.
"""
screenplay = self.parser.parse(text)
assert screenplay is not None
assert len(screenplay.segments) > 0
assert len(screenplay.characters) == 2
assert "JOHN" in screenplay.characters
assert "MARY" in screenplay.characters
def test_parse_master_and_sub_segments(self):
"""Test hierarchical segment parsing."""
text = """
INT. HOUSE - DAY
LIVING ROOM
John enters.
KITCHEN
Mary cooks.
EXT. GARDEN - CONTINUOUS
Birds chirp.
"""
screenplay = self.parser.parse(text)
# Find master segments
master_segments = [s for s in screenplay.segments if s.is_master_segment]
assert len(master_segments) == 2 # INT. HOUSE and EXT. GARDEN
def test_parse_transitions(self):
"""Test transition parsing."""
text = """
INT. OFFICE - DAY
John works.
CUT TO:
EXT. STREET - NIGHT
John walks.
MATCH CUT TO:
INT. HOME - NIGHT
John sleeps.
"""
screenplay = self.parser.parse(text)
assert len(screenplay.transitions) == 2
def test_parse_dialogue_with_parenthetical(self):
"""Test dialogue with parenthetical parsing."""
text = """
INT. ROOM - DAY
JOHN
(angrily)
I can't believe this!
MARY
(O.S.)
Neither can I.
"""
screenplay = self.parser.parse(text)
dialogue_segments = [s for s in screenplay.segments if s.dialogue]
assert len(dialogue_segments) == 2
# Check first dialogue
john_dialogue = dialogue_segments[0].dialogue
assert john_dialogue.character.name == "JOHN"
assert john_dialogue.parenthetical == "(angrily)"
assert "can't believe" in john_dialogue.text
# Check second dialogue with modifier
mary_dialogue = dialogue_segments[1].dialogue
assert mary_dialogue.character.name == "MARY"
assert mary_dialogue.character.modifier == "O.S."
def test_extract_caps_from_stage_direction(self):
"""Test extraction of capitalized words from stage direction."""
text = """
INT. SUBMARINE - NIGHT
The crew SCRAMBLES to their stations. A loud BANG echoes through the hull.
CAPTAIN SMITH enters.
"""
screenplay = self.parser.parse(text)
# Find stage direction segment
stage_segments = [
s for s in screenplay.segments
if s.type == SegmentType.STAGE_DIRECTION and s.content
]
if stage_segments:
caps = stage_segments[0].capitalized_words
assert "SCRAMBLES" in caps
assert "BANG" in caps
assert "CAPTAIN" in caps
assert "SMITH" in caps
# Test fixtures for sample screenplays
@pytest.fixture
def sample_screenplay_text():
"""Sample screenplay text for testing."""
return """
FADE IN:
INT. UNIVERSITY LIBRARY - DAY
DAVID, 30s, scholarly type with glasses, browses ancient texts.
ANGLE ON - MYSTERIOUS BOOK
The book glows faintly.
DAVID
(whispers)
This can't be real...
He opens the book carefully.
INSERT - BOOK PAGES
Strange symbols dance across the pages.
BACK TO DAVID
His eyes widen in amazement.
SARAH (O.S.)
David? Are you there?
DAVID
(startled)
Sarah! You have to see this!
SARAH, late 20s, adventurous spirit, enters frame.
SARAH
What is it?
CLOSE ON SARAH'S FACE
Wonder and fear mix in her expression.
CUT TO:
EXT. UNIVERSITY - MAGIC HOUR
They exit the building together, book in hand.
FADE OUT.
"""
def test_full_screenplay_parse(sample_screenplay_text):
"""Test parsing a complete screenplay sample."""
parser = ScreenplayParser()
screenplay = parser.parse(sample_screenplay_text)
# Check basic structure
assert screenplay is not None
assert len(screenplay.segments) > 0
# Check characters
assert "DAVID" in screenplay.characters
assert "SARAH" in screenplay.characters
# Check for different segment types
has_master = any(s.is_master_segment for s in screenplay.segments)
has_dialogue = any(s.dialogue for s in screenplay.segments)
has_stage_direction = any(
s.type == SegmentType.STAGE_DIRECTION for s in screenplay.segments
)
assert has_master
assert has_dialogue
assert has_stage_direction
# Check transitions
assert len(screenplay.transitions) > 0