%PDF- %PDF-
| Direktori : /lib/python3/dist-packages/pythran/ |
| Current File : //lib/python3/dist-packages/pythran/toolchain.py |
'''
This module contains all the stuff to make your way from python code to
a dynamic library, see __init__.py for exported interfaces.
'''
from pythran.backend import Cxx, Python
from pythran.config import cfg
from pythran.cxxgen import PythonModule, Include, Line, Statement
from pythran.cxxgen import FunctionBody, FunctionDeclaration, Value, Block
from pythran.cxxgen import ReturnStatement
from pythran.dist import PythranExtension, PythranBuildExt
from pythran.middlend import refine, mark_unexported_functions
from pythran.passmanager import PassManager
from pythran.tables import pythran_ward
from pythran.types import tog
from pythran.types.type_dependencies import pytype_to_deps
from pythran.types.conversion import pytype_to_ctype
from pythran.spec import load_specfile, Spec
from pythran.spec import spec_to_string
from pythran.syntax import check_specs, check_exports, PythranSyntaxError
from pythran.version import __version__
from pythran.utils import cxxid
import pythran.frontend as frontend
from datetime import datetime
from distutils.errors import CompileError
from distutils import sysconfig
from numpy.distutils.core import setup
from tempfile import mkdtemp, NamedTemporaryFile
import gast as ast
import logging
import os.path
import shutil
import glob
import hashlib
from functools import reduce
import sys
logger = logging.getLogger('pythran')
def _extract_specs_dependencies(specs):
""" Extract types dependencies from specs for each exported signature. """
deps = set()
# for each function
for signatures in specs.functions.values():
# for each signature
for signature in signatures:
# for each argument
for t in signature:
deps.update(pytype_to_deps(t))
# and each capsule
for signature in specs.capsules.values():
# for each argument
for t in signature:
deps.update(pytype_to_deps(t))
# Keep "include" first
return sorted(deps, key=lambda x: "include" not in x)
def _parse_optimization(optimization):
'''Turns an optimization of the form
my_optim
my_package.my_optim
into the associated symbol'''
splitted = optimization.split('.')
if len(splitted) == 1:
splitted = ['pythran', 'optimizations'] + splitted
return reduce(getattr, splitted[1:], __import__(splitted[0]))
def _write_temp(content, suffix):
'''write `content` to a temporary XXX`suffix` file and return the filename.
It is user's responsibility to delete when done.'''
with NamedTemporaryFile(mode='w', suffix=suffix, delete=False) as out:
out.write(content)
return out.name
def has_argument(module, fname):
'''Checks if a given function has arguments'''
for n in module.body:
if isinstance(n, ast.FunctionDef) and n.name == fname:
return [cxxid(arg.id) for arg in n.args.args]
return []
def front_middle_end(module_name, code, optimizations=None, module_dir=None,
entry_points=None):
"""Front-end and middle-end compilation steps"""
pm = PassManager(module_name, module_dir)
# front end
ir, docstrings = frontend.parse(pm, code)
if entry_points is not None:
ir = mark_unexported_functions(ir, entry_points)
# middle-end
if optimizations is None:
optimizations = cfg.get('pythran', 'optimizations').split()
optimizations = [_parse_optimization(opt) for opt in optimizations]
refine(pm, ir, optimizations)
return pm, ir, docstrings
# PUBLIC INTERFACE STARTS HERE
def generate_py(module_name, code, optimizations=None, module_dir=None):
'''python + pythran spec -> py code
Prints and returns the optimized python code.
'''
pm, ir, _ = front_middle_end(module_name, code, optimizations, module_dir)
return pm.dump(Python, ir)
def generate_cxx(module_name, code, specs=None, optimizations=None,
module_dir=None):
'''python + pythran spec -> c++ code
returns a PythonModule object and an error checker
the error checker can be used to print more detailed info on the origin of
a compile error (e.g. due to bad typing)
'''
if specs:
entry_points = set(specs.keys())
else:
entry_points = None
pm, ir, docstrings = front_middle_end(module_name, code, optimizations,
module_dir,
entry_points=entry_points)
# back-end
content = pm.dump(Cxx, ir)
# instantiate the meta program
if specs is None:
class Generable(object):
def __init__(self, content):
self.content = content
def __str__(self):
return str(self.content)
generate = __str__
mod = Generable(content)
def error_checker():
tog.typecheck(ir)
else:
# uniform typing
if isinstance(specs, dict):
specs = Spec(specs, {})
def error_checker():
types = tog.typecheck(ir)
check_specs(specs, types)
specs.to_docstrings(docstrings)
check_exports(pm, ir, specs)
if isinstance(code, bytes):
code_bytes = code
else:
code_bytes = code.encode('ascii', 'ignore')
metainfo = {'hash': hashlib.sha256(code_bytes).hexdigest(),
'version': __version__,
'date': datetime.now()}
mod = PythonModule(module_name, docstrings, metainfo)
mod.add_to_includes(
Include("pythonic/core.hpp"),
Include("pythonic/python/core.hpp"),
# FIXME: only include these when needed
Include("pythonic/types/bool.hpp"),
Include("pythonic/types/int.hpp"),
Line("#ifdef _OPENMP\n#include <omp.h>\n#endif")
)
mod.add_to_includes(*[Include(inc) for inc in
_extract_specs_dependencies(specs)])
mod.add_to_includes(*content.body)
mod.add_to_includes(
Include("pythonic/python/exception_handler.hpp"),
)
def warded(module_name, internal_name):
return pythran_ward + '{0}::{1}'.format(module_name, internal_name)
for function_name, signatures in specs.functions.items():
internal_func_name = cxxid(function_name)
# global variables are functions with no signatures :-)
if not signatures:
mod.add_global_var(function_name,
"{}()()".format(warded(module_name,
internal_func_name)))
for sigid, signature in enumerate(signatures):
numbered_function_name = "{0}{1}".format(internal_func_name,
sigid)
arguments_types = [pytype_to_ctype(t) for t in signature]
arguments_names = has_argument(ir, function_name)
arguments = [n for n, _ in
zip(arguments_names, arguments_types)]
name_fmt = pythran_ward + "{0}::{1}::type{2}"
args_list = ", ".join(arguments_types)
specialized_fname = name_fmt.format(module_name,
internal_func_name,
"<{0}>".format(args_list)
if arguments_names else "")
result_type = "typename %s::result_type" % specialized_fname
mod.add_pyfunction(
FunctionBody(
FunctionDeclaration(
Value(
result_type,
numbered_function_name),
[Value(t + '&&', a)
for t, a in zip(arguments_types, arguments)]),
Block([Statement("""
PyThreadState *_save = PyEval_SaveThread();
try {{
auto res = {0}()({1});
PyEval_RestoreThread(_save);
return res;
}}
catch(...) {{
PyEval_RestoreThread(_save);
throw;
}}
""".format(warded(module_name,
internal_func_name),
', '.join(arguments)))])
),
function_name,
arguments_types,
signature
)
for function_name, signature in specs.capsules.items():
internal_func_name = cxxid(function_name)
arguments_types = [pytype_to_ctype(t) for t in signature]
arguments_names = has_argument(ir, function_name)
arguments = [n for n, _ in
zip(arguments_names, arguments_types)]
name_fmt = pythran_ward + "{0}::{1}::type{2}"
args_list = ", ".join(arguments_types)
specialized_fname = name_fmt.format(module_name,
internal_func_name,
"<{0}>".format(args_list)
if arguments_names else "")
result_type = "typename %s::result_type" % specialized_fname
docstring = spec_to_string(function_name, signature)
mod.add_capsule(
FunctionBody(
FunctionDeclaration(
Value(result_type, function_name),
[Value(t, a)
for t, a in zip(arguments_types, arguments)]),
Block([ReturnStatement("{0}()({1})".format(
warded(module_name, internal_func_name),
', '.join(arguments)))])
),
function_name,
docstring
)
return mod, error_checker
def compile_cxxfile(module_name, cxxfile, output_binary=None, **kwargs):
'''c++ file -> native module
Return the filename of the produced shared library
Raises CompileError on failure
'''
builddir = mkdtemp()
buildtmp = mkdtemp()
extension = PythranExtension(module_name,
[cxxfile],
**kwargs)
try:
setup(name=module_name,
ext_modules=[extension],
cmdclass={"build_ext": PythranBuildExt},
# fake CLI call
script_name='setup.py',
script_args=['--verbose'
if logger.isEnabledFor(logging.INFO)
else '--quiet',
'build_ext',
'--build-lib', builddir,
'--build-temp', buildtmp]
)
except SystemExit as e:
raise CompileError(str(e))
def copy(src_file, dest_file):
# not using shutil.copy because it fails to copy stat across devices
with open(src_file, 'rb') as src:
with open(dest_file, 'wb') as dest:
dest.write(src.read())
ext = sysconfig.get_config_var('EXT_SUFFIX')
# Copy all generated files including the module name prefix (.pdb, ...)
for f in glob.glob(os.path.join(builddir, module_name + "*")):
if f.endswith(ext):
if output_binary:
output_binary = output_binary.replace('%{ext}', ext)
else:
output_binary = os.path.join(os.getcwd(), module_name + ext)
copy(f, output_binary)
else:
if output_binary:
output_binary = output_binary.replace('%{ext}', '')
output_directory = os.path.dirname(output_binary)
else:
output_directory = os.getcwd()
copy(f, os.path.join(output_directory, os.path.basename(f)))
shutil.rmtree(builddir)
shutil.rmtree(buildtmp)
logger.info("Generated module: " + module_name)
logger.info("Output: " + output_binary)
return output_binary
def compile_cxxcode(module_name, cxxcode, output_binary=None, keep_temp=False,
**kwargs):
'''c++ code (string) -> temporary file -> native module.
Returns the generated .so.
'''
# Get a temporary C++ file to compile
fdpath = _write_temp(cxxcode, '.cpp')
output_binary = compile_cxxfile(module_name, fdpath,
output_binary, **kwargs)
if not keep_temp:
# remove tempfile
os.remove(fdpath)
else:
logger.warning("Keeping temporary generated file:" + fdpath)
return output_binary
def compile_pythrancode(module_name, pythrancode, specs=None,
opts=None, cpponly=False, pyonly=False,
output_file=None, module_dir=None, **kwargs):
'''Pythran code (string) -> c++ code -> native module
if `cpponly` is set to true, return the generated C++ filename
if `pyonly` is set to true, prints the generated Python filename,
unless `output_file` is set
otherwise, return the generated native library filename
'''
if pyonly:
# Only generate the optimized python code
content = generate_py(module_name, pythrancode, opts, module_dir)
if output_file is None:
print(content)
return None
else:
tmp_file = _write_temp(content, '.py')
output_file = output_file.format('.py')
shutil.move(tmp_file, output_file)
logger.info("Generated Python source file: " + output_file)
# Autodetect the Pythran spec if not given as parameter
from pythran.spec import spec_parser
if specs is None:
specs = spec_parser(pythrancode)
# Generate C++, get a PythonModule object
module, error_checker = generate_cxx(module_name, pythrancode, specs, opts,
module_dir)
if 'ENABLE_PYTHON_MODULE' in kwargs.get('undef_macros', []):
module.preamble.insert(0, Line('#undef ENABLE_PYTHON_MODULE'))
module.preamble.insert(0, Line('#define PY_MAJOR_VERSION {}'.
format(sys.version_info.major)))
if cpponly:
# User wants only the C++ code
tmp_file = _write_temp(str(module), '.cpp')
if output_file:
output_file = output_file.replace('%{ext}', '.cpp')
else:
output_file = module_name + ".cpp"
shutil.move(tmp_file, output_file)
logger.info("Generated C++ source file: " + output_file)
else:
# Compile to binary
try:
output_file = compile_cxxcode(module_name,
str(module),
output_binary=output_file,
**kwargs)
except CompileError:
logger.warning("Compilation error, "
"trying hard to find its origin...")
error_checker()
logger.warning("Nop, I'm going to flood you with C++ errors!")
raise
return output_file
def compile_pythranfile(file_path, output_file=None, module_name=None,
cpponly=False, pyonly=False, **kwargs):
"""
Pythran file -> c++ file -> native module.
Returns the generated .so (or .cpp if `cpponly` is set to true).
Usage without an existing spec file
>>> with open('pythran_test.py', 'w') as fd:
... _ = fd.write('def foo(i): return i ** 2')
>>> cpp_path = compile_pythranfile('pythran_test.py', cpponly=True)
Usage with an existing spec file:
>>> with open('pythran_test.pythran', 'w') as fd:
... _ = fd.write('export foo(int)')
>>> so_path = compile_pythranfile('pythran_test.py')
Specify the output file:
>>> import sysconfig
>>> ext = sysconfig.get_config_vars()["SO"]
>>> so_path = compile_pythranfile('pythran_test.py', output_file='foo'+ext)
"""
if not output_file:
# derive module name from input file name
_, basename = os.path.split(file_path)
module_name = module_name or os.path.splitext(basename)[0]
else:
# derive module name from destination output_file name
_, basename = os.path.split(output_file.replace('%{ext}', ''))
module_name = module_name or basename.split(".", 1)[0]
module_dir = os.path.dirname(file_path)
# Look for an extra spec file
spec_file = os.path.splitext(file_path)[0] + '.pythran'
if os.path.isfile(spec_file):
specs = load_specfile(spec_file)
kwargs.setdefault('specs', specs)
try:
with open(file_path) as fd:
output_file = compile_pythrancode(module_name, fd.read(),
output_file=output_file,
cpponly=cpponly, pyonly=pyonly,
module_dir=module_dir,
**kwargs)
except PythranSyntaxError as e:
if e.filename is None:
e.filename = file_path
raise
return output_file
def test_compile():
'''Simple passthrough compile test.
May raises CompileError Exception.
'''
code = '''
#include <pythonic/core.hpp>
'''
output_file = compile_cxxcode('test', code)
output_file and os.remove(output_file)