Soporte a opción 'remove'
This commit is contained in:
parent
7b27bb105d
commit
9137e35896
15
README.md
15
README.md
|
@ -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
136
yasd.py
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue