Read about our upcoming Code of Conduct on this issue

Commit 97292f74 authored by Pierre Augier's avatar Pierre Augier
Browse files

Pythran using type annotations! à la C++11 template

--HG--
branch : dev
parent 3cb1417ead22
......@@ -16,3 +16,12 @@ pipelines:
script:
- pip install -U tox --user
- tox -e py36,codecov
dev:
- parallel:
- step:
image: fluiddyn/python3-stable
caches:
- pip
script:
- pip install -U tox --user
- tox -e py36,codecov
import numpy as np
import fluidpythran as fp
T = fp.TypeVar("T")
T1 = fp.TypeVar("T1")
D = fp.DimVar("D")
N = fp.NDimVar("N")
A = fp.Array[T, N]
A1 = fp.Array[T1, N + 1]
# for coverage
assert repr(N - 1) == "N - 1"
print(repr(A1))
A = fp.Array[T, D]
A1 = fp.Array[T1, D + 1]
@fp.pythran_def
def compute(a: A, b: A1, c: T, d: A, e: str):
......@@ -18,5 +21,9 @@ def compute(a: A, b: A1, c: T, d: A, e: str):
for dtype in [int, np.complex128]:
for ndim in [2, 3]:
fp.make_signature(compute, T=dtype, D=ndim, T1=float)
for ndim in [1, 3]:
fp.make_signature(compute, T=dtype, N=ndim, T1=float)
if __name__ == "__main__":
print(__name__)
import numpy as np
import fluidpythran as fp
T = fp.TypeVar("T")
T1 = fp.TypeVar("T1")
N = fp.NDimVar("N")
A = fp.Array[T, N]
A1 = fp.Array[T1, N + 1]
@fp.pythran_def
def compute(a: A, b: A, c: T, d: A1, e: str):
print(e)
tmp = a + b
return tmp
for dtype in [int, np.complex128]:
for ndim in [1, 3]:
fp.make_signature(compute, T=dtype, N=ndim, T1=float)
if __name__ == "__main__":
print(__name__)
shape = [2]
a = b = np.zeros(shape, dtype=int)
d = np.zeros(shape + [2], dtype=float)
compute(a, b, 5, d, "hello")
......@@ -9,14 +9,14 @@ except ImportError:
pass
from .annotation import Array, DimVar, TypeVar
from .annotation import Array, NDimVar, TypeVar
__all__ = [
"__version__",
"FluidPythran",
"path_data_tests",
"Array",
"DimVar",
"NDimVar",
"TypeVar",
]
......@@ -56,7 +56,7 @@ def make_signature(func, **kwargs):
def get_module_name(frame):
module_name = inspect.getmodule(frame[0]).__name__
if module_name == "__main__":
if module_name in ("__main__", "<run_path>"):
module_name = inspect.getmodulename(frame.filename)
return module_name
......@@ -66,11 +66,14 @@ class FluidPythran:
if frame is None:
frame = inspect.stack()[1]
module = inspect.getmodule(frame[0])
module_name = get_module_name(frame)
if is_compiling:
self.functions = {}
self.signatures = []
self.signatures_func = {}
_modules[module_name] = self
return
......@@ -87,6 +90,14 @@ class FluidPythran:
try:
self.module_pythran = importlib.import_module(module_pythran)
self.is_pythranized = True
try:
module.__pythran__ = self.module_pythran.__pythran__
except AttributeError:
pass
try:
module.__fluidpythran__ = self.module_pythran.__fluidpythran__
except AttributeError:
pass
except ModuleNotFoundError:
self.is_pythranized = False
else:
......@@ -111,7 +122,7 @@ class FluidPythran:
def make_signature(self, func, **kwargs):
signature = f"# pythran {func.__name__}("
signature = f"{func.__name__}("
sig = inspect.signature(func)
pythran_types = []
for k, p in sig.parameters.items():
......@@ -123,9 +134,11 @@ class FluidPythran:
pythran_types.append(pythran_type)
signature += ", ".join(pythran_types) + ")"
self.signatures.append(signature)
print(signature)
if func.__name__ not in self.signatures_func:
self.signatures_func[func.__name__] = []
self.signatures_func[func.__name__].append(signature)
def use_pythranized_block(self, name):
......
__version__ = "0.0.5"
__version__ = "0.0.6"
try:
from pyfiglet import figlet_format
......@@ -6,7 +6,7 @@ try:
__about__ = figlet_format("fluidpythran", font="big")
except ImportError:
__about__ = r"""
__ _ _ _ _ _
/ _| | (_) | | | | | |
| |_| |_ _ _ __| |_ __ _ _| |_| |__ _ __ __ _ _ __
| _| | | | | |/ _` | '_ \| | | | __| '_ \| '__/ _` | '_ \
......
import ast
import astunparse
class TypeVar:
def __init__(self, name):
self.__name__ = name
def __repr__(self):
return self.__name__
def get_pythran_type(self, **kwargs):
dtype = None
......@@ -9,6 +16,7 @@ class TypeVar:
for key, value in kwargs.items():
if key == self.__name__:
dtype = value
break
if dtype is None:
raise ValueError
......@@ -16,13 +24,21 @@ class TypeVar:
return dtype.__name__
class DimVar:
class NDimVar:
def __init__(self, name, shift=0):
self.__name__ = name
self.shift = shift
def __repr__(self):
return self.__name__
name = self.__name__
if self.shift < 0:
name = name + f" - {abs(self.shift)}"
elif self.shift > 0:
name = name + f" + {abs(self.shift)}"
return name
def __add__(self, number):
return type(self)(self.__name__, shift=number)
......@@ -42,7 +58,7 @@ class ArrayMeta(type):
raise ValueError
dtype = param
if isinstance(param, DimVar):
if isinstance(param, NDimVar):
if ndim is not None:
raise ValueError
ndim = param
......@@ -58,7 +74,9 @@ class ArrayMeta(type):
return ArrayBis
def __repr__(self):
return "Array[" + ", ".join(repr(p) for p in self.parameters) + "]"
return (
"Array[" + ", ".join(repr(p) for p in self.parameters.values()) + "]"
)
def get_pythran_type(self, **kwargs):
......@@ -72,7 +90,7 @@ class ArrayMeta(type):
if isinstance(template_var, TypeVar):
dtype = value
elif isinstance(template_var, DimVar):
elif isinstance(template_var, NDimVar):
ndim = value + template_var.shift
else:
raise ValueError
......@@ -80,8 +98,37 @@ class ArrayMeta(type):
if dtype is None or ndim is None:
raise ValueError
if ndim == 0:
raise NotImplementedError
return f"{dtype.__name__}[{', '.join([':']*ndim)}]"
class Array(metaclass=ArrayMeta):
pass
class TypeHintRemover(ast.NodeTransformer):
"""
from https://stackoverflow.com/a/42734810/1779806
"""
def visit_FunctionDef(self, node):
# remove the return type defintion
node.returns = None
# remove all argument annotations
if node.args.args:
for arg in node.args.args:
arg.annotation = None
return node
def strip_typehints(source):
# parse the source code into an AST
parsed_source = ast.parse(source)
# remove all type annotations, function return type definitions
# and import statements from 'typing'
transformed = TypeHintRemover().visit(parsed_source)
# convert the AST back to source code
return astunparse.unparse(transformed)
import os
from datetime import datetime
from pathlib import Path
......
......@@ -8,6 +8,7 @@ from token import tok_name
from io import BytesIO
from pathlib import Path
from runpy import run_path
import inspect
try:
import black
......@@ -15,6 +16,7 @@ except ImportError:
black = False
from .log import logger, set_log_level
from .annotation import strip_typehints
import fluidpythran
......@@ -113,12 +115,10 @@ def parse_py(path):
logger.debug((tok_name[toknum], tokval))
if "make_signature(" in code:
fluidpythran.is_compiling = True
namespace = run_path(str(path))
run_path(str(path))
fluidpythran.is_compiling = False
return (
blocks,
signatures_blocks,
......@@ -168,6 +168,36 @@ def get_code_functions(path_py, func_names):
return codes
def get_codes_from_functions(functions):
codes = {}
for name, func in functions.items():
code = inspect.getsource(func)
# remove the first line (should be the pythran_def decorator)
code = code.split("\n", 1)[1]
code = strip_typehints(code)
codes[name] = code
return codes
def make_pytran_code_functions(functions, signatures_func, codes_functions):
code_pythran = ""
for name_func in functions:
signatures = signatures_func[name_func]
for signature in signatures:
code_pythran += f"# pythran export {signature}\n"
code = codes_functions[name_func]
code_pythran += f"\n{code}\n\n"
return code_pythran
def make_pythran_code(path_py):
"""Create a pythran code from a Python file"""
......@@ -188,17 +218,28 @@ imports: {imports}\n"""
code_pythran = "\n" + "\n".join(imports) + "\n"
# Add functions and signatures introduced with Pythran commands
codes_functions = get_code_functions(path_py, functions)
for name_func in functions:
signatures = signatures_func[name_func]
for signature in signatures:
code_pythran += f"# pythran export {signature}\n"
code_pythran += make_pytran_code_functions(
functions, signatures_func, codes_functions
)
code = codes_functions[name_func]
# Add functions and signatures introduced with `make_signature`
# print(fluidpythran._modules)
module_name = Path(path_py).with_suffix("").name
if module_name in fluidpythran._modules:
fp = fluidpythran._modules[module_name]
code_pythran += f"\n{code}\n\n"
functions = fp.functions.keys()
signatures_func = fp.signatures_func
codes_functions = get_codes_from_functions(fp.functions)
code_pythran += make_pytran_code_functions(
functions, signatures_func, codes_functions
)
# blocks...
variables_types_block = {}
return_block = {}
for name_block, types_variables in signatures_blocks.items():
......@@ -244,6 +285,11 @@ imports: {imports}\n"""
code_pythran += "# pythran export arguments_blocks\n"
code_pythran += "arguments_blocks = " + str(arguments_blocks)
code_pythran += (
"# pythran export __fluidpythran__\n"
f"__fluidpythran__ = ('{fluidpythran.__version__}',)"
)
if black:
code_pythran = black.format_str(code_pythran, line_length=82)
......
from . import dist
from .dist import detect_pythran_extensions, modification_date
from . import path_data_tests
dist.can_import_pythran = True
def test_detect_pythran_extensions():
detect_pythran_extensions(path_data_tests)
def test_modification_date():
modification_date(path_data_tests / "no_pythran.py")
from .for_test_init import func, func1
from pathlib import Path
import shutil
import importlib
from . import FluidPythran
def test_not_fluidpythranized():
path_for_test = Path(__file__).parent / "for_test_init.py"
path_output = path_for_test.parent / "_pythran"
if path_output.exists():
shutil.rmtree(path_output)
from . import for_test_init
importlib.reload(for_test_init)
from .for_test_init import func, func1
func(1, 3.14)
func1(1.1, 2.2)
......
......@@ -4,32 +4,31 @@ import unittest
from .files_maker import make_pythran_file
path_for_test = Path(__file__).parent / "for_test_init.py"
assert path_for_test.exists()
class TestsInit(unittest.TestCase):
print(path_for_test)
path_for_test = Path(__file__).parent / "for_test_init.py"
path_output = path_for_test.parent / ("_pythran/_" + path_for_test.name)
assert path_for_test.exists()
if path_output.exists():
path_output.unlink()
path_output = path_for_test.parent / ("_pythran/_" + path_for_test.name)
make_pythran_file(path_for_test)
@classmethod
def tearDownClass(cls):
cls.path_output.unlink()
from . import for_test_init
def test_fluidpythranized(self):
importlib.reload(for_test_init)
if self.path_output.exists():
self.path_output.unlink()
make_pythran_file(self.path_for_test)
class TestsInit(unittest.TestCase):
@classmethod
def tearDownClass(cls):
path_output.unlink()
from . import for_test_init
def test_fluidpythranized(self):
importlib.reload(for_test_init)
assert path_output.exists()
assert self.path_output.exists()
assert for_test_init.fp.is_pythranized
for_test_init.func(1, 3.14)
......
......@@ -9,6 +9,7 @@ classifiers =
Programming Language :: Python :: 3.6
[options]
install_requires = astunparse
# install_requires = black # avoid this dependency for now (see https://github.com/ambv/black/issues/552)
[flake8]
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment