Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
pyckles / codegen.py
Size: Mime:
# -*- coding: utf-8 -*-
import logging
import os
import shutil
import sys
from collections import OrderedDict
from importlib.machinery import ModuleSpec

from jinja2 import Environment, FileSystemLoader

from frutils.jinja2_filters import ALL_FILTERS

log = logging.getLogger("freckles")

# adapted from: https://blog.quiltdata.com/import-almost-anything-in-python-an-intro-to-module-loaders-and-finders-f5e7b15cda47

MODULE_PATH = []


def generate_jinja_env(template_dir):

    template_filters = ALL_FILTERS

    jinja_env = Environment(loader=FileSystemLoader(template_dir))

    if template_filters:
        for tn, tf in template_filters.items():
            jinja_env.filters[tn] = tf["func"]

    return jinja_env


class PycklesCodegen(object):
    def __init__(self, freckles_context):

        # cc = ContextConfig(
        #     alias="codegen",
        #     config_chain={},
        #     extra_repos=frecklet_repos,
        #     use_community=False,
        #     config_unlocked=True,
        # )

        self._context = freckles_context

    def generate_frecklet_package(self, package_name):

        sys.meta_path.append(
            PyckletPackageFinder(package_name=package_name, pyckles_codegen=self)
        )

    def export(
        self,
        base_path,
        package_name,
        force=False,
        ignore_errors=False,
        delete_existing_base=False,
        delete_existing_package=False,
    ):

        if os.path.exists(base_path) and delete_existing_base:
            shutil.rmtree(base_path)

        package_base = os.path.join(base_path, package_name.replace(".", os.path.sep))
        resource_path = os.path.join(package_base, "resources")

        if os.path.exists(package_base) and not delete_existing_package and not force:
            raise Exception("Package base path already exists: {}".format(package_base))
        if delete_existing_package:
            if os.path.exists(package_base):
                shutil.rmtree(package_base)
            if os.path.exists(resource_path):
                shutil.rmtree(resource_path)

        log.debug("Creating pycklet sources in: {}".format(package_base))
        self.generate_pycklet_sources(
            base_path=base_path, package_name=package_name, force=force
        )
        log.debug("Exporting resources to: {}".format(resource_path))
        self.context.export(
            dest_path=resource_path,
            delete_destination_before_copy=False,
            ignore_errors=ignore_errors,
        )

    def generate_pycklet_sources(self, package_name, base_path, force=False):

        package_base = os.path.join(base_path, package_name.replace(".", os.path.sep))

        if os.path.exists(package_base) and not force:
            raise Exception("Package base path already exists: {}".format(package_base))

        os.makedirs(package_base, exist_ok=True)

        _temp = base_path
        for token in package_name.split("."):
            _temp = os.path.join(_temp, token)
            init_file = os.path.join(_temp, "__init__.py")
            open(init_file, "a").close()

        imps = OrderedDict()

        if not hasattr(sys, "frozen"):
            template_dir = os.path.join(os.path.dirname(__file__), "templates")
        else:
            template_dir = os.path.join(sys._MEIPASS, "pyckles", "templates")

        jinja_env = generate_jinja_env(template_dir=template_dir)
        template = jinja_env.get_template(name="python_src.j2")

        for frecklet_name, frecklet in self.context.frecklet_index.items():

            try:

                module_name = frecklet_name.replace("-", "_")
                f_module = os.path.join(package_base, "{}.py".format(module_name))
                with open(f_module, "w") as f:
                    f.write(
                        frecklet.render_template(
                            ting_repl_name="frecklet", template=template
                        )
                    )
                imps[module_name] = frecklet.class_name

            except (Exception) as e:
                log.warning(
                    "Could not generate python sources for frecklet '{}': {}".format(
                        frecklet_name, e
                    )
                )

        init_file = os.path.join(package_base, "__init__.py")
        init_template = jinja_env.get_template(name="__init__.py.j2")
        repl_dict = {"imports": imps, "package_name": package_name}
        rendered = init_template.render(repl_dict)
        with open(init_file, "w") as f:
            f.write(rendered)

    @property
    def context(self):
        return self._context


class PycklesPackageImporter(object):
    """
    Frecklet package module loader. Executes package import code and adds the package to the
    module cache.
    """

    def __init__(self, package_name, pyckles_codegen):

        self.package_name = package_name
        self.pyckles_codegen = pyckles_codegen

    @classmethod
    def create_module(cls, spec):
        """
        Module creator. Returning None causes Python to use the default module creator.
        """
        return None

    # @classmethod
    def exec_module(self, module):
        """
        Module executor.
        """

        module_name_req = module.__name__

        if len(module_name_req) < len(self.package_name):
            # __path__ must be set even if the package is virtual, but can be set to [].
            module.__path__ = MODULE_PATH
            return module
        elif len(module_name_req) == len(self.package_name):

            for f_name, f in self.pyckles_codegen.context.frecklet_index.items():
                module.__dict__[f_name] = f

                def _wrapper():

                    _f_name = f_name
                    _f = f
                    import_string = "{}.{}".format(
                        self.package_name, _f_name.replace("-", "_")
                    )

                    def _proxy_constructor(**kwargs):

                        import importlib

                        i = importlib.import_module(import_string)
                        return i.__dict__[_f.class_name](**kwargs)

                    return _proxy_constructor

                module.__dict__[f.class_name] = _wrapper()

            module.__path__ = MODULE_PATH

            return module

        relative_module = module_name_req.replace(self.package_name, "")

        # don't need the dot
        frecklet_name = relative_module[1:]

        if "." in frecklet_name:
            return None

        f = self.pyckles_codegen.context.get_frecklet(frecklet_name)
        if f is None:
            f = self.pyckles_codegen.context.get_frecklet(
                frecklet_name.replace("_", "-")
            )
            if f is None:
                return None

        try:
            exec(f.python_src, module.__dict__)
            module.__dict__["frecklet_class"] = module.__dict__[f.class_name]
        except (Exception) as e:
            log.warning(
                "Can't create python class for '{}': {}".format(frecklet_name, e)
            )


class PyckletPackageFinder:
    """
    Frecklet package module loader finder. This class sits on `sys.meta_path` and returns the
    loader it knows for a given path, if it knows a compatible loader.
    """

    def __init__(self, package_name, pyckles_codegen):
        self.package_name = package_name
        self.pyckles_codegen = pyckles_codegen
        self.pyckles_registry = PycklesPackageImporter(
            package_name=self.package_name, pyckles_codegen=self.pyckles_codegen
        )

    def find_spec(self, fullname, path=None, target=None):
        """
        This functions is what gets executed by the loader.
        """

        if not fullname.startswith(
            self.package_name
        ) and not self.package_name.startswith(fullname):
            return None
        else:
            return ModuleSpec(fullname, self.pyckles_registry)