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

Update fluent.runtime for fluent.syntax 0.10 #88

6 changes: 4 additions & 2 deletions fluent.runtime/fluent/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ def add_messages(self, source):
# TODO - warn/error about duplicates
for item in resource.body:
if isinstance(item, (Message, Term)):
if item.id.name not in self._messages_and_terms:
self._messages_and_terms[item.id.name] = item
prefix = "-" if isinstance(item, Term) else ""
full_id = prefix + item.id.name
if full_id not in self._messages_and_terms:
self._messages_and_terms[full_id] = item

def has_message(self, message_id):
if message_id.startswith('-'):
Expand Down
144 changes: 83 additions & 61 deletions fluent.runtime/fluent/runtime/resolver.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
from __future__ import absolute_import, unicode_literals

import contextlib
from datetime import date, datetime
from decimal import Decimal

import attr
import six

from fluent.syntax.ast import (AttributeExpression, CallExpression, Message,
MessageReference, NumberLiteral, Pattern,
Placeable, SelectExpression, StringLiteral, Term,
TermReference, TextElement, VariableReference,
VariantExpression, VariantList, Identifier)
from fluent.syntax.ast import (AttributeExpression, CallExpression, Identifier, Message, MessageReference,
NumberLiteral, Pattern, Placeable, SelectExpression, StringLiteral, Term, TermReference,
TextElement, VariableReference, VariantExpression, VariantList)

from .errors import FluentCyclicReferenceError, FluentReferenceError
from .errors import FluentCyclicReferenceError, FluentFormatError, FluentReferenceError
from .types import FluentDateType, FluentNone, FluentNumber, fluent_date, fluent_number
from .utils import numeric_to_native
from .utils import numeric_to_native, reference_to_id, unknown_reference_error_obj

try:
from functools import singledispatch
Expand All @@ -37,13 +36,38 @@
PDI = "\u2069"


@attr.s
class CurrentEnvironment(object):
# The parts of ResolverEnvironment that we want to mutate (and restore)
# temporarily for some parts of a call chain.
args = attr.ib()
error_for_missing_arg = attr.ib(default=True)


@attr.s
class ResolverEnvironment(object):
context = attr.ib()
args = attr.ib()
errors = attr.ib()
dirty = attr.ib(factory=set)
part_count = attr.ib(default=0)
current = attr.ib(factory=CurrentEnvironment)

@contextlib.contextmanager
def modified(self, **replacements):
"""
Context manager that modifies the 'current' attribute of the
environment, restoring the old data at the end.
"""
# CurrentEnvironment only has immutable args at the moment, so the
# shallow copy returned by attr.evolve is fine.
old_current = self.current
self.current = attr.evolve(old_current, **replacements)
yield self
self.current = old_current

def modified_for_term_reference(self, args=None):
return self.modified(args=args if args is not None else {},
error_for_missing_arg=False)


def resolve(context, message, args):
Expand All @@ -55,7 +79,7 @@ def resolve(context, message, args):
"""
errors = []
env = ResolverEnvironment(context=context,
args=args,
current=CurrentEnvironment(args=args),
errors=errors)
return fully_resolve(message, env), errors

Expand Down Expand Up @@ -156,33 +180,34 @@ def handle_number_expression(number_expression, env):

@handle.register(MessageReference)
def handle_message_reference(message_reference, env):
name = message_reference.id.name
return handle(lookup_reference(name, env), env)
return handle(lookup_reference(message_reference, env), env)


@handle.register(TermReference)
def handle_term_reference(term_reference, env):
name = term_reference.id.name
return handle(lookup_reference(name, env), env)
with env.modified_for_term_reference():
return handle(lookup_reference(term_reference, env), env)


def lookup_reference(name, env):
message = None
try:
message = env.context._messages_and_terms[name]
except LookupError:
if name.startswith("-"):
env.errors.append(
FluentReferenceError("Unknown term: {0}"
.format(name)))
def lookup_reference(ref, env):
ref_id = reference_to_id(ref)
if "." in ref_id:
parent_id, attr_name = ref_id.split('.')
if parent_id not in env.context._messages_and_terms:
env.errors.append(unknown_reference_error_obj(ref_id))
return FluentNone(ref_id)
else:
env.errors.append(
FluentReferenceError("Unknown message: {0}"
.format(name)))
if message is None:
message = FluentNone(name)

return message
parent = env.context._messages_and_terms[parent_id]
for attribute in parent.attributes:
if attribute.id.name == attr_name:
return attribute.value
env.errors.append(unknown_reference_error_obj(ref_id))
return parent
else:
if ref_id not in env.context._messages_and_terms:
env.errors.append(unknown_reference_error_obj(ref_id))
return FluentNone(ref_id)
return env.context._messages_and_terms[ref_id]


@handle.register(FluentNone)
Expand All @@ -200,10 +225,11 @@ def handle_none(none, env):
def handle_variable_reference(argument, env):
name = argument.id.name
try:
arg_val = env.args[name]
arg_val = env.current.args[name]
except LookupError:
env.errors.append(
FluentReferenceError("Unknown external: {0}".format(name)))
if env.current.error_for_missing_arg:
env.errors.append(
FluentReferenceError("Unknown external: {0}".format(name)))
return FluentNone(name)

if isinstance(arg_val,
Expand All @@ -217,21 +243,8 @@ def handle_variable_reference(argument, env):


@handle.register(AttributeExpression)
def handle_attribute_expression(attribute, env):
parent_id = attribute.ref.id.name
attr_name = attribute.name.name
message = lookup_reference(parent_id, env)
if isinstance(message, FluentNone):
return message

for message_attr in message.attributes:
if message_attr.id.name == attr_name:
return handle(message_attr.value, env)

env.errors.append(
FluentReferenceError("Unknown attribute: {0}.{1}"
.format(parent_id, attr_name)))
return handle(message, env)
def handle_attribute_expression(attribute_ref, env):
return handle(lookup_reference(attribute_ref, env), env)


@handle.register(VariantList)
Expand Down Expand Up @@ -318,7 +331,7 @@ def handle_indentifier(identifier, env):

@handle.register(VariantExpression)
def handle_variant_expression(expression, env):
message = lookup_reference(expression.ref.id.name, env)
message = lookup_reference(expression.ref, env)
if isinstance(message, FluentNone):
return message

Expand All @@ -334,21 +347,30 @@ def handle_variant_expression(expression, env):

@handle.register(CallExpression)
def handle_call_expression(expression, env):
function_name = expression.callee.name
try:
function = env.context._functions[function_name]
except LookupError:
env.errors.append(FluentReferenceError("Unknown function: {0}"
.format(function_name)))
return FluentNone(function_name + "()")

args = [handle(arg, env) for arg in expression.positional]
kwargs = {kwarg.name.name: handle(kwarg.value, env) for kwarg in expression.named}
try:
return function(*args, **kwargs)
except Exception as e:
env.errors.append(e)
return FluentNone(function_name + "()")

if isinstance(expression.callee, (TermReference, AttributeExpression)):
term = lookup_reference(expression.callee, env)
if args:
env.errors.append(FluentFormatError("Ignored positional arguments passed to term '{0}'"
.format(reference_to_id(expression.callee))))
with env.modified_for_term_reference(args=kwargs):
return handle(term, env)
else:
function_name = expression.callee.id.name
try:
function = env.context._functions[function_name]
except LookupError:
env.errors.append(FluentReferenceError("Unknown function: {0}"
.format(function_name)))
return FluentNone(function_name + "()")

try:
return function(*args, **kwargs)
except Exception as e:
env.errors.append(e)
return FluentNone(function_name + "()")


@handle.register(FluentNumber)
Expand Down
31 changes: 31 additions & 0 deletions fluent.runtime/fluent/runtime/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from fluent.syntax.ast import AttributeExpression, TermReference

from .errors import FluentReferenceError


def numeric_to_native(val):
"""
Given a numeric string (as defined by fluent spec),
Expand All @@ -9,3 +14,29 @@ def numeric_to_native(val):
return float(val)
else:
return int(val)


def reference_to_id(ref):
"""
Returns a string reference for a MessageReference, TermReference or AttributeExpression
AST node.

e.g.
message
message.attr
-term
-term.attr
"""
if isinstance(ref, AttributeExpression):
return "{0}.{1}".format(reference_to_id(ref.ref),
ref.name.name)
return ('-' if isinstance(ref, TermReference) else '') + ref.id.name


def unknown_reference_error_obj(ref_id):
if '.' in ref_id:
return FluentReferenceError("Unknown attribute: {0}".format(ref_id))
elif ref_id.startswith('-'):
return FluentReferenceError("Unknown term: {0}".format(ref_id))
else:
return FluentReferenceError("Unknown message: {0}".format(ref_id))
2 changes: 1 addition & 1 deletion fluent.runtime/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
],
packages=['fluent', 'fluent.runtime'],
install_requires=[
'fluent>=0.9,<0.10',
'fluent.syntax>=0.10',
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be a good idea to add an upper bound here as well. Syntax 0.9 will make a few changes to the AST (nothing extreme) which will likely break the resolver written against Syntax 0.8 (implemented by fluent.syntax 0.10).

'attrs',
'babel',
'pytz',
Expand Down
Loading