Repository: youknowone/ring

Issue:
Better support for variable number of arguments
Currently, 
```py
In [74]: import ring
    ...:
    ...: @ring.lru()
    ...: def a(*args, **kwargs):
    ...:     print(args, kwargs)
    ...:

In [75]:

In [75]: a(3)
() {'args': (3,), 'kwargs': {}}'

In [76]: ring.__version__
Out[76]: '0.7.3'
```

Instead, it should print
```
(3,) {}
```


Discussion:

Patch:
diff --git a/ring/callable.py b/ring/callable.py
index 9120071..a64cdf8 100644
--- a/ring/callable.py
+++ b/ring/callable.py
@@ -1,9 +1,8 @@
 from __future__ import absolute_import
 
-import collections
 from wirerope.callable import Callable
 from ._util import cached_property
-from ._compat import inspect, qualname
+from ._compat import qualname
 
 __all__ = ('Callable', )
 
@@ -20,97 +19,6 @@ def _annotations(self):
     return getattr(self.wrapped_callable, '__annotations__', None) or {}
 
 
-def _kwargify(self, args, kwargs, bound_args=()):
-    """Create a merged kwargs-like object with given args and kwargs."""
-    merged = collections.OrderedDict()
-
-    parameters = self.parameters
-    parameters_len = len(parameters)
-
-    consumed_i = 0
-    consumed_names = set()
-    i = 0
-
-    # no .POSITIONAL_ONLY support
-
-    if bound_args:
-        i += len(bound_args)
-
-    while i < parameters_len and \
-            parameters[i].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
-        p = parameters[i]
-        i += 1
-
-        name = p.name
-        try:
-            value = args[consumed_i]
-        except IndexError:
-            pass
-        else:
-            if name in kwargs:
-                raise TypeError(
-                    "{}() got multiple values for argument '{}'".format(
-                        self.code.co_name, name))
-            consumed_i += 1
-            merged[name] = value
-            continue
-
-        if name in kwargs:
-            merged[name] = kwargs[name]
-            consumed_names.add(name)
-            continue
-
-        value = p.default
-        if value is inspect.Parameter.empty:
-            message = \
-                "{}() missing required positional argument: '{}'".format(
-                    self.code.co_name, p.name)
-            raise TypeError(message)
-        merged[name] = value
-
-    if i < parameters_len and \
-            parameters[i].kind == inspect.Parameter.VAR_POSITIONAL:
-        p = parameters[i]
-        i += 1
-
-        merged[p.name] = args[consumed_i:]
-    else:
-        if consumed_i < len(args):
-            raise TypeError(
-                "{}() takes {} positional arguments but {} were given"
-                .format(self.code.co_name, i, i + len(args) - consumed_i))
-
-    while i < parameters_len and \
-            parameters[i].kind == inspect.Parameter.KEYWORD_ONLY:
-        p = parameters[i]
-        i += 1
-
-        name = p.name
-        if name in kwargs:
-            merged[name] = kwargs[name]
-            consumed_names.add(name)
-        elif p.default is not inspect.Parameter.empty:
-            merged[name] = p.default
-        else:
-            raise TypeError(
-                "{}() missing 1 required keyword-only argument: '{}'"
-                .format(self.code.co_name, p.name))
-
-    var_kws = {k: v for k, v in kwargs.items() if k not in consumed_names}
-    if i < parameters_len and \
-            parameters[i].kind == inspect.Parameter.VAR_KEYWORD:
-        p = parameters[i]
-        i += 1
-
-        merged[p.name] = var_kws
-    elif var_kws:
-        raise TypeError(
-            "{}() got multiple values for arguments '{}'".format(
-                self.code.co_name, list(var_kws.keys())))
-
-    return merged
-
-
 def _identifier(self):
     return '.'.join((
         self.wrapped_callable.__module__, qualname(self.wrapped_callable)))
@@ -118,5 +26,4 @@ def _identifier(self):
 
 Callable.code = cached_property(_code)
 Callable.annotations = property(_annotations)
-Callable.kwargify = _kwargify
 Callable.identifier = cached_property(_identifier)
diff --git a/ring/django.py b/ring/django.py
index ff50bb9..477dc0b 100644
--- a/ring/django.py
+++ b/ring/django.py
@@ -62,7 +62,7 @@ def transform_cache_page_args(wire, rules, args, kwargs):
     else:
         request = raw_request  # type error?
 
-    return (request, ) + args[1:], kwargs
+    return (request, ), wire._pack_args(args[1:], kwargs)
 
 
 class CachePageUserInterface(fbase.BaseUserInterface):
@@ -83,7 +83,7 @@ class CachePageUserInterface(fbase.BaseUserInterface):
     @fbase.interface_attrs(
         transform_args=transform_cache_page_args,
         return_annotation=Tuple[str, str])
-    def key(self, wire, request, *args, **kwargs):
+    def key(self, wire, request, pargs):
         middleware = self.middleware
         key_get = get_cache_key(
             request, middleware.key_prefix, 'GET', cache=middleware.cache)
@@ -93,11 +93,11 @@ class CachePageUserInterface(fbase.BaseUserInterface):
 
     @fbase.interface_attrs(
         transform_args=transform_cache_page_args)
-    def execute(self, wire, request, *args, **kwargs):
+    def execute(self, wire, request, pargs):
         middleware = self.middleware
         view_func = wire.__func__
         try:
-            response = view_func(request, *args, **kwargs)
+            response = view_func(request, *pargs.args, **pargs.kwargs)
         except Exception as e:
             if hasattr(middleware, 'process_exception'):
                 result = middleware.process_exception(request, e)
@@ -113,7 +113,7 @@ class CachePageUserInterface(fbase.BaseUserInterface):
     @fbase.interface_attrs(
         transform_args=transform_cache_page_args,
         return_annotation=lambda a: Optional[a.get('return', Any)])
-    def get(self, wire, request, *args, **kwargs):
+    def get(self, wire, request, pargs):
         middleware = self.middleware
         result = middleware.process_request(request)
         if result is not None:
@@ -127,7 +127,7 @@ class CachePageUserInterface(fbase.BaseUserInterface):
 
     @fbase.interface_attrs(
         transform_args=transform_cache_page_args, return_annotation=None)
-    def set(self, wire, response, request, *args, **kwargs):
+    def set(self, wire, response, request, pargs):
         if not hasattr(request, '_cache_update_cache'):
             request._cache_update_cache = request.method in ('GET', 'HEAD')
         middleware = self.middleware
@@ -143,31 +143,31 @@ class CachePageUserInterface(fbase.BaseUserInterface):
 
     @fbase.interface_attrs(
         transform_args=transform_cache_page_args, return_annotation=None)
-    def update(self, wire, request, *args, **kwargs):
-        response = self.execute(wire, request, *args, **kwargs)
-        self.set(wire, response, request, *args, **kwargs)
+    def update(self, wire, request, pargs):
+        response = self.execute(wire, request, pargs)
+        self.set(wire, response, request, pargs)
         return response
 
     @fbase.interface_attrs(
         transform_args=transform_cache_page_args)
-    def get_or_update(self, wire, request, *args, **kwargs):
-        response = self.get(wire, request, *args, **kwargs)
+    def get_or_update(self, wire, request, pargs):
+        response = self.get(wire, request, pargs)
         if response is not None:
             return response
-        response = self.execute(wire, request, *args, **kwargs)
-        self.set(wire, response, request, *args, **kwargs)
+        response = self.execute(wire, request, pargs)
+        self.set(wire, response, request, pargs)
         return response
 
     @fbase.interface_attrs(
         transform_args=transform_cache_page_args, return_annotation=None)
-    def delete(self, wire, request, *args, **kwargs):
+    def delete(self, wire, request, pargs):
         if not getattr(request, '_fake_request', None):
             warnings.warn(
                 "A request is given as first argument. If this is intended "
                 "try '(request, None)'. Otherwise, Use '(request, path)' "
                 "instead of 'request' to convert the actual request to have "
                 "the target path.")
-        key_get, key_head = self.key(wire, request, *args, **kwargs)
+        key_get, key_head = self.key(wire, request, pargs)
         if key_get:
             self.middleware.cache.delete(key_get)
         if key_head:
@@ -175,7 +175,7 @@ class CachePageUserInterface(fbase.BaseUserInterface):
 
     @fbase.interface_attrs(
         transform_args=transform_cache_page_args, return_annotation=bool)
-    def has(self, *args, **kwargs):
+    def has(self, wire, request, pargs):
         raise NotImplementedError
         # The below implementation is not reliable for the return value `True`.
         # `False` always means the cache doesn't exist; While `True` doesn't
@@ -184,7 +184,7 @@ class CachePageUserInterface(fbase.BaseUserInterface):
 
     @fbase.interface_attrs(
         transform_args=transform_cache_page_args, return_annotation=None)
-    def touch(self, wire, request, *args, **kwargs):  #
+    def touch(self, wire, request, pargs):
         raise NotImplementedError
 
 
diff --git a/ring/func/asyncio.py b/ring/func/asyncio.py
index 068d6d5..70bf317 100644
--- a/ring/func/asyncio.py
+++ b/ring/func/asyncio.py
@@ -142,7 +142,6 @@ class CacheUserInterface(fbase.BaseUserInterface):
     """
 
     @fbase.interface_attrs(
-        transform_args=fbase.transform_kwargs_only,
         return_annotation=lambda a: Optional[a.get('return', Any)])
     async def get(self, wire, **kwargs):
         key = self.key(wire, **kwargs)
@@ -152,14 +151,12 @@ class CacheUserInterface(fbase.BaseUserInterface):
             result = wire._rope.config.miss_value
         return result
 
-    @fbase.interface_attrs(transform_args=fbase.transform_kwargs_only)
     async def update(self, wire, **kwargs):
         key = self.key(wire, **kwargs)
         result = await self.execute(wire, **kwargs)
         await wire.storage.set(key, result)
         return result
 
-    @fbase.interface_attrs(transform_args=fbase.transform_kwargs_only)
     async def get_or_update(self, wire, **kwargs):
         key = self.key(wire, **kwargs)
         try:
@@ -170,26 +167,26 @@ class CacheUserInterface(fbase.BaseUserInterface):
         return result
 
     @fbase.interface_attrs(
-        transform_args=(fbase.transform_kwargs_only, {'prefix_count': 1}),
+        transform_args=(fbase.transform_args_prefix, {'prefix_count': 1}),
         return_annotation=None)
     def set(self, wire, _value, **kwargs):
         key = self.key(wire, **kwargs)
         return wire.storage.set(key, _value)
 
     @fbase.interface_attrs(
-        transform_args=fbase.transform_kwargs_only, return_annotation=None)
+        return_annotation=None)
     def delete(self, wire, **kwargs):
         key = self.key(wire, **kwargs)
         return wire.storage.delete(key)
 
     @fbase.interface_attrs(
-        transform_args=fbase.transform_kwargs_only, return_annotation=bool)
+        return_annotation=bool)
     def has(self, wire, **kwargs):
         key = self.key(wire, **kwargs)
         return wire.storage.has(key)
 
     @fbase.interface_attrs(
-        transform_args=fbase.transform_kwargs_only, return_annotation=None)
+        return_annotation=None)
     def touch(self, wire, **kwargs):
         key = self.key(wire, **kwargs)
         return wire.storage.touch(key)
@@ -203,43 +200,47 @@ class BulkInterfaceMixin(fbase.AbstractBulkUserInterfaceMixin):
     """
 
     @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
         return_annotation=lambda a: List[a.get('return', Any)])
-    def execute_many(self, wire, *args_list):
+    def execute_many(self, wire, pargs):
         return asyncio.gather(*(
-            fbase.execute_bulk_item(wire, args) for args in args_list))
+            fbase.execute_bulk_item(wire, args) for args in pargs.args))
 
     @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
         return_annotation=lambda a: List[Optional[a.get('return', Any)]])
-    def get_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
+    def get_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
         return wire.storage.get_many(
             keys, miss_value=wire._rope.config.miss_value)
 
     @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
         return_annotation=lambda a: List[a.get('return', Any)])
-    async def update_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
-        values = await self.execute_many(wire, *args_list)
+    async def update_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
+        values = await self.execute_many(wire, pargs)
         await wire.storage.set_many(keys, values)
         return values
 
     @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
         return_annotation=lambda a: List[a.get('return', Any)])
-    async def get_or_update_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
+    async def get_or_update_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
         miss_value = object()
         results = await wire.storage.get_many(
             keys, miss_value=miss_value)
 
         miss_indices = []
-        for i, akr in enumerate(zip(args_list, keys, results)):
+        for i, akr in enumerate(zip(pargs.args, keys, results)):
             args, key, result = akr
             if result is not miss_value:
                 continue
             miss_indices.append(i)
 
         new_results = await asyncio.gather(*(
-            fbase.execute_bulk_item(wire, args_list[i]) for i in miss_indices))
+            fbase.execute_bulk_item(wire, pargs.args[i]) for i in miss_indices))
         new_keys = [keys[i] for i in miss_indices]
         await wire.storage.set_many(new_keys, new_results)
 
@@ -247,24 +248,33 @@ class BulkInterfaceMixin(fbase.AbstractBulkUserInterfaceMixin):
             results[old_i] = new_results[new_i]
         return results
 
-    @fbase.interface_attrs(return_annotation=None)
-    def set_many(self, wire, args_list, value_list):
-        keys = self.key_many(wire, *args_list)
+    @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
+        return_annotation=None)
+    def set_many(self, wire, pargs):
+        args_list, value_list = pargs.args
+        keys = self.key_many(wire, fbase.ArgPack((), args_list, {}))
         return wire.storage.set_many(keys, value_list)
 
-    @fbase.interface_attrs(return_annotation=None)
-    def delete_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
+    @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
+        return_annotation=None)
+    def delete_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
         return wire.storage.delete_many(keys)
 
-    @fbase.interface_attrs(return_annotation=None)
-    def has_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
+    @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
+        return_annotation=None)
+    def has_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
         return wire.storage.has_many(keys)
 
-    @fbase.interface_attrs(return_annotation=None)
-    def touch_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
+    @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
+        return_annotation=None)
+    def touch_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
         return wire.storage.touch_many(keys)
 
 
diff --git a/ring/func/base.py b/ring/func/base.py
index 7778d00..78dec58 100644
--- a/ring/func/base.py
+++ b/ring/func/base.py
@@ -3,13 +3,14 @@
 
 """  # noqa: W605
 import abc
+import collections
 import types
 from typing import List
 
 import attr
 import six
 from wirerope import Wire, WireRope, RopeCore
-from .._compat import functools, qualname
+from .._compat import functools, inspect, qualname
 from ..callable import Callable
 from ..key import CallableKey
 from ..coder import registry as default_registry
@@ -32,6 +33,98 @@ __all__ = (
     'BaseUserInterface', 'BaseStorage', 'CommonMixinStorage', 'StorageMixin')
 
 
+class ArgPack(collections.namedtuple("_ArgPack", ["bounds", "args", "kwargs"])):
+    __slots__ = []
+
+    def labels(self, callable):
+        """Create a merged kwargs-like object with given args and kwargs."""
+        bound_args, args, kwargs = self
+
+        merged = collections.OrderedDict()
+        parameters = callable.parameters
+        parameters_len = len(parameters)
+
+        consumed_i = 0
+        consumed_names = set()
+        i = 0
+
+        # no .POSITIONAL_ONLY support
+
+        args = bound_args + args
+        while i < parameters_len and \
+                parameters[i].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
+            p = parameters[i]
+            i += 1
+
+            name = p.name
+            try:
+                value = args[consumed_i]
+            except IndexError:
+                pass
+            else:
+                if name in kwargs:
+                    raise TypeError(
+                        "{}() got multiple values for argument '{}'".format(
+                            callable.code.co_name, name))
+                consumed_i += 1
+                merged[name] = value
+                continue
+
+            if name in kwargs:
+                merged[name] = kwargs[name]
+                consumed_names.add(name)
+                continue
+
+            value = p.default
+            if value is inspect.Parameter.empty:
+                message = \
+                    "{}() missing required positional argument: '{}'".format(
+                        callable.code.co_name, p.name)
+                raise TypeError(message)
+            merged[name] = value
+
+        if i < parameters_len and \
+                parameters[i].kind == inspect.Parameter.VAR_POSITIONAL:
+            p = parameters[i]
+            i += 1
+
+            merged['*' + p.name] = args[consumed_i:]
+        else:
+            if consumed_i < len(args):
+                raise TypeError(
+                    "{}() takes {} positional arguments but {} were given"
+                    .format(callable.code.co_name, i, i + len(args) - consumed_i))
+
+        while i < parameters_len and \
+                parameters[i].kind == inspect.Parameter.KEYWORD_ONLY:
+            p = parameters[i]
+            i += 1
+
+            name = p.name
+            if name in kwargs:
+                merged[name] = kwargs[name]
+                consumed_names.add(name)
+            elif p.default is not inspect.Parameter.empty:
+                merged[name] = p.default
+            else:
+                raise TypeError(
+                    "{}() missing 1 required keyword-only argument: '{}'"
+                    .format(callable.code.co_name, p.name))
+
+        var_kws = {k: v for k, v in kwargs.items() if k not in consumed_names}
+        if i < parameters_len and \
+                parameters[i].kind == inspect.Parameter.VAR_KEYWORD:
+            p = parameters[i]
+            i += 1
+            merged['**' + p.name] = var_kws
+        elif var_kws:
+            raise TypeError(
+                "{}() got multiple values for arguments '{}'".format(
+                    callable.code.co_name, list(var_kws.keys())))
+
+        return merged
+
+
 def suggest_ignorable_keys(c, ignorable_keys):
     if ignorable_keys is None:
         _ignorable_keys = []
@@ -150,30 +243,6 @@ def coerce(v, in_memory_storage):
         "The given value '{}' of type '{}' is not a key-compatible type. {}".format(v, cls, msg))
 
 
-def create_key_builder(
-        c, key_prefix, ignorable_keys, coerce=coerce, encoding=None,
-        key_refactor=None, in_memory_storage=False):
-    assert isinstance(c, Callable)
-    key_generator = CallableKey(
-        c, format_prefix=key_prefix, ignorable_keys=ignorable_keys)
-
-    def compose_key(*bound_args, **kwargs):
-        full_kwargs = kwargs.copy()
-        for i, prearg in enumerate(bound_args):
-            full_kwargs[c.parameters[i].name] = bound_args[i]
-        coerced_kwargs = {
-            k: coerce(v, in_memory_storage) for k, v in full_kwargs.items()
-            if k not in ignorable_keys}
-        key = key_generator.build(coerced_kwargs)
-        if encoding:
-            key = key.encode(encoding)
-        if key_refactor:
-            key = key_refactor(key)
-        return key
-
-    return compose_key
-
-
 def interface_attrs(**kwargs):
     if 'return_annotation' in kwargs:
         kwargs['__annotations_override__'] = {
@@ -198,7 +267,7 @@ def interface_attrs(**kwargs):
     return _decorator
 
 
-def transform_kwargs_only(wire, rules, args, kwargs):
+def transform_args_prefix(wire, rules, args, kwargs):
     """`transform_args` for basic single-access methods in interfaces.
 
     Create and returns uniform fully keyword-annotated arguments except for
@@ -226,8 +295,14 @@ def transform_kwargs_only(wire, rules, args, kwargs):
     prefix_count = rules.get('prefix_count', 0)
     wrapper_args = args[:prefix_count]
     function_args = args[prefix_count:]
-    full_kwargs = wire._merge_args(function_args, kwargs)
-    return wrapper_args, full_kwargs
+    pargs = wire._pack_args(function_args, kwargs)
+    return wrapper_args, pargs
+
+
+def transform_positional_only(wire, rules, args, kwargs):
+    if kwargs:
+        raise TypeError("Keyword arguments are not allowed for many operations")
+    return (), wire._pack_args(args, {})
 
 
 @six.add_metaclass(abc.ABCMeta)
@@ -246,55 +321,48 @@ class BaseUserInterface(object):
 
     The parameter *transform_args* in :func:`ring.func.base.interface_attrs`
     defines the figure of method parameters. For the **BaseUserInterface**,
-    every method's *transform_args* is
-    :func:`ring.func.base.transform_kwargs_only` which force to pass uniform
-    keyword arguments to the interface methods.
+    some methods' *transform_args* are
+    :func:`ring.func.base.transform_args_prefix` which removes specified
+    count of prefix.
     Other mix-ins or subclasses may have different *transform_args*.
 
     The first parameter of interface method *always* is a **RingWire** object.
     The other parameters are composed by *transform_args*.
 
-    :see: :func:`ring.func.base.transform_kwargs_only` for the specific
+    :see: :func:`ring.func.base.transform_args_prefix` for the specific
         argument transformation rule for each methods.
 
     The parameters below describe common methods' parameters.
 
     :param ring.func.base.RingWire wire: The corresponding ring
         wire object.
-    :param Dict[str,Any] kwargs: Fully keyword-annotated arguments. When
-        actual function arguments are passed to each sub-function of the
-        wire, they are merged into the form of keyword arguments. This gives
-        the consistent interface for arguments handling. Note that it only
-        describes the methods' *transform_args* attribute is
-        :func:`ring.func.base.transform_kwargs_only`
+    :param ArgPack pargs: Refined arguments pack.
     """
 
     def __init__(self, ring):
         self._ring = ring
 
     @interface_attrs(
-        transform_args=transform_kwargs_only, return_annotation=str)
-    def key(self, wire, **kwargs):
+        return_annotation=str)
+    def key(self, wire, pargs):
         """Create and return the composed key for storage.
 
         :see: The class documentation for the parameter details.
         :return: The composed key with given arguments.
         :rtype: str
         """
-        return wire._rope.compose_key(*wire._bound_objects, **kwargs)
+        return wire._rope.compose_key(pargs=pargs)
 
-    @interface_attrs(transform_args=transform_kwargs_only)
-    def execute(self, wire, **kwargs):
+    def execute(self, wire, pargs):
         """Execute and return the result of the original function.
 
         :see: The class documentation for the parameter details.
         :return: The result of the original function.
         """
-        return wire.__func__(**kwargs)
+        return wire.__func__(*pargs.args, **pargs.kwargs)
 
     @abc.abstractmethod
-    @interface_attrs(transform_args=transform_kwargs_only)
-    def get(self, wire, **kwargs):  # pragma: no cover
+    def get(self, wire, pargs):  # pragma: no cover
         """Try to get and return the storage value of the corresponding key.
 
         :see: The class documentation for the parameter details.
@@ -305,8 +373,8 @@ class BaseUserInterface(object):
         raise NotImplementedError
 
     @interface_attrs(
-        transform_args=(transform_kwargs_only, {'prefix_count': 1}))
-    def set(self, wire, value, **kwargs):  # pragma: no cover
+        transform_args=(transform_args_prefix, {'prefix_count': 1}))
+    def set(self, wire, value, pargs):  # pragma: no cover
         """Set the storage value of the corresponding key as the given `value`.
 
         :see: :meth:`ring.func.base.BaseUserInterface.key` for the key.
@@ -318,8 +386,7 @@ class BaseUserInterface(object):
         raise NotImplementedError
 
     @abc.abstractmethod
-    @interface_attrs(transform_args=transform_kwargs_only)
-    def update(self, wire, **kwargs):  # pragma: no cover
+    def update(self, wire, pargs):  # pragma: no cover
         """Execute the original function and `set` the result as the value.
 
         This action is comprehensible as a concatenation of
@@ -336,8 +403,7 @@ class BaseUserInterface(object):
         raise NotImplementedError
 
     @abc.abstractmethod
-    @interface_attrs(transform_args=transform_kwargs_only)
-    def get_or_update(self, wire, **kwargs):  # pragma: no cover
+    def get_or_update(self, wire, pargs):  # pragma: no cover
         """Try to get and return the storage value; Otherwise, update and so.
 
         :see: :meth:`ring.func.base.BaseUserInterface.get` for get.
@@ -350,8 +416,7 @@ class BaseUserInterface(object):
         raise NotImplementedError
 
     @abc.abstractmethod
-    @interface_attrs(transform_args=transform_kwargs_only)
-    def delete(self, wire, **kwargs):  # pragma: no cover
+    def delete(self, wire, pargs):  # pragma: no cover
         """Delete the storage value of the corresponding key.
 
         :see: :meth:`ring.func.base.BaseUserInterface.key` for the key.
@@ -361,8 +426,7 @@ class BaseUserInterface(object):
         """
         raise NotImplementedError
 
-    @interface_attrs(transform_args=transform_kwargs_only)
-    def has(self, wire, **kwargs):  # pragma: no cover
+    def has(self, wire, pargs):  # pragma: no cover
         """Return whether the storage has a value of the corresponding key.
 
         This is an optional function.
@@ -375,8 +439,7 @@ class BaseUserInterface(object):
         """
         raise NotImplementedError
 
-    @interface_attrs(transform_args=transform_kwargs_only)
-    def touch(self, wire, **kwargs):  # pragma: no cover
+    def touch(self, wire, pargs):  # pragma: no cover
         """Touch the storage value of the corresponding key.
 
         This is an optional function.
@@ -392,15 +455,18 @@ class BaseUserInterface(object):
 
 def create_bulk_key(interface, wire, args):
     if isinstance(args, tuple):
-        kwargs = wire._merge_args(args, {})
-        return interface.key(wire, **kwargs)
+        args_pack = wire._pack_args(args, {})
     elif isinstance(args, dict):
-        return interface.key(wire, **args)
+        args_pack = wire._pack_args((), args)
+    elif isinstance(args, ArgPack):
+        args_pack = args
     else:
         raise TypeError(
             "Each parameter of '_many' suffixed sub-functions must be an "
             "instance of 'tuple' or 'dict'")
 
+    return interface.key(wire, args_pack)
+
 
 def execute_bulk_item(wire, args):
     if isinstance(args, tuple):
@@ -440,17 +506,20 @@ class AbstractBulkUserInterfaceMixin(object):
         don't have *transform_args* attribute.
     """
 
-    @interface_attrs(return_annotation=lambda a: List[str])
-    def key_many(self, wire, *args_list):
+    @interface_attrs(
+        transform_args=transform_positional_only,
+        return_annotation=lambda a: List[str])
+    def key_many(self, wire, pargs):
         """Create and return the composed keys for storage.
 
         :see: The class documentation for the parameter details.
         :return: A sequence of created keys.
         :rtype: Sequence[str]
         """
-        return [create_bulk_key(self, wire, args) for args in args_list]
+        return [create_bulk_key(self, wire, args) for args in pargs.args]
 
-    def execute_many(self, wire, *args_list):  # pragma: no cover
+    @interface_attrs(transform_args=transform_positional_only)
+    def execute_many(self, wire, pargs):  # pragma: no cover
         """Execute and return the results of the original function.
 
         :see: The class documentation for the parameter details.
@@ -459,7 +528,8 @@ class AbstractBulkUserInterfaceMixin(object):
         """
         raise NotImplementedError
 
-    def get_many(self, wire, *args_list):  # pragma: no cover
+    @interface_attrs(transform_args=transform_positional_only)
+    def get_many(self, wire, pargs):  # pragma: no cover
         """Try to get and returns the storage values.
 
         :see: The class documentation for the parameter details.
@@ -470,7 +540,8 @@ class AbstractBulkUserInterfaceMixin(object):
         """
         raise NotImplementedError
 
-    def update_many(self, wire, *args_list):  # pragma: no cover
+    @interface_attrs(transform_args=transform_positional_only)
+    def update_many(self, wire, pargs):  # pragma: no cover
         """Execute the original function and `set` the result as the value.
 
         :see: The class documentation for the parameter details.
@@ -479,7 +550,8 @@ class AbstractBulkUserInterfaceMixin(object):
         """
         raise NotImplementedError
 
-    def get_or_update_many(self, wire, *args_list):  # pragma: no cover
+    @interface_attrs(transform_args=transform_positional_only)
+    def get_or_update_many(self, wire, pargs):  # pragma: no cover
         """Try to get and returns the storage values.
 
         :note: The semantics of this function may vary by the implementation.
@@ -491,7 +563,8 @@ class AbstractBulkUserInterfaceMixin(object):
         """
         raise NotImplementedError
 
-    def set_many(self, wire, args_list, value_list):  # pragma: no cover
+    @interface_attrs(transform_args=transform_positional_only)
+    def set_many(self, wire, pargs):  # pragma: no cover
         """Set the storage values of the corresponding keys as the given values.
 
         :see: The class documentation for common parameter details.
@@ -501,7 +574,8 @@ class AbstractBulkUserInterfaceMixin(object):
         """
         raise NotImplementedError
 
-    def delete_many(self, wire, *args_list):  # pragma: no cover
+    @interface_attrs(transform_args=transform_positional_only)
+    def delete_many(self, wire, pargs):  # pragma: no cover
         """Delete the storage values of the corresponding keys.
 
         :see: The class documentation for the parameter details.
@@ -509,7 +583,8 @@ class AbstractBulkUserInterfaceMixin(object):
         """
         raise NotImplementedError
 
-    def has_many(self, wire, *args_list):  # pragma: no cover
+    @interface_attrs(transform_args=transform_positional_only)
+    def has_many(self, wire, pargs):  # pragma: no cover
         """Return whether the storage has values of the corresponding keys.
 
         :see: The class documentation for the parameter details.
@@ -517,7 +592,8 @@ class AbstractBulkUserInterfaceMixin(object):
         """
         raise NotImplementedError
 
-    def touch_many(self, wire, *args_list):  # pragma: no cover
+    @interface_attrs(transform_args=transform_positional_only)
+    def touch_many(self, wire, pargs):  # pragma: no cover
         """Touch the storage values of the corresponding keys.
 
         :see: The class documentation for the parameter details.
@@ -559,7 +635,7 @@ class RingWire(Wire):
     def decode(self, v):
         return self._rope.decode(v)
 
-    def _merge_args(self, args, kwargs):
+    def _pack_args(self, args, kwargs):
         """Create a fake kwargs object by merging actual arguments.
 
         The merging follows the signature of wrapped function and current
@@ -569,10 +645,8 @@ class RingWire(Wire):
         if type(self.__func__) is types.FunctionType:  # noqa
             bound_args = ()
         else:
-            bound_args = range(len(self._bound_objects))
-        full_kwargs = self._callable.kwargify(
-            args, kwargs, bound_args=bound_args)
-        return full_kwargs
+            bound_args = self._bound_objects
+        return ArgPack(bound_args, args, kwargs)
 
     def run(self, action, *args, **kwargs):
         attr = getattr(self, action)
@@ -599,9 +673,12 @@ class RingWire(Wire):
             def impl_f(*args, **kwargs):
                 if transform_args:
                     transform_func, transform_rules = transform_args
-                    args, kwargs = transform_func(
+                    fargs, pargs = transform_func(
                         self, transform_rules, args, kwargs)
-                return attr(self, *args, **kwargs)
+                else:
+                    fargs = ()
+                    pargs = self._pack_args(args, kwargs)
+                return attr(self, *fargs, pargs=pargs)
 
             cc = self._callable.wrapped_callable
             functools.wraps(cc)(impl_f)
@@ -631,7 +708,7 @@ class PublicRing(object):
         self._rope = rope
 
     def key(self, func):
-        self._rope.compose_key = func
+        self._rope.compose_key = lambda pargs: func(*pargs.args, **pargs.kwargs)
 
     def encode(self, func):
         self._rope._encode = func
@@ -649,7 +726,7 @@ class RingRope(RopeCore):
 
         self.ring = PublicRing(self)
 
-    def compose_key(self, *bound_args, **kwargs):
+    def compose_key(self, pargs):
         config = self.config
 
         _ignorable_keys = suggest_ignorable_keys(
@@ -660,13 +737,10 @@ class RingRope(RopeCore):
         key_generator = CallableKey(
             c, format_prefix=_key_prefix, ignorable_keys=_ignorable_keys)
 
-        full_kwargs = kwargs.copy()
-        for i, prearg in enumerate(bound_args):
-            full_kwargs[c.parameters[i].name] = bound_args[i]
-
         in_memory_storage = hasattr(config.storage_class, 'in_memory_storage')
+        labels = pargs.labels(key_generator.provider)
         coerced_kwargs = {
-            k: coerce(v, in_memory_storage) for k, v in full_kwargs.items()
+            k: coerce(v, in_memory_storage) for k, v in labels.items()
             if k not in _ignorable_keys}
         key = key_generator.build(coerced_kwargs)
         if config.key_encoding:
diff --git a/ring/func/sync.py b/ring/func/sync.py
index c4deecb..9386f60 100644
--- a/ring/func/sync.py
+++ b/ring/func/sync.py
@@ -24,56 +24,53 @@ class CacheUserInterface(fbase.BaseUserInterface):
     """
 
     @fbase.interface_attrs(
-        transform_args=fbase.transform_kwargs_only,
         return_annotation=lambda a: Optional[a.get('return', Any)])
-    def get(self, wire, **kwargs):
-        key = self.key(wire, **kwargs)
+    def get(self, wire, pargs):
+        key = self.key(wire, pargs=pargs)
         try:
             result = wire.storage.get(key)
         except fbase.NotFound:
             result = wire._rope.config.miss_value
         return result
 
-    @fbase.interface_attrs(transform_args=fbase.transform_kwargs_only)
-    def update(self, wire, **kwargs):
-        key = wire.key(**kwargs)
-        result = wire.execute(**kwargs)
+    def update(self, wire, pargs):
+        key = self.key(wire, pargs=pargs)
+        result = self.execute(wire, pargs=pargs)
         wire.storage.set(key, result)
         return result
 
-    @fbase.interface_attrs(transform_args=fbase.transform_kwargs_only)
-    def get_or_update(self, wire, **kwargs):
-        key = self.key(wire, **kwargs)
+    def get_or_update(self, wire, pargs):
+        key = self.key(wire, pargs=pargs)
         try:
             result = wire.storage.get(key)
         except fbase.NotFound:
-            result = self.execute(wire, **kwargs)
+            result = self.execute(wire, pargs=pargs)
             wire.storage.set(key, result)
         return result
 
     @fbase.interface_attrs(
-        transform_args=(fbase.transform_kwargs_only, {'prefix_count': 1}),
+        transform_args=(fbase.transform_args_prefix, {'prefix_count': 1}),
         return_annotation=None)
-    def set(self, wire, _value, **kwargs):
-        key = self.key(wire, **kwargs)
+    def set(self, wire, _value, pargs):
+        key = self.key(wire, pargs=pargs)
         wire.storage.set(key, _value)
 
     @fbase.interface_attrs(
-        transform_args=fbase.transform_kwargs_only, return_annotation=None)
-    def delete(self, wire, **kwargs):
-        key = self.key(wire, **kwargs)
+        return_annotation=None)
+    def delete(self, wire, pargs):
+        key = self.key(wire, pargs=pargs)
         wire.storage.delete(key)
 
     @fbase.interface_attrs(
-        transform_args=fbase.transform_kwargs_only, return_annotation=bool)
-    def has(self, wire, **kwargs):
-        key = self.key(wire, **kwargs)
+        return_annotation=bool)
+    def has(self, wire, pargs):
+        key = self.key(wire, pargs=pargs)
         return wire.storage.has(key)
 
     @fbase.interface_attrs(
-        transform_args=fbase.transform_kwargs_only, return_annotation=None)
-    def touch(self, wire, **kwargs):
-        key = self.key(wire, **kwargs)
+        return_annotation=None)
+    def touch(self, wire, pargs):
+        key = self.key(wire, pargs=pargs)
         wire.storage.touch(key)
 
 
@@ -85,43 +82,47 @@ class BulkInterfaceMixin(fbase.AbstractBulkUserInterfaceMixin):
     """
 
     @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
         return_annotation=lambda a: List[a.get('return', Any)])
-    def execute_many(self, wire, *args_list):
-        values = [fbase.execute_bulk_item(wire, args) for args in args_list]
+    def execute_many(self, wire, pargs):
+        values = [fbase.execute_bulk_item(wire, args) for args in pargs.args]
         return values
 
     @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
         return_annotation=lambda a: List[Optional[a.get('return', Any)]])
-    def get_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
+    def get_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
         results = wire.storage.get_many(
             keys, miss_value=wire._rope.config.miss_value)
         return results
 
     @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
         return_annotation=lambda a: List[a.get('return', Any)])
-    def update_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
-        values = self.execute_many(wire, *args_list)
+    def update_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
+        values = self.execute_many(wire, pargs)
         wire.storage.set_many(keys, values)
         return values
 
     @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
         return_annotation=lambda a: List[a.get('return', Any)])
-    def get_or_update_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
+    def get_or_update_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
         miss_value = object()
         results = wire.storage.get_many(keys, miss_value=miss_value)
 
         miss_indices = []
-        for i, akr in enumerate(zip(args_list, keys, results)):
+        for i, akr in enumerate(zip(pargs.args, keys, results)):
             args, key, result = akr
             if result is not miss_value:
                 continue
             miss_indices.append(i)
 
         new_results = [
-            fbase.execute_bulk_item(wire, args_list[i]) for i in miss_indices]
+            fbase.execute_bulk_item(wire, pargs.args[i]) for i in miss_indices]
         new_keys = [keys[i] for i in miss_indices]
         wire.storage.set_many(new_keys, new_results)
 
@@ -129,24 +130,33 @@ class BulkInterfaceMixin(fbase.AbstractBulkUserInterfaceMixin):
             results[old_i] = new_results[new_i]
         return results
 
-    @fbase.interface_attrs(return_annotation=None)
-    def set_many(self, wire, args_list, value_list):
-        keys = self.key_many(wire, *args_list)
+    @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
+        return_annotation=None)
+    def set_many(self, wire, pargs):
+        args_list, value_list = pargs.args
+        keys = self.key_many(wire, fbase.ArgPack((), args_list, {}))
         wire.storage.set_many(keys, value_list)
 
-    @fbase.interface_attrs(return_annotation=None)
-    def delete_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
+    @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
+        return_annotation=None)
+    def delete_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
         wire.storage.delete_many(keys)
 
-    @fbase.interface_attrs(return_annotation=None)
-    def has_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
+    @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
+        return_annotation=None)
+    def has_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
         wire.storage.has_many(keys)
 
-    @fbase.interface_attrs(return_annotation=None)
-    def touch_many(self, wire, *args_list):
-        keys = self.key_many(wire, *args_list)
+    @fbase.interface_attrs(
+        transform_args=fbase.transform_positional_only,
+        return_annotation=None)
+    def touch_many(self, wire, pargs):
+        keys = self.key_many(wire, pargs)
         wire.storage.touch_many(keys)
 
 
diff --git a/ring/key.py b/ring/key.py
index 0cf3e2c..6f87f0e 100644
--- a/ring/key.py
+++ b/ring/key.py
@@ -1,6 +1,7 @@
 from __future__ import absolute_import
 
 import re
+from ._compat import inspect
 from ._util import cached_property
 from .callable import Callable
 
@@ -43,6 +44,15 @@ class FormatKey(Key):
         return keys
 
 
+def _param_name(param):
+    if param.kind == inspect.Parameter.VAR_POSITIONAL:
+        return '*' + param.name
+    elif param.kind == inspect.Parameter.VAR_KEYWORD:
+        return '**' + param.name
+    else:
+        return param.name
+
+
 class CallableKey(Key):
 
     def __init__(
@@ -63,7 +73,7 @@ class CallableKey(Key):
 
     @cached_property
     def ordered_provider_keys(self):
-        keys = [arg.name for arg in self.provider.parameters]
+        keys = [_param_name(p) for p in self.provider.parameters]
         for key in self.ignorable_keys:
             if key not in keys:
                 raise KeyError(
@@ -79,20 +89,13 @@ class CallableKey(Key):
     @staticmethod
     def default_format_body(provider_keys, verbose):
         parts = []
+        if verbose:
+            arg_form = ':{key}={{{key}}}'
+        else:
+            arg_form = ':{{{key}}}'
         for key in provider_keys:
-            if verbose:
-                parts.append(':{key}={{{key}}}'.format(key=key))
-            else:
-                parts.append(':{{{key}}}'.format(key=key))
+            parts.append(arg_form.format(key=key))
         return ''.join(parts)
 
-    def merge_kwargs(self, args, kwargs):
-        merged = self.provider.kwargify(args, kwargs)
-        for key in self.ignorable_keys:
-            del merged[key]
-        return merged
-
-    def build(self, full_kwargs):
-        # print(self.format, full_kwargs)
-        key = self.format.format(**full_kwargs)
-        return key
+    def build(self, labels):
+        return self.format.format(**labels)


Test patch:
diff --git a/tests/_test_callable_py3.py b/tests/_test_callable_py3.py
index 0f847cb..8a54087 100644
--- a/tests/_test_callable_py3.py
+++ b/tests/_test_callable_py3.py
@@ -1,29 +1,28 @@
 import asyncio
 from typing import Any, Optional
 from ring.callable import Callable
+from ring.func.base import ArgPack
 
 import pytest
 
 
-@pytest.mark.parametrize('f,args,kwargs,merged', [
-    (lambda x, *args, y=10, **kw: None, (1,), {}, dict(x=1, args=(), y=10, kw={})),
-    (lambda x, *, y, z=20: None, (1,), dict(y=10), dict(x=1, y=10, z=20)),
+@pytest.mark.parametrize('f,pargs,merged', [
+    (lambda x, *args, y=10, **kw: None, ArgPack((), (1,), {}), {"x": 1, "*args": (), "y": 10, "**kw": {}}),
+    (lambda x, *, y, z=20: None, ArgPack((), (1,), dict(y=10)), {"x": 1, "y": 10, "z": 20}),
 ])
-def test_kwargify_py3(f, args, kwargs, merged):
-    kwargified = Callable(f).kwargify(args, kwargs)
-    print(kwargified)
-    print(merged)
+def test_make_labels_py3(f, pargs, merged):
+    kwargified = pargs.labels(Callable(f))
     assert kwargified == merged, (kwargified, merged)
 
 
-@pytest.mark.parametrize('f,args,kwargs,exc', [
-    (lambda x, *args, y=30: None, (2), {'x': 1}, TypeError),
-    (lambda x, *, y, z=20: None, (1,), {}, TypeError),
-    (lambda x, *, z=20: None, (1,), {'w': 2}, TypeError),
+@pytest.mark.parametrize('f,pargs,exc', [
+    (lambda x, *args, y=30: None, ArgPack((), (2), {'x': 1}), TypeError),
+    (lambda x, *, y, z=20: None, ArgPack((), (1,), {}), TypeError),
+    (lambda x, *, z=20: None, ArgPack((), (1,), {'w': 2}), TypeError),
 ])
-def test_kwargify_exc_py3(f, args, kwargs, exc):
+def test_make_labels_exc_py3(f, pargs, exc):
     with pytest.raises(exc):
-        Callable(f).kwargify(args, kwargs)
+        pargs.labels(Callable(f))
 
 
 def test_annotations():
diff --git a/tests/_test_interface.py b/tests/_test_interface.py
index 3c48c14..1903a06 100644
--- a/tests/_test_interface.py
+++ b/tests/_test_interface.py
@@ -6,16 +6,16 @@ from ring.func.base import BaseUserInterface, NotFound, factory
 
 class DoubleCacheUserInterface(BaseUserInterface):
 
-    async def execute(self, wire, **kwargs):
-        result = await wire.__func__(**kwargs)
+    async def execute(self, wire, pargs):
+        result = await wire.__func__(*pargs.args, **pargs.kwargs)
         return result
 
-    def key2(self, wire, **kwargs):
-        return wire.key(**kwargs) + ':back'
+    def key2(self, wire, pargs):
+        return self.key(wire, pargs) + ':back'
 
-    async def get(self, wire, **kwargs):
-        key1 = wire.key(**kwargs)
-        key2 = wire.key2(**kwargs)
+    async def get(self, wire, pargs):
+        key1 = self.key(wire, pargs)
+        key2 = self.key2(wire, pargs)
 
         result = ...
         for key in [key1, key2]:
@@ -29,22 +29,24 @@ class DoubleCacheUserInterface(BaseUserInterface):
             result = wire._rope.config.miss_value
         return result
 
-    async def update(self, wire, **kwargs):
-        key = wire.key(**kwargs)
-        key2 = wire.key2(**kwargs)
-        result = await wire.execute(**kwargs)
-        await wire._rope.storage.set(key, result)
+    async def update(self, wire, pargs):
+        key1 = self.key(wire, pargs)
+        key2 = self.key2(wire, pargs)
+
+        result = await self.execute(wire, pargs)
+        await wire._rope.storage.set(key1, result)
         await wire._rope.storage.set(key2, result, None)
         return result
 
-    async def get_or_update(self, wire, **kwargs):
-        key = wire.key(**kwargs)
-        key2 = wire.key2(**kwargs)
+    async def get_or_update(self, wire, pargs):
+        key1 = self.key(wire, pargs)
+        key2 = self.key2(wire, pargs)
+
         try:
-            result = await wire._rope.storage.get(key)
+            result = await wire._rope.storage.get(key1)
         except NotFound:
             try:
-                result = await wire.execute(**kwargs)
+                result = await self.execute(wire, pargs)
             except Exception:
                 try:
                     result = await wire._rope.storage.get(key2)
@@ -54,20 +56,22 @@ class DoubleCacheUserInterface(BaseUserInterface):
                     return result
                 raise
             else:
-                await wire._rope.storage.set(key, result)
+                await wire._rope.storage.set(key1, result)
                 await wire._rope.storage.set(key2, result, None)
         return result
 
-    async def delete(self, wire, **kwargs):
-        key = wire.key(**kwargs)
-        key2 = wire.key2(**kwargs)
-        await wire._rope.storage.delete(key)
+    async def delete(self, wire, pargs):
+        key1 = self.key(wire, pargs)
+        key2 = self.key2(wire, pargs)
+
+        await wire._rope.storage.delete(key1)
         await wire._rope.storage.delete(key2)
 
-    async def touch(self, wire, **kwargs):
-        key = wire.key(**kwargs)
-        key2 = wire.key(**kwargs)
-        await wire._rope.storage.touch(key)
+    async def touch(self, wire, pargs):
+        key1 = self.key(wire, pargs)
+        key2 = self.key2(wire, pargs)
+
+        await wire._rope.storage.touch(key1)
         await wire._rope.storage.touch(key2)
 
 
diff --git a/tests/test_callable.py b/tests/test_callable.py
index 8d81d6c..fc0ca65 100644
--- a/tests/test_callable.py
+++ b/tests/test_callable.py
@@ -3,40 +3,39 @@ import sys
 import pytest
 
 from ring.callable import Callable
+from ring.func.base import ArgPack
 
 
 if sys.version_info[0] >= 3:
     from ._test_callable_py3 import *  # noqa
 
 
-@pytest.mark.parametrize('f,args,kwargs,merged', [
-    (lambda: None, (), {}, {}),
-    (lambda x, y: None, (1, 2), {}, dict(x=1, y=2)),
-    (lambda x, y, z=30: None, (1, 2), {}, dict(x=1, y=2, z=30)),
-    (lambda x, y, z=30: None, (1,), {'y': 2}, dict(x=1, y=2, z=30)),
-    (lambda x, y, z=30: None, (1, 2, 3), {}, dict(x=1, y=2, z=3)),
-    (lambda x, y, z=30: None, (1,), {'y': 20}, dict(x=1, y=20, z=30)),
-    (lambda x, y, *args: None, (1,), {'y': 20}, dict(x=1, y=20, args=())),
-    (lambda x, y, *args: None, (1, 2, 3, 4, 5), {}, dict(x=1, y=2, args=(3, 4, 5))),
-    (lambda x, **kw: None, (), {'x': 10, 'y': 20, 'z': 30}, dict(x=10, kw={'y': 20, 'z': 30})),
-    (lambda x, *args, **kw: None, (1, 2, 3, 4), {'y': 20, 'z': 30}, dict(x=1, args=(2, 3, 4), kw={'y': 20, 'z': 30})),
+@pytest.mark.parametrize('f,pargs,merged', [
+    (lambda: None, ArgPack((), (), {}), {}),
+    (lambda x, y: None, ArgPack((), (1, 2), {}), {"x": 1, "y": 2}),
+    (lambda x, y, z=30: None, ArgPack((), (1, 2), {}), {"x": 1, "y": 2, "z": 30}),
+    (lambda x, y, z=30: None, ArgPack((), (1,), {'y': 2}), {"x": 1, "y": 2, "z": 30}),
+    (lambda x, y, z=30: None, ArgPack((), (1, 2, 3), {}), {"x": 1, "y": 2, "z": 3}),
+    (lambda x, y, z=30: None, ArgPack((), (1,), {'y': 20}), {"x": 1, "y": 20, "z": 30}),
+    (lambda x, y, *args: None, ArgPack((), (1,), {'y': 20}), {"x": 1, "y": 20, "*args": ()}),
+    (lambda x, y, *args: None, ArgPack((), (1, 2, 3, 4, 5), {}), {"x": 1, "y": 2, "*args": (3, 4, 5)}),
+    (lambda x, **kw: None, ArgPack((), (), {'x': 10, 'y': 20, 'z': 30}), {"x": 10, "**kw": {'y': 20, 'z': 30}}),
+    (lambda x, *args, **kw: None, ArgPack((), (1, 2, 3, 4), {'y': 20, 'z': 30}), {"x": 1, "*args": (2, 3, 4), "**kw": {'y': 20, 'z': 30}}),
 ])
-def test_kwargify(f, args, kwargs, merged):
-    kwargified = Callable(f).kwargify(args, kwargs)
-    print(kwargified)
-    print(merged)
+def test_make_labels(f, pargs, merged):
+    kwargified = pargs.labels(Callable(f))
     assert kwargified == merged
 
 
-@pytest.mark.parametrize('f,args,kwargs,exc', [
-    (lambda: None, (1, 2), {}, TypeError),
-    (lambda x, y: None, (2, 3), {'x': 1}, TypeError),
-    (lambda x, y, z=30: None, (1,), {}, TypeError),
-    (lambda x, y, z=30: None, (1,), {'x': 2}, TypeError),
+@pytest.mark.parametrize('f,pargs,exc', [
+    (lambda: None, ArgPack((), (1, 2), {}), TypeError),
+    (lambda x, y: None, ArgPack((), (2, 3), {'x': 1}), TypeError),
+    (lambda x, y, z=30: None, ArgPack((), (1,), {}), TypeError),
+    (lambda x, y, z=30: None, ArgPack((), (1,), {'x': 2}), TypeError),
 ])
-def test_kwargify_exc(f, args, kwargs, exc):
+def test_make_labels_exc(f, pargs, exc):
     with pytest.raises(exc):
-        Callable(f).kwargify(args, kwargs)
+        pargs.labels(Callable(f))
 
 
 def test_empty_annotations():
diff --git a/tests/test_ring.py b/tests/test_ring.py
index 0e337c0..0e63479 100644
--- a/tests/test_ring.py
+++ b/tests/test_ring.py
@@ -120,3 +120,13 @@ def test_package_version():
     parts = ring.__version__.split('.')
     assert len(parts) == 3
     assert parts[0] == '0'
+
+
+def test_varargs():
+    def a(*args, **kwargs):
+        return (args, kwargs)
+
+    b = ring.lru()(a)
+
+    assert a(3) == b(3)
+    assert a(3, x=1) == b(3, x=1)