From 9137e358966fd88fbdf19be64beed50ff4a3cac9 Mon Sep 17 00:00:00 2001 From: perro Date: Wed, 1 Mar 2023 16:39:25 -0800 Subject: [PATCH] =?UTF-8?q?Soporte=20a=20opci=C3=B3n=20'remove'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 ++++-- yasd.py | 136 +++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 110 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 3e6f303..7dd1abb 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/yasd.py b/yasd.py index f954fbd..1c32850 100755 --- a/yasd.py +++ b/yasd.py @@ -1,15 +1,17 @@ #!/usr/bin/env python -# (c) 2023 Perro Tuerto . +# (c) 2023 perro . # Founded by Mexican Academy of Language . -# Licensed under GPLv3 . +# 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'\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 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 'bold' 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 . """, "epilog": """ - (c) 2023 Perro Tuerto . Founded by Mexican - Academy of Language . Licensed under GPLv3 - . + (c) 2023 perro . Founded by Mexican Academy of + Language . 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.