diff --git a/antigen/__init__.py b/antigen/__init__.py index e65784e..2b0fc29 100644 --- a/antigen/__init__.py +++ b/antigen/__init__.py @@ -1 +1,4 @@ -__version__ = 0.0.1 \ No newline at end of file +__version__ = "0.0.1" + +from .blueprint import * +from .utils import * diff --git a/antigen/blueprint.py b/antigen/blueprint.py new file mode 100644 index 0000000..c9a387f --- /dev/null +++ b/antigen/blueprint.py @@ -0,0 +1,72 @@ +import glob +import os + +from jinja2 import Environment, FileSystemLoader, PackageLoader, Template + +from antigen import components_dir + +from .utils import files_in_folder, stdout_logger + +log = stdout_logger(__name__) + +jinja_env = Environment(loader=FileSystemLoader(components_dir)) + +############################################################################### +## Blueprint +############################################################################### + + +def component_is_valid(component): + + if "component" not in component: + log.warning("component field missing in definition") + return False + + fields = ["component", "id", "props", "children"] + for f in fields: + if f not in component: + log.warning(f"{f} missing in component: {component['component']}") + return False + return True + + +def create_component(component: dict, path: str, templates: str) -> str: + """ + Args: + component: component definition ('component', 'id', 'props', 'children' keys are used in generation) + path: path to create component js file + templates: path to template folder + + Returns: + str: path to component js file + """ + + if os.path.isfile(path): + raise Exception(f"{path} already exists") + + if not component_is_valid(component): + raise Exception(f"invalid component") + + templates = jinja_env.list_templates() + log.debug(templates) + + if f"{component['component']}.js" in templates: + template = f"{component['component']}.js" + elif "Default.js" in templates: + log.debug(f"no template found for f{component['component']} using Default.js") + template = "Default.js" + else: + raise Exception(f"no component templates found for {component['component']}") + + with open(path, "w") as f: + log.debug(f"Generating template {template} -> {path} ...") + f.write( + jinja_env.get_template(template).render( + id=component["id"], + component=component["component"], + children=component["children"], + props=component["props"], + ) + ) + + return path diff --git a/antigen/resources/components/Default.js b/antigen/resources/components/Default.js new file mode 100644 index 0000000..6728cd6 --- /dev/null +++ b/antigen/resources/components/Default.js @@ -0,0 +1,6 @@ +function {{component}}(props) { + + return ( +
{{children}}
+ ) +} \ No newline at end of file diff --git a/antigen/resources/examples/blueprints/simple_blueprint.yaml b/antigen/resources/examples/blueprints/simple_blueprint.yaml new file mode 100644 index 0000000..f4a5a05 --- /dev/null +++ b/antigen/resources/examples/blueprints/simple_blueprint.yaml @@ -0,0 +1,7 @@ +components: + + - component: TextBox + props: + id: standard-basic + label: Standard + children: Hello World! \ No newline at end of file diff --git a/antigen/resources/templates/README.md b/antigen/resources/templates/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/antigen/utils.py b/antigen/utils.py new file mode 100644 index 0000000..2d6d7d0 --- /dev/null +++ b/antigen/utils.py @@ -0,0 +1,78 @@ +import logging +import os +import shutil +import sys + +import yaml + +############################################################################### +## Important Paths +############################################################################### + +module_dir = os.path.dirname(sys.modules["antigen"].__file__) +resources_dir = os.path.join(module_dir, "resources") +components_dir = os.path.join(resources_dir, "components") +examples_dir = os.path.join(resources_dir, "examples") + +############################################################################### +## General +############################################################################### + + +def read_yaml(file): + with open(file, "r") as f: + return yaml.load(f, Loader=yaml.FullLoader) + + +def reverse_dictionary(d): + return {v: k for k, v in d.items()} + + +def delete_directory(directory): + if os.path.isdir(directory): + shutil.rmtree(directory) + + +def make_directory(directory, delete=False, exist_ok=True): + if delete: + delete_directory(directory) + if not os.path.isdir(directory): + os.makedirs(directory, exist_ok=exist_ok) + return os.path.abspath(directory) + + +def files_in_folder(folder): + return [ + os.path.join(folder, n) + for n in os.listdir(folder) + if os.path.isfile(os.path.join(folder, n)) + ] + + +def folders_in_folder(folder): + return [ + os.path.join(folder, n) + for n in os.listdir(folder) + if os.path.isdir(os.path.join(folder, n)) + ] + + +def stdout_logger(name): + """ + Use this logger to standardize log output + """ + log = logging.getLogger(name) + log.propagate = False + stream_handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter("%(levelname)-8s %(message)s") + stream_handler.setFormatter(formatter) + stream_handler.setLevel(logging.DEBUG) + log.handlers = [stream_handler] + log.setLevel(logging.DEBUG) + return log + + +class class_or_instance_method(classmethod): + def __get__(self, instance, type_): + descr_get = super().__get__ if instance is None else self.__func__.__get__ + return descr_get(instance, type_) diff --git a/bin/antigen b/bin/antigen index e69de29..a1689c0 100755 --- a/bin/antigen +++ b/bin/antigen @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +import argparse +import os +import re +import shutil +import subprocess +import sys +import tempfile +from glob import glob +from importlib import import_module + +from jinja2 import Environment, FileSystemLoader, PackageLoader, Template + +from antigen import folders_in_folder, read_yaml, stdout_logger + +log = stdout_logger(__name__) +module_folder = os.path.dirname(sys.modules["antigen"].__file__) +resources_folder = os.path.join(module_folder, "resources") +current_folder = os.path.abspath(os.curdir) + +templates = { + os.path.basename(t): t + for t in folders_in_folder(os.path.join(module_folder, "resources", "templates")) +} + +# parsers +parser = argparse.ArgumentParser(description="declarative ui generation") +subparsers = parser.add_subparsers(dest="subcommand") + +# parsers - blueprint +generate_parser = subparsers.add_parser( + "blueprint", description="generate components from blueprint" +) +generate_parser.add_argument("path", help="path to blueprint YAML") + +args = parser.parse_args() + +try: + + if args.subcommand == "blueprint": + log.debug(read_yaml(args.path)) + + else: + parser.print_help() + +except Exception as e: + log.error(e) + parser.print_help() + diff --git a/setup.py b/setup.py index 6e40a14..ef170c6 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,21 @@ from distutils.core import setup -from antigen import __version__ + from setuptools import find_packages +from antigen import __version__ + setup( name="antigen", version=__version__, author="shirecoding", author_email="shirecoding@gmail.com", scripts=["bin/antigen"], - install_requires=["Jinja2"], + install_requires=[ + "Jinja2", + "pytest", + "pytest-html", + "pytest-cov", + ], url="https://github.com/shirecoding/Antigen", download_url=f"https://github.com/shirecoding/Antigen/archive/{__version__}.tar.gz", long_description=open("README.md").read(), diff --git a/tests/.coveragerc b/tests/.coveragerc new file mode 100644 index 0000000..d05a577 --- /dev/null +++ b/tests/.coveragerc @@ -0,0 +1,2 @@ +[run] +data_file = reports/.coverage \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0f7cb2e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,52 @@ +from datetime import datetime + +import pytest +from py.xml import html + +########################################################################################### +## REPORT +########################################################################################### + + +def stringToCellData(string, class_): + return html.td([html.p(s) for s in string.split("\n")], class_=class_) + + +@pytest.mark.optionalhook +def pytest_html_results_table_header(cells): + cells.insert(1, html.th("Specification", class_="spec", col="spec")) + cells.insert(2, html.th("Description")) + cells.insert(3, html.th("Procedure", class_="proc", col="proc")) + cells.insert(4, html.th("Expected", class_="expec", col="expec")) + cells.pop() # remove links column + + +@pytest.mark.optionalhook +def pytest_html_results_table_row(report, cells): + cells.insert(1, stringToCellData(report.specification, class_="col-spec")) + cells.insert(2, stringToCellData(report.description, class_="col-desc")) + cells.insert(3, stringToCellData(report.procedure, class_="col-proc")) + cells.insert(4, stringToCellData(report.expected, class_="col-expec")) + cells.pop() # remove links column + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + report = outcome.get_result() + report.description = str(item.function.__doc__ if item.function.__doc__ else "") + marker = item.get_closest_marker("report") + + report.specification = ( + marker.kwargs["specification"] + if marker and "specification" in marker.kwargs + else "" + ) + + report.procedure = ( + marker.kwargs["procedure"] if marker and "procedure" in marker.kwargs else "" + ) + + report.expected = ( + marker.kwargs["expected"] if marker and "expected" in marker.kwargs else "" + ) diff --git a/tests/pytest.ini b/tests/pytest.ini new file mode 100644 index 0000000..078f85c --- /dev/null +++ b/tests/pytest.ini @@ -0,0 +1,14 @@ +[pytest] +addopts = + --assert='plain' + --html=reports/pytest_report.html + --junitxml=reports/pytest_report.xml + --self-contained-html + --cov=antigen + --cov-report=xml:reports/coverage.xml + --cov-report=html:reports/coverage + --cov-config=tests/.coveragerc +log_cli = true +log_level = DEBUG +markers = + report \ No newline at end of file diff --git a/tests/test_blueprint.py b/tests/test_blueprint.py new file mode 100644 index 0000000..ad57abf --- /dev/null +++ b/tests/test_blueprint.py @@ -0,0 +1,70 @@ +import logging +import os +import re +import tempfile + +import pytest + +log = logging.getLogger(__name__) + + +@pytest.mark.report( + specification=""" + """, + procedure=""" + """, + expected=""" + """, +) +def test_valid_component(): + """ + Test component validation function + """ + from antigen import component_is_valid, components_dir, create_component + + c1 = { + "component": "TextField", + "id": "TextField1", + "props": {}, + "children": "Hello World", + } + + assert component_is_valid(c1) == True + + c2 = {"id": "TextField1", "props": {}, "children": "Hello World"} + + assert component_is_valid(c2) == False + + +@pytest.mark.report( + specification=""" + """, + procedure=""" + """, + expected=""" + """, +) +def test_create_component(): + """ + Test component creation function + """ + + from antigen import components_dir, create_component + + c1 = { + "component": "TextField", + "id": "TextField1", + "props": {}, + "children": "Hello World", + } + + with tempfile.TemporaryDirectory() as folder: + component_path = create_component( + c1, os.path.join(folder, "c1"), components_dir + ) + + with open(component_path, "r") as file: + data = file.read() + assert re.match(r"^.*id=\"TextField1\".*$", data, re.DOTALL) != None + assert re.match(r"^.*>Hello World<.*$", data, re.DOTALL) != None + assert re.match(r"^.*TextField\(props\).*$", data, re.DOTALL) != None