Files
km-hershey/deps/inkex/tester/inx.py
2026-01-17 23:40:16 -05:00

183 lines
6.4 KiB
Python

#!/usr/bin/env python3
# coding=utf-8
"""
Test elements extra logic from svg xml lxml custom classes.
"""
import os
import sys
from importlib import resources
from lxml import etree, isoschematron
from ..utils import PY3
from ..inx import InxFile
INTERNAL_ARGS = ("help", "output", "id", "selected-nodes")
ARG_TYPES = {
"Boolean": "bool",
"Color": "color",
"str": "string",
"int": "int",
"float": "float",
}
class InxMixin:
"""Tools for Testing INX files, use as a mixin class:
class MyTests(InxMixin, TestCase):
def test_inx_file(self):
self.assertInxIsGood("some_inx_file.inx")
"""
def assertInxIsGood(self, inx_file): # pylint: disable=invalid-name
"""Test the inx file for consistancy and correctness"""
self.assertTrue(PY3, "INX files can only be tested in python3")
inx = InxFile(inx_file)
if "help" in inx.ident or inx.script.get("interpreter", None) != "python":
return
cls = inx.extension_class
# Check class can be matched in python file
self.assertTrue(cls, f"Can not find class for {inx.filename}")
# Check name is reasonable for the class
if not cls.multi_inx:
self.assertEqual(
cls.__name__,
inx.slug,
f"Name of extension class {cls.__module__}.{cls.__name__} "
f"is different from ident {inx.slug}",
)
self.assertParams(inx, cls)
def assertParams(self, inx, cls): # pylint: disable=invalid-name
"""Confirm the params in the inx match the python script
.. versionchanged:: 1.2
Also checks that the default values are identical"""
params = {param.name: self.parse_param(param) for param in inx.params}
args = dict(self.introspect_arg_parser(cls().arg_parser))
mismatch_a = list(set(params) ^ set(args) & set(params))
mismatch_b = list(set(args) ^ set(params) & set(args))
self.assertFalse(
mismatch_a, f"{inx.filename}: Inx params missing from arg parser"
)
self.assertFalse(
mismatch_b, f"{inx.filename}: Script args missing from inx xml"
)
for param in args:
if params[param]["type"] and args[param]["type"]:
self.assertEqual(
params[param]["type"],
args[param]["type"],
f"Type is not the same for {inx.filename}:param:{param}",
)
inxdefault = params[param]["default"]
argsdefault = args[param]["default"]
if inxdefault and argsdefault:
# for booleans, the inx is lowercase and the param is uppercase
if params[param]["type"] == "bool":
argsdefault = str(argsdefault).lower()
elif params[param]["type"] not in ["string", None, "color"] or args[
param
]["type"] in ["int", "float"]:
# try to parse the inx value to compare numbers to numbers
inxdefault = float(inxdefault)
if args[param]["type"] == "color" or callable(args[param]["default"]):
# skip color, method types
continue
self.assertEqual(
argsdefault,
inxdefault,
f"Default value is not the same for {inx.filename}:param:{param}",
)
inxchoices = params[param]["choices"]
argschoices = args[param]["choices"]
if argschoices is not None and len(argschoices) > 0:
assert set(inxchoices).issubset(argschoices), (
f"params don't match: inx={inxchoices}, py={argschoices}"
)
def introspect_arg_parser(self, arg_parser):
"""Pull apart the arg parser to find out what we have in it"""
for action in arg_parser._optionals._actions: # pylint: disable=protected-access
for opt in action.option_strings:
# Ignore params internal to inkscape (thus not in the inx)
if opt.startswith("--") and opt[2:] not in INTERNAL_ARGS:
yield (opt[2:], self.introspect_action(action))
@staticmethod
def introspect_action(action):
"""Pull apart a single action to get at the juicy insides"""
return {
"type": ARG_TYPES.get((action.type or str).__name__, "string"),
"default": action.default,
"choices": action.choices,
"help": action.help,
}
@staticmethod
def parse_param(param):
"""Pull apart the param element in the inx file"""
if param.param_type in ("optiongroup", "notebook"):
options = param.options
return {
"type": None,
"choices": options,
"default": options and options[0] or None,
}
param_type = param.param_type
if param.param_type in ("path",):
param_type = "string"
return {
"type": param_type,
"default": param.text,
"choices": None,
}
def assertInxSchemaValid(self, inx_file): # pylint: disable=invalid-name
"""Validate inx file schema."""
self.assertTrue(INX_SCHEMAS, "no schema files found")
with open(inx_file, "rb") as fp:
inx_doc = etree.parse(fp)
for schema_name, schema in INX_SCHEMAS.items():
with self.subTest(schema_file=schema_name):
schema.assert_(inx_doc)
def _load_inx_schemas():
_SCHEMA_CLASSES = {
".rng": etree.RelaxNG,
".schema": isoschematron.Schematron,
}
if sys.version_info > (3, 9):
def _contents(pkg):
return [path.name for path in resources.files(pkg).iterdir()]
else:
_contents = resources.contents
for name in _contents(__package__):
_, ext = os.path.splitext(name)
schema_class = _SCHEMA_CLASSES.get(ext)
if schema_class is None:
continue
if sys.version_info > (3, 9):
def _open_binary(pkg, res):
return resources.files(pkg).joinpath(res).open("rb")
else:
_open_binary = resources.open_binary
with _open_binary(__package__, name) as fp:
schema_doc = etree.parse(fp)
yield name, schema_class(schema_doc)
INX_SCHEMAS = dict(_load_inx_schemas())