nubia/internal/typing/builder.py (140 lines of code) (raw):
#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
#
import ast
import re
import sys
import typing
from functools import wraps
from nubia.internal.helpers import issubclass_
from nubia.internal.typing.inspect import (
NEW_TYPING,
is_iterable_type,
is_mapping_type,
is_optional_type,
is_tuple_type,
is_typevar,
)
def build_value(string, tp=None, python_syntax=False):
value = _safe_eval(string) if python_syntax else _build_simple_value(string, tp)
if tp:
value = apply_typing(value, tp)
return value
def apply_typing(value, tp):
return get_typing_function(tp)(value)
def get_list_arg_type_as_str(tp):
"""
This takes a type (typing.List[int]) and returns a string representation of
the type argument, or "any" if it's not defined
"""
assert is_iterable_type(tp)
args = getattr(tp, "__args__", None)
return args[0].__name__ if args else "any"
def is_dict_value_iterable(tp):
assert is_mapping_type(tp), f"{tp} is not a mapping type"
args = getattr(tp, "__args__", None)
if args and len(args) == 2:
return is_iterable_type(args[1])
return False
def get_dict_kv_arg_type_as_str(tp):
"""
This takes a type (typing.Mapping[str, int]) and returns a tuple (key_type,
value_type) that contains string representations of the type arguments, or
"any" if it's not defined
"""
assert is_mapping_type(tp), f"{tp} is not a mapping type"
args = getattr(tp, "__args__", None)
key_type = "any"
value_type = "any"
if args and len(args) >= 2:
key_type = getattr(args[0], "__name__", str(args[0]))
value_type = getattr(args[1], "__name__", str(args[1]))
return key_type, value_type
def get_typing_function(tp):
func = None
# TypeVars are a problem as they can defined multiple possible types.
# While a single type TypeVar is somewhat useless, no reason to deny it
# though
if is_typevar(tp):
if len(tp.__constraints__) == 0:
# Unconstrained TypeVars may come from generics
func = _identity_function
elif len(tp.__constraints__) == 1:
assert not NEW_TYPING, "Python 3.7+ forbids single constraint for `TypeVar'"
func = get_typing_function(tp.__constraints__[0])
else:
raise ValueError(
"Cannot resolve typing function for TypeVar({constraints}) "
"as it declares multiple types".format(
constraints=", ".join(
getattr(c, "_name", c.__name__) for c in tp.__constraints__
)
)
)
elif tp == typing.Any:
func = _identity_function
elif issubclass_(tp, str):
func = str
elif is_mapping_type(tp):
func = _apply_dict_type
elif is_tuple_type(tp):
func = _apply_tuple_type
elif is_iterable_type(tp):
func = _apply_list_type
elif is_optional_type(tp):
func = _apply_optional_type
elif callable(tp):
func = tp
else:
raise ValueError('Cannot find a function to apply type "{}"'.format(tp))
args = getattr(tp, "__args__", None)
if args:
# this can be a Generic type from the typing module, like
# List[str], Mapping[int, str] and so on. In that case we need to
# also deal with the generic typing
args_types = [get_typing_function(arg) for arg in args]
func = _partial_builder(args_types)(func)
return func
def _safe_eval(string):
try:
return ast.literal_eval(string)
except ValueError as e:
_, e, tb = sys.exc_info()
if str(e) == "malformed string":
# Raise a more meaningful, nicer error
raise ValueError(f"`{string}' uses unsafe token/symbols") from e
else:
raise
def _build_simple_value(string, tp):
if not tp or issubclass_(tp, str):
return string
elif is_mapping_type(tp):
entries = (
re.split(r"\s*[:=]\s*", entry, maxsplit=1) for entry in string.split(";")
)
if is_dict_value_iterable(tp):
entries = ((k, re.split(r"\s*,\s*", v)) for k, v in entries)
return {k.strip(): v for k, v in entries}
elif is_tuple_type(tp):
return tuple(item for item in string.split(","))
elif is_iterable_type(tp):
return [item for item in string.split(",")]
else:
return string
def _apply_dict_type(value, key_type=None, value_type=None):
if not key_type and not value_type:
return dict(value)
key_type = key_type or _identity_function
value_type = value_type or _identity_function
return {key_type(key): value_type(value) for key, value in value.items()}
def _apply_tuple_type(value, *types):
if not types:
return tuple(value)
if len(value) != len(types):
raise ValueError(
"Cannot build a tuple of {} elements with {} "
'values: "{}"'.format(len(types), len(value), value)
)
return tuple(function(value) for function, value in zip(types, value))
def _apply_list_type(value, value_type=None):
if not isinstance(value, list):
value = [value]
if not value_type:
return list(value)
return [value_type(item) for item in value]
def _apply_optional_type(value, left_type=None, _right_type=None):
if value is None:
return None
elif left_type is None:
return value
else:
return left_type(value)
def _partial_builder(args_builders):
def decorator(function):
@wraps(function)
def wrapped(string):
return function(string, *args_builders)
return wrapped
return decorator
def _identity_function(x):
return x