Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add import directive #46

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 96 additions & 16 deletions bin/fypp
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ _SET_PARAM_REGEXP = re.compile(
_DEL_PARAM_REGEXP = re.compile(
r'^(?:[(]\s*)?[a-zA-Z_]\w*(?:\s*,\s*[a-zA-Z_]\w*)*(?:\s*[)])?$')

_IMPORT_PARAM_REGEXP = re.compile(
r'^\s*(?P<modname>[a-zA-Z]\w*(?:\.[a-zA-Z]\w*)*)(?:\s+as\s+(?P<modalias>[a-zA-Z]\w*))?\s*$')

_FOR_PARAM_REGEXP = re.compile(
r'^(?P<loopexpr>[a-zA-Z_]\w*(\s*,\s*[a-zA-Z_]\w*)*)\s+in\s+(?P<iter>.+)$')

Expand Down Expand Up @@ -349,6 +352,18 @@ class Parser:
self._log_event('del', span, name=name)


def handle_import(self, span, name):
'''Called when parser encounters an import directive.

It is a dummy method and should be overridden for actual use.

Args:
span (tuple of int): Start and end line of the directive.
name (str): Name of the python module to import.
'''
self._log_event('import', span, name=name)


def handle_if(self, span, cond):
'''Called when parser encounters an if directive.

Expand Down Expand Up @@ -652,6 +667,9 @@ class Parser:
elif directive == 'del':
self._check_param_presence(True, 'del', param, span)
self._process_del(param, span)
elif directive == 'import':
self._check_param_presence(True, 'import', param, span)
self._process_import(param, span)
elif directive == 'for':
self._check_param_presence(True, 'for', param, span)
self._process_for(param, span)
Expand Down Expand Up @@ -765,6 +783,14 @@ class Parser:
self.handle_del(span, param)


def _process_import(self, param, span):
match = _IMPORT_PARAM_REGEXP.match(param)
if not match:
msg = "invalid module name specification '{0}'".format(param)
raise FyppFatalError(msg, self._curfile, span)
self.handle_import(span, match.group('modname'), match.group('modalias'))


def _process_for(self, param, span):
match = _FOR_PARAM_REGEXP.match(param)
if not match:
Expand Down Expand Up @@ -1196,6 +1222,17 @@ class Builder:
self._curnode.append(('del', self._curfile, span, name))


def handle_import(self, span, name, alias):
'''Should be called to signalize an import directive.

Args:
span (tuple of int): Start and end line of the directive.
name (str): Name of the module to import.
alias (str): Local name to be used.
'''
self._curnode.append(('import', self._curfile, span, name, alias))


def handle_eval(self, span, expr):
'''Should be called to signalize an eval directive.

Expand Down Expand Up @@ -1424,6 +1461,8 @@ class Renderer:
output.append(result)
elif cmd == 'del':
self._delete_variable(*node[1:4])
elif cmd == 'import':
self._load_module(*node[1:5])
elif cmd == 'for':
out, ieval, peval = self._get_iterated_content(*node[1:6])
eval_inds += _shiftinds(ieval, len(output))
Expand Down Expand Up @@ -1687,6 +1726,20 @@ class Renderer:
return result


def _load_module(self, fname, span, name, alias):
result = ''
try:
self._evaluator.loadmodule(name, alias)
except Exception as exc:
msg = "exception occurred when importing module(s) '{0}'"\
.format(name)
raise FyppFatalError(msg, fname, span) from exc
multiline = (span[0] != span[1])
if self._linenums and not self._diverted and multiline:
result = self._linenumdir(span[1], fname)
return result


def _add_global(self, fname, span, name):
result = ''
try:
Expand Down Expand Up @@ -1974,7 +2027,7 @@ class Evaluator:
return result


def import_module(self, module):
def import_module(self, module, alias=None):
'''Import a module into the evaluator.

Note: Import only trustworthy modules! Module imports are global,
Expand All @@ -1983,15 +2036,19 @@ class Evaluator:

Args:
module (str): Python module to import.
alias (str): Local alias name for the module.

Raises:
FyppFatalError: If module could not be imported.

'''
rootmod = module.split('.', 1)[0]
rootmod, *xtramod = module.split('.', 1)
if alias is None:
alias = rootmod
xtramod = None
try:
imported = __import__(module, self._scope)
self.define(rootmod, imported)
imported = __import__(module, globals=self._scope, fromlist=xtramod)
self.define(alias, imported)
except Exception as exc:
msg = "failed to import module '{0}'".format(module)
raise FyppFatalError(msg) from exc
Expand Down Expand Up @@ -2059,6 +2116,20 @@ class Evaluator:
raise FyppFatalError(msg)


def loadmodule(self, modname, alias=None):
'''Load modules in current space name.

Args:
modname (str): Name(s) of the module(s) to load.
alias (str): Local name for the module (optional).
'''
if alias is None:
self._check_module_name(modname)
else:
self._check_module_name(alias)
self.import_module(modname, alias)


def addglobal(self, name):
'''Define a given entity as global.

Expand Down Expand Up @@ -2203,6 +2274,16 @@ class Evaluator:
.format(varname)
raise FyppFatalError(msg, None, None)

@staticmethod
def _check_module_name(modname):
if modname.startswith(_RESERVED_PREFIX):
msg = "Local module name '{0}' starts with reserved prefix '{1}'"\
.format(modname, _RESERVED_PREFIX)
raise FyppFatalError(msg, None, None)
if modname in _RESERVED_NAMES:
msg = "Name '{0}' is reserved and can not be redefined as a local module name"\
.format(modname)
raise FyppFatalError(msg, None, None)

def _func_defined(self, var):
defined = var in self._scope
Expand Down Expand Up @@ -2374,6 +2455,7 @@ class Processor:
self._parser.handle_enddef = self._builder.handle_enddef
self._parser.handle_set = self._builder.handle_set
self._parser.handle_del = self._builder.handle_del
self._parser.handle_import = self._builder.handle_import
self._parser.handle_global = self._builder.handle_global
self._parser.handle_for = self._builder.handle_for
self._parser.handle_endfor = self._builder.handle_endfor
Expand Down Expand Up @@ -2496,18 +2578,22 @@ class Fypp:
def __init__(self, options=None, evaluator_factory=Evaluator,
parser_factory=Parser, builder_factory=Builder,
renderer_factory=Renderer):
syspath = self._get_syspath_without_scriptdir()
self._adjust_syspath(syspath)
if options is None:
options = FyppOptions()
syspath = self._get_syspath_without_scriptdir()
lookuppath = []
if options.moduledirs is not None:
lookuppath += [os.path.abspath(moddir) for moddir in options.moduledirs]
lookuppath.append(os.path.abspath('.'))
lookuppath += syspath
self._adjust_syspath(lookuppath)
Comment on lines +2583 to +2589
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is somewhat problematic, because it would change the sys.path permanently for the rest of the code. Originally, the syspath manipulation was in effect during the import of the user specified modules only. Is there any reason, why you want to make the changes permanent instead?

if inspect.signature(evaluator_factory) == inspect.signature(Evaluator):
evaluator = evaluator_factory()
else:
raise FyppFatalError('evaluator_factory has incorrect signature')
self._encoding = options.encoding
if options.modules:
self._import_modules(options.modules, evaluator, syspath,
options.moduledirs)
self._import_modules(options.modules, evaluator)
if options.defines:
self._apply_definitions(options.defines, evaluator)
if inspect.signature(parser_factory) == inspect.signature(Parser):
Expand Down Expand Up @@ -2603,16 +2689,10 @@ class Fypp:
evaluator.define(name, value)


def _import_modules(self, modules, evaluator, syspath, moduledirs):
lookuppath = []
if moduledirs is not None:
lookuppath += [os.path.abspath(moddir) for moddir in moduledirs]
lookuppath.append(os.path.abspath('.'))
lookuppath += syspath
self._adjust_syspath(lookuppath)
@staticmethod
def _import_modules(modules, evaluator):
for module in modules:
evaluator.import_module(module)
self._adjust_syspath(syspath)


@staticmethod
Expand Down