Source code for simudo.pyaml.load


import hashlib
import importlib
import io
import os
import re
import tempfile
from os import path as osp

from cached_property import cached_property
from pkg_resources import resource_string

from . import pyaml1
from ..util import generate_base32_token


'''

There's a global instance of SourceTranslator in this module called
`translator`.

You can mostly just use the `load_res` function (which is actually a
method of that instance).

To control where the intermediate source translated files are stored,
you can use the PYAML_CACHE_PATH environment variable.

'''


class BaseSourceTranslator(object):
    @cached_property
    def cache_path(self):
        d = self.get_cache_path()
        os.makedirs(d, exist_ok=True)
        return d

    def get_cache_path(self):
        return os.environ.get(
            'PYAML_CACHE_PATH',
            osp.join(
                tempfile.gettempdir(), 'pyaml_cache.{}'.format(os.getuid())))

    NICENAME_BADCHAR_RE = re.compile(r'[^a-zA-Z0-9_-]')
    NICENAME_LEN = 60
    def make_nicename(self, path):
        return self.NICENAME_BADCHAR_RE.sub('_', path)[-self.NICENAME_LEN:]

    NICENAME_RANDOM_LEN = 30 # base32 characters

    def store_and_get_hashname(self, source):
        h = hashlib.sha256()
        h.update(source)
        shash = h.hexdigest()
        del h

        fn = osp.join(self.cache_path, 'h_{}.py'.format(shash))

        if not osp.exists(fn):
            fn_temp = fn+'.tmp'
            with open(fn_temp, 'wb') as h:
                h.write(source)
            os.rename(fn_temp, fn) # atomic

        return fn

    def load_string(self, source_string, module_name, original_filename):
        tsource = self.translate_code(source_string, original_filename)
        nicename = self.make_nicename(original_filename)

        nice_filename = 'link_{}_{}.py'.format(
            nicename,
            generate_base32_token(self.NICENAME_RANDOM_LEN))
        nice_filename = osp.join(self.cache_path, nice_filename)

        hashname = self.store_and_get_hashname(tsource)

        os.symlink(osp.relpath(
            hashname, start=osp.dirname(nice_filename)), nice_filename)

        spec = importlib.util.spec_from_file_location(
            module_name, nice_filename)
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)

        return mod

    def load_res(self, package_or_requirement, resource_name,
                 use_parent_package=True):
        '''
        Use this to load a source-translated file from inside your
        source tree.

        For example, `translator.load_res(__name__, 'example.py1')`. '''

        if use_parent_package:
            before, sep, after = package_or_requirement.rpartition('.')
            if sep:
                package_or_requirement = before
            del before, sep, after

        module_name = '.'.join((package_or_requirement,
                                resource_name.partition('.')[0]))

        original_filename = '/'.join((package_or_requirement.replace('.', '/'),
                                      resource_name))

        return self.load_string(resource_string(
            package_or_requirement, resource_name),
                                module_name, original_filename)

class SourceTranslator(BaseSourceTranslator):
    def translate_code(self, source_string, co_filename):
        MAGIC = b'#pyaml1'
        if not source_string.startswith(MAGIC):
            raise ValueError("file does not start with '{}'".format(
                MAGIC.decode('ascii')))
        stream = io.BytesIO(source_string)
        return pyaml1.CodeGenerator.pyaml_to_python(
            stream=stream).encode('utf8')

translator = SourceTranslator()

load_res = translator.load_res

__all__ = ['load_res']