Skip to content

Commit

Permalink
streaming parser; reporting on last visited tag; maybe fix through no…
Browse files Browse the repository at this point in the history
…t re-enconding file's contents

simplified, rearranged code
allowing garbage collection
capturing error context and providing it to user
  • Loading branch information
why-not-try-calmer committed Aug 29, 2023
1 parent 5885c49 commit 6dc3f96
Showing 1 changed file with 79 additions and 9 deletions.
88 changes: 79 additions & 9 deletions docker-qgis/process_projectfile.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import io
import logging
from pathlib import Path
from typing import Dict
from typing import NamedTuple
from xml.etree import ElementTree

from qfieldcloud.qgis.utils import BaseException, get_layers_data, layers_data_to_string
Expand Down Expand Up @@ -37,20 +38,89 @@ class FailedThumbnailGenerationException(BaseException):
message = "Failed to generate project thumbnail:\n%(reason)s"


class LocationError(NamedTuple):
line: int
column: int

@classmethod
def from_exception_msg(
cls, maybe_invalid_token_error_msg: str
) -> "LocationError" | None:
"""Get column and line numbers from the provided error message."""
if "invalid token" not in maybe_invalid_token_error_msg.lower():
logger.error("Unable to find 'invalid token' details in the given message")
return None

_, details = maybe_invalid_token_error_msg.split(":")
line, column = details.split(",")
_, line_number = line.strip().split(" ")
_, column_number = column.strip().split(" ")
return cls(int(line_number), int(column_number))


class ErrorReport(NamedTuple):
above: list[str]
below: list[str]
erroneous_line: str
culprit: bytes

@classmethod
def wrap_context(
cls, fh: io.TextIOWrapper, location: LocationError
) -> "ErrorReport":
"""Expand lines above and below error location to provide some context as feedback."""
above = []
below = []
erroneous_line = ""
culprit = b""

fh.seek(0)
for cursor_pos, line in enumerate(fh, start=1):
if location.line - cursor_pos in range(1, 3):
above.append(line)
elif cursor_pos - location.line in range(1, 3):
below.append(line)
elif location.line == cursor_pos:
erroneous_line = line
culprit = bytes(line[location.column], encoding="utf-8")

return cls(
above=above, below=below, erroneous_line=erroneous_line, culprit=culprit
)

def render(self) -> str:
above = "\n".join(self.above)
below = "\n".join(self.below)
return f"""
Lines above: ***\n{above}\n***
Erroneous line: ***\n{self.erroneous_line}\n***
Lines below: ***\n{below}
"""


def check_valid_project_file(project_filename: Path) -> None:
logger.info("Check QGIS project file validity…")

if not project_filename.exists():
raise ProjectFileNotFoundException(project_filename=project_filename)

if project_filename.suffix == ".qgs":
try:
with open(project_filename) as f:
ElementTree.fromstring(f.read())
except ElementTree.ParseError as err:
raise InvalidXmlFileException(
project_filename=project_filename, xml_error=err
)
with open(project_filename, encoding="utf-8") as fh:
try:
for event, elem in ElementTree.iterparse(fh):
continue
except ElementTree.ParseError as error:
error_msg = str(error)
location = LocationError.from_exception_msg(error_msg)
if location:
contextualized_error = ErrorReport.wrap_context(fh, location)
xml_error = contextualized_error.render()
else:
xml_error = error_msg
raise InvalidXmlFileException(
project_filename=project_filename,
xml_error=xml_error,
)
elif project_filename.suffix != ".qgz":
raise InvalidFileExtensionException(
project_filename=project_filename, extension=project_filename.suffix
Expand All @@ -71,7 +141,7 @@ def load_project_file(project_filename: Path) -> QgsProject:
return project


def extract_project_details(project: QgsProject) -> Dict[str, str]:
def extract_project_details(project: QgsProject) -> dict[str, str]:
"""Extract project details"""
logger.info("Extract project details…")

Expand Down

0 comments on commit 6dc3f96

Please sign in to comment.