monasca-analytics/monasca_analytics/banana/typeck/type_util.py

591 lines
18 KiB
Python

#!/usr/bin/env python
# Copyright (c) 2016 Hewlett Packard Enterprise Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Util files to manipulates banana types.
The list of possible types is as follow:
* `Number`
* `Boolean`
* `String`
* `Object` (Json-like object)
* `Component.Source.<class-name>`
* `Component.Ingestor.<class-name>`
* `Component.Sink.<class-name>`
* `Component.Voter.<class-name>`
* `Component.Ldp.<class-name>`
* `Component.Sml.<class-name>`
where <class-name> will be the component class name defined
in the code base.
For type defined in banana such as Json parsers, <class-name>
refers the name they are defined with.
"""
import abc
import six
import monasca_analytics.banana.grammar.ast as ast
import monasca_analytics.exception.banana as exception
import monasca_analytics.util.string_util as strut
@six.add_metaclass(abc.ABCMeta)
class IsType(object):
"""
Any class that represents a Banana type should inherit
from this class.
"""
def __ne__(self, other):
# Dispatch to eq function
return not self.__eq__(other)
@abc.abstractmethod
def default_value(self):
pass
@abc.abstractmethod
def to_json(self):
pass
class Any(IsType):
"""
Any type. This type should be used by component's writer when
they have a complex handling of parameters. This is not
recommended though as it move the error handling to
the component writer.
"""
def __str__(self):
return "TypeAny"
def __eq__(self, _):
# Any type is equal to nothing not even itself.
return False
def __getitem__(self, _):
return Any()
def __hash__(self):
raise Exception("Any type should not be used in dictionaries.")
def default_value(self):
return {}
def to_json(self):
return {"id": "any"}
class String(IsType):
"""
String Type.
"""
def __str__(self):
return "TypeString"
def __eq__(self, other):
return isinstance(other, String)
def __hash__(self):
return hash(str(self))
def default_value(self):
return ""
def to_json(self):
return {"id": "string"}
class Number(String):
"""
Number type. Banana has only floating point value.
"""
def __str__(self):
return "TypeNumber"
def __eq__(self, other):
return isinstance(other, Number)
def __hash__(self):
return hash(str(self))
def default_value(self):
return 0
def to_json(self):
return {"id": "number"}
class Enum(String):
"""
Enum type. This type is a way to constraint a string or number,
to a specific set of values.
"""
def __init__(self, variants):
self.variants = variants
def __eq__(self, other):
return isinstance(other, Enum) and self.variants == other.variants
def __hash__(self):
return hash(self.variants)
def __str__(self):
return "TypeEnum < {} >".format(','.join(self.variants))
def default_value(self):
return ""
def to_json(self):
return {
"id": "enum",
"variants": self.variants
}
def attach_to_root(root_obj, obj1, span, erase_existing=False):
"""
Attach the object obj1 to the root_obj object type.
:type root_obj: Object
:param root_obj: The root object
:type obj1: Object
:param obj1: The object to attach.
:type span: Span
:param span: The span for this change.
:type erase_existing: bool
:param erase_existing: Set to true if the root type should
always be erased.
"""
for key, child_type in obj1.props.iteritems():
if key in root_obj.props:
root_sub_type = root_obj.props[key]
# Both are object -> recurse
if isinstance(root_sub_type, Object) and\
isinstance(child_type, Object):
attach_to_root(root_sub_type, child_type, span, erase_existing)
elif erase_existing:
root_obj.props[key] = child_type
else:
raise exception.BananaTypeError(
expected_type=root_sub_type,
found_type=child_type,
span=span
)
else:
# We can simply attach the new type!
root_obj.props[key] = child_type
def create_object_tree(dot_path, value):
"""
Create a linear tree of object type from the dot_path.
Also work when dot_path is an Ident or StringLit.
:type dot_path: ast.DotPath | ast.Ident | ast.StringLit
:param dot_path: The ast node that forms a linear tree of type.
:type value: Object | String | Number
:param value: the value to set at the end of the linear tree.
:rtype: Object
:return: Returns the created object
"""
if is_comp(value):
raise exception.BananaAssignCompError(dot_path.span)
# {a.b.c: value}
root_object = Object(strict_checking=False)
if isinstance(dot_path, ast.DotPath):
# {a: value}
if len(dot_path.properties) == 0:
root_object.props[dot_path.varname.inner_val()] = value
else:
# {a: <Object>}
root_object.props[dot_path.varname.inner_val()] = \
Object(strict_checking=False)
# {b.c: value}
current_obj = root_object.props[dot_path.varname.inner_val()]
last_index = len(dot_path.properties) - 1
for index, sub_prop in enumerate(dot_path.properties):
sub_prop_name = sub_prop.inner_val()
if index != last_index:
current_obj.props[sub_prop_name] = \
Object(strict_checking=False)
current_obj = current_obj.props[sub_prop_name]
else:
current_obj.props[sub_prop_name] = value
else:
# Ident and StringLit are captured here.
root_object.props[dot_path.inner_val()] = value
return root_object
class Object(String):
"""
Object Type. The value that are dictionary-like have this type.
"""
def __init__(self, props=None, strict_checking=True):
if props is None:
props = {}
self.props = props
# Strict checking is off for all objects defined within the banana
# language. It is on by default for components so that they can
# force the type checker to throw errors when we try to access
# or to modify unknown properties
self.strict_checking = strict_checking
def __getitem__(self, key):
# a.b or a."b"
if isinstance(key, ast.Ident) or isinstance(key, ast.StringLit):
if key.inner_val() not in self.props:
raise exception.BananaPropertyDoesNotExists(key,
on_type=self)
return self.props[key.inner_val()]
# a.b.c
if isinstance(key, ast.DotPath):
if key.varname.inner_val() not in self.props:
raise exception.BananaPropertyDoesNotExists(key.varname,
on_type=self)
sub_object = self.props[key.varname.inner_val()]
if len(key.properties) == 0:
return sub_object
# Recurse
if isinstance(sub_object, Object):
return sub_object[key.next_dot_path()]
if isinstance(sub_object, Any):
return sub_object
raise exception.BananaPropertyDoesNotExists(key.next_dot_path(),
on_type=sub_object)
raise exception.BananaTypeCheckerBug(
"Unreachable code in Object.__getitem__ reached."
)
def __str__(self):
if self.strict_checking:
return "TypeStruct < {} >".format(strut.dict_to_str(self.props))
else:
return "TypeObject < {} >".format(strut.dict_to_str(self.props))
def __eq__(self, other):
return self.props == other
def __hash__(self):
return hash(self.props)
def default_value(self):
default_value = {}
for key, val in self.props.iteritems():
default_value[key] = val.default_value()
return default_value
def to_json(self):
res = {"id": "object", "props": {}}
for key, val in self.props.iteritems():
res["props"][key] = val.to_json()
return res
class Component(IsType):
"""
Type of all components. While not strictly used directly, it
is very useful to performs checks on variable that are supposed
to be any of the available components.
"""
def __init__(self, ctor_properties=None, class_name=None):
"""
Component type
:type ctor_properties:
list[monasca_analytics.component.params.ParamDescriptor]
:param ctor_properties:
:type class_name: str
:param class_name: Name of the class if there's any.
"""
self.ctor_properties = ctor_properties
self.class_name = class_name
def __str__(self):
if self.class_name is None:
return "TypeComponent"
else:
return self.class_name + "(" +\
",".join(map(lambda x: x.param_name + "=" + str(x.param_type),
self.ctor_properties))\
+ ")"
def __setitem__(self, dot_path, value):
"""
Attempt to set the value at 'dot_path' to 'value'.
:type dot_path: ast.DotPath
:param dot_path: The path of the property
:type value: String | Enum | Object | Number
:param value: The new type to set.
"""
if self.ctor_properties is None:
raise exception.BananaTypeCheckerBug(
"Component type can't have properties"
)
if len(dot_path.properties) == 0:
for arg in self.ctor_properties:
if arg.param_name == dot_path.varname.inner_val():
if not can_be_cast_to(value, arg.param_type):
raise exception.BananaArgumentTypeError(
expected_type=arg.param_type,
received_type=value,
where=dot_path.span
)
else:
return
else:
for arg in self.ctor_properties:
if arg.param_name == dot_path.varname.inner_val():
if isinstance(arg.param_type, Any):
return
elif isinstance(arg.param_type, Object):
next_dot_path = dot_path.next_dot_path()
sub_arg_type = arg.param_type[next_dot_path]
if not can_be_cast_to(value, sub_arg_type):
raise exception.BananaArgumentTypeError(
expected_type=sub_arg_type,
received_type=value,
where=next_dot_path.span
)
else:
return
else:
raise exception.BananaPropertyDoesNotExists(
dot_path.next_dot_path(),
arg.param_type
)
raise exception.BananaPropertyDoesNotExists(dot_path, on_type=self)
def __getitem__(self, dot_path):
"""
Return the type of the given item.
:type dot_path: ast.DotPath
:param dot_path: The path to follow
:return:
"""
if self.ctor_properties is None:
raise exception.BananaTypeCheckerBug(
"Component type can't have properties"
)
if len(dot_path.properties) == 0:
for arg in self.ctor_properties:
if arg.param_name == dot_path.varname.inner_val():
return arg.param_type
else:
for arg in self.ctor_properties:
if arg.param_name == dot_path.varname.inner_val():
if isinstance(arg.param_type, Object):
return arg.param_type[dot_path.next_dot_path()]
else:
raise exception.BananaPropertyDoesNotExists(
dot_path.next_dot_path(),
arg.param_type
)
raise exception.BananaPropertyDoesNotExists(dot_path, on_type=self)
def __eq__(self, other):
return isinstance(other, Component)
def __hash__(self):
return hash(str(self))
def default_value(self):
return None
def to_json(self):
res = {"id": "component", "name": self.class_name, "args": []}
for arg in self.ctor_properties:
res["args"].append(arg.to_json())
return res
class Source(Component):
"""
Source type. All component that inherits from BaseSource have
this type in Banana.
"""
def __init__(self, class_name, ctor_properties):
super(Source, self).__init__(ctor_properties, class_name)
def __eq__(self, other):
return self.class_name == other.class_name
def __hash__(self):
return hash(self.class_name)
class Ingestor(Component):
"""
Ingestor type. All component that inherits from BaseIngestor have
this type in Banana.
"""
def __init__(self, class_name, ctor_properties):
super(Ingestor, self).__init__(ctor_properties, class_name)
def __eq__(self, other):
return self.class_name == other.class_name
def __hash__(self):
return hash(self.class_name)
class Sink(Component):
"""
Sink type. All component that inherits from BaseSink have
this type in Banana.
"""
def __init__(self, class_name, ctor_properties):
super(Sink, self).__init__(ctor_properties, class_name)
def __eq__(self, other):
return self.class_name == other.class_name
def __hash__(self):
return hash(self.class_name)
class Voter(Component):
"""
Voter type. All component that inherits from BaseVoter have
this type in Banana.
"""
def __init__(self, class_name, ctor_properties):
super(Voter, self).__init__(ctor_properties, class_name)
def __eq__(self, other):
return self.class_name == other.class_name
def __hash__(self):
return hash(self.class_name)
class Ldp(Component):
"""
Ldp type. All component that inherits from BaseLdp have
this type in Banana.
"""
def __init__(self, class_name, ctor_properties):
super(Ldp, self).__init__(ctor_properties, class_name)
def __eq__(self, other):
return self.class_name == other.class_name
def __hash__(self):
return hash(self.class_name)
class Sml(Component):
"""
Sml type. All component that inherits from BaseSml have
this type in Banana.
"""
def __init__(self, class_name, ctor_properties):
super(Sml, self).__init__(ctor_properties, class_name)
def __eq__(self, other):
return self.class_name == other.class_name
def __hash__(self):
return hash(self.class_name)
def get_type(ast_node):
"""
Returns the type for the given ast node.
This function only works for literal node such as
Number, StringLit and JsonObj.
:type ast_node: ast.Number | ast.StringLit | ast.JsonObj | ast.Component
:param ast_node: the node.
:return: Returns the appropriate type.
"""
if isinstance(ast_node, ast.Number):
return Number()
if isinstance(ast_node, ast.StringLit):
return String()
if isinstance(ast_node, ast.JsonObj):
return Object(strict_checking=False)
if isinstance(ast_node, ast.Component):
return Component()
return None
def can_to_str(_type):
"""
Check if we the type can be cast to str.
:param _type: Type to check
:return: Returns True if it can be casted
"""
return isinstance(_type, String)
def is_comp(_type):
"""
:type _type: String | Number | Object | Component
:param _type: Type to check.
:rtype: bool
:return: Returns True if the provided _type is a component
"""
return isinstance(_type, Component)
def can_be_cast_to(_type1, _type2):
"""
Check if the given type `_type1` can be cast into `_type2`.
:type _type1: String | Number | Enum | Object
:param _type1: Type to try to change into _type2
:type _type2: String | Number | Enum | Object
:param _type2: Type reference.
:return: Returns true if the conversion can be done.
"""
if isinstance(_type2, Any):
return True
elif _type1 == _type2:
return True
elif _type2 == String():
return can_to_str(_type1)
elif isinstance(_type2, Enum):
return isinstance(_type1, String) or isinstance(_type2, Enum)
elif isinstance(_type1, Object) and isinstance(_type2, Object):
if not _type2.strict_checking:
return True
else:
for prop_name, prop_type in _type2.props.iteritems():
if prop_name not in _type1.props:
return False
if not can_be_cast_to(_type1.props[prop_name], prop_type):
return False
return True
return False