22
33import asyncio
44from collections .abc import Iterator
5+ from dataclasses import dataclass
56from datetime import date
67import logging
78from pathlib import Path
@@ -215,6 +216,85 @@ async def run_python_checks(
215216 return check_results
216217
217218
219+ @dataclass
220+ class ValidationResult :
221+ """Normalized result of RNG validation.
222+
223+ :ivar success: True when validation passed.
224+ :ivar exit_code: Exit code to return when validation fails (0 for success).
225+ :ivar message: Optional human-readable message describing the failure.
226+ """
227+
228+ success : bool
229+ exit_code : int
230+ message : str = ""
231+
232+
233+ def build_shortname (filepath : Path | str ) -> str :
234+ """Return a shortened display name for ``filepath``.
235+
236+ :param filepath: Path-like object to shorten.
237+ :returns: Shortened display name (last two path components or full path).
238+ """
239+ path_obj = Path (filepath )
240+ return "/" .join (path_obj .parts [- 2 :]) if len (path_obj .parts ) >= 2 else str (filepath )
241+
242+
243+ async def run_validation (filepath : Path | str , method : str ) -> ValidationResult :
244+ """Run RNG validation using the selected method and normalize result.
245+
246+ :param filepath: Path to the XML file to validate.
247+ :param method: Validation method name ("jing" or "lxml").
248+ :returns: A :class:`ValidationResult` describing the outcome.
249+ """
250+ path_obj = Path (filepath )
251+ if method == "lxml" :
252+ rng_success , rng_output = await asyncio .to_thread (
253+ validate_rng_lxml , path_obj , XMLDATADIR / "product-config-schema.rng"
254+ )
255+ if rng_success :
256+ return ValidationResult (True , 0 , "" )
257+ return ValidationResult (False , 10 , rng_output or "" )
258+
259+ if method == "jing" :
260+ jing_result = await validate_rng (path_obj , idcheck = True )
261+ if jing_result .returncode != 0 :
262+ output = (jing_result .stdout or "" ) + (jing_result .stderr or "" )
263+ return ValidationResult (False , 10 , output .strip ())
264+ return ValidationResult (True , 0 , "" )
265+
266+ return ValidationResult (False , 11 , f"Unknown validation method: { method } " )
267+
268+
269+ async def parse_tree (filepath : Path | str ) -> etree ._ElementTree :
270+ """Parse XML file using lxml in a background thread.
271+
272+ Exceptions from :func:`lxml.etree.parse` (for example
273+ :class:`lxml.etree.XMLSyntaxError`) are propagated to the caller.
274+
275+ :param filepath: Path to the XML file to parse.
276+ :returns: Parsed :class:`lxml.etree._ElementTree`.
277+ """
278+ return await asyncio .to_thread (etree .parse , str (filepath ), parser = None )
279+
280+
281+ async def run_checks_and_display (
282+ tree : etree ._ElementTree , shortname : str , context : DocBuildContext , max_len : int
283+ ) -> bool :
284+ """Execute registered Python checks and print formatted results.
285+
286+ :param tree: Parsed XML tree to check.
287+ :param shortname: Short name used for display output.
288+ :param context: :class:`DocBuildContext` used to read verbosity.
289+ :param max_len: Maximum length used for aligned output.
290+ :returns: True when all checks succeeded (or when no checks are registered).
291+ """
292+ check_results = await run_python_checks (tree )
293+ if check_results :
294+ display_results (shortname , check_results , context .verbose , max_len )
295+ return all (result .success for _ , result in check_results )
296+
297+
218298async def process_file (
219299 filepath : Path | str ,
220300 context : DocBuildContext ,
@@ -228,63 +308,23 @@ async def process_file(
228308 :param rng_schema_path: Optional path to an RNG schema for validation.
229309 :return: An exit code (0 for success, non-zero for failure).
230310 """
231- # Shorten the filename to last two parts for display
232- path_obj = Path (filepath )
233- shortname = (
234- "/" .join (path_obj .parts [- 2 :]) if len (path_obj .parts ) >= 2 else str (filepath )
235- )
236-
237- # IDEA: Should we replace jing and validate with etree.RelaxNG?
238- #
239- # 1. RNG Validation
240- validation_method = context .validation_method
311+ shortname = build_shortname (filepath )
241312
242- if validation_method == "lxml" :
243- # Use lxml-based validator (requires .rng schema)
244- rng_success , rng_output = await asyncio .to_thread (
245- validate_rng_lxml ,
246- path_obj ,
247- XMLDATADIR / "product-config-schema.rng" ,
248- )
249- elif validation_method == "jing" :
250- # Use existing jing-based validator (.rnc or .rng)
251- jing_result = await validate_rng (path_obj , idcheck = True )
252- else :
313+ # 1. RNG Validation (normalized)
314+ validation = await run_validation (filepath , context .validation_method )
315+ if not validation .success :
253316 console_err .print (
254317 f"{ shortname :<{max_len }} : RNG validation => [red]failed[/red]"
255318 )
256- console_err .print (
257- f" [bold red]Error:[/] Unknown validation method: { validation_method } "
258- )
259- return 11 # Custom error code for unknown validation method
319+ if validation .message :
320+ console_err .print (f" [bold red]Error:[/] { validation .message } " )
321+ return validation .exit_code
260322
261- # Handle validation result for jing
262- if validation_method == "jing" :
263- if jing_result .returncode != 0 :
264- console_err .print (
265- f"{ shortname :<{max_len }} : RNG validation => [red]failed[/red]"
266- )
267- output = (jing_result .stdout or "" ) + (jing_result .stderr or "" )
268- if output :
269- console_err .print (f" [bold red]Error:[/] { output .strip ()} " )
270- return 10 # Specific error code for RNG failure
271-
272- # Handle validation result for lxml
273- if validation_method == "lxml" :
274- if not rng_success :
275- console_err .print (
276- f"{ shortname :<{max_len }} : RNG validation => [red]failed[/red]"
277- )
278- if rng_output :
279- console_err .print (f" [bold red]Error:[/] { rng_output } " )
280- return 10
281-
282- # 2. Python-based checks
323+ # 2. Parse XML and run Python checks
283324 try :
284- tree = await asyncio . to_thread ( etree . parse , str ( filepath ), parser = None )
325+ tree = await parse_tree ( filepath )
285326
286327 except etree .XMLSyntaxError as err :
287- # This can happen if xmllint passes but lxml's parser is stricter.
288328 console_err .print (
289329 f"{ shortname :<{max_len }} : XML Syntax Error => [red]failed[/red]"
290330 )
@@ -295,14 +335,8 @@ async def process_file(
295335 console_err .print (f" [bold red]Error:[/] { err } " )
296336 return 200
297337
298- # Run all checks for this file
299- check_results = await run_python_checks (tree )
300-
301- if check_results :
302- # Display results based on verbosity level
303- display_results (shortname , check_results , context .verbose , max_len )
304-
305- return 0 if all (result .success for _ , result in check_results ) else 1
338+ success = await run_checks_and_display (tree , shortname , context , max_len )
339+ return 0 if success else 1
306340
307341
308342async def process (
0 commit comments