Read about our upcoming Code of Conduct on this issue

Commit 7386102e authored by Pierre Augier's avatar Pierre Augier
Browse files

Merged in dev (pull request #2)

Dev fluidpythran.pythran_class
......@@ -2,8 +2,16 @@
Future
------
- :code:`cachedjit` for simple methods (without assignation to attributes
and call of other methods)
0.1.4 (2018-12-06)
------------------
- :code:`pythran_def` for simple methods (without assignation to attributes
and call of other methods)
- Bugfixes
- :code:`FLUIDPYTHRAN_DIR`
0.1.3 (2018-12-04)
------------------
......
......@@ -271,38 +271,29 @@ export`.
Python classes: :code:`@pythran_def` for methods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Just a NotImplemented idea! See https://bitbucket.org/fluiddyn/fluidpythran/issues/3
For simple methods only using simple attributes, if could be simple and *very*
useful to support this:
For simple methods **only using attributes**, we can write:
.. code :: python
from fluidpythran import Type, NDim, Array, pythran_def
import numpy as np
T = Type(int, np.float64)
N = NDim(1)
from fluidpythran import pythran_def, pythran_class
A1 = Array[T, N]
A2 = Array[float, N+1]
A = "float[:]"
@pythran_class
class MyClass:
arr0: A1
arr1: A1
arr2: A2
arr0: A
arr1: A
def __init__(self, n, dtype=int):
self.arr0 = np.zeros(n, dtype=dtype)
self.arr1 = np.zeros(n, dtype=dtype)
self.arr2 = np.zeros(n)
def __init__(self, n):
self.arr0 = np.zeros(n)
self.arr1 = np.zeros(n)
@pythran_def
def compute(self, alpha: int):
tmp = (self.arr0 + self.arr1).mean()
return tmp ** alpha * self.arr2
def compute(self, alpha: float):
return (self.arr0 + self.arr1).mean() ** alpha
Make the Pythran files
----------------------
......
import numpy as np
# pythran import numpy as np
from fluidpythran import pythran_class, pythran_def
@pythran_class
class Transmitter:
freq: float
def __init__(self, freq):
self.freq = float(freq)
@pythran_def
def __call__(self, inp: "float[]"):
"""My docstring"""
return inp * np.exp(np.arange(len(inp)) * self.freq * 1j)
if __name__ == "__main__":
inp = np.ones(2)
freq = 1.0
trans = Transmitter(freq)
def for_check(freq, inp):
return inp * np.exp(np.arange(len(inp)) * freq * 1j)
assert np.allclose(trans(inp), for_check(freq, inp))
import numpy as np
# pythran import numpy as np
from fluidpythran import pythran_class, pythran_def
@pythran_class
class Transmitter:
freq: float
def __init__(self, freq):
self.freq = float(freq)
@pythran_def
def __call__(self, inp: "float[]"):
"""My docstring"""
return inp * np.exp(np.arange(len(inp)) * self.freq * 1j)
if __name__ == "__main__":
inp = np.ones(2)
freq = 1.0
trans = Transmitter(freq)
def for_check(freq, inp):
return inp * np.exp(np.arange(len(inp)) * freq * 1j)
assert np.allclose(trans(inp), for_check(freq, inp))
Object oriented programming!
============================
With FluidPythran, one can easily use Pythran for methods:
.. literalinclude:: methods.py
For implementation reasons, we *need* to decorate the method and the class...
......@@ -12,6 +12,7 @@ Examples
examples/type_hints
examples/using_cachedjit
examples/blocks
examples/methods
ipynb/executed/demo_pythranize_at_import
ipynb/executed/demo_cachedjit
......
......@@ -8,7 +8,7 @@ except ImportError:
from .annotation import Array, NDim, Type, Shape
from .aheadoftime import FluidPythran, pythran_def, make_signature
from .aheadoftime import FluidPythran, pythran_def, pythran_class, make_signature
from .util import set_pythranize_at_import
from .pythranizer import wait_for_all_extensions
......@@ -17,6 +17,7 @@ __all__ = [
"__version__",
"FluidPythran",
"pythran_def",
"pythran_class",
"make_signature",
"path_data_tests",
"Array",
......
__version__ = "0.1.3"
__version__ = "0.1.4"
try:
from pyfiglet import figlet_format
......
......@@ -6,6 +6,8 @@ User API
.. autofunction:: pythran_def
.. autofunction:: pythran_class
.. autofunction:: make_signature
.. autoclass:: FluidPythran
......@@ -114,6 +116,20 @@ def pythran_def(func):
return fp.pythran_def(func)
def pythran_class(cls):
"""Decorator to declare that a class contains pythran_def
Parameters
----------
cls: a class
"""
fp = _get_fluidpythran_calling_module()
return fp.pythran_class(cls)
def make_signature(func, **kwargs):
"""Create signature for a function with values for the template types
......@@ -206,6 +222,7 @@ class FluidPythran:
if is_transpiling:
self.functions = {}
self.classes = {}
self.signatures_func = {}
modules[module_name] = self
self.is_transpiled = False
......@@ -306,24 +323,16 @@ class FluidPythran:
if path_ext_alt.exists():
self.path_extension = path_ext = path_ext_alt
if path_ext.exists() and not self.is_compiling:
self.module_pythran = import_from_path(
self.path_extension, module_pythran_name
)
elif path_pythran.exists():
self.module_pythran = import_from_path(
path_pythran, module_pythran_name
)
else:
self.is_transpiled = False
self.is_compiled = False
self.reload_module_pythran(module_pythran_name)
if self.is_transpiled:
self.is_compiled = hasattr(self.module_pythran, "__pythran__")
if self.is_compiled:
module = inspect.getmodule(frame[0])
module.__pythran__ = self.module_pythran.__pythran__
module.__fluidpythran__ = self.module_pythran.__fluidpythran__
# module can be None if (at least) it has been run with runpy
if module is not None:
module.__pythran__ = self.module_pythran.__pythran__
module.__fluidpythran__ = self.module_pythran.__fluidpythran__
if hasattr(self.module_pythran, "arguments_blocks"):
self.arguments_blocks = getattr(
......@@ -332,6 +341,21 @@ class FluidPythran:
modules[module_name] = self
def reload_module_pythran(self, module_pythran_name=None):
if module_pythran_name is None:
module_pythran_name = self.module_pythran.__name__
if self.path_extension.exists() and not self.is_compiling:
self.module_pythran = import_from_path(
self.path_extension, module_pythran_name
)
elif self.path_pythran.exists():
self.module_pythran = import_from_path(
self.path_pythran, module_pythran_name
)
else:
self.is_transpiled = False
self.is_compiled = False
def pythran_def(self, func):
"""Decorator used for functions
......@@ -342,6 +366,15 @@ class FluidPythran:
"""
signature = inspect.signature(func)
try:
is_method = next(iter(signature.parameters.keys())) == "self"
except StopIteration:
is_method = False
if is_method:
return self.pythran_def_method(func)
if is_transpiling:
self.functions[func.__name__] = func
return func
......@@ -349,6 +382,9 @@ class FluidPythran:
if not self.is_transpiled:
return func
if not hasattr(self.module_pythran, func.__name__):
self.reload_module_pythran()
try:
func_tmp = getattr(self.module_pythran, func.__name__)
except AttributeError:
......@@ -360,6 +396,71 @@ class FluidPythran:
return func_tmp
def pythran_def_method(self, func):
"""Decorator used for methods
Parameters
----------
func: a function
"""
if is_transpiling:
func.__fluidpythran__ = "pythran_def_method"
return func
if not self.is_transpiled:
return func
return FluidPythranTemporaryMethod(func)
def pythran_class(self, cls: type):
"""Decorator used for classes
Parameters
----------
cls: a class
"""
if is_transpiling:
self.classes[cls.__name__] = cls
return cls
if not self.is_transpiled:
return cls
cls_name = cls.__name__
for key, value in cls.__dict__.items():
if not isinstance(value, FluidPythranTemporaryMethod):
continue
func = value.func
func_name = func.__name__
name_pythran_func = f"__for_method__{cls_name}__{func_name}"
name_var_code_new_method = (
f"__code_new_method__{cls_name}__{func_name}"
)
if not hasattr(self.module_pythran, name_pythran_func):
self.reload_module_pythran()
try:
pythran_func = getattr(self.module_pythran, name_pythran_func)
code_new_method = getattr(
self.module_pythran, name_var_code_new_method
)
except AttributeError:
logger.warning("Pythran file does not seem to be up-to-date.")
# setattr(cls, key, func)
else:
namespace = {"pythran_func": pythran_func}
exec(code_new_method, namespace)
setattr(cls, key, functools.wraps(func)(namespace["new_method"]))
return cls
def make_signature(self, func, _signature=None, **kwargs):
"""Create signature for a function with values for the template types
......@@ -431,3 +532,14 @@ class FluidPythran:
if func.__name__ not in self.signatures_func:
self.signatures_func[func.__name__] = []
self.signatures_func[func.__name__].extend(signatures)
class FluidPythranTemporaryMethod:
def __init__(self, func):
self.func = func
def __call__(self, self_bis, *args, **kwargs):
raise RuntimeError(
"Did you forget to decorate a class using methods decorated "
"with fluidpythran? Please decorate it with @pythran_class."
)
......@@ -2,7 +2,7 @@ import numpy as np
# pythran import numpy as np
from fluidpythran import FluidPythran, pythran_def
from fluidpythran import FluidPythran, pythran_def, pythran_class
# pythran def func(int, float)
......@@ -39,3 +39,32 @@ def func1(a, b):
result = 0.0
for _ in range(n):
result += a ** 2 + b ** 3
@pythran_class
class Transmitter:
freq: float
def __init__(self, freq):
self.freq = float(freq)
@pythran_def
def __call__(self, inp: "float[]"):
"""My docstring"""
return inp * np.exp(np.arange(len(inp)) * self.freq * 1j)
def check_class():
inp = np.ones(2)
freq = 1.0
trans = Transmitter(freq)
def for_check(freq, inp):
return inp * np.exp(np.arange(len(inp)) * freq * 1j)
assert np.allclose(trans(inp), for_check(freq, inp))
if __name__ == "__main__":
check_class()
\ No newline at end of file
......@@ -154,8 +154,6 @@ class SchedulerPopen:
cwd = str(cwd)
words_command = [str(word) for word in words_command]
# FIXME lock file ?
self.block_until_avail(parallel)
process = None
......
......@@ -10,12 +10,12 @@ from . import path_data_tests
dist.can_import_pythran = True
shutil.rmtree(path_data_tests / "__pythran__", ignore_errors=True)
@pytest.mark.skipif(nb_proc > 1, reason="No dist in MPI")
def test_detect_pythran_extensions():
shutil.rmtree(path_data_tests / "__pythran__", ignore_errors=True)
names = [
"block_fluidsim.py",
"class_blocks.py",
......@@ -34,6 +34,9 @@ def test_detect_pythran_extensions():
ext_names = detect_pythran_extensions(path_data_tests)
assert len(ext_names) == len(names) - 1
shutil.rmtree(path_data_tests / "__pythran__", ignore_errors=True)
@pytest.mark.skipif(nb_proc > 1, reason="No dist in MPI")
def test_modification_date():
......
......@@ -20,10 +20,11 @@ def test_not_fluidpythranized():
importlib.reload(for_test_init)
from .for_test_init import func, func1
from .for_test_init import func, func1, check_class
func(1, 3.14)
func1(1.1, 2.2)
check_class()
def test_use_pythran_false():
......
......@@ -73,6 +73,7 @@ class TestsInit(unittest.TestCase):
for_test_init.func(1, 3.14)
for_test_init.func1(1.1, 2.2)
for_test_init.check_class()
@unittest.skipIf(not pythran, "Pythran is required for PYTHRANIZE_AT_IMPORT")
def test_pythranize(self):
......@@ -101,6 +102,8 @@ class TestsInit(unittest.TestCase):
if not for_test_init.fp.is_compiling:
importlib.reload(for_test_init)
assert module_name in modules, modules
assert self.path_pythran.exists()
fp = for_test_init.fp
......@@ -122,3 +125,4 @@ class TestsInit(unittest.TestCase):
for_test_init.func(1, 3.14)
for_test_init.func1(1.1, 2.2)
for_test_init.check_class()
......@@ -48,7 +48,7 @@ def test_create_pythran_classic():
rmtree(path_dir_out)
path_file = path_data_tests / "classic.py"
sys.argv = f"fluidpythran -np -v {path_file}".split()
sys.argv = f"fluidpythran -np {path_file}".split()
run()
print("after first build")
......
......@@ -25,12 +25,21 @@ Internal API
"""
from tokenize import tokenize, untokenize, COMMENT, INDENT, DEDENT, STRING, NAME
from tokenize import (
tokenize,
untokenize,
COMMENT,
INDENT,
DEDENT,
STRING,
NAME,
OP,
)
import os
from logging import DEBUG
from token import tok_name
import inspect
from io import BytesIO
from runpy import run_module, run_path
import sys
......@@ -46,7 +55,11 @@ except ImportError:
from .log import logger, set_log_level
from .annotation import compute_pythran_types_from_valued_types
from .util import has_to_build, get_source_without_decorator
from .util import (
has_to_build,
get_source_without_decorator,
find_module_name_from_path,
)
from .compat import open
from .mpi import Path
import fluidpythran
......@@ -245,6 +258,124 @@ def make_pythran_code_functions(
return code_pythran
def produce_pythran_code_class(cls):
pythran_code = ""
for key, value in cls.__dict__.items():
if (
hasattr(value, "__fluidpythran__")
and value.__fluidpythran__ == "pythran_def_method"
):
pythran_code += produce_pythran_code_class_func(cls, key)
return pythran_code
def produce_pythran_code_class_func(cls, func_name):
cls_name = cls.__name__
cls_annotations = cls.__annotations__
func = cls.__dict__[func_name]
signature = inspect.signature(func)
types_func = [param.annotation for param in signature.parameters.values()][1:]
args_func = list(signature.parameters.keys())[1:]
src = get_source_without_decorator(func)
tokens = []
attributes = set()
using_self = False
g = tokenize(BytesIO(src.encode("utf-8")).readline)
for toknum, tokval, a, b, c in g:
logger.debug((tok_name[toknum], tokval))
if using_self == "self":
if toknum == OP and tokval == ".":
using_self = tokval
continue
elif toknum == OP and tokval == ",":
tokens.append((NAME, "self"))
using_self = False
else:
raise NotImplementedError(
f"self{tokval} not supported by FluidPythran"
)
if using_self == ".":
if toknum == NAME and tokval in cls_annotations:
using_self = False
tokens.append((NAME, "self_" + tokval))
attributes.add(tokval)
continue
else:
raise NotImplementedError(
f"self.{tokval} used but {tokval} not in class annotations"
)
if toknum == NAME and tokval == "self":
using_self = tokval
continue
tokens.append((toknum, tokval))
attributes = sorted(attributes)
types_attrs = [cls_annotations[attr] for attr in attributes]
attributes_self = ["self_" + attr for attr in attributes]
index_self = tokens.index((NAME, "self"))
tokens_attr = []
for ind, attr in enumerate(attributes_self):
tokens_attr.append((NAME, attr))
tokens_attr.append((OP, ","))
tokens = tokens[:index_self] + tokens_attr + tokens[index_self + 2 :]
index_func_name = tokens.index((NAME, func_name))
name_pythran_func = f"__for_method__{cls_name}__{func_name}"
tokens[index_func_name] = (NAME, name_pythran_func)
new_code = untokenize(tokens).decode("utf-8")
if black:
new_code = black.format_str(new_code, line_length=82)
# args_pythran = attributes + args_func
types_pythran = types_attrs + types_func
pythran_signatures = "\n"
for