Soporte a opción 'remove'

This commit is contained in:
perro tuerto 2023-03-01 16:39:25 -08:00
parent 7b27bb105d
commit 9137e35896
2 changed files with 110 additions and 41 deletions

View File

@ -81,9 +81,11 @@ To better understad what YASD does, see the inputs and outputs:
- ref: element_name - ref: element_name
maxOccurs: INTEGER|unbounded maxOccurs: INTEGER|unbounded
minOccurs: INTEGER minOccurs: INTEGER
- group_ref: group_name - group: group_name
maxOccurs: INTEGER|unbounded maxOccurs: INTEGER|unbounded
minOccurs: INTEGER minOccurs: INTEGER
remove:
- element_or_attribute_name
... ...
### Attribute (ATTR) Structure ### Attribute (ATTR) Structure
@ -295,9 +297,8 @@ Naming rules:
- Element names are case-sensitive - Element names are case-sensitive
- Element names must start with a letter or underscore - Element names must start with a letter or underscore
- Element names cannot start with the letters xml (or XML, or Xml, etc) - Element names cannot start with the letters xml (or XML, or Xml, etc)
- Element names can contain letters, digits, hyphens, underscores, and - Element names can contain letters, digits and underscores
periods - Element names cannot contain spaces or hyphens
- Element names cannot contain spaces
### `ref` ### `ref`
@ -305,6 +306,12 @@ References element or attribute by name.
Mandatory. Mandatory.
### `remove`
Removes element or attribute from group by name.
Optional.
### `restriction` ### `restriction`
Indicates accepted constrained values for attribute. Indicates accepted constrained values for attribute.

136
yasd.py
View File

@ -1,15 +1,17 @@
#!/usr/bin/env python #!/usr/bin/env python
# (c) 2023 Perro Tuerto <hi@perrotuerto.blog>. # (c) 2023 perro <hi@perrotuerto.blog>.
# Founded by Mexican Academy of Language <https://academia.org.mx>. # Founded by Mexican Academy of Language <https://academia.org.mx>.
# Licensed under GPLv3 <https://www.gnu.org/licenses/gpl-3.0.en.html>. # Licensed under GPLv3.
# Requirements: python > 3.10, pyyaml, lxml, bs4, xmlschema, rich # Requirements: python > 3.10, pyyaml, lxml, bs4, xmlschema, rich
import sys import sys
import yaml import yaml
import copy
import argparse import argparse
import xmlschema import xmlschema
import urllib.request import urllib.request
from pathlib import Path from pathlib import Path
from datetime import datetime
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from bs4.formatter import XMLFormatter from bs4.formatter import XMLFormatter
from rich.console import Console from rich.console import Console
@ -95,16 +97,17 @@ class YASD:
:param validate: if XSD is validated; 'False' by default :param validate: if XSD is validated; 'False' by default
:type validate: True or False :type validate: True or False
""" """
self.msgr = YASDMessenger(quiet=quiet, log=log) if outfile is None:
self.msgr = YASDMessenger(quiet, log)
self.outfile = None
else:
self.msgr = YASDMessenger(quiet, log, outfile.parent)
self.outfile = YASDCheck.file(outfile, self.msgr)
self.yaml = YASDCheck(indata, self.msgr).yaml self.yaml = YASDCheck(indata, self.msgr).yaml
self.formatter = XMLFormatter(indent=2) self.formatter = XMLFormatter(indent=2)
self.stdout = stdout self.stdout = stdout
self.validate = validate self.validate = validate
self.tests = set(tests) if tests is not None else None self.tests = set(tests) if tests is not None else None
if outfile is None:
self.outfile = None
else:
self.outfile = YASDCheck.file(outfile, self.msgr)
def convert(self): def convert(self):
""" """
@ -114,7 +117,7 @@ class YASD:
:rtype: bs4.element.Tag :rtype: bs4.element.Tag
""" """
self.xsd = YASDXSD(self.yaml, self.msgr).xsd self.xsd = YASDXSD(self.yaml, self.msgr).xsd
out = self.__output(self.xsd, self.msgr) out = self.__output(self.xsd)
if self.validate: if self.validate:
return YASDCheck.xsd(out, self.tests, self.msgr) return YASDCheck.xsd(out, self.tests, self.msgr)
else: else:
@ -128,7 +131,7 @@ class YASD:
:rtype: bs4.element.Tag :rtype: bs4.element.Tag
""" """
self.xml = YASDXML(self.yaml, self.msgr).xml self.xml = YASDXML(self.yaml, self.msgr).xml
return self.__output(self.xml, self.msgr, extname=".xml") return self.__output(self.xml, extname=".xml")
def document(self): def document(self):
""" """
@ -138,16 +141,14 @@ class YASD:
:rtype: str :rtype: str
""" """
self.rst = YASDRST(self.yaml, self.msgr).rst self.rst = YASDRST(self.yaml, self.msgr).rst
return self.__output(self.rst, self.msgr, extname=".rst") return self.__output(self.rst, extname=".rst")
def __output(self, outdata="", msgr=None, extname=".xsd"): def __output(self, outdata="", extname=".xsd"):
""" """
Prints in the terminal or writes into a file. Prints in the terminal or writes into a file.
:param outdata: Data for output :param outdata: Data for output
:type outdata: bs4.BeautifulSoup or str :type outdata: bs4.BeautifulSoup or str
:param messenger: Messenger object
:type messenger: None or YASDMessenger
:param str extname: Extension name for file output :param str extname: Extension name for file output
:return: Output data :return: Output data
:rtype: str :rtype: str
@ -157,7 +158,7 @@ class YASD:
outdata = f'<?xml version="1.0"?>\n{outdata}' outdata = f'<?xml version="1.0"?>\n{outdata}'
if self.stdout: if self.stdout:
if self.outfile is None: if self.outfile is None:
msgr.run(outdata) sys.stdout.write(outdata)
else: else:
suffix = self.outfile.suffix suffix = self.outfile.suffix
if len(suffix) > 0 and suffix == suffix.replace(" ", ""): if len(suffix) > 0 and suffix == suffix.replace(" ", ""):
@ -270,11 +271,11 @@ class YASDXSD:
complex_type = self.__build_complex_type(el) complex_type = self.__build_complex_type(el)
if "children" in el.keys(): if "children" in el.keys():
if "group" in el["children"][0].keys(): if "group" in el["children"][0].keys():
self.__add_references(complex_type, el) self.__add_ref(complex_type, el)
self.__add_occurs(complex_type.group, el) self.__add_occurs(complex_type.group, el)
else: else:
complex_type.append(self.__build_indicator(el)) complex_type.append(self.__build_indicator(el))
self.__add_references(complex_type, el, is_attr=True) self.__add_ref(complex_type, el, is_attr=True)
element.append(complex_type) element.append(complex_type)
self.xsd.schema.append(element) self.xsd.schema.append(element)
@ -315,7 +316,7 @@ class YASDXSD:
simple_content = self.xsd.new_tag("simpleContent", nsprefix="xs") simple_content = self.xsd.new_tag("simpleContent", nsprefix="xs")
extension = self.xsd.new_tag("extension", nsprefix="xs") extension = self.xsd.new_tag("extension", nsprefix="xs")
extension["base"] = f"xs:{el['datatype']}" extension["base"] = f"xs:{el['datatype']}"
self.__add_references(extension, el, is_attr=True) self.__add_ref(extension, el, is_attr=True)
simple_content.append(extension) simple_content.append(extension)
return simple_content return simple_content
@ -348,7 +349,7 @@ class YASDXSD:
""" """
other_tag = el["children_order"] other_tag = el["children_order"]
indicator = self.xsd.new_tag(other_tag, nsprefix="xs") indicator = self.xsd.new_tag(other_tag, nsprefix="xs")
self.__add_references(indicator, el) self.__add_ref(indicator, el)
return indicator return indicator
def __get_base(self, restrictions): def __get_base(self, restrictions):
@ -369,7 +370,7 @@ class YASDXSD:
else: else:
return "xs:integer" return "xs:integer"
def __get_references(self, el, is_attr): def __get_ref(self, el, is_attr):
""" """
Gets required variables values for references. Gets required variables values for references.
@ -386,7 +387,23 @@ class YASDXSD:
name = "ref" name = "ref"
return key, tag, name return key, tag, name
def __add_references(self, root, el, is_attr=False): def __get_dict_el(self, mylist, key, val):
"""
Gets dict element inside a list
It gets list<el> if el dict key is equal to val.
For example, for l = [{"k": "v1"}, {"k": "v2"}]
self.__get_dict_el(l, "k", "v2") will return {"k": "v2"}
:param list mylist: list that contains the element
:param str el: element key
:param str val: element value
:return: element found
:rtype: dict
"""
return [el for el in mylist if el[key] == val][0]
def __add_ref(self, root, el, is_attr=False):
""" """
Adds element or attribute references to root node. Adds element or attribute references to root node.
@ -395,9 +412,11 @@ class YASDXSD:
:param is_attr: if is an attribute reference; 'False' by default :param is_attr: if is an attribute reference; 'False' by default
:type is_attr: True or False :type is_attr: True or False
""" """
key, tag, name = self.__get_references(el, is_attr) key, tag, name = self.__get_ref(el, is_attr)
if key in el.keys(): if key in el.keys():
for element in el[key]: for element in el[key]:
if key == "children" and tag == "group":
self.__add_group(el[key])
node = self.xsd.new_tag(tag, nsprefix="xs") node = self.xsd.new_tag(tag, nsprefix="xs")
self.__add_occurs(node, element) self.__add_occurs(node, element)
node["ref"] = element[name] node["ref"] = element[name]
@ -406,6 +425,34 @@ class YASDXSD:
root.append(node) root.append(node)
del el[key] del el[key]
def __add_group(self, children):
"""
Adds groups dynamically
The key option 'remove' allows to add groups without a list of
elements. This allows to remove elements in groups for XMLSchema10
or avoids the use of 'assert' in XMLSchema11.
The key 'remove' is for this case in mind: you want to reuse a group
but without certain elements; for example, you want to reuse the group
'inlines' (which includes elements 'i', 'b') for the element 'b' but
without itself, so the syntax '<b><b>bold</b></b>' is invalid.
:param list children: element children
"""
groups = self.yaml["groups"]
for group in children:
if "remove" in group.keys():
name = group["group"]
original = self.__get_dict_el(groups, "name", name)
clone = copy.deepcopy(original)
name = f"dyn_{name}-%s" % "-".join(group["remove"])
group["group"] = clone["name"] = name
for unwanted in group["remove"]:
el = self.__get_dict_el(clone["children"], "ref", unwanted)
clone["children"].remove(el)
self.yaml["groups"].append(clone)
def __add_occurs(self, node, el): def __add_occurs(self, node, el):
""" """
Adds occurrences to node. Adds occurrences to node.
@ -629,7 +676,6 @@ class YASDCheck:
for result in results: for result in results:
level = "warn" if result["error"] else "info" level = "warn" if result["error"] else "info"
res = "FAILED" if result["error"] else "PASSED" res = "FAILED" if result["error"] else "PASSED"
msg = (" " * 10) + result["msg"]
expectation = YASDCheck.__get_expectation(result["path"]) expectation = YASDCheck.__get_expectation(result["path"])
msgr.run("testing", path=result["path"]) msgr.run("testing", path=result["path"])
msgr.run( msgr.run(
@ -638,7 +684,7 @@ class YASDCheck:
path=result["path"], path=result["path"],
expectation=expectation, expectation=expectation,
res=res, res=res,
msg=msg, msg=result["msg"],
) )
def __get_expectation(filepath=""): def __get_expectation(filepath=""):
@ -719,9 +765,8 @@ class YASDMessenger:
unreadable <syntaxis who_can_read_this="?" />. unreadable <syntaxis who_can_read_this="?" />.
""", """,
"epilog": """ "epilog": """
(c) 2023 Perro Tuerto <hi@perrotuerto.blog>. Founded by Mexican (c) 2023 perro <hi@perrotuerto.blog>. Founded by Mexican Academy of
Academy of Language <https://academia.org.mx>. Licensed under GPLv3 Language <https://academia.org.mx>. Licensed under GPLv3.
<https://www.gnu.org/licenses/gpl-3.0.en.html>.
""", """,
"readme": "".join( "readme": "".join(
[ [
@ -733,12 +778,10 @@ class YASDMessenger:
"help_action": "action to perform", "help_action": "action to perform",
"help_input": "input file in YAML format", "help_input": "input file in YAML format",
"help_output": "output file", "help_output": "output file",
"help_tests": " ".join( "help_tests": """
[ one or more XML test files;
"one or more XML test files;", use 'pass' or 'fail' as file name prefix for expectation
"use 'pass' or 'fail' as file name prefix for expectation", """,
]
),
"help_quiet": "enable quiet mode", "help_quiet": "enable quiet mode",
"help_log": "write log", "help_log": "write log",
"action_convert": "creating XSD schema", "action_convert": "creating XSD schema",
@ -762,18 +805,21 @@ class YASDMessenger:
"test output:", "test output:",
" File: @path", " File: @path",
" Expectation: @expectation", " Expectation: @expectation",
" Result: @res\n@msg", " Result: @res",
" Message: @msg",
] ]
), ),
"passed": "Test passed!", "passed": "Test passed!",
} }
def __init__(self, quiet=False, log=False): def __init__(self, quiet=False, log=False, logpath=Path.cwd()):
""" """
Inits YASD Messenger. Inits YASD Messenger.
""" """
self.quiet = quiet self.quiet = quiet
self.log = log self.log = log
self.logfile = logpath / "log.txt"
self.timestamp = "[%s]" % datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
def run(self, key="", level="info", **kwargs): def run(self, key="", level="info", **kwargs):
""" """
@ -787,12 +833,28 @@ class YASDMessenger:
self.__check_level(level) self.__check_level(level)
name = YASDMessenger.keys()["prog"] name = YASDMessenger.keys()["prog"]
msg = self.__get_msg(key, **kwargs) msg = self.__get_msg(key, **kwargs)
msg = f"{name}: {level}: {msg}" msg = f"{name}: {level}: {msg}\n"
# TODO print or save depending on self.quiet and self.log if not self.quiet:
print(msg) sys.stdout.write(msg)
if self.log:
self.__write(msg)
if level in ["error", "fatal"]: if level in ["error", "fatal"]:
sys.exit(1) sys.exit(1)
def __write(self, msg):
"""
Writes log file.
:param str msg: Output message
"""
if self.logfile.parent.exists():
if self.logfile.exists():
file = open(self.logfile, "a")
else:
file = open(self.logfile, "w")
file.write(f"{self.timestamp} {msg}")
file.close()
def __check_level(self, level): def __check_level(self, level):
""" """
Verifies log level. Verifies log level.