Repository: quora/pyanalyze
Pull request #317 (2021-12-06 04:11:46)

Internal support for overloads
Part of #182


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index e966be7..135dbf6 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Internal support for overloaded functions (#316)
 - Support `TypeVar` bounds and constraints (#315)
 - Improve error messages involving concrete dictionary and sequence values (#312)
 - More precise type inference for dict literals (#312)
diff --git a/pyanalyze/config.py b/pyanalyze/config.py
index 6141e4e..f011798 100644
--- a/pyanalyze/config.py
+++ b/pyanalyze/config.py
@@ -18,7 +18,7 @@ from .extensions import CustomCheck
 
 if TYPE_CHECKING:
     from .arg_spec import ArgSpecCache
-    from .signature import Signature
+    from .signature import Signature, ConcreteSignature
     from .reexport import ImplicitReexportTracker
 
 
@@ -41,7 +41,7 @@ class Config(object):
 
     def get_constructor(
         self, cls: type
-    ) -> Union[None, "Signature", inspect.Signature, Callable[..., Any]]:
+    ) -> Union[None, "ConcreteSignature", inspect.Signature, Callable[..., Any]]:
         """Return a constructor signature for this class.
 
         May return either a function that pyanalyze will use the signature of, an inspect
@@ -281,7 +281,7 @@ class Config(object):
     #
     def get_known_argspecs(
         self, arg_spec_cache: "ArgSpecCache"
-    ) -> Dict[object, "Signature"]:
+    ) -> Dict[object, "ConcreteSignature"]:
         """Initialize any hardcoded argspecs."""
         return {}
 
diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py
index 7b101f2..ea2541f 100644
--- a/pyanalyze/name_check_visitor.py
+++ b/pyanalyze/name_check_visitor.py
@@ -96,7 +96,9 @@ from .stacked_scopes import (
 from .signature import (
     ANY_SIGNATURE,
     BoundMethodSignature,
+    ConcreteSignature,
     MaybeSignature,
+    OverloadedSignature,
     Signature,
     make_bound_method,
     ARGS,
@@ -833,12 +835,16 @@ class NameCheckVisitor(
     ) -> GenericBases:
         return self.arg_spec_cache.get_generic_bases(typ, generic_args)
 
-    def get_signature(self, obj: object, is_asynq: bool = False) -> Optional[Signature]:
+    def get_signature(
+        self, obj: object, is_asynq: bool = False
+    ) -> Optional[ConcreteSignature]:
         sig = self.arg_spec_cache.get_argspec(obj, is_asynq=is_asynq)
         if isinstance(sig, Signature):
             return sig
         elif isinstance(sig, BoundMethodSignature):
             return sig.get_signature()
+        elif isinstance(sig, OverloadedSignature):
+            return sig
         return None
 
     def __reduce_ex__(self, proto: object) -> object:
diff --git a/pyanalyze/node_visitor.py b/pyanalyze/node_visitor.py
index a4aec9a..ef59ae9 100644
--- a/pyanalyze/node_visitor.py
+++ b/pyanalyze/node_visitor.py
@@ -500,14 +500,14 @@ class BaseNodeVisitor(ast.NodeVisitor):
             }
 
     @contextmanager
-    def catch_errors(self) -> Iterator[List[Tuple[Any, ...]]]:
+    def catch_errors(self) -> Iterator[List[Dict[str, Any]]]:
         caught_errors = []
         with qcore.override(self, "caught_errors", caught_errors):
             yield caught_errors
 
-    def show_caught_errors(self, errors: Iterable[Tuple[Any, ...]]) -> None:
+    def show_caught_errors(self, errors: Iterable[Dict[str, Any]]) -> None:
         for error in errors:
-            self.show_error(*error)
+            self.show_error(**error)
 
     def show_error(
         self,
@@ -538,7 +538,15 @@ class BaseNodeVisitor(ast.NodeVisitor):
         """
         if self.caught_errors is not None:
             self.caught_errors.append(
-                (node, e, error_code, replacement, obey_ignore, ignore_comment)
+                {
+                    "node": node,
+                    "e": e,
+                    "error_code": error_code,
+                    "replacement": replacement,
+                    "obey_ignore": obey_ignore,
+                    "ignore_comment": ignore_comment,
+                    "detail": detail,
+                }
             )
             return None
 
diff --git a/pyanalyze/safe.py b/pyanalyze/safe.py
index 1f0929f..8ec31b0 100644
--- a/pyanalyze/safe.py
+++ b/pyanalyze/safe.py
@@ -5,14 +5,11 @@
 """
 import inspect
 import sys
-from typing import Any, Dict, Tuple, Union, Container, NewType, Type, TypeVar, Iterable
-from typing_extensions import Annotated
+from typing import Any, Dict, Sequence, Tuple, Union, Container, NewType, Type, TypeVar
 import typing
 import typing_extensions
 import mypy_extensions
 
-from .extensions import ParameterTypeGuard
-
 T = TypeVar("T")
 
 
@@ -97,8 +94,8 @@ def is_hashable(obj: object) -> bool:
 
 
 def all_of_type(
-    elts: Iterable[object], typ: Type[T]
-) -> Annotated[bool, ParameterTypeGuard["elts", Iterable[T]]]:
+    elts: Sequence[object], typ: Type[T]
+) -> typing_extensions.TypeGuard[Sequence[T]]:
     """Returns whether all elements of elts are instances of typ."""
     return all(isinstance(elt, typ) for elt in elts)
 
diff --git a/pyanalyze/signature.py b/pyanalyze/signature.py
index 0792368..17d60df 100644
--- a/pyanalyze/signature.py
+++ b/pyanalyze/signature.py
@@ -7,6 +7,7 @@ calls.
 """
 
 from collections import defaultdict
+import itertools
 
 from .error_code import ErrorCode
 from .safe import all_of_type
@@ -986,10 +987,27 @@ class Signature:
         else:
             return KnownValue(value)
 
-    def can_assign(self, other: "Signature", ctx: CanAssignContext) -> CanAssign:
+    def can_assign(
+        self, other: "ConcreteSignature", ctx: CanAssignContext
+    ) -> CanAssign:
         """Equivalent of :meth:`pyanalyze.value.Value.can_assign`. Checks
         whether another ``Signature`` is compatible with this ``Signature``.
         """
+        if isinstance(other, OverloadedSignature):
+            # An overloaded signature can be assigned if any of the component signatures
+            # can be assigned. Strictly, an overloaded signature could satisfy a non-overloaded
+            # signature through a combination of overloads, but we make no attempt to support
+            # that.
+            errors = []
+            for sig in other.signatures:
+                can_assign = self.can_assign(sig, ctx)
+                if isinstance(can_assign, CanAssignError):
+                    errors.append(
+                        CanAssignError(f"overload {sig} is incompatible", [can_assign])
+                    )
+                else:
+                    return can_assign
+            return CanAssignError("overloaded function is incompatible", errors)
         if self.is_asynq and not other.is_asynq:
             return CanAssignError("callable is not asynq")
         their_return = other.signature.return_annotation
@@ -1330,6 +1348,29 @@ class Signature:
         if render_pos_only_separator:
             yield "/"
 
+    def bind_self(self, *, preserve_impl: bool = False) -> Optional["Signature"]:
+        if self.is_ellipsis_args:
+            return ANY_SIGNATURE
+        params = list(self.signature.parameters.values())
+        if not params or params[0].kind not in (
+            SigParameter.POSITIONAL_ONLY,
+            SigParameter.POSITIONAL_OR_KEYWORD,
+        ):
+            return None
+        return Signature(
+            signature=inspect.Signature(
+                params[1:], return_annotation=self.return_value
+            ),
+            # We don't carry over the implementation function by default, because it
+            # may not work when passed different arguments.
+            impl=self.impl if preserve_impl else None,
+            callable=self.callable,
+            is_asynq=self.is_asynq,
+            has_return_annotation=self.has_return_value(),
+            is_ellipsis_args=self.is_ellipsis_args,
+            allow_call=self.allow_call,
+        )
+
     # TODO: do we need these?
     def has_return_value(self) -> bool:
         return self.has_return_annotation
@@ -1346,11 +1387,118 @@ ANY_SIGNATURE = Signature.make(
 :class:`Signature`."""
 
 
+@dataclass(frozen=True)
+class OverloadedSignature:
+    """Represent an overloaded function."""
+
+    signatures: Tuple[Signature, ...]
+
+    def __init__(self, sigs: Sequence[Signature]) -> None:
+        object.__setattr__(self, "signatures", tuple(sigs))
+
+    def check_call(
+        self, args: Iterable[Argument], visitor: "NameCheckVisitor", node: ast.AST
+    ) -> ImplReturn:
+        # TODO this has a number of issues:
+        # - Need to read up more about how exactly overload should work
+        # - Do we really always match the first overload that works? What if one of the args is Any?
+        # - What if one of the args is a Union? Do we always unpack it?
+        errors_per_overload = []
+        rets = []
+        for sig in self.signatures:
+            with visitor.catch_errors() as caught_errors:
+                ret = sig.check_call(args, visitor, node)
+            if not caught_errors:
+                return ret
+            rets.append(ret)
+            errors_per_overload.append(caught_errors)
+        # None of the signatures matched
+
+        errors = list(itertools.chain.from_iterable(errors_per_overload))
+        codes = set(error["error_code"] for error in errors)
+        if len(codes) == 1:
+            (error_code,) = codes
+        else:
+            error_code = ErrorCode.incompatible_call
+        detail = self._make_detail(errors_per_overload)
+        visitor.show_error(
+            node, "Cannot call overloaded function", error_code, detail=str(detail)
+        )
+        return ImplReturn(unite_values(*[ret.return_value for ret in rets]))
+
+    def _make_detail(
+        self, errors_per_overload: Sequence[Sequence[Dict[str, Any]]]
+    ) -> CanAssignError:
+        details = []
+        for sig, errors in zip(self.signatures, errors_per_overload):
+            for error in errors:
+                inner = CanAssignError(
+                    error["e"],
+                    [CanAssignError(error["detail"])] if error["detail"] else [],
+                )
+                details.append(CanAssignError(f"In overload {sig}", [inner]))
+        return CanAssignError(children=details)
+
+    def substitute_typevars(self, typevars: TypeVarMap) -> "OverloadedSignature":
+        return OverloadedSignature(
+            [sig.substitute_typevars(typevars) for sig in self.signatures]
+        )
+
+    def bind_self(
+        self, *, preserve_impl: bool = False
+    ) -> Optional["OverloadedSignature"]:
+        bound_sigs = [
+            sig.bind_self(preserve_impl=preserve_impl) for sig in self.signatures
+        ]
+        if all_of_type(bound_sigs, Signature):
+            return OverloadedSignature(bound_sigs)
+        return None
+
+    def has_return_value(self) -> bool:
+        return all(sig.has_return_value() for sig in self.signatures)
+
+    @property
+    def return_value(self) -> Value:
+        return unite_values(*[sig.return_value for sig in self.signatures])
+
+    def __str__(self) -> str:
+        sigs = ", ".join(map(str, self.signatures))
+        return f"overloaded ({sigs})"
+
+    def walk_values(self) -> Iterable[Value]:
+        for sig in self.signatures:
+            yield from sig.walk_values()
+
+    def get_asynq_value(self) -> "OverloadedSignature":
+        return OverloadedSignature([sig.get_asynq_value() for sig in self.signatures])
+
+    @property
+    def is_asynq(self) -> bool:
+        return all(sig.is_asynq for sig in self.signatures)
+
+    def can_assign(
+        self, other: "ConcreteSignature", ctx: CanAssignContext
+    ) -> CanAssign:
+        # A signature can be assigned if it can be assigned to all the component signatures.
+        tv_maps = []
+        for sig in self.signatures:
+            can_assign = sig.can_assign(other, ctx)
+            if isinstance(can_assign, CanAssignError):
+                return CanAssignError(
+                    f"{other} is incompatible with overload {sig}", [can_assign]
+                )
+            tv_maps.append(can_assign)
+        return unify_typevar_maps(tv_maps)
+
+
+ConcreteSignature = Union[Signature, OverloadedSignature]
+
+
 @dataclass(frozen=True)
 class BoundMethodSignature:
     """Signature for a method bound to a particular value."""
 
-    signature: Signature
+    signature: ConcreteSignature
     self_composite: Composite
     return_override: Optional[Value] = None
 
@@ -1366,28 +1514,10 @@ class BoundMethodSignature:
             )
         return ret
 
-    def get_signature(self, *, preserve_impl: bool = False) -> Optional[Signature]:
-        if self.signature.is_ellipsis_args:
-            return ANY_SIGNATURE
-        params = list(self.signature.signature.parameters.values())
-        if not params or params[0].kind not in (
-            SigParameter.POSITIONAL_ONLY,
-            SigParameter.POSITIONAL_OR_KEYWORD,
-        ):
-            return None
-        return Signature(
-            signature=inspect.Signature(
-                params[1:], return_annotation=self.return_value
-            ),
-            # We don't carry over the implementation function by default, because it
-            # may not work when passed different arguments.
-            impl=self.signature.impl if preserve_impl else None,
-            callable=self.signature.callable,
-            is_asynq=self.signature.is_asynq,
-            has_return_annotation=self.has_return_value(),
-            is_ellipsis_args=self.signature.is_ellipsis_args,
-            allow_call=self.signature.allow_call,
-        )
+    def get_signature(
+        self, *, preserve_impl: bool = False
+    ) -> Optional[ConcreteSignature]:
+        return self.signature.bind_self(preserve_impl=preserve_impl)
 
     def has_return_value(self) -> bool:
         if self.return_override is not None:
@@ -1396,7 +1526,7 @@ class BoundMethodSignature:
 
     @property
     def return_value(self) -> Value:
-        if self.signature.has_return_value():
+        if isinstance(self.signature, Signature) and self.signature.has_return_value():
             return self.signature.return_value
         if self.return_override is not None:
             return self.return_override
@@ -1433,7 +1563,9 @@ class PropertyArgSpec:
         )
 
 
-MaybeSignature = Union[None, Signature, BoundMethodSignature, PropertyArgSpec]
+MaybeSignature = Union[
+    None, Signature, BoundMethodSignature, PropertyArgSpec, OverloadedSignature
+]
 
 
 def make_bound_method(
@@ -1443,7 +1575,7 @@ def make_bound_method(
 ) -> Optional[BoundMethodSignature]:
     if argspec is None:
         return None
-    if isinstance(argspec, Signature):
+    if isinstance(argspec, (Signature, OverloadedSignature)):
         return BoundMethodSignature(argspec, self_composite, return_override)
     elif isinstance(argspec, BoundMethodSignature):
         if return_override is None:
diff --git a/pyanalyze/value.py b/pyanalyze/value.py
index e8f81ba..290ade7 100644
--- a/pyanalyze/value.py
+++ b/pyanalyze/value.py
@@ -187,7 +187,9 @@ class CanAssignContext:
         """
         return {}
 
-    def get_signature(self, obj: object) -> Optional["pyanalyze.signature.Signature"]:
+    def get_signature(
+        self, obj: object
+    ) -> Optional["pyanalyze.signature.ConcreteSignature"]:
         """Return a :class:`pyanalyze.signature.Signature` for this object.
 
         Return None if the object is not callable.
@@ -494,7 +496,7 @@ class UnboundMethodValue(Value):
 
     def get_signature(
         self, ctx: CanAssignContext
-    ) -> Optional["pyanalyze.signature.Signature"]:
+    ) -> Optional["pyanalyze.signature.ConcreteSignature"]:
         signature = ctx.signature_from_value(self)
         if signature is None:
             return None
@@ -1114,9 +1116,9 @@ class CallableValue(TypedValue):
 
     """
 
-    signature: "pyanalyze.signature.Signature"
+    signature: "pyanalyze.signature.ConcreteSignature"
 
-    def __init__(self, signature: "pyanalyze.signature.Signature") -> None:
+    def __init__(self, signature: "pyanalyze.signature.ConcreteSignature") -> None:
         super().__init__(collections.abc.Callable)
         self.signature = signature
 
@@ -1139,43 +1141,19 @@ class CallableValue(TypedValue):
                 return CanAssignError(f"{other} is not a callable type")
             if isinstance(signature, pyanalyze.signature.BoundMethodSignature):
                 signature = signature.get_signature()
-            if isinstance(signature, pyanalyze.signature.Signature):
+            if isinstance(
+                signature,
+                (
+                    pyanalyze.signature.Signature,
+                    pyanalyze.signature.OverloadedSignature,
+                ),
+            ):
                 return self.signature.can_assign(signature, ctx)
 
         return super().can_assign(other, ctx)
 
     def __str__(self) -> str:
-        is_asynq = "Asynq" if self.signature.is_asynq else ""
-        return_value = self.signature.signature.return_annotation
-        if self.signature.is_ellipsis_args:
-            args = "..."
-        else:
-            parts = ["["]
-            added_star = False
-            for i, param in enumerate(self.signature.signature.parameters.values()):
-                if i > 0:
-                    parts.append(", ")
-                if param.kind is pyanalyze.signature.SigParameter.POSITIONAL_ONLY:
-                    parts.append(str(param.get_annotation()))
-                elif (
-                    param.kind is pyanalyze.signature.SigParameter.POSITIONAL_OR_KEYWORD
-                ):
-                    parts.append(f"{param.name}: {param.get_annotation()}")
-                elif param.kind is pyanalyze.signature.SigParameter.KEYWORD_ONLY:
-                    if not added_star:
-                        parts.append("*, ")
-                        added_star = True
-                    parts.append(f"{param.name}: {param.get_annotation()}")
-                elif param.kind is pyanalyze.signature.SigParameter.VAR_POSITIONAL:
-                    added_star = True
-                    parts.append(f"*{param.get_annotation()}")
-                elif param.kind is pyanalyze.signature.SigParameter.VAR_KEYWORD:
-                    parts.append(f"**{param.get_annotation()}")
-                if param.default is not pyanalyze.signature.EMPTY:
-                    parts.append(" = ...")
-            parts.append("]")
-            args = "".join(parts)
-        return f"{is_asynq}Callable[{args}, {return_value}]"
+        return str(self.signature)
 
 
 @dataclass(frozen=True)


Test patch:
diff --git a/pyanalyze/test_config.py b/pyanalyze/test_config.py
index 832ae82..356e0e0 100644
--- a/pyanalyze/test_config.py
+++ b/pyanalyze/test_config.py
@@ -6,7 +6,13 @@ Configuration file specific to tests.
 from typing import Dict, Optional
 
 from .arg_spec import ArgSpecCache
-from .signature import CallContext, Signature, SigParameter
+from .signature import (
+    CallContext,
+    ConcreteSignature,
+    OverloadedSignature,
+    Signature,
+    SigParameter,
+)
 from .config import Config
 from . import tests
 from . import value
@@ -63,7 +69,7 @@ class TestConfig(Config):
 
     def get_known_argspecs(
         self, arg_spec_cache: ArgSpecCache
-    ) -> Dict[object, Signature]:
+    ) -> Dict[object, ConcreteSignature]:
         failing_impl_sig = arg_spec_cache.get_argspec(
             tests.FailingImpl, impl=_failing_impl
         )
@@ -81,6 +87,24 @@ class TestConfig(Config):
                 callable=tests.takes_kwonly_argument,
             ),
             tests.FailingImpl: failing_impl_sig,
+            tests.overloaded: OverloadedSignature(
+                [
+                    Signature.make(
+                        [], value.TypedValue(int), callable=tests.overloaded
+                    ),
+                    Signature.make(
+                        [
+                            SigParameter(
+                                "x",
+                                SigParameter.POSITIONAL_ONLY,
+                                annotation=value.TypedValue(str),
+                            )
+                        ],
+                        value.TypedValue(str),
+                        callable=tests.overloaded,
+                    ),
+                ]
+            ),
         }
 
     def unwrap_cls(self, cls: type) -> type:
diff --git a/pyanalyze/test_signature.py b/pyanalyze/test_signature.py
index 03e5c35..4d667be 100644
--- a/pyanalyze/test_signature.py
+++ b/pyanalyze/test_signature.py
@@ -17,7 +17,12 @@ from .value import (
 from .test_name_check_visitor import TestNameCheckVisitorBase
 from .test_node_visitor import assert_fails, assert_passes, skip_before
 from .error_code import ErrorCode
-from .signature import Signature, SigParameter as P
+from .signature import (
+    ConcreteSignature,
+    OverloadedSignature,
+    Signature,
+    SigParameter as P,
+)
 from .test_value import CTX
 
 TupleInt = GenericValue(tuple, [TypedValue(int)])
@@ -38,16 +43,23 @@ def test_stringify() -> None:
     assert "(x: int) -> int" == str(
         Signature.make([P("x", annotation=TypedValue(int))], TypedValue(int))
     )
+    overload = OverloadedSignature(
+        [
+            Signature.make([], TypedValue(str)),
+            Signature.make([P("x", annotation=TypedValue(int))], TypedValue(int)),
+        ]
+    )
+    assert str(overload) == "overloaded (() -> str, (x: int) -> int)"
 
 
 class TestCanAssign:
-    def can(self, left: Signature, right: Signature) -> None:
+    def can(self, left: ConcreteSignature, right: ConcreteSignature) -> None:
         tv_map = left.can_assign(right, CTX)
         assert isinstance(
             tv_map, dict
         ), f"cannot assign {right} to {left} due to {tv_map}"
 
-    def cannot(self, left: Signature, right: Signature) -> None:
+    def cannot(self, left: ConcreteSignature, right: ConcreteSignature) -> None:
         tv_map = left.can_assign(right, CTX)
         assert isinstance(tv_map, CanAssignError), f"can assign {right} to {left}"
 
@@ -326,6 +338,23 @@ class TestCanAssign:
             Signature.make([P("a", annotation=bad_td, kind=P.VAR_KEYWORD)]),
         )
 
+    def test_overloads(self) -> None:
+        sig1 = Signature.make([], TypedValue(str))
+        sig2 = Signature.make([P("x", annotation=TypedValue(int))], TypedValue(int))
+        overload = OverloadedSignature([sig1, sig2])
+        self.can(overload, overload)
+        self.can(sig1, overload)
+        self.can(sig2, overload)
+        self.cannot(overload, sig1)
+        self.cannot(overload, sig2)
+
+        sig3 = Signature.make([P("y", annotation=TypedValue(float))], TypedValue(float))
+        overload2 = OverloadedSignature([sig1, sig2, sig3])
+        self.can(overload, overload2)
+        self.cannot(overload2, overload)
+        self.can(sig1, overload2)
+        self.cannot(sig3, overload)
+
 
 class TestProperty(TestNameCheckVisitorBase):
     @assert_passes()
@@ -426,7 +455,7 @@ class TestCalls(TestNameCheckVisitorBase):
         def capybara(x):
             obj = WithCall()
             assert_is_value(obj, TypedValue(WithCall))
-            assert_is_value(obj(x), AnyValue(AnySource.from_another))
+            assert_is_value(obj(x), AnyValue(AnySource.unannotated))
 
     @assert_fails(ErrorCode.incompatible_call)
     def test_unbound_method(self):
@@ -959,3 +988,15 @@ class TestAnnotated(TestNameCheckVisitorBase):
         def caller(x: Annotated[int, 42]):
             assert_is_value(x, AnnotatedValue(TypedValue(int), [KnownValue(42)]))
             assert_is_value(f(x), AnnotatedValue(TypedValue(int), [KnownValue(42)]))
+
+
+class TestOverload(TestNameCheckVisitorBase):
+    @assert_passes()
+    def test(self):
+        from pyanalyze.tests import overloaded
+
+        def capybara():
+            assert_is_value(overloaded(), TypedValue(int))
+            assert_is_value(overloaded("x"), TypedValue(str))
+            overloaded(1)  # E: incompatible_call
+            overloaded("x", "y")  # E: incompatible_call
diff --git a/pyanalyze/tests.py b/pyanalyze/tests.py
index 611a296..12351ac 100644
--- a/pyanalyze/tests.py
+++ b/pyanalyze/tests.py
@@ -5,7 +5,7 @@ Functions to be used in test_scope unit tests.
 
 """
 
-from typing import ClassVar
+from typing import ClassVar, Union, overload
 from asynq import asynq, async_proxy, AsyncTask, ConstFuture, get_async_fn, result
 from asynq.decorators import AsyncDecorator
 import qcore
@@ -203,3 +203,22 @@ WhatIsMyName.__init__.__name__ = "capybara"
 class FailingImpl:
     def __init__(self) -> None:
         pass
+
+
+@overload
+def overloaded() -> int:
+    ...
+
+
+@overload
+def overloaded(x: str) -> str:
+    ...
+
+
+def overloaded(*args: str) -> Union[int, str]:
+    if len(args) == 0:
+        return len(args)
+    elif len(args) == 1:
+        return args[0]
+    else:
+        raise TypeError("too many arguments")