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

136
yasd.py
View File

@ -1,15 +1,17 @@
#!/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>.
# 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
import sys
import yaml
import copy
import argparse
import xmlschema
import urllib.request
from pathlib import Path
from datetime import datetime
from bs4 import BeautifulSoup
from bs4.formatter import XMLFormatter
from rich.console import Console
@ -95,16 +97,17 @@ class YASD:
:param validate: if XSD is validated; 'False' by default
: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.formatter = XMLFormatter(indent=2)
self.stdout = stdout
self.validate = validate
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):
"""
@ -114,7 +117,7 @@ class YASD:
:rtype: bs4.element.Tag
"""
self.xsd = YASDXSD(self.yaml, self.msgr).xsd
out = self.__output(self.xsd, self.msgr)
out = self.__output(self.xsd)
if self.validate:
return YASDCheck.xsd(out, self.tests, self.msgr)
else:
@ -128,7 +131,7 @@ class YASD:
:rtype: bs4.element.Tag
"""
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):
"""
@ -138,16 +141,14 @@ class YASD:
:rtype: str
"""
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.
:param outdata: Data for output
:type outdata: bs4.BeautifulSoup or str
:param messenger: Messenger object
:type messenger: None or YASDMessenger
:param str extname: Extension name for file output
:return: Output data
:rtype: str
@ -157,7 +158,7 @@ class YASD:
outdata = f'<?xml version="1.0"?>\n{outdata}'
if self.stdout:
if self.outfile is None:
msgr.run(outdata)
sys.stdout.write(outdata)
else:
suffix = self.outfile.suffix
if len(suffix) > 0 and suffix == suffix.replace(" ", ""):
@ -270,11 +271,11 @@ class YASDXSD:
complex_type = self.__build_complex_type(el)
if "children" in el.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)
else:
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)
self.xsd.schema.append(element)
@ -315,7 +316,7 @@ class YASDXSD:
simple_content = self.xsd.new_tag("simpleContent", nsprefix="xs")
extension = self.xsd.new_tag("extension", nsprefix="xs")
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)
return simple_content
@ -348,7 +349,7 @@ class YASDXSD:
"""
other_tag = el["children_order"]
indicator = self.xsd.new_tag(other_tag, nsprefix="xs")
self.__add_references(indicator, el)
self.__add_ref(indicator, el)
return indicator
def __get_base(self, restrictions):
@ -369,7 +370,7 @@ class YASDXSD:
else:
return "xs:integer"
def __get_references(self, el, is_attr):
def __get_ref(self, el, is_attr):
"""
Gets required variables values for references.
@ -386,7 +387,23 @@ class YASDXSD:
name = "ref"
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.
@ -395,9 +412,11 @@ class YASDXSD:
:param is_attr: if is an attribute reference; 'False' by default
: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():
for element in el[key]:
if key == "children" and tag == "group":
self.__add_group(el[key])
node = self.xsd.new_tag(tag, nsprefix="xs")
self.__add_occurs(node, element)
node["ref"] = element[name]
@ -406,6 +425,34 @@ class YASDXSD:
root.append(node)
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):
"""
Adds occurrences to node.
@ -629,7 +676,6 @@ class YASDCheck:
for result in results:
level = "warn" if result["error"] else "info"
res = "FAILED" if result["error"] else "PASSED"
msg = (" " * 10) + result["msg"]
expectation = YASDCheck.__get_expectation(result["path"])
msgr.run("testing", path=result["path"])
msgr.run(
@ -638,7 +684,7 @@ class YASDCheck:
path=result["path"],
expectation=expectation,
res=res,
msg=msg,
msg=result["msg"],
)
def __get_expectation(filepath=""):
@ -719,9 +765,8 @@ class YASDMessenger:
unreadable <syntaxis who_can_read_this="?" />.
""",
"epilog": """
(c) 2023 Perro Tuerto <hi@perrotuerto.blog>. Founded by Mexican
Academy of Language <https://academia.org.mx>. Licensed under GPLv3
<https://www.gnu.org/licenses/gpl-3.0.en.html>.
(c) 2023 perro <hi@perrotuerto.blog>. Founded by Mexican Academy of
Language <https://academia.org.mx>. Licensed under GPLv3.
""",
"readme": "".join(
[
@ -733,12 +778,10 @@ class YASDMessenger:
"help_action": "action to perform",
"help_input": "input file in YAML format",
"help_output": "output file",
"help_tests": " ".join(
[
"one or more XML test files;",
"use 'pass' or 'fail' as file name prefix for expectation",
]
),
"help_tests": """
one or more XML test files;
use 'pass' or 'fail' as file name prefix for expectation
""",
"help_quiet": "enable quiet mode",
"help_log": "write log",
"action_convert": "creating XSD schema",
@ -762,18 +805,21 @@ class YASDMessenger:
"test output:",
" File: @path",
" Expectation: @expectation",
" Result: @res\n@msg",
" Result: @res",
" Message: @msg",
]
),
"passed": "Test passed!",
}
def __init__(self, quiet=False, log=False):
def __init__(self, quiet=False, log=False, logpath=Path.cwd()):
"""
Inits YASD Messenger.
"""
self.quiet = quiet
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):
"""
@ -787,12 +833,28 @@ class YASDMessenger:
self.__check_level(level)
name = YASDMessenger.keys()["prog"]
msg = self.__get_msg(key, **kwargs)
msg = f"{name}: {level}: {msg}"
# TODO print or save depending on self.quiet and self.log
print(msg)
msg = f"{name}: {level}: {msg}\n"
if not self.quiet:
sys.stdout.write(msg)
if self.log:
self.__write(msg)
if level in ["error", "fatal"]:
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):
"""
Verifies log level.