Skip to content
This repository has been archived by the owner on May 30, 2022. It is now read-only.

Commit

Permalink
Make typing introspection compatible with Python 3.7+ (#37)
Browse files Browse the repository at this point in the history
Summary:
This PR makes Nubia compatible with Python 3.7+ by taking into account the changes introduced by [PEP 560.](https://www.python.org/dev/peps/pep-0560/) The implementation was heavily inspired by [ilevkivskyi/typing_inspect](https://github.com/ilevkivskyi/typing_inspect) project.

Also run TravisCI tests against Python 3.7.

Closes #2.
Pull Request resolved: #37

Reviewed By: AhmedSoliman

Differential Revision: D15197740

Pulled By: ylectric

fbshipit-source-id: ff8f0df842aed47ad55f57fefe6f781a36033d25
  • Loading branch information
Alexander Fomin authored and facebook-github-bot committed May 12, 2019
1 parent 0878b43 commit 088e6f3
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 120 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
dist: xenial
language: python
python:
- "3.6"
- "3.7"

install:
- pip install codecov
Expand Down
36 changes: 0 additions & 36 deletions nubia/internal/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import subprocess

from collections import namedtuple
from typing import _Union, Any, Iterable # noqa T484


def add_command_arguments(parser, options):
Expand Down Expand Up @@ -165,41 +164,6 @@ def issubclass_(obj, class_):
return False


def is_union(t: Any) -> bool:
"""Check whether type is a Union.
@param t: type to check
@type: Any
@returns: `True` if type is a Union, `False` otherwise
@rtype: bool
@note: This is a hack. See https://bugs.python.org/issue29262 and
https://github.com/ilevkivskyi/typing_inspect for the rationale behind the
implementation.
"""
return type(t) is _Union


def is_optional(t: Any) -> bool:
"""Check whether a type is an Optional.
@note: This is a hack.
"""
if not is_union(t):
return False
elif len(t.__args__) > 2: # `Optional` is a `Union` of 2 values
return False
else:
_, right = t.__args__
return right is type(None) # noqa E721


def get_first_type_variable(t: Any) -> Any:
"""Given an List[T], return T."""
assert hasattr(t, "__args__") and len(t.__args__) > 0
return t.__args__[0]


def catchall(func, *args):
"""
Run the given function with the given arguments,
Expand Down
18 changes: 9 additions & 9 deletions nubia/internal/typing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ def foo(arg1: str, arg2: typing.List[int]):
...
"""

import inspect

from collections import namedtuple, Container, OrderedDict
from functools import partial
from inspect import ismethod, isclass

from nubia.internal.helpers import (
get_arg_spec,
Expand Down Expand Up @@ -168,7 +168,7 @@ def decorator(function):
raise ValueError(msg)

# reject positional=True if we are applied over a class
if inspect.isclass(function) and positional:
if isclass(function) and positional:
raise ValueError(
"Cannot set positional arguments for super " "commands"
)
Expand Down Expand Up @@ -202,7 +202,7 @@ def command(
"""

def decorator(function, name=None):
is_class = inspect.isclass(name_or_function)
is_supercommand = isclass(name_or_function)
exclusive_arguments_ = _normalize_exclusive_arguments(
exclusive_arguments
)
Expand All @@ -214,7 +214,7 @@ def decorator(function, name=None):
else:
function.__command["name"] = (
transform_name(function.__name__)
if not is_class
if not is_supercommand
else transform_class_name(function.__name__)
)
function.__command["help"] = help
Expand Down Expand Up @@ -249,7 +249,7 @@ def inspect_object(obj, accept_bound_methods=False):
args = argspec.args
# remove the first argument in case this is a method (normally the first
# arg is 'self')
if inspect.ismethod(obj):
if ismethod(obj):
args = args[1:]

result = {"arguments": OrderedDict(), "command": None, "subcommands": {}}
Expand All @@ -263,10 +263,10 @@ def inspect_object(obj, accept_bound_methods=False):
)

# Is this a super command?
is_class = inspect.isclass(obj)
is_supercommand = isclass(obj)

for i, arg in enumerate(args):
if (is_class or accept_bound_methods) and arg == "self":
if (is_supercommand or accept_bound_methods) and arg == "self":
continue
arg_idx_with_default = len(args) - len(argspec.defaults)
default_value_set = bool(argspec.defaults and i >= arg_idx_with_default)
Expand All @@ -277,7 +277,7 @@ def inspect_object(obj, accept_bound_methods=False):
)
# We will reject classes (super-commands) that has required arguments to
# reduce complexity
if is_class and not default_value_set:
if is_supercommand and not default_value_set:
raise ValueError(
"Cannot accept super commands that has required "
"arguments with no default value "
Expand Down Expand Up @@ -320,7 +320,7 @@ def inspect_object(obj, accept_bound_methods=False):
)

# Super Command Support
if is_class:
if is_supercommand:
result["subcommands"] = []
for attr in dir(obj):
if attr.startswith("_"): # ignore "private" methods
Expand Down
28 changes: 13 additions & 15 deletions nubia/internal/typing/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,29 @@
#

import argparse
import copy
import os
import shutil
import subprocess
import copy
import sys

from collections import Iterable, Mapping, defaultdict
from collections import defaultdict
from functools import partial
from typing import Any, Dict, List, Tuple # noqa F401

from nubia.internal.typing.builder import (
build_value,
get_dict_kv_arg_type_as_str,
get_list_arg_type_as_str,
)

from nubia.internal.helpers import (
is_optional,
issubclass_,
get_first_type_variable,
from nubia.internal.typing.inspect import (
get_first_type_argument,
is_iterable_type,
is_mapping_type,
is_optional_type,
)

from . import command, inspect_object, transform_name

from typing import Any, Tuple, List, Dict # noqa F401


def create_subparser_class(opts_parser):
# This is a hack to add the main parser arguments to each subcommand in
Expand Down Expand Up @@ -198,8 +196,8 @@ def _argument_to_argparse_input(arg):

argument_type = (
arg.type
if not is_optional(arg.type)
else get_first_type_variable(arg.type)
if not is_optional_type(arg.type)
else get_first_type_argument(arg.type)
)
if argument_type in [int, float, str]:
add_argument_kwargs["type"] = argument_type
Expand All @@ -208,13 +206,13 @@ def _argument_to_argparse_input(arg):
add_argument_kwargs["action"] = "store_true"
elif arg.default_value is True:
add_argument_kwargs["action"] = "store_false"
elif issubclass_(argument_type, Mapping):
elif is_mapping_type(argument_type):
add_argument_kwargs["type"] = _parse_dict(argument_type)
add_argument_kwargs["metavar"] = "DICT[{}: {}]".format(
*get_dict_kv_arg_type_as_str(argument_type)
)
elif issubclass_(argument_type, Iterable):
add_argument_kwargs["type"] = get_first_type_variable(argument_type)
elif is_iterable_type(argument_type):
add_argument_kwargs["type"] = get_first_type_argument(argument_type)
add_argument_kwargs["nargs"] = "+"
add_argument_kwargs["metavar"] = "{}".format(
get_list_arg_type_as_str(argument_type)
Expand Down
97 changes: 55 additions & 42 deletions nubia/internal/typing/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,57 +8,64 @@
#

import ast
import collections
import re
import sys
import typing

from functools import wraps

from nubia.internal.helpers import issubclass_, is_union
from nubia.internal.helpers import issubclass_
from nubia.internal.typing.inspect import (
PEP_560,
is_iterable_type,
is_mapping_type,
is_optional_type,
is_tuple_type,
is_typevar,
)


def build_value(string, type=None, python_syntax=False):
def build_value(string, tp=None, python_syntax=False):
value = (
_safe_eval(string)
if python_syntax
else _build_simple_value(string, type)
else _build_simple_value(string, tp)
)
if type:
value = apply_typing(value, type)
if tp:
value = apply_typing(value, tp)
return value


def apply_typing(value, type):
return get_typing_function(type)(value)
def apply_typing(value, tp):
return get_typing_function(tp)(value)


def get_list_arg_type_as_str(type):
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 issubclass_(type, collections.Iterable)
args = getattr(type, "__args__", None)
assert is_iterable_type(tp)
args = getattr(tp, "__args__", None)
return args[0].__name__ if args else "any"


def is_dict_value_iterable(type):
assert issubclass_(type, collections.Mapping)
args = getattr(type, "__args__", None)
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 issubclass_(args[1], typing.List)
return is_iterable_type(args[1])
return False


def get_dict_kv_arg_type_as_str(type):
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 issubclass_(type, collections.Mapping)
args = getattr(type, "__args__", None)
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:
Expand All @@ -67,42 +74,48 @@ def get_dict_kv_arg_type_as_str(type):
return key_type, value_type


def get_typing_function(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 type == typing.TypeVar:
subtypes = type.__constraints__
if len(subtypes) != 1:
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 PEP_560, "Python 3.7+ forbids single constraint for `TypeVar'"
func = get_typing_function(tp.__constraints__[0])
else:
raise ValueError(
"Cannot resolve typing function for TypeVar({}) "
"as it declares none or multiple types".format(
", ".format(str(x) for x in subtypes)
"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__
)
)
)
func = get_typing_function(subtypes[0])
elif type == typing.Any:
elif tp == typing.Any:
func = _identity_function
elif issubclass_(type, str):
elif issubclass_(tp, str):
func = str
elif issubclass_(type, collections.Mapping):
elif is_mapping_type(tp):
func = _apply_dict_type
elif issubclass_(type, tuple):
elif is_tuple_type(tp):
func = _apply_tuple_type
elif issubclass_(type, collections.Iterable):
elif is_iterable_type(tp):
func = _apply_list_type
elif is_union(type):
elif is_optional_type(tp):
func = _apply_optional_type
elif callable(type):
func = type
elif callable(tp):
func = tp
else:
raise ValueError(
'Cannot find a function to apply type "{}"'.format(type)
'Cannot find a function to apply type "{}"'.format(tp)
)

args = getattr(type, "__args__", None)
args = getattr(tp, "__args__", None)

if args:
# this can be a Generic type from the typing module, like
Expand All @@ -126,20 +139,20 @@ def _safe_eval(string):
raise


def _build_simple_value(string, type):
if not type or issubclass_(type, str):
def _build_simple_value(string, tp):
if not tp or issubclass_(tp, str):
return string
elif issubclass_(type, collections.Mapping):
elif is_mapping_type(tp):
entries = (
re.split(r"\s*[:=]\s*", entry, maxsplit=1)
for entry in string.split(";")
)
if is_dict_value_iterable(type):
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 issubclass_(type, tuple):
elif is_tuple_type(tp):
return tuple(item for item in string.split(","))
elif issubclass_(type, collections.Iterable):
elif is_iterable_type(tp):
return [item for item in string.split(",")]
else:
return string
Expand Down
Loading

0 comments on commit 088e6f3

Please sign in to comment.