Skip to content

Commit

Permalink
Removed legacy code (Python 2); more typing.
Browse files Browse the repository at this point in the history
  • Loading branch information
peteradrichem committed Jan 13, 2025
1 parent b626b0a commit 7d870f0
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 201 deletions.
14 changes: 8 additions & 6 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ Changelog

This document records all notable changes to `Xul <https://xul.readthedocs.io/>`_.

`Unreleased <https://github.com/peteradrichem/Xul/compare/2.5.1...py3k>`_ (2025-01-12)
`Unreleased <https://github.com/peteradrichem/Xul/compare/2.5.1...py3k>`_ (2025-01-13)
--------------------------------------------------------------------------------------
* Drop support for Python <= 3.8.
* Drop support for Python < 3.9.
* :doc:`xp <xp>`: fix boolean result (Python >= 3.12).
* :doc:`xp <xp>`: fix string result representation (Python 3).
* :doc:`xp <xp>`: improved printing of namespaces.
* Better error messages.
* Code checks: ruff, black, isort, mypy.
* Updated Sphinx configuration.
* Test script for local testing with Docker Compose.
* GitHub Action: code checks.
* Typing.
* Updated Sphinx configuration.
* Output formatting (f-strings).
* :doc:`xp <xp>`: fix boolean result (Python >= 3.12).
* :doc:`xp <xp>`: fix string result representation (Python 3).
* :doc:`xp <xp>`: improved printing of namespaces.

`2.5.1 <https://github.com/peteradrichem/Xul/compare/2.5.0...2.5.1>`_ (2024-12-26)
----------------------------------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ version = { attr = "xul.__version__" }
[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
xul = [
"py.typed"
]

[tool.black]
color = true
line-length = 100
Expand Down
10 changes: 4 additions & 6 deletions src/xul/cmd/ppx.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
"""Pretty Print XML source in human readable form."""

from argparse import ArgumentParser
import argparse
from sys import stderr, stdin

# pylint: disable=no-name-in-module
from lxml.etree import XMLParser

# Import my own modules.
from .. import __version__
from ..log import setup_logger_console
from ..ppxml import pp_xml


def parse_cl():
def parse_cl() -> argparse.Namespace:
"""Parse the command line for options and XML sources."""
parser = ArgumentParser(description=__doc__)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("-V", "--version", action="version", version="%(prog)s " + __version__)
parser.add_argument(
"xml_sources",
Expand All @@ -41,7 +39,7 @@ def parse_cl():
return parser.parse_args()


def main():
def main() -> None:
"""Entry point for command line script ppx."""
# Logging to the console.
setup_logger_console()
Expand Down
59 changes: 27 additions & 32 deletions src/xul/cmd/transform.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
"""Transform XML source with XSLT."""

import argparse
import sys
from argparse import ArgumentParser, FileType
from typing import TextIO, Union

# pylint: disable=no-name-in-module
from lxml.etree import XMLParser, tostring
from lxml import etree

# Import my own modules.
from .. import __version__
from ..log import setup_logger_console
from ..xsl import build_xsl_transform, xml_transformer


def parse_cl():
def parse_cl() -> argparse.Namespace:
"""Parse the command line for options, XSLT source and XML sources."""
parser = ArgumentParser(description=__doc__)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("-V", "--version", action="version", version="%(prog)s " + __version__)
parser.add_argument("xslt_source", help="XSLT source (file, http://...)")
parser.add_argument(
"xml_source",
nargs="?",
default=sys.stdin,
type=FileType("r"),
type=argparse.FileType("r"),
help="XML source (file, <stdin>, http://...)",
)
output_group = parser.add_mutually_exclusive_group(required=False)
Expand All @@ -45,7 +44,7 @@ def parse_cl():
return parser.parse_args()


def print_result(result):
def print_result(result) -> None:
"""Print transformation result (catch broken pipe and lookup errors)."""
try:
print(result)
Expand All @@ -56,47 +55,45 @@ def print_result(result):
sys.stderr.write(f"Cannot print XSLT result (LookupError): {e}\n")


def save_to_file(result, target_file):
def save_to_file(result, target_file: str) -> None:
"""Save transformation result to file."""
file_mode = "bx"
try:
with open(target_file, file_mode) as file_object:
with open(target_file, "bx") as file_object:
file_object.write(result)
except OSError as e:
sys.stderr.write(f"Saving result to {target_file} failed: {e.strerror}\n")
sys.exit(80)


def output_xslt(xml_source, transformer, parser, args):
def output_xslt(
xml_source: Union[TextIO, str],
transformer: etree.XSLT,
parser: etree.XMLParser,
args: argparse.Namespace,
) -> None:
"""Print or save the result of an XSL Transformation.
xml_source -- XML file, file-like object or URL
transformer -- XSL Transformer (lxml.etree.XSLT)
parser -- XML parser (lxml.etree.XMLParser)
args -- Command-line arguments
:param xml_source: XML file, file-like object or URL
:param transformer: XSL Transformer
:param parser: XML parser
:param args: command-line arguments
"""
result = xml_transformer(xml_source, transformer, parser)
if not result:
return None

# https://lxml.de/apidoc/lxml.etree.html#lxml.etree._XSLTResultTree
# _XSLTResultTree (./src/lxml/xslt.pxi):
if result.getroot() is None:
# Result is not an ElementTree.
# Result (lxml.etree._XSLTResultTree) is not an ElementTree.
if args.file:
save_to_file(result, args.file)
else:
print_result(result)
return None

# https://lxml.de/xpathxslt.html#xslt-result-objects
# https://lxml.de/apidoc/lxml.etree.html#lxml.etree._XSLTResultTree
# _XSLTResultTree (./src/lxml/xslt.pxi):
if args.xsl_output:
# https://lxml.de/apidoc/lxml.etree.html#lxml.etree.XSLT
# XSLT.tostring(). Deprecated: use str(result_tree) instead.
#
# Python 2: str(_XSLTResultTree) == bytes(_XSLTResultTree).
#
# Python 3: str(_XSLTResultTree) != bytes(_XSLTResultTree).
if args.file:
save_to_file(result, args.file)
else:
Expand All @@ -110,17 +107,15 @@ def output_xslt(xml_source, transformer, parser, args):
# For normal byte encodings, the tostring() function automatically adds
# a declaration as needed that reflects the encoding of the returned string.
else:
etree_result = tostring(
# lxml.etree.tostring returns bytes.
etree_result = etree.tostring(
result, encoding=result.docinfo.encoding, xml_declaration=args.declaration
)
if args.file:
save_to_file(etree_result, args.file)
else:
# lxml.etree.tostring returns bytes (bytestring).
if not isinstance(etree_result, str):
# Bytes => unicode string (Python 3).
etree_result = etree_result.decode(result.docinfo.encoding)
print_result(etree_result)
# Bytes => unicode string (Python 3).
print_result(etree_result.decode(result.docinfo.encoding)) # type: ignore[arg-type,union-attr]

return None

Expand All @@ -145,6 +140,6 @@ def main():
sys.exit(50)

# Initialise XML parser.
parser = XMLParser()
parser = etree.XMLParser()
# Transform XML source with XSL Transformer.
output_xslt(args.xml_source, transformer, parser, args)
26 changes: 19 additions & 7 deletions src/xul/cmd/validate.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
"""Validate XML source with XSD, DTD or RELAX NG."""

import argparse
import sys
from argparse import ArgumentParser
from typing import TextIO, Union

from lxml import etree

# Import my own modules.
from .. import __version__
from ..log import setup_logger_console
from ..validate import build_dtd, build_relaxng, build_xml_schema, validate_xml


def parse_cl():
def parse_cl() -> argparse.Namespace:
"""Parse the command line for options and XML sources."""
parser = ArgumentParser(description=__doc__)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("-V", "--version", action="version", version="%(prog)s " + __version__)
lang_group = parser.add_mutually_exclusive_group(required=True)
lang_group.add_argument(
Expand Down Expand Up @@ -55,8 +57,17 @@ def parse_cl():
return parser.parse_args()


def apply_validator(xml_source, validator, args):
"""Apply XML validator on an XML source."""
def apply_validator(
xml_source: Union[TextIO, str],
validator: Union[etree.XMLSchema, etree.DTD, etree.RelaxNG],
args: argparse.Namespace,
) -> None:
"""Apply XML validator on an XML source.
:param xml_source: XML file, file-like object or URL
:param validator: XMLSchema, DTD or RELAX NG validator
:param args: command-line arguments
"""
if args.validated_files or args.invalidated_files:
valid = validate_xml(xml_source, validator, silent=True)
if (valid and args.validated_files) or (not valid and args.invalidated_files):
Expand All @@ -69,7 +80,7 @@ def apply_validator(xml_source, validator, args):
validate_xml(xml_source, validator)


def main():
def main() -> None:
"""Entry point for command line script validate."""
# Logging to the console.
setup_logger_console()
Expand All @@ -78,6 +89,7 @@ def main():
args = parse_cl()

# XSD, DTD or RelaxNG Validator?
validator: Union[etree.XMLSchema, etree.DTD, etree.RelaxNG]
if args.xsd_source:
validator = build_xml_schema(args.xsd_source)
elif args.dtd_source:
Expand Down
8 changes: 1 addition & 7 deletions src/xul/cmd/xp.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,6 @@ def element_repr(node) -> str:
if node.text:
if node.text.isspace():
return f"<{node.tag}> contains whitespace"
if not isinstance(node.text, str):
# Python 2 Unicode naar Bytestring.
return f"<{node.tag}> contains {node.text.encode('utf-8')}"
return f"<{node.tag}> contains '{node.text}'"

return f"<{node.tag}> is empty"
Expand Down Expand Up @@ -466,10 +463,7 @@ def main() -> None:
# Command line.
args = parse_cl()

# XPath expression.
if isinstance(args.xpath_expr, bytes):
# Python 2 Bytestring to Unicode.
args.xpath_expr = args.xpath_expr.decode("utf-8")
# Valid XPath expression?
if not build_xpath(args.xpath_expr):
sys.exit(60)

Expand Down
29 changes: 17 additions & 12 deletions src/xul/etree.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,34 @@
https://docs.python.org/3/library/xml.etree.elementtree.html
lxml ElementTree
https://lxml.de
https://lxml.de/apidoc/lxml.etree.html#lxml.etree._ElementTree
https://lxml.de/compatibility.html
The lxml.etree Tutorial
https://lxml.de/tutorial.html
"""

import sys
from logging import getLogger
from typing import Optional, TextIO, Union

# pylint: disable=no-member
from lxml import etree

# Module logging initialisation.
logger = getLogger(__name__)


def build_etree(xml_source, parser=None, lenient=True, silent=False):
def build_etree(
xml_source: Union[TextIO, str],
parser: Optional[etree.XMLParser] = None,
lenient: bool = True,
silent: bool = False,
) -> Optional[etree._ElementTree]:
"""Parse XML source into an ElementTree.
xml_source -- XML file, file-like object or URL
parser -- (optional) XML parser (lxml.etree.XMLParser)
lenient -- log XMLSyntaxError as warnings instead of errors
silent -- no logging
:param xml_source: XML file, file-like object or URL
:param parser: (optional) XML parser
:param lenient: log XMLSyntaxError as warnings instead of errors
:param silent: disable logging
Return ElementTree (lxml.etree._ElementTree) on success.
Return None on error.
Expand All @@ -42,7 +47,8 @@ def build_etree(xml_source, parser=None, lenient=True, silent=False):

try:
etree.clear_error_log()
el_tree = etree.parse(xml_source, parser)
return etree.parse(xml_source, parser)

# Catch XML syntax errors.
# https://lxml.de/api.html#error-handling-on-exceptions
# error log copy attached to the exception: global error log of all errors
Expand All @@ -62,7 +68,6 @@ def build_etree(xml_source, parser=None, lenient=True, silent=False):
else:
name = xml_source
xml_type = "file"

xmllogger("%s is not a valid XML %s:", name, xml_type)

# Parsers have an error_log property that lists the errors and warnings
Expand All @@ -76,15 +81,15 @@ def build_etree(xml_source, parser=None, lenient=True, silent=False):
else:
xmllogger("line %i, column %i: %s", e.line, e.column, e.message)
return None

# Catch UnicodeDecodeError exceptions, for example:
# "'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"
except UnicodeDecodeError as e:
logger.error(e)
return None

# Catch OSError exceptions, for example:
# "failed to load external entity" (lxml.etree._raiseParseError)
except OSError as e:
logger.error(e)
return None

return el_tree
Loading

0 comments on commit 7d870f0

Please sign in to comment.