Coverage for pyEDAA/OutputFilter/Xilinx/__init__.py: 83%

394 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-27 22:12 +0000

1# ==================================================================================================================== # 

2# _____ ____ _ _ ___ _ _ _____ _ _ _ # 

3# _ __ _ _| ____| _ \ / \ / \ / _ \ _ _| |_ _ __ _ _| |_| ___(_) | |_ ___ _ __ # 

4# | '_ \| | | | _| | | | |/ _ \ / _ \ | | | | | | | __| '_ \| | | | __| |_ | | | __/ _ \ '__| # 

5# | |_) | |_| | |___| |_| / ___ \ / ___ \ | |_| | |_| | |_| |_) | |_| | |_| _| | | | || __/ | # 

6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)___/ \__,_|\__| .__/ \__,_|\__|_| |_|_|\__\___|_| # 

7# |_| |___/ |_| # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2025-2025 Electronic Design Automation Abstraction (EDA²) # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31"""Basic classes for outputs from AMD/Xilinx Vivado.""" 

32from datetime import datetime 

33from enum import Flag 

34from pathlib import Path 

35from re import compile as re_compile, Pattern 

36from typing import ClassVar, Self, Optional as Nullable, Dict, Type, Callable, List, Generator, Union, Tuple 

37 

38from pyTooling.Decorators import export, readonly 

39from pyTooling.MetaClasses import ExtendedType, abstractmethod 

40from pyTooling.Stopwatch import Stopwatch 

41from pyTooling.TerminalUI import TerminalApplication 

42from pyTooling.Versioning import YearReleaseVersion 

43 

44from pyEDAA.OutputFilter import OutputFilterException 

45 

46 

47@export 

48class ProcessorException(OutputFilterException): 

49 pass 

50 

51 

52@export 

53class ClassificationException(ProcessorException): 

54 _lineNumber: int 

55 _rawMessage: str 

56 

57 def __init__(self, errorMessage: str, lineNumber: int, rawMessageLine: str): 

58 super().__init__(errorMessage) 

59 

60 self._lineNumber = lineNumber 

61 self._rawMessage = rawMessageLine 

62 

63 

64@export 

65class ParserStateException(ProcessorException): 

66 pass 

67 

68 

69@export 

70class LineKind(Flag): 

71 Unprocessed = 0 

72 ProcessorError = 2** 0 

73 Empty = 2** 1 

74 Delimiter = 2** 2 

75 

76 Verbose = 2**10 

77 Normal = 2**11 

78 Info = 2**12 

79 Warning = 2**13 

80 CriticalWarning = 2**14 

81 Error = 2**15 

82 Fatal = 2**16 

83 

84 Start = 2**20 

85 End = 2**21 

86 Header = 2**22 

87 Content = 2**23 

88 Footer = 2**24 

89 

90 Last = 2**29 

91 

92 Message = 2**30 

93 InfoMessage = Message | Info 

94 WarningMessage = Message | Warning 

95 CriticalWarningMessage = Message | CriticalWarning 

96 ErrorMessage = Message | Error 

97 

98 Section = 2**31 

99 SectionDelimiter = Section | Delimiter 

100 SectionStart = Section | Start 

101 SectionEnd = Section | End 

102 

103 SubSection = 2**32 

104 SubSectionDelimiter = SubSection | Delimiter 

105 SubSectionStart = SubSection | Start 

106 SubSectionEnd = SubSection | End 

107 

108 Paragraph = 2**33 

109 ParagraphHeadline = Paragraph | Header 

110 

111 Table = 2**34 

112 TableFrame = Table | Delimiter 

113 TableHeader = Table | Header 

114 TableRow = Table | Content 

115 TableFooter = Table | Footer 

116 

117 Command = 2**35 

118 

119 

120@export 

121class Line(metaclass=ExtendedType, slots=True): 

122 """ 

123 This class represents any line in a log file. 

124 """ 

125 _lineNumber: int 

126 _kind: LineKind 

127 _message: str 

128 

129 def __init__(self, lineNumber: int, kind: LineKind, message: str) -> None: 

130 self._lineNumber = lineNumber 

131 self._kind = kind 

132 self._message = message 

133 

134 @readonly 

135 def LineNumber(self) -> int: 

136 return self._lineNumber 

137 

138 @readonly 

139 def Kind(self) -> LineKind: 

140 return self._kind 

141 

142 @readonly 

143 def Message(self) -> str: 

144 return self._message 

145 

146 def __str__(self) -> str: 

147 return self._message 

148 

149 

150@export 

151class InfoMessage(metaclass=ExtendedType, mixin=True): 

152 pass 

153 

154 

155@export 

156class WarningMessage(metaclass=ExtendedType, mixin=True): 

157 pass 

158 

159 

160@export 

161class CriticalWarningMessage(metaclass=ExtendedType, mixin=True): 

162 pass 

163 

164 

165@export 

166class ErrorMessage(metaclass=ExtendedType, mixin=True): 

167 pass 

168 

169 

170@export 

171class VivadoMessage(Line): 

172 """ 

173 This class represents an AMD/Xilinx Vivado message. 

174 

175 The usual message format is: 

176 

177 .. code-block:: text 

178 

179 INFO: [Synth 8-7079] Multithreading enabled for synth_design using a maximum of 2 processes. 

180 WARNING: [Synth 8-3332] Sequential element (gen[0].Sync/FF2) is unused and will be removed from module sync_Bits_Xilinx. 

181 

182 The following message severities are defined: 

183 

184 * ``INFO`` 

185 * ``WARNING`` 

186 * ``CRITICAL WARNING`` 

187 * ``ERROR`` 

188 

189 .. seealso:: 

190 

191 :class:`VivadoInfoMessage` 

192 Representing a Vivado info message. 

193 

194 :class:`VivadoWarningMessage` 

195 Representing a Vivado warning message. 

196 

197 :class:`VivadoCriticalWarningMessage` 

198 Representing a Vivado critical warning message. 

199 

200 :class:`VivadoErrorMessage` 

201 Representing a Vivado error message. 

202 """ 

203 # _MESSAGE_KIND: ClassVar[str] 

204 # _REGEXP: ClassVar[Pattern] 

205 

206 _toolID: int 

207 _toolName: str 

208 _messageKindID: int 

209 

210 def __init__(self, lineNumber: int, kind: LineKind, tool: str, toolID: int, messageKindID: int, message: str) -> None: 

211 super().__init__(lineNumber, kind, message) 

212 self._toolID = toolID 

213 self._toolName = tool 

214 self._messageKindID = messageKindID 

215 

216 @readonly 

217 def ToolName(self) -> str: 

218 return self._toolName 

219 

220 @readonly 

221 def ToolID(self) -> int: 

222 return self._toolID 

223 

224 @readonly 

225 def MessageKindID(self) -> int: 

226 return self._messageKindID 

227 

228 @classmethod 

229 def Parse(cls, lineNumber: int, kind: LineKind, rawMessage: str) -> Nullable[Self]: 

230 if (match := cls._REGEXP.match(rawMessage)) is not None: 

231 return cls(lineNumber, kind, match[1], int(match[2]), int(match[3]), match[4]) 

232 

233 return None 

234 

235 def __str__(self) -> str: 

236 return f"{self._MESSAGE_KIND}: [{self._toolName} {self._toolID}-{self._messageKindID}] {self._message}" 

237 

238 

239@export 

240class VivadoInfoMessage(VivadoMessage, InfoMessage): 

241 """ 

242 This class represents an AMD/Xilinx Vivado info message. 

243 """ 

244 

245 _MESSAGE_KIND: ClassVar[str] = "INFO" 

246 _REGEXP: ClassVar[Pattern] = re_compile(r"""INFO: \[(\w+) (\d+)-(\d+)\] (.*)""") 

247 

248 @classmethod 

249 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]: 

250 return super().Parse(lineNumber, LineKind.InfoMessage, rawMessage) 

251 

252 

253@export 

254class VivadoIrregularInfoMessage(VivadoMessage, InfoMessage): 

255 """ 

256 This class represents an irregular AMD/Xilinx Vivado info message. 

257 """ 

258 

259 _MESSAGE_KIND: ClassVar[str] = "INFO" 

260 _REGEXP: ClassVar[Pattern] = re_compile(r"""INFO: \[(\w+)-(\d+)\] (.*)""") 

261 

262 @classmethod 

263 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]: 

264 if (match := cls._REGEXP.match(rawMessage)) is not None: 

265 return cls(lineNumber, LineKind.InfoMessage, match[1], None, int(match[2]), match[3]) 

266 

267 return None 

268 

269 def __str__(self) -> str: 

270 return f"{self._MESSAGE_KIND}: [{self._toolName}-{self._messageKindID}] {self._message}" 

271 

272 

273@export 

274class VivadoWarningMessage(VivadoMessage, WarningMessage): 

275 """ 

276 This class represents an AMD/Xilinx Vivado warning message. 

277 """ 

278 

279 _MESSAGE_KIND: ClassVar[str] = "WARNING" 

280 _REGEXP: ClassVar[Pattern] = re_compile(r"""WARNING: \[(\w+) (\d+)-(\d+)\] (.*)""") 

281 

282 @classmethod 

283 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]: 

284 return super().Parse(lineNumber, LineKind.WarningMessage, rawMessage) 

285 

286 

287@export 

288class VivadoIrregularWarningMessage(VivadoMessage, WarningMessage): 

289 """ 

290 This class represents an AMD/Xilinx Vivado warning message. 

291 """ 

292 

293 _MESSAGE_KIND: ClassVar[str] = "WARNING" 

294 _REGEXP: ClassVar[Pattern] = re_compile(r"""WARNING: (.*)""") 

295 

296 @classmethod 

297 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]: 

298 if (match := cls._REGEXP.match(rawMessage)) is not None: 

299 return cls(lineNumber, LineKind.WarningMessage, None, None, None, match[1]) 

300 

301 return None 

302 

303 def __str__(self) -> str: 

304 return f"{self._MESSAGE_KIND}: {self._message}" 

305 

306 

307@export 

308class VivadoCriticalWarningMessage(VivadoMessage, CriticalWarningMessage): 

309 """ 

310 This class represents an AMD/Xilinx Vivado critical warning message. 

311 """ 

312 

313 _MESSAGE_KIND: ClassVar[str] = "CRITICAL WARNING" 

314 _REGEXP: ClassVar[Pattern] = re_compile(r"""CRITICAL WARNING: \[(\w+) (\d+)-(\d+)\] (.*)""") 

315 

316 @classmethod 

317 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]: 

318 return super().Parse(lineNumber, LineKind.CriticalWarningMessage, rawMessage) 

319 

320 

321@export 

322class VivadoErrorMessage(VivadoMessage, ErrorMessage): 

323 """ 

324 This class represents an AMD/Xilinx Vivado error message. 

325 """ 

326 

327 _MESSAGE_KIND: ClassVar[str] = "ERROR" 

328 _REGEXP: ClassVar[Pattern] = re_compile(r"""ERROR: \[(\w+) (\d+)-(\d+)\] (.*)""") 

329 

330 @classmethod 

331 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]: 

332 return super().Parse(lineNumber, LineKind.ErrorMessage, rawMessage) 

333 

334 

335@export 

336class VHDLReportMessage(VivadoInfoMessage): 

337 _REGEXP: ClassVar[Pattern ] = re_compile(r"""RTL report: "(.*)" \[(.*):(\d+)\]""") 

338 

339 _reportMessage: str 

340 _sourceFile: Path 

341 _sourceLineNumber: int 

342 

343 def __init__(self, lineNumber: int, tool: str, toolID: int, messageKindID: int, rawMessage: str, reportMessage: str, sourceFile: Path, sourceLineNumber: int): 

344 super().__init__(lineNumber, LineKind.InfoMessage, tool, toolID, messageKindID, rawMessage) 

345 

346 self._reportMessage = reportMessage 

347 self._sourceFile = sourceFile 

348 self._sourceLineNumber = sourceLineNumber 

349 

350 @classmethod 

351 def Convert(cls, line: VivadoInfoMessage) -> Nullable[Self]: 

352 if (match := cls._REGEXP.match(line._message)) is not None: 352 ↛ 355line 352 didn't jump to line 355 because the condition on line 352 was always true

353 return cls(line._lineNumber, line._toolName, line._toolID, line._messageKindID, line._message, match[1], Path(match[2]), int(match[3])) 

354 

355 return None 

356 

357 

358@export 

359class VHDLAssertionMessage(VHDLReportMessage): 

360 _REGEXP: ClassVar[Pattern ] = re_compile(r"""RTL assertion: "(.*)" \[(.*):(\d+)\]""") 

361 

362 

363@export 

364class VivadoTclCommand(Line): 

365 _PREFIX: ClassVar[str] = "Command:" 

366 

367 _command: str 

368 _args: Tuple[str, ...] 

369 

370 def __init__(self, lineNumber: int, command: str, arguments: Tuple[str, ...], tclCommand: str) -> None: 

371 super().__init__(lineNumber, LineKind.Command, tclCommand) 

372 

373 self._command = command 

374 self._args = arguments 

375 

376 @classmethod 

377 def Parse(cls, lineNumber: int, rawMessage: str) -> Nullable[Self]: 

378 command = rawMessage[len(cls._PREFIX) + 1:] 

379 args = command.split() 

380 

381 return cls(lineNumber, args[0], tuple(args[1:]), command) 

382 

383 def __str__(self) -> str: 

384 return f"{self._PREFIX} {self._command} {' '.join(self._args)}" 

385 

386 

387@export 

388class ProcessingState(Flag): 

389 Processed = 1 

390 Skipped = 2 

391 EmptyLine = 4 

392 CommentLine = 8 

393 DelimiterLine = 16 

394 TableLine = 32 

395 TableHeader = 64 

396 Reprocess = 512 

397 Last = 1024 

398 

399 

400@export 

401class BaseProcessor(metaclass=ExtendedType, slots=True): 

402 # _parsers: Dict[Type["Parser"], "Parsers"] 

403 # _state: Callable[[int, str], bool] 

404 _duration: float 

405 

406 _infoMessages: List[VivadoInfoMessage] 

407 _warningMessages: List[VivadoWarningMessage] 

408 _criticalWarningMessages: List[VivadoCriticalWarningMessage] 

409 _errorMessages: List[VivadoErrorMessage] 

410 _toolIDs: Dict[int, str] 

411 _toolNames: Dict[str, int] 

412 _messagesByID: Dict[int, Dict[int, List[VivadoMessage]]] 

413 

414 def __init__(self): 

415 # self._parsers = {} 

416 # self._state = None 

417 self._duration = 0.0 

418 

419 self._infoMessages = [] 

420 self._warningMessages = [] 

421 self._criticalWarningMessages = [] 

422 self._errorMessages = [] 

423 self._toolIDs = {} 

424 self._toolNames = {} 

425 self._messagesByID = {} 

426 

427 @readonly 

428 def Duration(self) -> float: 

429 return self._duration 

430 

431 @readonly 

432 def ToolIDs(self) -> Dict[int, str]: 

433 return self._toolIDs 

434 

435 @readonly 

436 def ToolNames(self) -> Dict[str, int]: 

437 return self._toolNames 

438 

439 @readonly 

440 def MessagesByID(self) -> Dict[int, Dict[int, List[VivadoMessage]]]: 

441 return self._messagesByID 

442 

443 @readonly 

444 def InfoMessages(self) -> List[VivadoInfoMessage]: 

445 return self._infoMessages 

446 

447 @readonly 

448 def WarningMessages(self) -> List[VivadoWarningMessage]: 

449 return self._warningMessages 

450 

451 @readonly 

452 def CriticalWarningMessages(self) -> List[VivadoCriticalWarningMessage]: 

453 return self._criticalWarningMessages 

454 

455 @readonly 

456 def ErrorMessages(self) -> List[VivadoErrorMessage]: 

457 return self._errorMessages 

458 

459 @readonly 

460 def VHDLReportMessages(self) -> List[VHDLReportMessage]: 

461 if 8 in self._messagesByID: 

462 if 6031 in (synthMessages := self._messagesByID[8]): 

463 return [message for message in synthMessages[6031]] 

464 

465 return [] 

466 

467 @readonly 

468 def VHDLAssertMessages(self) -> List[VHDLReportMessage]: 

469 if 8 in self._messagesByID: 

470 if 63 in (synthMessages := self._messagesByID[8]): 

471 return [message for message in synthMessages[63]] 

472 

473 return [] 

474 

475 # def __getitem__(self, item: Type["Parser"]) -> "Parsers": 

476 # return self._parsers[item] 

477 

478 def _AddMessageByID(self, message: VivadoMessage) -> None: 

479 if message._toolID in self._messagesByID: 

480 sub = self._messagesByID[message._toolID] 

481 if message._messageKindID in sub: 

482 sub[message._messageKindID].append(message) 

483 else: 

484 sub[message._messageKindID] = [message] 

485 else: 

486 self._toolIDs[message._toolID] = message._toolName 

487 self._toolNames[message._toolName] = message._toolID 

488 self._messagesByID[message._toolID] = {message._messageKindID: [message]} 

489 

490 def LineClassification(self, documentSlicer: Generator[Union[Line, ProcessorException], Line, None]) -> Generator[Union[Line, ProcessorException], str, None]: 

491 # Initialize generator 

492 next(documentSlicer) 

493 

494 # wait for first line 

495 rawMessageLine = yield 

496 lineNumber = 0 

497 _errorMessage = "Unknown processing error." 

498 errorMessage = _errorMessage 

499 

500 while rawMessageLine is not None: 500 ↛ exitline 500 didn't return from function 'LineClassification' because the condition on line 500 was always true

501 lineNumber += 1 

502 rawMessageLine = rawMessageLine.rstrip() 

503 errorMessage = _errorMessage 

504 

505 if rawMessageLine.startswith(VivadoInfoMessage._MESSAGE_KIND): 

506 if (line := VivadoInfoMessage.Parse(lineNumber, rawMessageLine)) is None: 506 ↛ 507line 506 didn't jump to line 507 because the condition on line 506 was never true

507 line = VivadoIrregularInfoMessage.Parse(lineNumber, rawMessageLine) 

508 

509 errorMessage = f"Line starting with 'INFO' was not a VivadoInfoMessage." 

510 elif rawMessageLine.startswith(VivadoWarningMessage._MESSAGE_KIND): 

511 if (line := VivadoWarningMessage.Parse(lineNumber, rawMessageLine)) is None: 511 ↛ 512line 511 didn't jump to line 512 because the condition on line 511 was never true

512 line = VivadoIrregularWarningMessage.Parse(lineNumber, rawMessageLine) 

513 

514 errorMessage = f"Line starting with 'WARNING' was not a VivadoWarningMessage." 

515 elif rawMessageLine.startswith(VivadoCriticalWarningMessage._MESSAGE_KIND): 515 ↛ 516line 515 didn't jump to line 516 because the condition on line 515 was never true

516 line = VivadoCriticalWarningMessage.Parse(lineNumber, rawMessageLine) 

517 

518 errorMessage = f"Line starting with 'CRITICAL WARNING' was not a VivadoCriticalWarningMessage." 

519 elif rawMessageLine.startswith(VivadoErrorMessage._MESSAGE_KIND): 519 ↛ 520line 519 didn't jump to line 520 because the condition on line 519 was never true

520 line = VivadoErrorMessage.Parse(lineNumber, rawMessageLine) 

521 

522 errorMessage = f"Line starting with 'ERROR' was not a VivadoErrorMessage." 

523 elif len(rawMessageLine) == 0: 

524 line = Line(lineNumber, LineKind.Empty, rawMessageLine) 

525 elif rawMessageLine.startswith("Command: "): 

526 line = VivadoTclCommand.Parse(lineNumber, rawMessageLine) 

527 else: 

528 line = Line(lineNumber, LineKind.Unprocessed, rawMessageLine) 

529 errorMessage = "Line starting with 'Command:' was not a VivadoTclCommand." 

530 

531 if line is None: 531 ↛ 532line 531 didn't jump to line 532 because the condition on line 531 was never true

532 line = Line(lineNumber, LineKind.ProcessorError, rawMessageLine) 

533 

534 line = documentSlicer.send(line) 

535 

536 if isinstance(line, VivadoMessage): 

537 self._AddMessageByID(line) 

538 if isinstance(line, InfoMessage): 

539 self._infoMessages.append(line) 

540 elif isinstance(line, WarningMessage): 540 ↛ 542line 540 didn't jump to line 542 because the condition on line 540 was always true

541 self._warningMessages.append(line) 

542 elif isinstance(line, CriticalWarningMessage): 

543 self._criticalWarningMessages.append(line) 

544 elif isinstance(line, ErrorMessage): 

545 self._errorMessages.append(line) 

546 

547 if line._kind is LineKind.ProcessorError: 547 ↛ 548line 547 didn't jump to line 548 because the condition on line 547 was never true

548 line = ClassificationException(errorMessage, rawMessageLine, line) 

549 

550 rawMessageLine = yield line 

551 

552 

553@export 

554class Parser(metaclass=ExtendedType, slots=True): 

555 _processor: "BaseProcessor" 

556 

557 def __init__(self, processor: "BaseProcessor"): 

558 self._processor = processor 

559 

560 @readonly 

561 def Processor(self) -> "BaseProcessor": 

562 return self._processor 

563 

564 

565@export 

566class Preamble(Parser): 

567 _toolVersion: Nullable[YearReleaseVersion] 

568 _startDatetime: Nullable[datetime] 

569 

570 _VERSION: ClassVar[Pattern] = re_compile(r"""# Vivado v(\d+\.\d(\.\d)?) \(64-bit\)""") 

571 _STARTTIME: ClassVar[Pattern] = re_compile(r"""# Start of session at: (\w+ \w+ \d+ \d+:\d+:\d+ \d+)""") 

572 

573 def __init__(self, processor: "BaseProcessor"): 

574 super().__init__(processor) 

575 

576 self._toolVersion = None 

577 self._startDatetime = None 

578 

579 @readonly 

580 def ToolVersion(self) -> YearReleaseVersion: 

581 return self._toolVersion 

582 

583 @readonly 

584 def StartDatetime(self) -> datetime: 

585 return self._startDatetime 

586 

587 def Generator(self, line: Line) -> Generator[Line, Line, Line]: 

588 rawMessage = line._message 

589 if rawMessage.startswith("#----"): 589 ↛ 592line 589 didn't jump to line 592 because the condition on line 589 was always true

590 line._kind = LineKind.SectionDelimiter 

591 else: 

592 line._kind |= LineKind.ProcessorError 

593 

594 line = yield line 

595 

596 while line is not None: 596 ↛ 613line 596 didn't jump to line 613 because the condition on line 596 was always true

597 rawMessage = line._message 

598 

599 if (match := self._VERSION.match(rawMessage)) is not None: 

600 self._toolVersion = YearReleaseVersion.Parse(match[1]) 

601 line._kind = LineKind.Normal 

602 elif (match := self._STARTTIME.match(rawMessage)) is not None: 

603 self._startDatetime = datetime.strptime(match[1], "%a %b %d %H:%M:%S %Y") 

604 line._kind = LineKind.Normal 

605 elif rawMessage.startswith("#----"): 

606 line._kind = LineKind.SectionDelimiter | LineKind.Last 

607 break 

608 else: 

609 line._kind = LineKind.Verbose 

610 

611 line = yield line 

612 

613 check = yield line 

614 

615 

616@export 

617class BaseDocument(BaseProcessor): 

618 _logfile: Path 

619 _lines: List[Line] 

620 # _duration: float 

621 

622 def __init__(self, logfile: Path) -> None: 

623 super().__init__() 

624 

625 self._logfile = logfile 

626 self._lines = [] 

627 

628 def Parse(self) -> None: 

629 with Stopwatch() as sw: 

630 with self._logfile.open("r", encoding="utf-8") as f: 

631 content = f.read() 

632 

633 lines = content.splitlines() 

634 next(generator := self.LineClassification(self.DocumentSlicer())) 

635 self._lines = [generator.send(rawLine) for rawLine in lines] 

636 

637 self._duration = sw.Duration 

638 

639 def DocumentSlicer(self, line: Line) -> Generator[Union[Line, ProcessorException], Line, Line]: 

640 while line is not None: 

641 if line._kind is LineKind.Unprocessed: 

642 line._kind = LineKind.Normal 

643 

644 line = yield line