diff --git a/palimport/__init__.py b/palimport/__init__.py index 5186ccd..c4909c6 100644 --- a/palimport/__init__.py +++ b/palimport/__init__.py @@ -16,6 +16,7 @@ # Importing Parser's Grammar importers from ._lark import Importer as LarkImporter +from ._coconut import Importer as CoconutImporter from .finder import Finder from .loader import Loader diff --git a/palimport/_coconut/__init__.py b/palimport/_coconut/__init__.py new file mode 100644 index 0000000..ef7adef --- /dev/null +++ b/palimport/_coconut/__init__.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import + +import sys +import filefinder2 + +# we rely on filefinder2 as a py2/3 wrapper of importlib + +from .finder import CoconutFinder +from .loader import CoconutLoader + + +class Importer(filefinder2.Py3Importer): + + def __enter__(self): + super(Importer, self).__enter__() + + # we hook the grammar customized loader + self.path_hook = CoconutFinder.path_hook((CoconutLoader, ['.coco', '.coc', '.coconut']), ) + + if self.path_hook not in sys.path_hooks: + ffidx = sys.path_hooks.index(filefinder2.ff_path_hook) + sys.path_hooks.insert(ffidx, self.path_hook ) + + def __exit__(self, exc_type, exc_val, exc_tb): + + # removing path_hook + sys.path_hooks.pop(sys.path_hooks.index(self.path_hook)) + + super(Importer, self).__exit__(exc_type, exc_val, exc_tb) + + +__all__ = [ + Importer +] diff --git a/palimport/_coconut/finder.py b/palimport/_coconut/finder.py new file mode 100644 index 0000000..404ed52 --- /dev/null +++ b/palimport/_coconut/finder.py @@ -0,0 +1,75 @@ +from __future__ import absolute_import, division, print_function + +""" +A module to setup custom importer for .lark files + +""" + +# We need to be extra careful with python versions +# Ref : https://docs.python.org/dev/library/importlib.html#importlib.import_module + +# Ref : http://stackoverflow.com/questions/67631/how-to-import-a-module-given-the-full-path +# Note : Couldn't find a way to make imp.load_source deal with packages or relative imports (necessary for our generated message classes) +import os + +from filefinder2.machinery import FileFinder as filefinder2_FileFinder + +from .._utils import _ImportError + + +# TODO: This is a commonly used class -> make it easier to use, so that most importers in palimport uses the same code. +class CoconutFinder(filefinder2_FileFinder): + """PathEntryFinder to handle finding Coconut modules""" + + def __init__(self, path, *loader_details): + super(CoconutFinder, self).__init__(path, *loader_details) + + def __repr__(self): + return 'CoconutFinder({!r})'.format(self.path) + + @classmethod + def path_hook(cls, *loader_details): + """A class method which returns a closure to use on sys.path_hook + which will return an instance using the specified loaders and the path + called on the closure. + + If the path called on the closure is not a directory, or doesnt contain + any files with the supported extension, ImportError is raised. + + This is different from default python behavior + but prevent polluting the cache with custom finders + """ + def path_hook_for_CoconutFinder(path): + """Path hook for importlib.machinery.FileFinder.""" + + if not (os.path.isdir(path)): + raise _ImportError('only directories are supported') + + exts = [x for ld in loader_details for x in ld[1]] + if not any(fname.endswith(ext) for fname in os.listdir(path) for ext in exts): + raise _ImportError( + 'only directories containing {ext} files are supported'.format(ext=", ".join(exts)), + path=path) + return cls(path, *loader_details) + + return path_hook_for_CoconutFinder + + def find_spec(self, fullname, target=None): + """ + Try to find a spec for the specified module. + :param fullname: the name of the package we are trying to import + :return: the matching spec, or None if not found. + """ + + # We attempt to load a .lark file as a module + tail_module = fullname.rpartition('.')[2] + base_path = os.path.join(self.path, tail_module) + for suffix, loader_class in self._loaders: + full_path = base_path + suffix + if os.path.isfile(full_path): # maybe we need more checks here (importlib filefinder checks its cache...) + return self._get_spec(loader_class, fullname, full_path, None, target) + + # Otherwise, we try find python modules (to be able to embed .lark files within python packages) + return super(CoconutFinder, self).find_spec(fullname=fullname, target=target) + + diff --git a/palimport/_coconut/loader.py b/palimport/_coconut/loader.py new file mode 100644 index 0000000..85ff58f --- /dev/null +++ b/palimport/_coconut/loader.py @@ -0,0 +1,43 @@ + +import filefinder2 + +from palimport._utils import _verbose_message + +from coconut.compiler.compiler import Compiler + + + + +class CoconutLoader(filefinder2.machinery.SourceFileLoader): + + def __init__(self, fullname, path): + """Initializes Coconut's compiler""" + self.compiler = Compiler(target=None, strict=True, minify=False, line_numbers=True, keep_lines=True, no_tco=False) + super(CoconutLoader, self).__init__(fullname=fullname, path=path) + + # TODO : investigate : removing get_code breaks loader !!! + def get_code(self, fullname): + source = self.get_source(fullname) + + _verbose_message('transpiling coconut for "{0}"'.format(fullname)) + pysource = self.compiler.parse_file(source, addhash=False) # hash is needed only if we produce bytecode (TODO) + + _verbose_message('compiling code for "{0}"'.format(fullname)) + try: + code = self.source_to_code(pysource, self.get_filename(fullname)) + return code + except TypeError: + raise + + def get_source(self, name): + """Implementing actual python code from file content""" + path = self.get_filename(name) + + + # pypath = CocoCmd.compile_file(path) + + # Returns decoded string from source python file + pystr = super(CoconutLoader, self).get_source(name) + + return pystr + diff --git a/tests/coconut/__init__.py b/tests/coconut/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/coconut/fact.coco b/tests/coconut/fact.coco new file mode 100644 index 0000000..0e27ba0 --- /dev/null +++ b/tests/coconut/fact.coco @@ -0,0 +1,7 @@ +def factorial(0) = 1 + +@addpattern(factorial) +def factorial(n is int if n > 0) = + """Compute n! where n is an integer >= 0.""" + range(1, n+1) |> reduce$(*) + diff --git a/tests/coconut/test_fact.py b/tests/coconut/test_fact.py new file mode 100644 index 0000000..7654f36 --- /dev/null +++ b/tests/coconut/test_fact.py @@ -0,0 +1,30 @@ +import palimport + +with palimport.CoconutImporter(): + if __package__: # attempting relative import when possible + from . import fact + else: + import fact + +import pytest + +#import coconut +#import coconut.convenience # should be enough to setup the importer in metapath (actually turning on autocompilation) + + +def test_fact_neg(): + with pytest.raises(fact.MatchError): + fact.factorial(-1) + + +def test_fact_nint(): + with pytest.raises(fact.MatchError): + fact.factorial(0.5) + + +def test_fact_stop(): + assert fact.factorial(0) == 1 + + +def test_fact(): + assert fact.factorial(3) == 6