def test_unhashable_star_empty():
    '''Test `ArgumentsProfile` hashing and handling of `*()`.'''
    def func(a, b, c=3, d=4, **kwargs):
        pass

    a2 = ArgumentsProfile(func,
                          7, ({
                              'a': 'b'
                          }, ), {1, (3, 4)},
                          meow=[1, 2, {
                              1: [1, 2]
                          }])
    assert a2.args == (7, ({'a': 'b'}, ), {1, (3, 4)})
    assert a2.kwargs == OrderedDict((('meow', [1, 2, {1: [1, 2]}]), ))

    a3 = ArgumentsProfile(func,
                          *(),
                          b=({
                              'a': 'b'
                          }, ),
                          c={1, (3, 4)},
                          a=7,
                          meow=[1, 2, {
                              1: [1, 2]
                          }])
    assert a3.args == (7, ({'a': 'b'}, ), {1, (3, 4)})
    assert a3.kwargs == OrderedDict((('meow', [1, 2, {1: [1, 2]}]), ))
    assert hash(a2) == hash(a3)
def test_many_defaultfuls_some_long_2():
    '''
    Test `ArgumentsProfile` with many defaultful arguments, some of them long.
    '''
    def func(a, b, c=3, dragon=4, e=5, f=6, glide=7, human=8, iris=9):
        pass

    a1 = ArgumentsProfile(func, 1, 2, glide='boom')
    assert a1.args == (1, 2)
    assert a1.kwargs == OrderedDict((('glide', 'boom'), ))

    a2 = ArgumentsProfile(func, 1, 2, 3, 4, 5, 6, 'boom')
    a3 = ArgumentsProfile(func, 1, 2, 3, glide='boom')
    assert a1 == a2 == a3

    a4 = ArgumentsProfile(func,
                          1,
                          2,
                          glide='boom',
                          human='pow',
                          iris='badabang')
    a5 = ArgumentsProfile(func, 1, 2, 3, 4, 5, 6, 'boom', 'pow', 'badabang')
    assert a4 == a5
    assert a4.args == (1, 2, 3, 4, 5, 6, 'boom', 'pow', 'badabang')
    assert not a4.kwargs
def test_many_defaultfuls_and_star_args():
    '''Test `ArgumentsProfile` with many defaultful arguments and `*args`.'''
    def func(a, b, c='three', d='four', e='five', f='six', *args):
        pass

    a1 = ArgumentsProfile(func, 'one', 'two', f='roar')
    assert a1.args == ('one', 'two')
    assert a1.kwargs == OrderedDict((('f', 'roar'), ))

    a2 = ArgumentsProfile(func, 'one', 'two', 'three', 'four', 'five', 'roar')
    assert a1 == a2

    # Specifying `*args`, so can't specify pre-`*args` arguments by keyword:
    a3 = ArgumentsProfile(func, 'one', 'two', 'three', 'four', 'five', 'roar',
                          'meow_frr')
    assert a3.args == ('one', 'two', 'three', 'four', 'five', 'roar',
                       'meow_frr')
    assert not a3.kwargs

    a4 = ArgumentsProfile(func, 'one', 'two', 'three', 'four', 'five', 'six',
                          3, 1, 4, 1, 5, 9, 2)
    assert a4.args == ('one', 'two', 'three', 'four', 'five', 'six', 3, 1, 4,
                       1, 5, 9, 2)
    assert not a4.kwargs
    assert a4['*'] == (3, 1, 4, 1, 5, 9, 2)
Esempio n. 4
0
def get_default_args_dict(function):
    '''
    Get ordered dict from arguments which have a default to their default.
    
    Example:
    
        >>> def f(a, b, c=1, d='meow'): pass
        >>> get_default_args_dict(f)
        OrderedDict([('c', 1), ('d', 'meow')])
        
    '''
    arg_spec = cute_inspect.getargspec(function)
    (s_args, s_star_args, s_star_kwargs, s_defaults) = arg_spec

    # `getargspec` has a weird policy, when inspecting a function with no
    # defaults, to give a `defaults` of `None` instead of the more consistent
    # `()`. We fix that here:
    if s_defaults is None:
        s_defaults = ()

    # The number of args which have default values:
    n_defaultful_args = len(s_defaults)

    defaultful_args = s_args[-n_defaultful_args:] if n_defaultful_args \
                       else []

    return OrderedDict(zip(defaultful_args, s_defaults))
def test_empty():
    '''Test `get_default_args_dict` on a function with no defaultful args.'''
    def f(a, b, c, *args, **kwargs):
        pass

    assert get_default_args_dict(f) == \
        OrderedDict()
def test_generator():
    '''Test `get_default_args_dict` on a generator function.'''
    def f(a, meow='frr', d={}):
        yield None

    assert get_default_args_dict(f) == \
        OrderedDict((('meow', 'frr'), ('d', {})))
def test():
    '''Test the basic workings of `get_default_args_dict`.'''
    def f(a, b, c=3, d=4):
        pass

    assert get_default_args_dict(f) == \
        OrderedDict((('c', 3), ('d', 4)))
def test_many_defaultfuls_some_long():
    '''
    Test `ArgumentsProfile` with many defaultful arguments, some of them long.
    '''
    def func(a, b, c=3, dragon=4, e=5, f=6, glide=7, human=8):
        pass

    a1 = ArgumentsProfile(func, 1, 2, glide='boom')
    assert a1.args == (1, 2)
    assert a1.kwargs == OrderedDict((('glide', 'boom'), ))

    a2 = ArgumentsProfile(func, 1, 2, 3, 4, 5, 6, 'boom')
    a3 = ArgumentsProfile(func, 1, 2, 3, glide='boom')
    assert a1 == a2 == a3

    a4 = ArgumentsProfile(func, 1, 2, glide='boom', human='pow')
    a5 = ArgumentsProfile(func, 1, 2, 3, 4, 5, 6, 'boom', 'pow')
    # edge case, second priority
    assert a4.args == (1, 2)
    assert a4.kwargs == OrderedDict((('glide', 'boom'), ('human', 'pow')))
    assert a4 == a5
def test_only_defaultless():
    '''
    Test `ArgumentsProfile` on a function with defaultless arguments only.
    '''
    def func(a, b, c):
        pass

    a1 = ArgumentsProfile(func, 1, 2, 3)
    assert a1.args == (1, 2, 3)
    assert not a1.kwargs

    a2 = ArgumentsProfile(func, 1, c=3, b=2)
    a3 = ArgumentsProfile(func, c=3, a=1, b=2)
    a4 = ArgumentsProfile(func, 1, **{'c': 3, 'b': 2})
    a5 = ArgumentsProfile(func, **OrderedDict((('c', 3), ('b', 2), ('a', 1))))
    assert a1 == a2 == a3 == a4 == a5

    for arg_prof in [a1, a2, a3, a4, a5]:

        ### Testing `.iteritems`: #############################################
        #                                                                     #
        assert dict(arg_prof) == {'a': 1, 'b': 2, 'c': 3}
        assert OrderedDict(arg_prof) == \
            OrderedDict((('a', 1), ('b', 2), ('c', 3)))
        #                                                                     #
        ### Finished testing `.iteritems`. ####################################

        ### Testing `.__getitem__`: ###########################################
        #                                                                     #
        assert (arg_prof['a'], arg_prof['b'], arg_prof['c']) == (1, 2, 3)
        with cute_testing.RaiseAssertor(KeyError):
            arg_prof['non_existing_key']
        #                                                                     #
        ### Finished testing `.__getitem__`. ##################################

        ### Testing `.get`: ###################################################
        #                                                                     #
        assert arg_prof.get('a') == arg_prof.get('a', 'asdfasdf') == 1
        assert arg_prof.get('non_existing_key', 7) == 7
        assert arg_prof.get('non_existing_key') is None
Esempio n. 10
0
    def __init__(self,
                 parameters=None,
                 return_annotation=_empty,
                 __validate_parameters__=True):
        '''Constructs Signature from the given list of Parameter
        objects and 'return_annotation'.  All arguments are optional.
        '''

        if parameters is None:
            params = OrderedDict()
        else:
            if __validate_parameters__:
                params = OrderedDict()
                top_kind = _POSITIONAL_ONLY

                for idx, param in enumerate(parameters):
                    kind = param.kind
                    if kind < top_kind:
                        msg = 'wrong parameter order: {0} before {1}'
                        msg = msg.format(top_kind, param.kind)
                        raise ValueError(msg)
                    else:
                        top_kind = kind

                    name = param.name
                    if name is None:
                        name = str(idx)
                        param = param.replace(name=name)

                    if name in params:
                        msg = 'duplicate parameter name: {0!r}'.format(name)
                        raise ValueError(msg)
                    params[name] = param
            else:
                params = OrderedDict(
                    ((param.name, param) for param in parameters))

        self._parameters = params
        self._return_annotation = return_annotation
Esempio n. 11
0
 def test_only_positive_ints_or_zero(self):
     assert self.bag_type(
         OrderedDict([('a', 0), ('b', 0.0), ('c', 1), ('d', 2.0),
                      ('e', decimal_module.Decimal('3.0'))])) == \
                                                     self.bag_type('cddeee')
     with cute_testing.RaiseAssertor(TypeError):
         self.bag_type({
             'a': 1.1,
         })
     with cute_testing.RaiseAssertor(TypeError):
         self.bag_type({
             'a': -2,
         })
     with cute_testing.RaiseAssertor(TypeError):
         self.bag_type({
             'a': -3,
         })
     with cute_testing.RaiseAssertor(TypeError):
         self.bag_type({
             'a': decimal_module.Decimal('-3'),
         })
     with cute_testing.RaiseAssertor(TypeError):
         self.bag_type({
             'a': infinity,
         })
     with cute_testing.RaiseAssertor(TypeError):
         self.bag_type({
             'a': -infinity,
         })
     with cute_testing.RaiseAssertor(TypeError):
         self.bag_type({
             'a': 'whatever',
         })
     with cute_testing.RaiseAssertor(TypeError):
         self.bag_type({
             'a': b'whateva',
         })
     with cute_testing.RaiseAssertor(TypeError):
         self.bag_type({
             'a': ('still', 'nope'),
         })
def test_simplest_defaultful():
    '''
    Test `ArgumentsProfile` on a function with defaultful arguments.
    '''
    def func(a, b, c='three', d='four'):
        pass

    a1 = ArgumentsProfile(func, 'one', 'two')
    assert a1.args == ('one', 'two')
    assert not a1.kwargs

    a2 = ArgumentsProfile(func, 'one', 'two', 'three')
    a3 = ArgumentsProfile(func, 'one', 'two', 'three', 'four')
    assert a1 == a2 == a3

    a4 = ArgumentsProfile(func, 'one', 'two', 'dynamite')
    assert a1 != a4
    assert a4.args == ('one', 'two', 'dynamite')
    assert not a4.kwargs

    a5 = ArgumentsProfile(func, 'one', 'two', c='dynamite')
    a6 = ArgumentsProfile(func, 'one', 'two', 'dynamite', 'four')
    a7 = ArgumentsProfile(func, 'one', 'two', c='dynamite', d='four')
    a8 = ArgumentsProfile(func, 'one', 'two', 'dynamite', d='four')
    a9 = ArgumentsProfile(func, a='one', b='two', c='dynamite', d='four')
    a10 = ArgumentsProfile(func, d='four', c='dynamite', b='two', a='one')
    a11 = ArgumentsProfile(func, 'one', c='dynamite', d='four', b='two')
    assert a4 == a5 == a6 == a7 == a8 == a9 == a10 == a11

    a12 = ArgumentsProfile(func, 'one', 'two', d='bang')
    assert a12.args == ('one', 'two')
    assert a12.kwargs == OrderedDict((('d', 'bang'), ))

    a13 = ArgumentsProfile(func, 'one', 'two', 'three', d='bang')
    a14 = ArgumentsProfile(func, 'one', 'two', c='three', d='bang')
    a15 = ArgumentsProfile(func, 'one', 'two', 'three', 'bang')
    a16 = ArgumentsProfile(func, a='one', b='two', c='three', d='bang')
    a17 = ArgumentsProfile(func, b='two', c='three', d='bang', a='one')
    assert a13 == a14 == a15 == a16 == a17
def test_unhashable():
    '''Test hashing of `ArgumentsProfile` that has unhashable arguments.'''
    def func(a, b, c=3, d=4, **kwargs):
        pass

    a1 = ArgumentsProfile(func, 7, {1: 2})
    assert a1.args == (7, {1: 2})
    assert not a1.kwargs
    hash(a1)

    a2 = ArgumentsProfile(func,
                          7, ({
                              'a': 'b'
                          }, ), {1, (3, 4)},
                          meow=[1, 2, {
                              1: [1, 2]
                          }])
    assert a2.args == (7, ({'a': 'b'}, ), {1, (3, 4)})
    assert a2.kwargs == OrderedDict((('meow', [1, 2, {1: [1, 2]}]), ))

    d = {a1: 1, a2: 2}
    assert d[a1] == 1
    assert d[a2] == 2
def test_defaultful_long_first():
    '''
    Test `ArgumentsProfile` on function with long first defaultful argument.
    '''
    def func(a, b, creativity=3, d=4):
        pass

    a1 = ArgumentsProfile(func, 1, 2)
    assert a1.args == (1, 2)
    assert not a1.kwargs

    a2 = ArgumentsProfile(func, 1, 2, 3, 4)
    a3 = ArgumentsProfile(func, a=1, b=2, creativity=3, d=4)
    a4 = ArgumentsProfile(func, creativity=3, d=4, a=1, b=2)
    a5 = ArgumentsProfile(func, 1, 2, creativity=3, d=4)
    assert a1 == a2 == a3 == a4 == a5

    a6 = ArgumentsProfile(func, 1, 2, d='booyeah')
    assert a6.args == (1, 2)
    assert a6.kwargs == OrderedDict((('d', 'booyeah'), ))

    a7 = ArgumentsProfile(func, 1, 2, 3, 'booyeah')
    a8 = ArgumentsProfile(func, 1, 2, creativity=3, d='booyeah')
    assert a6 == a7 == a8
Esempio n. 15
0
 def _asdict(self):
     '''
     Return a new `OrderedDict` which maps field names to their values.
     '''
     from python_toolbox.nifty_collections import OrderedDict
     return OrderedDict(zip(self._fields, self))
Esempio n. 16
0
    def _bind(self, args, kwargs, partial=False):
        '''Private method.  Don't use directly.'''

        arguments = OrderedDict()

        parameters = iter(self.parameters.values())
        parameters_ex = ()
        arg_vals = iter(args)

        if partial:
            # Support for binding arguments to 'functools.partial' objects.
            # See 'functools.partial' case in 'signature()' implementation
            # for details.
            for param_name, param in self.parameters.items():
                if (param._partial_kwarg and param_name not in kwargs):
                    # Simulating 'functools.partial' behavior
                    kwargs[param_name] = param.default

        while True:
            # Let's iterate through the positional arguments and corresponding
            # parameters
            try:
                arg_val = next(arg_vals)
            except StopIteration:
                # No more positional arguments
                try:
                    param = next(parameters)
                except StopIteration:
                    # No more parameters. That's it. Just need to check that
                    # we have no `kwargs` after this while loop
                    break
                else:
                    if param.kind == _VAR_POSITIONAL:
                        # That's OK, just empty *args.  Let's start parsing
                        # kwargs
                        break
                    elif param.name in kwargs:
                        if param.kind == _POSITIONAL_ONLY:
                            msg = '{arg!r} parameter is positional only, ' \
                                  'but was passed as a keyword'
                            msg = msg.format(arg=param.name)
                            raise TypeError(msg)
                        parameters_ex = (param, )
                        break
                    elif (param.kind == _VAR_KEYWORD
                          or param.default is not _empty):
                        # That's fine too - we have a default value for this
                        # parameter.  So, lets start parsing `kwargs`, starting
                        # with the current parameter
                        parameters_ex = (param, )
                        break
                    else:
                        if partial:
                            parameters_ex = (param, )
                            break
                        else:
                            msg = '{arg!r} parameter lacking default value'
                            msg = msg.format(arg=param.name)
                            raise TypeError(msg)
            else:
                # We have a positional argument to process
                try:
                    param = next(parameters)
                except StopIteration:
                    raise TypeError('too many positional arguments')
                else:
                    if param.kind in (_VAR_KEYWORD, _KEYWORD_ONLY):
                        # Looks like we have no parameter for this positional
                        # argument
                        raise TypeError('too many positional arguments')

                    if param.kind == _VAR_POSITIONAL:
                        # We have an '*args'-like argument, let's fill it with
                        # all positional arguments we have left and move on to
                        # the next phase
                        values = [arg_val]
                        values.extend(arg_vals)
                        arguments[param.name] = tuple(values)
                        break

                    if param.name in kwargs:
                        raise TypeError('multiple values for argument '
                                        '{arg!r}'.format(arg=param.name))

                    arguments[param.name] = arg_val

        # Now, we iterate through the remaining parameters to process
        # keyword arguments
        kwargs_param = None
        for param in itertools.chain(parameters_ex, parameters):
            if param.kind == _POSITIONAL_ONLY:
                # This should never happen in case of a properly built
                # Signature object (but let's have this check here
                # to ensure correct behaviour just in case)
                raise TypeError('{arg!r} parameter is positional only, '
                                'but was passed as a keyword'. \
                                format(arg=param.name))

            if param.kind == _VAR_KEYWORD:
                # Memorize that we have a '**kwargs'-like parameter
                kwargs_param = param
                continue

            param_name = param.name
            try:
                arg_val = kwargs.pop(param_name)
            except KeyError:
                # We have no value for this parameter.  It's fine though,
                # if it has a default value, or it is an '*args'-like
                # parameter, left alone by the processing of positional
                # arguments.
                if (not partial and param.kind != _VAR_POSITIONAL
                        and param.default is _empty):
                    raise TypeError('{arg!r} parameter lacking default value'. \
                                    format(arg=param_name))

            else:
                arguments[param_name] = arg_val

        if kwargs:
            if kwargs_param is not None:
                # Process our '**kwargs'-like parameter
                arguments[kwargs_param.name] = kwargs
            else:
                raise TypeError('too many keyword arguments')

        return self._bound_arguments_cls(self, arguments)
Esempio n. 17
0
 def parameters(self):
     try:
         return types.MappingProxyType(self._parameters)
     except AttributeError:
         return OrderedDict(self._parameters.items())
def test_many_defaultfuls_and_star_args_and_star_kwargs():
    '''
    Test `ArgumentsProfile` with defaultful arguments, `*args` and `**kwargs`.
    '''
    def func(a, b, c='three', d='four', e='five', f='six', *args, **kwargs):
        pass

    func(None, None)

    a1 = ArgumentsProfile(func,
                          'one',
                          'two',
                          f='boomboomboom',
                          __awesome=True,
                          big=True)
    assert a1.args == ('one', 'two')
    assert a1.kwargs == OrderedDict(
        (('f', 'boomboomboom'), ('big', True), ('__awesome', True)))

    a2 = ArgumentsProfile(func,
                          'one',
                          'two',
                          'three',
                          'four',
                          'five',
                          'bombastic',
                          'meow_frr',
                          __funky=None,
                          zany=True,
                          _wet=False,
                          blue=True)
    assert a2.args == ('one', 'two', 'three', 'four', 'five', 'bombastic',
                       'meow_frr')
    assert a2.kwargs == OrderedDict(
        (('blue', True), ('zany', True), ('_wet', False), ('__funky', None)))

    a3 = ArgumentsProfile(func,
                          'one',
                          'two',
                          'three',
                          'four',
                          'five',
                          'bombastic',
                          'meow_frr',
                          zany=True,
                          __funky=None,
                          blue=True,
                          _wet=False,
                          **OrderedDict())
    assert a2 == a3

    for arg_prof in [a2, a3]:
        # Testing `.iteritems`:
        assert OrderedDict(arg_prof) == OrderedDict(
            (('a', 'one'), ('b', 'two'), ('c', 'three'), ('d', 'four'),
             ('e', 'five'), ('f', 'bombastic'), ('*', ('meow_frr', )),
             ('blue', True), ('zany', True), ('_wet', False), ('__funky',
                                                               None)))

        ### Testing `.__getitem__`: ###########################################
        #                                                                     #
        assert (arg_prof['a'], arg_prof['b'], arg_prof['c'], arg_prof['d'],
                arg_prof['e'], arg_prof['f'], arg_prof['*'], arg_prof['blue'],
                arg_prof['zany'],  arg_prof['_wet'], arg_prof['__funky']) == \
               ('one', 'two', 'three', 'four', 'five', 'bombastic',
                ('meow_frr',), True, True, False, None)

        with cute_testing.RaiseAssertor(KeyError):
            arg_prof['non_existing_key']
        #                                                                     #
        ### Finished testing `.__getitem__`. ##################################

        ### Testing `.get`: ###################################################
        #                                                                     #
        assert arg_prof.get('d') == arg_prof.get('d', 7) == 'four'
        assert arg_prof.get('non_existing_key', 7) == 7
        assert arg_prof.get('non_existing_key') is None
        #                                                                     #
        ### Finished testing `.get`. ##########################################

        ### Testing `.iterkeys`, `.keys` and `__iter__`: ######################
        #                                                                     #
        assert list(arg_prof.iterkeys()) == list(arg_prof.keys()) == \
            list(arg_prof) == \
            ['a', 'b', 'c', 'd', 'e', 'f', '*', 'blue', 'zany', '_wet',
             '__funky']
        #                                                                     #
        ### Finished testing `.iterkeys`, `.keys` and `__iter__`. #############

        ### Testing `.itervalues` and `.values`: ##############################
        #                                                                     #
        assert list(arg_prof.itervalues()) == list(arg_prof.values()) == \
            ['one', 'two', 'three', 'four', 'five', 'bombastic', ('meow_frr',),
             True, True, False, None]
        #                                                                     #
        ### Finished testing `.itervalues` and `.values`. #####################

        ### Testing `.iteritems` and `.items`: ################################
        #                                                                     #
        items_1 = list(arg_prof.iteritems())
        items_2 = arg_prof.items()
        assert items_1 == items_2 == zip(arg_prof.keys(), arg_prof.values())
        #                                                                     #
        ### Finished testing `.iteritems` and `.items`. #######################

        ### Testing `.__contains__`: ##########################################
        #                                                                     #
        for key in arg_prof:
            assert key in arg_prof
class ArgumentsProfile:
    '''
    A canonical arguments profile for a function.
    
    (This should be used only on functions that don't modify the arguments they
    receive. Also, you should never modify any arguments you use in an
    arguments profile, even outside the function.)
    
    What is an arguments profile and what is it good for?
    
    It's possible to call the same function with the same arguments in
    different ways. For example, take this function:

        def f(a, bb=2, ccc=3, **kwargs):
            return (a, bb, ccc)
            
    You can call it like `f(1)` or `f(a=1)` or `f(1, ccc=3, bb=2, **{})` or
    many other different ways which will result in exactly the same arguments.
    
    To organize the different ways a function can be called, an arguments
    profile provides a canonical way to call the function, so all the different
    examples in the last paragraphs would be reduced to the same canonical
    arguments profile. (In this case `f(1)`.)
    
    The canonical arguments profile is defined as the one which satisfies the
    following criteria, with the first one being the most important, the second
    one being a tie-breaker to the first, and the third one being a tie-breaker
    to the second:
    
     1. It has as few characters as possible. (e.g. `f(1)` is better than    
        `f(1, 2)`.)
        
     2. It has as many keyword arguments as possible. (e.g. `f(bb=3)` is
        better than `f(1, 3)`.)
        
     3. The extraneous keywords (i.e. `**kwargs`) are sorted alphabetically, 
        with "_" being the highest/last character. (e.g. `f(1, cat=7, meow=7,
        _house=7)` is better than `f(1, _house=7, meow=7, cat=7)`)
    
    # Accessing the data of an arguments profile #
    
    Say you have this function:
    
        def f(x, y, *args, **kwargs):
            pass
    
    And you create an arguments profile:
    
        arguments_profile = ArgumentsProfile(f, 1, 2, 3, 4, meow='frr')
            
    There are two ways to access the data of this arguments profile:
    
     1. Use `arguments_profile.args` and `arguments_profile.kwargs`, which are,
        respectively, a tuple of positional arguments and an ordered dict of
        keyword arguments. In this case, `.args` would be `(1, 2, 3, 4)` and
        `.kwargs` would be `OrderedDict((('meow', 'frr'),))`.
        
     2. Use `arguments_profile`'s ordered-dict-like interface. A few examples:
     
            arguments_profile['x'] == 1
            arguments_profile['y'] == 2
            arguments_profile['*'] == (3, 4)
            arguments_profile['meow'] == 'frr'
            
        The special asterisk argument indicates the arguments that go into
        `*args`.
        
    '''

    # todo: we're using an ad-hoc third way, `self.getcallargs_result`, think
    # hard about that...

    def __init__(self, function, *args, **kwargs):
        '''
        Construct the arguments profile.
        
        `*args` and `**kwargs` are the arguments that go into the `function`.
        '''

        if not callable(function):
            raise Exception('%s is not a callable object.' % function)
        self.function = function

        raw_args = args
        raw_kwargs = kwargs
        del args, kwargs

        self.args = ()
        '''Tuple of positional arguments.'''

        self.kwargs = OrderedDict()
        '''Ordered dict of keyword arguments.'''

        args_spec = cute_inspect.getargspec(function)

        (s_args, s_star_args, s_star_kwargs, s_defaults) = args_spec

        # `getargspec` has a weird policy, when inspecting a function with no
        # defaults, to give a `defaults` of `None` instead of the more
        # consistent `()`. We fix that here:
        if s_defaults is None:
            s_defaults = ()

        getcallargs_result = cute_inspect.getcallargs(function, *raw_args,
                                                      **raw_kwargs)
        self.getcallargs_result = getcallargs_result

        # The number of args which have default values:
        n_defaultful_args = len(s_defaults)
        # The word "defaultful" means "something which has a default."

        #######################################################################
        #######################################################################
        # Now we'll create the arguments profile, using a 4-phases algorithm. #
        #                                                                     #

        #######################################################################
        # Phase 1: We specify all the args that don't have a default as
        # positional args:
        defaultless_args = s_args[:-n_defaultful_args] if n_defaultful_args \
                           else s_args[:]
        self.args += tuple(
            dict_tools.get_list(getcallargs_result, defaultless_args))

        #######################################################################
        # Phase 2: We now have to deal with args that have a default. Some of
        # them, possibly none and possibly all of them, should be given
        # positionally. Some of them, possibly none, should be given by
        # keyword. And some of them, possibly none and possibly all of them,
        # should not be given at all. It is our job to figure out in which way
        # each argument should be given.

        # In this variable:
        n_defaultful_args_to_specify_positionally = None
        # We will put the number of defaultful arguments that should be
        # specified positionally.

        defaultful_args = s_args[-n_defaultful_args:] if n_defaultful_args \
                          else []

        # `dict` that maps from argument name to default value:
        defaults = OrderedDict(zip(defaultful_args, s_defaults))

        defaultful_args_differing_from_defaults = {
            defaultful_arg
            for defaultful_arg in defaultful_args
            if defaults[defaultful_arg] != getcallargs_result[defaultful_arg]
        }

        if s_star_args and getcallargs_result[s_star_args]:
            # We have some arguments that go into `*args`! This means that we
            # don't even need to think hard, we can already be sure that we're
            # going to have to specify *all* of the defaultful arguments
            # positionally, otherwise it will be impossible to put arguments in
            # `*args`.
            n_defaultful_args_to_specify_positionally = n_defaultful_args

        else:

            # `dict` mapping from each defaultful arg to the "price" of
            # specifying its value:
            prices_of_values = OrderedDict({
                defaultful_arg: len(repr(getcallargs_result[defaultful_arg]))
                for defaultful_arg in defaultful_args
            })
            # The price is simply the string length of the value's `repr`.

            # `dict` mapping from each defaultful arg to the "price" of
            # specifying it as a keyword (not including the length of the
            # value):
            prices_of_keyword_prefixes = OrderedDict({
                defaultful_arg: len(defaultful_arg) + 1
                for defaultful_arg in defaultful_args
            })
            # For example, if we have a defaultful arg "gravity_strength", then
            # specifiying it by keyword will require using the string
            # "gravity_strength=", which is 17 characters long, therefore the
            # price is 17.

            # Now we need to decide just how many defaultful args we are going
            # to specify positionally. The options are anything from `0` to
            # `n_defaultful_args`. We're going to go one by one, and calcluate
            # the price for each candidate, and put it in this dict:
            total_price_for_n_dasp_candidate = OrderedDict()
            # (The `n_dasp` here is an abbreivation of the
            # `n_defaultful_args_to_specify_positionally` variable defined
            # before.)
            #
            # After we have the price for each option, we'll select the one
            # with the lowest price.

            # One thing to do before iterating on the candidates is to find out
            # whether the "lonely comma discount" is in effect.
            #
            # The "lonely comma discount" is given when there's nothing but
            # defaultful arguments to this function, and therefore the number
            # of ", " strings needed here is not `candidate`, but `candidate -
            # 1`, unless of course candidate is zero.

            if not defaultless_args and \
                (not s_star_args or not getcallargs_result[s_star_args]) and \
                (not s_star_kwargs or not getcallargs_result[s_star_kwargs]):

                lonely_comma_discount_may_be_given = True

            else:

                lonely_comma_discount_may_be_given = False

            # Now we iterate on the candidates to find out which one has the
            # lowest price:

            for candidate in range(n_defaultful_args + 1):

                defaultful_args_to_specify_positionally = \
                    defaultful_args[:candidate]

                price_for_positionally_specified_defaultful_args = \
                    2 * candidate + \
                    sum(
                        dict_tools.get_list(
                            prices_of_values,
                            defaultful_args_to_specify_positionally
                        )
                    )
                # The `2 * candidate` addend is to account for the ", " parts
                # between the arguments.

                defaultful_args_to_specify_by_keyword = list(
                    filter(
                        defaultful_args_differing_from_defaults.__contains__,
                        defaultful_args[candidate:]))

                price_for_defaultful_args_specified_by_keyword = \
                    2 * len(defaultful_args_to_specify_by_keyword) + \
                    sum(
                        dict_tools.get_list(
                            prices_of_keyword_prefixes,
                            defaultful_args_to_specify_by_keyword
                        )
                    ) + \
                    sum(
                        dict_tools.get_list(
                            prices_of_values,
                            defaultful_args_to_specify_by_keyword
                        )
                    )
                # The `2 * len(...)` addend is to account for the ", " parts
                # between the arguments.

                # Now we need to figure out if this candidate gets the "lonely
                # comma discount".
                if lonely_comma_discount_may_be_given and \
                   (defaultful_args_to_specify_by_keyword or \
                    defaultful_args_to_specify_positionally):

                    lonely_comma_discount = -2

                else:
                    lonely_comma_discount = 0

                price = price_for_positionally_specified_defaultful_args + \
                        price_for_defaultful_args_specified_by_keyword + \
                        lonely_comma_discount

                total_price_for_n_dasp_candidate[candidate] = price

            # Finished iterating on candidates! Time to pick our winner.

            minimum_price = min(total_price_for_n_dasp_candidate.values())

            leading_candidates = [
                candidate
                for candidate in total_price_for_n_dasp_candidate.keys()
                if total_price_for_n_dasp_candidate[candidate] == minimum_price
            ]

            if len(leading_candidates) == 1:
                # We finished with one candidate which has the minimum price.
                # This is our winner.
                (winner, ) = leading_candidates
                n_defaultful_args_to_specify_positionally = winner

            else:
                # We have a draw! We're gonna have to settle it by picking the
                # lowest candidate, because in our definition of "canonical
                # arguments profile", our second priority after "as few
                # characters as possible" is "as many keyword arguments as
                # possible".
                winner = leading_candidates[0]

            n_defaultful_args_to_specify_positionally = winner

        # We have a winner! Now we know exactly which defaultful args should
        # be specified positionally and which should be specified by
        # keyword.

        # First we add the positionally specified:

        defaultful_args_to_specify_positionally = \
            defaultful_args[:n_defaultful_args_to_specify_positionally]
        self.args += tuple(
            (getcallargs_result[defaultful_arg]
             for defaultful_arg in defaultful_args_to_specify_positionally))

        # Now we add those specified by keyword:

        defaultful_args_to_specify_by_keyword = list(
            filter(
                defaultful_args_differing_from_defaults.__contains__,
                defaultful_args[n_defaultful_args_to_specify_positionally:]))
        for defaultful_arg in defaultful_args_to_specify_by_keyword:
            self.kwargs[defaultful_arg] = getcallargs_result[defaultful_arg]

        #######################################################################
        # Phase 3: Add the star args:

        if s_star_args and getcallargs_result[s_star_args]:

            assert not self.kwargs
            # Just making sure that no non-star args were specified by keyword,
            # which would make it impossible for us to put stuff in `*args`.

            self.args += getcallargs_result[s_star_args]

        #######################################################################
        # Phase 4: Add the star kwargs:

        if s_star_kwargs and getcallargs_result[s_star_kwargs]:

            # We can't just add the `**kwargs` as is; we need to add them
            # according to canonical ordering. So we need to sort them first.

            unsorted_star_kwargs_names = \
                list(getcallargs_result[s_star_kwargs].keys())
            sorted_star_kwargs_names = sorted(
                unsorted_star_kwargs_names,
                key=comparison_tools.underscore_hating_key)

            sorted_star_kwargs = OrderedDict(
                zip(
                    sorted_star_kwargs_names,
                    dict_tools.get_list(getcallargs_result[s_star_kwargs],
                                        sorted_star_kwargs_names)))

            self.kwargs.update(sorted_star_kwargs)

        # Our 4-phases algorithm is done! The argument profile is canonical.  #
        #######################################################################
        #######################################################################

        #######################################################################
        # Now a bit of post-processing:

        _arguments = OrderedDict()

        dict_of_positional_arguments = OrderedDict(
            dict_tools.filter_items(
                getcallargs_result,
                lambda key, value: ((key not in self.kwargs) and \
                                    (key != s_star_args) and \
                                    (key != s_star_kwargs))
            )
        )
        dict_of_positional_arguments.sort(key=s_args.index)
        _arguments.update(dict_of_positional_arguments)

        if s_star_args:
            _arguments['*'] = getcallargs_result[s_star_args]

        _arguments.update(self.kwargs)

        self._arguments = _arguments
        '''Ordered dict of arguments, both positional- and keyword-.'''

        # Caching the hash, since its computation can take a long time:
        self._hash = cheat_hashing.cheat_hash(
            (self.function, self.args, tuple(self.kwargs)))

    def __getitem__(self, argument_name):
        '''Get the value of a specified argument.'''
        return self._arguments.__getitem__(argument_name)

    def get(self, argument_name, default=None):
        '''Get the value of a specified argument, if missing get `default`.'''
        return self._arguments.get(argument_name, default)

    def keys(self):
        '''Get all the argument names.'''
        return list(self._arguments.keys())

    def values(self):
        '''Get all the argument values.'''
        return list(self._arguments.values())

    def items(self):
        '''Get a tuple of all the `(argument_name, argument_value)` item.'''
        return list(self._arguments.items())

    def __iter__(self):
        '''Iterate on the argument names according to their order.'''
        return self._arguments.__iter__()

    def iterkeys(self):
        '''Iterate on the argument names according to their order.'''
        return iter(self._arguments.keys())

    def itervalues(self):
        '''Iterate on the argument value according to their order.'''
        return iter(self._arguments.values())

    def iteritems(self):
        '''Iterate on `(argument_name, argument_value)` items by order.'''
        return iter(self._arguments.items())

    def __contains__(self, argument_name):
        '''Return whether the arguments profile contains the given argument.'''
        return self._arguments.__contains__(argument_name)

    @classmethod
    def create_from_dld_format(cls, function, args_dict, star_args_list,
                               star_kwargs_dict):
        '''
        Create an arguments profile from data given in "dict-list-dict" format.
        
        The "dict-list-dict" format means that in addition to a function, we
        get a `dict` of arguments, a `list` of `*args`, and a `dict` of
        `**kwargs`.
        '''
        args_spec = cute_inspect.getargspec(function)
        new_args = [args_dict[name] for name in args_spec.args] + \
                   list(star_args_list)
        return cls(function, *new_args, **star_kwargs_dict)

    def __eq__(self, other):
        # todo: maybe raise warning when unbound method is compared with same
        # method just bound with the object passed to the unbound method, and
        # the result of both functions would be the same, but we're not smart
        # enough to say it's the same arguments profile, so raise a warning.
        if not isinstance(other, ArgumentsProfile):
            return NotImplemented
        # Note that we're comparing the functions with a `==` here. This lesson
        # cost me a couple of days: `MyClass.method == MyClass.method` but
        # `MyClass.method is not MyClass.method`.
        return (self.function == other.function) and \
               (self.args == other.args) and \
               (self.kwargs == other.kwargs)

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return self._hash
    def __init__(self, function, *args, **kwargs):
        '''
        Construct the arguments profile.
        
        `*args` and `**kwargs` are the arguments that go into the `function`.
        '''

        if not callable(function):
            raise Exception('%s is not a callable object.' % function)
        self.function = function

        raw_args = args
        raw_kwargs = kwargs
        del args, kwargs

        self.args = ()
        '''Tuple of positional arguments.'''

        self.kwargs = OrderedDict()
        '''Ordered dict of keyword arguments.'''

        args_spec = cute_inspect.getargspec(function)

        (s_args, s_star_args, s_star_kwargs, s_defaults) = args_spec

        # `getargspec` has a weird policy, when inspecting a function with no
        # defaults, to give a `defaults` of `None` instead of the more
        # consistent `()`. We fix that here:
        if s_defaults is None:
            s_defaults = ()

        getcallargs_result = cute_inspect.getcallargs(function, *raw_args,
                                                      **raw_kwargs)
        self.getcallargs_result = getcallargs_result

        # The number of args which have default values:
        n_defaultful_args = len(s_defaults)
        # The word "defaultful" means "something which has a default."

        #######################################################################
        #######################################################################
        # Now we'll create the arguments profile, using a 4-phases algorithm. #
        #                                                                     #

        #######################################################################
        # Phase 1: We specify all the args that don't have a default as
        # positional args:
        defaultless_args = s_args[:-n_defaultful_args] if n_defaultful_args \
                           else s_args[:]
        self.args += tuple(
            dict_tools.get_list(getcallargs_result, defaultless_args))

        #######################################################################
        # Phase 2: We now have to deal with args that have a default. Some of
        # them, possibly none and possibly all of them, should be given
        # positionally. Some of them, possibly none, should be given by
        # keyword. And some of them, possibly none and possibly all of them,
        # should not be given at all. It is our job to figure out in which way
        # each argument should be given.

        # In this variable:
        n_defaultful_args_to_specify_positionally = None
        # We will put the number of defaultful arguments that should be
        # specified positionally.

        defaultful_args = s_args[-n_defaultful_args:] if n_defaultful_args \
                          else []

        # `dict` that maps from argument name to default value:
        defaults = OrderedDict(zip(defaultful_args, s_defaults))

        defaultful_args_differing_from_defaults = {
            defaultful_arg
            for defaultful_arg in defaultful_args
            if defaults[defaultful_arg] != getcallargs_result[defaultful_arg]
        }

        if s_star_args and getcallargs_result[s_star_args]:
            # We have some arguments that go into `*args`! This means that we
            # don't even need to think hard, we can already be sure that we're
            # going to have to specify *all* of the defaultful arguments
            # positionally, otherwise it will be impossible to put arguments in
            # `*args`.
            n_defaultful_args_to_specify_positionally = n_defaultful_args

        else:

            # `dict` mapping from each defaultful arg to the "price" of
            # specifying its value:
            prices_of_values = OrderedDict({
                defaultful_arg: len(repr(getcallargs_result[defaultful_arg]))
                for defaultful_arg in defaultful_args
            })
            # The price is simply the string length of the value's `repr`.

            # `dict` mapping from each defaultful arg to the "price" of
            # specifying it as a keyword (not including the length of the
            # value):
            prices_of_keyword_prefixes = OrderedDict({
                defaultful_arg: len(defaultful_arg) + 1
                for defaultful_arg in defaultful_args
            })
            # For example, if we have a defaultful arg "gravity_strength", then
            # specifiying it by keyword will require using the string
            # "gravity_strength=", which is 17 characters long, therefore the
            # price is 17.

            # Now we need to decide just how many defaultful args we are going
            # to specify positionally. The options are anything from `0` to
            # `n_defaultful_args`. We're going to go one by one, and calcluate
            # the price for each candidate, and put it in this dict:
            total_price_for_n_dasp_candidate = OrderedDict()
            # (The `n_dasp` here is an abbreivation of the
            # `n_defaultful_args_to_specify_positionally` variable defined
            # before.)
            #
            # After we have the price for each option, we'll select the one
            # with the lowest price.

            # One thing to do before iterating on the candidates is to find out
            # whether the "lonely comma discount" is in effect.
            #
            # The "lonely comma discount" is given when there's nothing but
            # defaultful arguments to this function, and therefore the number
            # of ", " strings needed here is not `candidate`, but `candidate -
            # 1`, unless of course candidate is zero.

            if not defaultless_args and \
                (not s_star_args or not getcallargs_result[s_star_args]) and \
                (not s_star_kwargs or not getcallargs_result[s_star_kwargs]):

                lonely_comma_discount_may_be_given = True

            else:

                lonely_comma_discount_may_be_given = False

            # Now we iterate on the candidates to find out which one has the
            # lowest price:

            for candidate in range(n_defaultful_args + 1):

                defaultful_args_to_specify_positionally = \
                    defaultful_args[:candidate]

                price_for_positionally_specified_defaultful_args = \
                    2 * candidate + \
                    sum(
                        dict_tools.get_list(
                            prices_of_values,
                            defaultful_args_to_specify_positionally
                        )
                    )
                # The `2 * candidate` addend is to account for the ", " parts
                # between the arguments.

                defaultful_args_to_specify_by_keyword = list(
                    filter(
                        defaultful_args_differing_from_defaults.__contains__,
                        defaultful_args[candidate:]))

                price_for_defaultful_args_specified_by_keyword = \
                    2 * len(defaultful_args_to_specify_by_keyword) + \
                    sum(
                        dict_tools.get_list(
                            prices_of_keyword_prefixes,
                            defaultful_args_to_specify_by_keyword
                        )
                    ) + \
                    sum(
                        dict_tools.get_list(
                            prices_of_values,
                            defaultful_args_to_specify_by_keyword
                        )
                    )
                # The `2 * len(...)` addend is to account for the ", " parts
                # between the arguments.

                # Now we need to figure out if this candidate gets the "lonely
                # comma discount".
                if lonely_comma_discount_may_be_given and \
                   (defaultful_args_to_specify_by_keyword or \
                    defaultful_args_to_specify_positionally):

                    lonely_comma_discount = -2

                else:
                    lonely_comma_discount = 0

                price = price_for_positionally_specified_defaultful_args + \
                        price_for_defaultful_args_specified_by_keyword + \
                        lonely_comma_discount

                total_price_for_n_dasp_candidate[candidate] = price

            # Finished iterating on candidates! Time to pick our winner.

            minimum_price = min(total_price_for_n_dasp_candidate.values())

            leading_candidates = [
                candidate
                for candidate in total_price_for_n_dasp_candidate.keys()
                if total_price_for_n_dasp_candidate[candidate] == minimum_price
            ]

            if len(leading_candidates) == 1:
                # We finished with one candidate which has the minimum price.
                # This is our winner.
                (winner, ) = leading_candidates
                n_defaultful_args_to_specify_positionally = winner

            else:
                # We have a draw! We're gonna have to settle it by picking the
                # lowest candidate, because in our definition of "canonical
                # arguments profile", our second priority after "as few
                # characters as possible" is "as many keyword arguments as
                # possible".
                winner = leading_candidates[0]

            n_defaultful_args_to_specify_positionally = winner

        # We have a winner! Now we know exactly which defaultful args should
        # be specified positionally and which should be specified by
        # keyword.

        # First we add the positionally specified:

        defaultful_args_to_specify_positionally = \
            defaultful_args[:n_defaultful_args_to_specify_positionally]
        self.args += tuple(
            (getcallargs_result[defaultful_arg]
             for defaultful_arg in defaultful_args_to_specify_positionally))

        # Now we add those specified by keyword:

        defaultful_args_to_specify_by_keyword = list(
            filter(
                defaultful_args_differing_from_defaults.__contains__,
                defaultful_args[n_defaultful_args_to_specify_positionally:]))
        for defaultful_arg in defaultful_args_to_specify_by_keyword:
            self.kwargs[defaultful_arg] = getcallargs_result[defaultful_arg]

        #######################################################################
        # Phase 3: Add the star args:

        if s_star_args and getcallargs_result[s_star_args]:

            assert not self.kwargs
            # Just making sure that no non-star args were specified by keyword,
            # which would make it impossible for us to put stuff in `*args`.

            self.args += getcallargs_result[s_star_args]

        #######################################################################
        # Phase 4: Add the star kwargs:

        if s_star_kwargs and getcallargs_result[s_star_kwargs]:

            # We can't just add the `**kwargs` as is; we need to add them
            # according to canonical ordering. So we need to sort them first.

            unsorted_star_kwargs_names = \
                list(getcallargs_result[s_star_kwargs].keys())
            sorted_star_kwargs_names = sorted(
                unsorted_star_kwargs_names,
                key=comparison_tools.underscore_hating_key)

            sorted_star_kwargs = OrderedDict(
                zip(
                    sorted_star_kwargs_names,
                    dict_tools.get_list(getcallargs_result[s_star_kwargs],
                                        sorted_star_kwargs_names)))

            self.kwargs.update(sorted_star_kwargs)

        # Our 4-phases algorithm is done! The argument profile is canonical.  #
        #######################################################################
        #######################################################################

        #######################################################################
        # Now a bit of post-processing:

        _arguments = OrderedDict()

        dict_of_positional_arguments = OrderedDict(
            dict_tools.filter_items(
                getcallargs_result,
                lambda key, value: ((key not in self.kwargs) and \
                                    (key != s_star_args) and \
                                    (key != s_star_kwargs))
            )
        )
        dict_of_positional_arguments.sort(key=s_args.index)
        _arguments.update(dict_of_positional_arguments)

        if s_star_args:
            _arguments['*'] = getcallargs_result[s_star_args]

        _arguments.update(self.kwargs)

        self._arguments = _arguments
        '''Ordered dict of arguments, both positional- and keyword-.'''

        # Caching the hash, since its computation can take a long time:
        self._hash = cheat_hashing.cheat_hash(
            (self.function, self.args, tuple(self.kwargs)))
def test_defaultfuls_and_star_kwargs():
    '''Test `ArgumentsProfile` with defaultful arguments and `**kwargs`.'''
    def func(a, b, c=3, d=4, **kwargs):
        pass

    a1 = ArgumentsProfile(func, 1, 2)
    assert a1.args == (1, 2)
    assert not a1.kwargs

    # Alphabetic ordering among the `**kwargs`, but `d` is first because it's a
    # non-star:
    a2 = ArgumentsProfile(func, 1, 2, d='bombastic', zany=True, blue=True)
    assert a2.args == (1, 2)
    assert a2.kwargs == OrderedDict(
        (('d', 'bombastic'), ('blue', True), ('zany', True)))

    a3 = ArgumentsProfile(func, 1, b=2, blue=True, d='bombastic', zany=True)
    a4 = ArgumentsProfile(func, zany=True, a=1, b=2, blue=True, d='bombastic')
    a5 = ArgumentsProfile(func, 1, 2, 3, 'bombastic', zany=True, blue=True)
    assert a2 == a3 == a4 == a5

    for arg_prof in [a2, a3, a4, a5]:
        # Testing `.iteritems`:
        assert OrderedDict(arg_prof) == OrderedDict(
            (('a', 1), ('b', 2), ('c', 3), ('d', 'bombastic'), ('blue', True),
             ('zany', True)))

        ### Testing `.__getitem__`: ###########################################
        #                                                                     #
        assert (arg_prof['a'], arg_prof['b'], arg_prof['c'], arg_prof['d'],
                arg_prof['blue'], arg_prof['zany']) == \
               (1, 2, 3, 'bombastic', True, True)

        with cute_testing.RaiseAssertor(KeyError):
            arg_prof['non_existing_key']
        #                                                                     #
        ### Finished testing `.__getitem__`. ##################################

        ### Testing `.get`: ###################################################
        #                                                                     #
        assert arg_prof.get('d') == arg_prof.get('d', 7) == 'bombastic'
        assert arg_prof.get('non_existing_key', 7) == 7
        assert arg_prof.get('non_existing_key') is None
        #                                                                     #
        ### Finished testing `.get`. ##########################################

        ### Testing `.iterkeys`, `.keys` and `__iter__`: ######################
        #                                                                     #
        assert list(arg_prof.iterkeys()) == list(arg_prof.keys()) == \
            list(arg_prof) == ['a', 'b', 'c', 'd', 'blue', 'zany']
        #                                                                     #
        ### Finished testing `.iterkeys`, `.keys` and `__iter__`. #############

        ### Testing `.itervalues` and `.values`: ##############################
        #                                                                     #
        assert list(arg_prof.itervalues()) == list(arg_prof.values()) == \
            [1, 2, 3, 'bombastic', True, True]
        #                                                                     #
        ### Finished testing `.itervalues` and `.values`. #####################

        ### Testing `.__contains__`: ##########################################
        #                                                                     #
        for key in arg_prof:
            assert key in arg_prof
        assert 'agaofgnafgadf' not in arg_prof
        assert '**' not in arg_prof
Esempio n. 22
0
def signature(obj):
    '''Get a signature object for the passed callable.'''

    if not callable(obj):
        raise TypeError('{0!r} is not a callable object'.format(obj))

    if isinstance(obj, types.MethodType):
        sig = signature(obj.__func__)
        if obj.__self__ is None:
            # Unbound method: the first parameter becomes positional-only
            if sig.parameters:
                first = sig.parameters.values()[0].replace(
                    kind=_POSITIONAL_ONLY)
                return sig.replace(
                    parameters=(first,) + tuple(sig.parameters.values())[1:])
            else:
                return sig
        else:
            # In this case we skip the first parameter of the underlying
            # function (usually `self` or `cls`).
            return sig.replace(parameters=tuple(sig.parameters.values())[1:])

    try:
        sig = obj.__signature__
    except AttributeError:
        pass
    else:
        if sig is not None:
            return sig

    try:
        # Was this function wrapped by a decorator?
        wrapped = obj.__wrapped__
    except AttributeError:
        pass
    else:
        return signature(wrapped)

    if isinstance(obj, types.FunctionType):
        return Signature.from_function(obj)

    if isinstance(obj, functools.partial):
        sig = signature(obj.func)

        new_params = OrderedDict(sig.parameters.items())

        partial_args = obj.args or ()
        partial_keywords = obj.keywords or {}
        try:
            ba = sig.bind_partial(*partial_args, **partial_keywords)
        except TypeError as ex:
            msg = 'partial object {0!r} has incorrect arguments'.format(obj)
            raise ValueError(msg)

        for arg_name, arg_value in ba.arguments.items():
            param = new_params[arg_name]
            if arg_name in partial_keywords:
                # We set a new default value, because the following code
                # is correct:
                #
                #   >>> def foo(a): print(a)
                #   >>> print(partial(partial(foo, a=10), a=20)())
                #   20
                #   >>> print(partial(partial(foo, a=10), a=20)(a=30))
                #   30
                #
                # So, with 'partial' objects, passing a keyword argument is
                # like setting a new default value for the corresponding
                # parameter
                #
                # We also mark this parameter with '_partial_kwarg'
                # flag.  Later, in '_bind', the 'default' value of this
                # parameter will be added to 'kwargs', to simulate
                # the 'functools.partial' real call.
                new_params[arg_name] = param.replace(default=arg_value,
                                                     _partial_kwarg=True)

            elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL) and
                            not param._partial_kwarg):
                new_params.pop(arg_name)

        return sig.replace(parameters=new_params.values())

    sig = None
    if isinstance(obj, type):
        # obj is a class or a metaclass

        # First, let's see if it has an overloaded __call__ defined
        # in its metaclass
        call = _get_user_defined_method(type(obj), '__call__')
        if call is not None:
            sig = signature(call)
        else:
            # Now we check if the 'obj' class has a '__new__' method
            new = _get_user_defined_method(obj, '__new__')
            if new is not None:
                sig = signature(new)
            else:
                # Finally, we should have at least __init__ implemented
                init = _get_user_defined_method(obj, '__init__')
                if init is not None:
                    sig = signature(init)
    elif not isinstance(obj, _NonUserDefinedCallables):
        # An object with __call__
        # We also check that the 'obj' is not an instance of
        # _WrapperDescriptor or _MethodWrapper to avoid
        # infinite recursion (and even potential segfault)
        call = _get_user_defined_method(type(obj), '__call__', 'im_func')
        if call is not None:
            sig = signature(call)

    if sig is not None:
        # For classes and objects we skip the first parameter of their
        # __call__, __new__, or __init__ methods
        return sig.replace(parameters=tuple(sig.parameters.values())[1:])

    if isinstance(obj, types.BuiltinFunctionType):
        # Raise a nicer error message for builtins
        msg = 'no signature found for builtin function {0!r}'.format(obj)
        raise ValueError(msg)

    raise ValueError('callable {0!r} is not supported by signature'.format(obj))
Esempio n. 23
0
    def test_operations(self):
        bag_0 = self.bag_type('abbccc')
        bag_1 = self.bag_type('bcc')
        bag_2 = self.bag_type('cddddd')

        assert bag_0 + bag_1 == self.bag_type('abbccc' + 'bcc')
        assert bag_1 + bag_0 == self.bag_type('bcc' + 'abbccc')
        assert bag_0 + bag_2 == self.bag_type('abbccc' + 'cddddd')
        assert bag_2 + bag_0 == self.bag_type('cddddd' + 'abbccc')
        assert bag_1 + bag_2 == self.bag_type('bcc' + 'cddddd')
        assert bag_2 + bag_1 == self.bag_type('cddddd' + 'bcc')

        assert bag_0 - bag_1 == self.bag_type('abc')
        assert bag_1 - bag_0 == self.bag_type()
        assert bag_0 - bag_2 == self.bag_type('abbcc')
        assert bag_2 - bag_0 == self.bag_type('ddddd')
        assert bag_1 - bag_2 == self.bag_type('bc')
        assert bag_2 - bag_1 == self.bag_type('ddddd')

        assert bag_0 * 2 == self.bag_type('abbccc' * 2)
        assert bag_1 * 2 == self.bag_type('bcc' * 2)
        assert bag_2 * 2 == self.bag_type('cddddd' * 2)
        assert 3 * bag_0 == self.bag_type('abbccc' * 3)
        assert 3 * bag_1 == self.bag_type('bcc' * 3)
        assert 3 * bag_2 == self.bag_type('cddddd' * 3)

        assert bag_0 // 2 == self.bag_type('bc')
        assert bag_1 // 2 == self.bag_type('c')
        assert bag_2 // 2 == self.bag_type('dd')
        assert bag_0 // self.bag_type('ab') == 1
        assert bag_1 // self.bag_type('ab') == 0
        assert bag_2 // self.bag_type('ab') == 0

        with cute_testing.RaiseAssertor(ZeroDivisionError):
            bag_0 // 0
        with cute_testing.RaiseAssertor(ZeroDivisionError):
            bag_0 // self.bag_type()

        assert bag_0 % 2 == self.bag_type('ac') == bag_0 - ((bag_0 // 2) * 2) \
               == self.bag_type(OrderedDict((key, count % 2) for (key, count)
                                                             in bag_0.items()))
        assert bag_1 % 2 == self.bag_type('b') == bag_1 - ((bag_1 // 2) * 2) \
               == self.bag_type(OrderedDict((key, count % 2) for (key, count)
                                                             in bag_1.items()))
        assert bag_2 % 2 == self.bag_type('cd') == bag_2 - ((bag_2 // 2) * 2)\
               == self.bag_type(OrderedDict((key, count % 2) for (key, count)
                                                             in bag_2.items()))
        assert bag_0 % self.bag_type('ac') == self.bag_type('bbcc')
        assert bag_1 % self.bag_type('b') == self.bag_type('cc')
        assert bag_2 % self.bag_type('cd') == self.bag_type('dddd')

        assert bag_0**2 == pow(bag_0, 2) == self.bag_type('abbbbccccccccc')
        assert bag_1**2 == pow(bag_1, 2) == self.bag_type('bcccc')
        assert bag_2 ** 2 == pow(bag_2, 2) == \
                                    self.bag_type('cddddddddddddddddddddddddd')
        assert pow(bag_0, 2, 3) == self.bag_type('ab')
        assert pow(bag_1, 2, 3) == self.bag_type('bc')
        assert pow(bag_2, 2, 3) == self.bag_type('cd')

        assert divmod(bag_0, 3) == (bag_0 // 3, bag_0 % 3)
        assert divmod(bag_1, 3) == (bag_1 // 3, bag_1 % 3)
        assert divmod(bag_2, 3) == (bag_2 // 3, bag_2 % 3)
        assert divmod(bag_0, self.bag_type('cd')) == \
                    (bag_0 // self.bag_type('cd'), bag_0 % self.bag_type('cd'))
        assert divmod(bag_1, self.bag_type('cd')) == \
                    (bag_1 // self.bag_type('cd'), bag_1 % self.bag_type('cd'))
        assert divmod(bag_2, self.bag_type('cd')) == \
                    (bag_2 // self.bag_type('cd'), bag_2 % self.bag_type('cd'))
Esempio n. 24
0
class ArgumentsProfile:
    '''
    A canonical arguments profile for a function.
    
    (This should be used only on functions that don't modify the arguments they
    receive. Also, you should never modify any arguments you use in an
    arguments profile, even outside the function.)
    
    What is an arguments profile and what is it good for?
    
    It's possible to call the same function with the same arguments in
    different ways. For example, take this function:

        def f(a, bb=2, ccc=3, **kwargs):
            return (a, bb, ccc)
            
    You can call it like `f(1)` or `f(a=1)` or `f(1, ccc=3, bb=2, **{})` or
    many other different ways which will result in exactly the same arguments.
    
    To organize the different ways a function can be called, an arguments
    profile provides a canonical way to call the function, so all the different
    examples in the last paragraphs would be reduced to the same canonical
    arguments profile. (In this case `f(1)`.)
    
    The canonical arguments profile is defined as the one which satisfies the
    following criteria, with the first one being the most important, the second
    one being a tie-breaker to the first, and the third one being a tie-breaker
    to the second:
    
     1. It has as few characters as possible. (e.g. `f(1)` is better than    
        `f(1, 2)`.)
        
     2. It has as many keyword arguments as possible. (e.g. `f(bb=3)` is
        better than `f(1, 3)`.)
        
     3. The extraneous keywords (i.e. `**kwargs`) are sorted alphabetically, 
        with "_" being the highest/last character. (e.g. `f(1, cat=7, meow=7,
        _house=7)` is better than `f(1, _house=7, meow=7, cat=7)`)
    
    # Accessing the data of an arguments profile #
    
    Say you have this function:
    
        def f(x, y, *args, **kwargs):
            pass
    
    And you create an arguments profile:
    
        arguments_profile = ArgumentsProfile(f, 1, 2, 3, 4, meow='frr')
            
    There are two ways to access the data of this arguments profile:
    
     1. Use `arguments_profile.args` and `arguments_profile.kwargs`, which are,
        respectively, a tuple of positional arguments and an ordered dict of
        keyword arguments. In this case, `.args` would be `(1, 2, 3, 4)` and
        `.kwargs` would be `OrderedDict((('meow', 'frr'),))`.
        
     2. Use `arguments_profile`'s ordered-dict-like interface. A few examples:
     
            arguments_profile['x'] == 1
            arguments_profile['y'] == 2
            arguments_profile['*'] == (3, 4)
            arguments_profile['meow'] == 'frr'
            
        The special asterisk argument indicates the arguments that go into
        `*args`.
        
    '''
    # todo: we're using an ad-hoc third way, `self.getcallargs_result`, think
    # hard about that...
    
    def __init__(self, function, *args, **kwargs):
        '''
        Construct the arguments profile.
        
        `*args` and `**kwargs` are the arguments that go into the `function`.
        '''
        
        if not callable(function):
            raise Exception('%s is not a callable object.' % function)
        self.function = function
        
        raw_args = args
        raw_kwargs = kwargs
        del args, kwargs
        
        self.args = ()
        '''Tuple of positional arguments.'''
        
        self.kwargs = OrderedDict()
        '''Ordered dict of keyword arguments.'''
        
        
        args_spec = cute_inspect.getargspec(function)
        
        (s_args, s_star_args, s_star_kwargs, s_defaults) = args_spec
        
        # `getargspec` has a weird policy, when inspecting a function with no
        # defaults, to give a `defaults` of `None` instead of the more
        # consistent `()`. We fix that here:
        if s_defaults is None:
            s_defaults = ()
        
        getcallargs_result = cute_inspect.getcallargs(function,
                                                      *raw_args,
                                                      **raw_kwargs)
        self.getcallargs_result = getcallargs_result
        
        
        # The number of args which have default values:
        n_defaultful_args = len(s_defaults)
        # The word "defaultful" means "something which has a default."
        
        #######################################################################
        #######################################################################
        # Now we'll create the arguments profile, using a 4-phases algorithm. #
        #                                                                     #
        
        #######################################################################
        # Phase 1: We specify all the args that don't have a default as
        # positional args:
        defaultless_args = s_args[:-n_defaultful_args] if n_defaultful_args \
                           else s_args[:]
        self.args += tuple(
            dict_tools.get_list(getcallargs_result, defaultless_args)
        )

        
        #######################################################################
        # Phase 2: We now have to deal with args that have a default. Some of
        # them, possibly none and possibly all of them, should be given
        # positionally. Some of them, possibly none, should be given by
        # keyword. And some of them, possibly none and possibly all of them,
        # should not be given at all. It is our job to figure out in which way
        # each argument should be given.
        
        # In this variable:
        n_defaultful_args_to_specify_positionally = None
        # We will put the number of defaultful arguments that should be
        # specified positionally.
        
        defaultful_args = s_args[-n_defaultful_args:] if n_defaultful_args \
                          else []
        
        # `dict` that maps from argument name to default value:
        defaults = OrderedDict(zip(defaultful_args, s_defaults))
        
        defaultful_args_differing_from_defaults = {
            defaultful_arg for defaultful_arg in defaultful_args
            if defaults[defaultful_arg] != getcallargs_result[defaultful_arg]
        }
        
        if s_star_args and getcallargs_result[s_star_args]:
            # We have some arguments that go into `*args`! This means that we
            # don't even need to think hard, we can already be sure that we're
            # going to have to specify *all* of the defaultful arguments
            # positionally, otherwise it will be impossible to put arguments in
            # `*args`.
            n_defaultful_args_to_specify_positionally = n_defaultful_args
            
            
        else:

            # `dict` mapping from each defaultful arg to the "price" of
            # specifying its value:
            prices_of_values = OrderedDict({
                defaultful_arg: len(repr(getcallargs_result[defaultful_arg]))
                 for defaultful_arg in defaultful_args
            })
            # The price is simply the string length of the value's `repr`.
            
            # `dict` mapping from each defaultful arg to the "price" of
            # specifying it as a keyword (not including the length of the
            # value):
            prices_of_keyword_prefixes = OrderedDict({
                defaultful_arg: len(defaultful_arg)+1 for 
                                              defaultful_arg in defaultful_args
            })
            # For example, if we have a defaultful arg "gravity_strength", then
            # specifiying it by keyword will require using the string
            # "gravity_strength=", which is 17 characters long, therefore the
            # price is 17.
            
            # Now we need to decide just how many defaultful args we are going
            # to specify positionally. The options are anything from `0` to
            # `n_defaultful_args`. We're going to go one by one, and calcluate
            # the price for each candidate, and put it in this dict:
            total_price_for_n_dasp_candidate = OrderedDict()
            # (The `n_dasp` here is an abbreivation of the
            # `n_defaultful_args_to_specify_positionally` variable defined
            # before.)
            #
            # After we have the price for each option, we'll select the one
            # with the lowest price.
            
            # One thing to do before iterating on the candidates is to find out
            # whether the "lonely comma discount" is in effect.
            #
            # The "lonely comma discount" is given when there's nothing but
            # defaultful arguments to this function, and therefore the number
            # of ", " strings needed here is not `candidate`, but `candidate -
            # 1`, unless of course candidate is zero.
            
            if not defaultless_args and \
                (not s_star_args or not getcallargs_result[s_star_args]) and \
                (not s_star_kwargs or not getcallargs_result[s_star_kwargs]):
                
                lonely_comma_discount_may_be_given = True
            
            else:
                
                lonely_comma_discount_may_be_given = False
            
            # Now we iterate on the candidates to find out which one has the
            # lowest price:
            
            for candidate in range(n_defaultful_args + 1):

                defaultful_args_to_specify_positionally = \
                    defaultful_args[:candidate]
                
                price_for_positionally_specified_defaultful_args = \
                    2 * candidate + \
                    sum(
                        dict_tools.get_list(
                            prices_of_values,
                            defaultful_args_to_specify_positionally
                        )
                    )
                # The `2 * candidate` addend is to account for the ", " parts
                # between the arguments.
                    
                defaultful_args_to_specify_by_keyword = list(filter(
                    defaultful_args_differing_from_defaults.__contains__,
                    defaultful_args[candidate:]
                ))
                
                price_for_defaultful_args_specified_by_keyword = \
                    2 * len(defaultful_args_to_specify_by_keyword) + \
                    sum(
                        dict_tools.get_list(
                            prices_of_keyword_prefixes,
                            defaultful_args_to_specify_by_keyword
                        )
                    ) + \
                    sum(
                        dict_tools.get_list(
                            prices_of_values,
                            defaultful_args_to_specify_by_keyword
                        )
                    )
                # The `2 * len(...)` addend is to account for the ", " parts
                # between the arguments.
                
                # Now we need to figure out if this candidate gets the "lonely
                # comma discount".
                if lonely_comma_discount_may_be_given and \
                   (defaultful_args_to_specify_by_keyword or \
                    defaultful_args_to_specify_positionally):
                    
                    lonely_comma_discount = -2
                    
                else:
                    lonely_comma_discount = 0
                
                price = price_for_positionally_specified_defaultful_args + \
                        price_for_defaultful_args_specified_by_keyword + \
                        lonely_comma_discount
                
                total_price_for_n_dasp_candidate[candidate] = price


            # Finished iterating on candidates! Time to pick our winner.
                
            minimum_price = min(total_price_for_n_dasp_candidate.values())
            
            leading_candidates = [
                candidate for candidate in 
                total_price_for_n_dasp_candidate.keys() if
                total_price_for_n_dasp_candidate[candidate] == minimum_price
            ]
            
            if len(leading_candidates) == 1:
                # We finished with one candidate which has the minimum price.
                # This is our winner.
                (winner,) = leading_candidates
                n_defaultful_args_to_specify_positionally = winner
            
            else:
                # We have a draw! We're gonna have to settle it by picking the
                # lowest candidate, because in our definition of "canonical
                # arguments profile", our second priority after "as few
                # characters as possible" is "as many keyword arguments as
                # possible".
                winner = leading_candidates[0]
                
            n_defaultful_args_to_specify_positionally = winner
            
        # We have a winner! Now we know exactly which defaultful args should
        # be specified positionally and which should be specified by
        # keyword.
            
        # First we add the positionally specified:
            
        defaultful_args_to_specify_positionally = \
            defaultful_args[:n_defaultful_args_to_specify_positionally]
        self.args += tuple(
            (getcallargs_result[defaultful_arg] for defaultful_arg
             in defaultful_args_to_specify_positionally)
        )
        
        # Now we add those specified by keyword:

        defaultful_args_to_specify_by_keyword = list(filter(
                defaultful_args_differing_from_defaults.__contains__,
                defaultful_args[n_defaultful_args_to_specify_positionally:]
            ))
        for defaultful_arg in defaultful_args_to_specify_by_keyword:
            self.kwargs[defaultful_arg] = getcallargs_result[defaultful_arg]
                
        
        #######################################################################
        # Phase 3: Add the star args:
        
        if s_star_args and getcallargs_result[s_star_args]:
            
            assert not self.kwargs
            # Just making sure that no non-star args were specified by keyword,
            # which would make it impossible for us to put stuff in `*args`.
            
            self.args += getcallargs_result[s_star_args]        

            
        #######################################################################
        # Phase 4: Add the star kwargs:
        
        if s_star_kwargs and getcallargs_result[s_star_kwargs]:
            
            # We can't just add the `**kwargs` as is; we need to add them
            # according to canonical ordering. So we need to sort them first.
            
            unsorted_star_kwargs_names = \
                list(getcallargs_result[s_star_kwargs].keys())
            sorted_star_kwargs_names = sorted(
                unsorted_star_kwargs_names,
                key=comparison_tools.underscore_hating_key
            )
            
            sorted_star_kwargs = OrderedDict(
                zip(
                    sorted_star_kwargs_names,
                    dict_tools.get_list(
                        getcallargs_result[s_star_kwargs],
                        sorted_star_kwargs_names
                    )
                )
            )
            
            
            self.kwargs.update(sorted_star_kwargs)
            
        # Our 4-phases algorithm is done! The argument profile is canonical.  #
        #######################################################################
        #######################################################################
        
        
        #######################################################################
        # Now a bit of post-processing:
        
        _arguments = OrderedDict()
        
        dict_of_positional_arguments = OrderedDict(
            dict_tools.filter_items(
                getcallargs_result,
                lambda key, value: ((key not in self.kwargs) and \
                                    (key != s_star_args) and \
                                    (key != s_star_kwargs))
            )
        )
        dict_of_positional_arguments.sort(key=s_args.index)
        _arguments.update(dict_of_positional_arguments)
        
        if s_star_args:
            _arguments['*'] = getcallargs_result[s_star_args]
            
        _arguments.update(self.kwargs)
        
        self._arguments = _arguments
        '''Ordered dict of arguments, both positional- and keyword-.'''
        
        # Caching the hash, since its computation can take a long time:
        self._hash = cheat_hashing.cheat_hash(
            (
                self.function,
                self.args,
                tuple(self.kwargs)
            )
        )
        
        
    def __getitem__(self, argument_name):
        '''Get the value of a specified argument.'''
        return self._arguments.__getitem__(argument_name)
        
    
    def get(self, argument_name, default=None):
        '''Get the value of a specified argument, if missing get `default`.'''
        return self._arguments.get(argument_name, default)
    
    
    def keys(self):
        '''Get all the argument names.'''
        return list(self._arguments.keys())
    
    
    def values(self):
        '''Get all the argument values.'''
        return list(self._arguments.values())
    
    
    def items(self):
        '''Get a tuple of all the `(argument_name, argument_value)` item.'''
        return list(self._arguments.items())
    
    
    def __iter__(self):
        '''Iterate on the argument names according to their order.'''
        return self._arguments.__iter__()
    
    
    def iterkeys(self):
        '''Iterate on the argument names according to their order.'''
        return iter(self._arguments.keys())
    
    
    def itervalues(self):        
        '''Iterate on the argument value according to their order.'''
        return iter(self._arguments.values())
        
    
    def iteritems(self):
        '''Iterate on `(argument_name, argument_value)` items by order.'''
        return iter(self._arguments.items())
    
    
    def __contains__(self, argument_name):
        '''Return whether the arguments profile contains the given argument.'''
        return self._arguments.__contains__(argument_name)
    
    
    @classmethod
    def create_from_dld_format(cls, function, args_dict, star_args_list,
                               star_kwargs_dict):
        '''
        Create an arguments profile from data given in "dict-list-dict" format.
        
        The "dict-list-dict" format means that in addition to a function, we
        get a `dict` of arguments, a `list` of `*args`, and a `dict` of
        `**kwargs`.
        '''
        args_spec = cute_inspect.getargspec(function)
        new_args = [args_dict[name] for name in args_spec.args] + \
                   list(star_args_list)
        return cls(function, *new_args, **star_kwargs_dict)
        
            
                    
    def __eq__(self, other):
        # todo: maybe raise warning when unbound method is compared with same
        # method just bound with the object passed to the unbound method, and
        # the result of both functions would be the same, but we're not smart
        # enough to say it's the same arguments profile, so raise a warning.
        if not isinstance(other, ArgumentsProfile):
            return NotImplemented
        # Note that we're comparing the functions with a `==` here. This lesson
        # cost me a couple of days: `MyClass.method == MyClass.method` but
        # `MyClass.method is not MyClass.method`.
        return (self.function == other.function) and \
               (self.args == other.args) and \
               (self.kwargs == other.kwargs)
    
    
    def __ne__(self, other):
        return not self == other
    
    
    def __hash__(self):
        return self._hash
Esempio n. 25
0
    def decorator(function):

        # In case we're being given a function that is already cached:
        if getattr(function, 'is_cached', False): return function

        if max_size == infinity:

            if time_to_keep:

                sorting_key_function = lambda sleek_call_args: \
                                              cached._cache[sleek_call_args][1]

                def remove_expired_entries():
                    almost_cutting_point = \
                                          binary_search.binary_search_by_index(
                        list(cached._cache.keys()),
                        _get_now(),
                        sorting_key_function,
                        rounding=binary_search.LOW
                    )
                    if almost_cutting_point is not None:
                        cutting_point = almost_cutting_point + 1
                        for key in cached._cache.keys()[:cutting_point]:
                            del cached._cache[key]

                @misc_tools.set_attributes(_cache=OrderedDict())
                def cached(function, *args, **kwargs):
                    remove_expired_entries()
                    sleek_call_args = \
                        SleekCallArgs(cached._cache, function, *args, **kwargs)
                    try:
                        return cached._cache[sleek_call_args][0]
                    except KeyError:
                        value = function(*args, **kwargs)
                        cached._cache[sleek_call_args] = (value, _get_now() +
                                                          time_to_keep)
                        cached._cache.sort(key=sorting_key_function)
                        return value

            else:  # not time_to_keep

                @misc_tools.set_attributes(_cache={})
                def cached(function, *args, **kwargs):
                    sleek_call_args = \
                        SleekCallArgs(cached._cache, function, *args, **kwargs)
                    try:
                        return cached._cache[sleek_call_args]
                    except KeyError:
                        cached._cache[sleek_call_args] = value = \
                              function(*args, **kwargs)
                        return value

        else:  # max_size < infinity

            @misc_tools.set_attributes(_cache=OrderedDict())
            def cached(function, *args, **kwargs):
                sleek_call_args = \
                    SleekCallArgs(cached._cache, function, *args, **kwargs)
                try:
                    result = cached._cache[sleek_call_args]
                    cached._cache.move_to_end(sleek_call_args)
                    return result
                except KeyError:
                    cached._cache[sleek_call_args] = value = \
                        function(*args, **kwargs)
                    if len(cached._cache) > max_size:
                        cached._cache.popitem(last=False)
                    return value

        result = decorator_tools.decorator(cached, function)

        def cache_clear(key=CLEAR_ENTIRE_CACHE):
            if key is CLEAR_ENTIRE_CACHE:
                cached._cache.clear()
            else:
                try:
                    del cached._cache[key]
                except KeyError:
                    pass

        result.cache_clear = cache_clear

        result.is_cached = True

        return result
Esempio n. 26
0
    def test_operations(self):
        bag_0 = self.bag_type('abbccc')
        bag_1 = self.bag_type('bcc')
        bag_2 = self.bag_type('cddddd')

        assert bag_0 + bag_1 == self.bag_type('abbccc' + 'bcc')
        assert bag_1 + bag_0 == self.bag_type('bcc' + 'abbccc')
        assert bag_0 + bag_2 == self.bag_type('abbccc' + 'cddddd')
        assert bag_2 + bag_0 == self.bag_type('cddddd' + 'abbccc')
        assert bag_1 + bag_2 == self.bag_type('bcc' + 'cddddd')
        assert bag_2 + bag_1 == self.bag_type('cddddd' + 'bcc')

        assert bag_0 - bag_1 == self.bag_type('abc')
        assert bag_1 - bag_0 == self.bag_type()
        assert bag_0 - bag_2 == self.bag_type('abbcc')
        assert bag_2 - bag_0 == self.bag_type('ddddd')
        assert bag_1 - bag_2 == self.bag_type('bc')
        assert bag_2 - bag_1 == self.bag_type('ddddd')

        assert bag_0 * 2 == self.bag_type('abbccc' * 2)
        assert bag_1 * 2 == self.bag_type('bcc' * 2)
        assert bag_2 * 2 == self.bag_type('cddddd' * 2)
        assert 3 * bag_0 == self.bag_type('abbccc' * 3)
        assert 3 * bag_1 == self.bag_type('bcc' * 3)
        assert 3 * bag_2 == self.bag_type('cddddd' * 3)

        # We only allow floor division on bags, not regular divison, because a
        # decimal bag is unheard of.
        with cute_testing.RaiseAssertor(TypeError):
            bag_0 / 2
        with cute_testing.RaiseAssertor(TypeError):
            bag_1 / 2
        with cute_testing.RaiseAssertor(TypeError):
            bag_2 / 2
        with cute_testing.RaiseAssertor(TypeError):
            bag_0 / self.bag_type('ab')
        with cute_testing.RaiseAssertor(TypeError):
            bag_1 / self.bag_type('ab')
        with cute_testing.RaiseAssertor(TypeError):
            bag_2 / self.bag_type('ab')

        assert bag_0 // 2 == self.bag_type('bc')
        assert bag_1 // 2 == self.bag_type('c')
        assert bag_2 // 2 == self.bag_type('dd')
        assert bag_0 // self.bag_type('ab') == 1
        assert bag_1 // self.bag_type('ab') == 0
        assert bag_2 // self.bag_type('ab') == 0

        with cute_testing.RaiseAssertor(ZeroDivisionError):
            bag_0 // 0
        with cute_testing.RaiseAssertor(ZeroDivisionError):
            bag_0 // self.bag_type()

        assert bag_0 % 2 == self.bag_type('ac') == bag_0 - ((bag_0 // 2) * 2) \
               == self.bag_type(OrderedDict((key, count % 2) for (key, count)
                                                             in bag_0.items()))
        assert bag_1 % 2 == self.bag_type('b') == bag_1 - ((bag_1 // 2) * 2) \
               == self.bag_type(OrderedDict((key, count % 2) for (key, count)
                                                             in bag_1.items()))
        assert bag_2 % 2 == self.bag_type('cd') == bag_2 - ((bag_2 // 2) * 2)\
               == self.bag_type(OrderedDict((key, count % 2) for (key, count)
                                                             in bag_2.items()))
        assert bag_0 % self.bag_type('ac') == self.bag_type('bbcc')
        assert bag_1 % self.bag_type('b') == self.bag_type('cc')
        assert bag_2 % self.bag_type('cd') == self.bag_type('dddd')

        assert bag_0**2 == pow(bag_0, 2) == self.bag_type('abbbbccccccccc')
        assert bag_1**2 == pow(bag_1, 2) == self.bag_type('bcccc')
        assert bag_2 ** 2 == pow(bag_2, 2) == \
                                    self.bag_type('cddddddddddddddddddddddddd')
        assert pow(bag_0, 2, 3) == self.bag_type('ab')
        assert pow(bag_1, 2, 3) == self.bag_type('bc')
        assert pow(bag_2, 2, 3) == self.bag_type('cd')

        assert divmod(bag_0, 3) == (bag_0 // 3, bag_0 % 3)
        assert divmod(bag_1, 3) == (bag_1 // 3, bag_1 % 3)
        assert divmod(bag_2, 3) == (bag_2 // 3, bag_2 % 3)
        assert divmod(bag_0, self.bag_type('cd')) == \
                    (bag_0 // self.bag_type('cd'), bag_0 % self.bag_type('cd'))
        assert divmod(bag_1, self.bag_type('cd')) == \
                    (bag_1 // self.bag_type('cd'), bag_1 % self.bag_type('cd'))
        assert divmod(bag_2, self.bag_type('cd')) == \
                    (bag_2 // self.bag_type('cd'), bag_2 % self.bag_type('cd'))
Esempio n. 27
0
    def __init__(self, function, *args, **kwargs):
        '''
        Construct the arguments profile.
        
        `*args` and `**kwargs` are the arguments that go into the `function`.
        '''
        
        if not callable(function):
            raise Exception('%s is not a callable object.' % function)
        self.function = function
        
        raw_args = args
        raw_kwargs = kwargs
        del args, kwargs
        
        self.args = ()
        '''Tuple of positional arguments.'''
        
        self.kwargs = OrderedDict()
        '''Ordered dict of keyword arguments.'''
        
        
        args_spec = cute_inspect.getargspec(function)
        
        (s_args, s_star_args, s_star_kwargs, s_defaults) = args_spec
        
        # `getargspec` has a weird policy, when inspecting a function with no
        # defaults, to give a `defaults` of `None` instead of the more
        # consistent `()`. We fix that here:
        if s_defaults is None:
            s_defaults = ()
        
        getcallargs_result = cute_inspect.getcallargs(function,
                                                      *raw_args,
                                                      **raw_kwargs)
        self.getcallargs_result = getcallargs_result
        
        
        # The number of args which have default values:
        n_defaultful_args = len(s_defaults)
        # The word "defaultful" means "something which has a default."
        
        #######################################################################
        #######################################################################
        # Now we'll create the arguments profile, using a 4-phases algorithm. #
        #                                                                     #
        
        #######################################################################
        # Phase 1: We specify all the args that don't have a default as
        # positional args:
        defaultless_args = s_args[:-n_defaultful_args] if n_defaultful_args \
                           else s_args[:]
        self.args += tuple(
            dict_tools.get_list(getcallargs_result, defaultless_args)
        )

        
        #######################################################################
        # Phase 2: We now have to deal with args that have a default. Some of
        # them, possibly none and possibly all of them, should be given
        # positionally. Some of them, possibly none, should be given by
        # keyword. And some of them, possibly none and possibly all of them,
        # should not be given at all. It is our job to figure out in which way
        # each argument should be given.
        
        # In this variable:
        n_defaultful_args_to_specify_positionally = None
        # We will put the number of defaultful arguments that should be
        # specified positionally.
        
        defaultful_args = s_args[-n_defaultful_args:] if n_defaultful_args \
                          else []
        
        # `dict` that maps from argument name to default value:
        defaults = OrderedDict(zip(defaultful_args, s_defaults))
        
        defaultful_args_differing_from_defaults = {
            defaultful_arg for defaultful_arg in defaultful_args
            if defaults[defaultful_arg] != getcallargs_result[defaultful_arg]
        }
        
        if s_star_args and getcallargs_result[s_star_args]:
            # We have some arguments that go into `*args`! This means that we
            # don't even need to think hard, we can already be sure that we're
            # going to have to specify *all* of the defaultful arguments
            # positionally, otherwise it will be impossible to put arguments in
            # `*args`.
            n_defaultful_args_to_specify_positionally = n_defaultful_args
            
            
        else:

            # `dict` mapping from each defaultful arg to the "price" of
            # specifying its value:
            prices_of_values = OrderedDict({
                defaultful_arg: len(repr(getcallargs_result[defaultful_arg]))
                 for defaultful_arg in defaultful_args
            })
            # The price is simply the string length of the value's `repr`.
            
            # `dict` mapping from each defaultful arg to the "price" of
            # specifying it as a keyword (not including the length of the
            # value):
            prices_of_keyword_prefixes = OrderedDict({
                defaultful_arg: len(defaultful_arg)+1 for 
                                              defaultful_arg in defaultful_args
            })
            # For example, if we have a defaultful arg "gravity_strength", then
            # specifiying it by keyword will require using the string
            # "gravity_strength=", which is 17 characters long, therefore the
            # price is 17.
            
            # Now we need to decide just how many defaultful args we are going
            # to specify positionally. The options are anything from `0` to
            # `n_defaultful_args`. We're going to go one by one, and calcluate
            # the price for each candidate, and put it in this dict:
            total_price_for_n_dasp_candidate = OrderedDict()
            # (The `n_dasp` here is an abbreivation of the
            # `n_defaultful_args_to_specify_positionally` variable defined
            # before.)
            #
            # After we have the price for each option, we'll select the one
            # with the lowest price.
            
            # One thing to do before iterating on the candidates is to find out
            # whether the "lonely comma discount" is in effect.
            #
            # The "lonely comma discount" is given when there's nothing but
            # defaultful arguments to this function, and therefore the number
            # of ", " strings needed here is not `candidate`, but `candidate -
            # 1`, unless of course candidate is zero.
            
            if not defaultless_args and \
                (not s_star_args or not getcallargs_result[s_star_args]) and \
                (not s_star_kwargs or not getcallargs_result[s_star_kwargs]):
                
                lonely_comma_discount_may_be_given = True
            
            else:
                
                lonely_comma_discount_may_be_given = False
            
            # Now we iterate on the candidates to find out which one has the
            # lowest price:
            
            for candidate in range(n_defaultful_args + 1):

                defaultful_args_to_specify_positionally = \
                    defaultful_args[:candidate]
                
                price_for_positionally_specified_defaultful_args = \
                    2 * candidate + \
                    sum(
                        dict_tools.get_list(
                            prices_of_values,
                            defaultful_args_to_specify_positionally
                        )
                    )
                # The `2 * candidate` addend is to account for the ", " parts
                # between the arguments.
                    
                defaultful_args_to_specify_by_keyword = list(filter(
                    defaultful_args_differing_from_defaults.__contains__,
                    defaultful_args[candidate:]
                ))
                
                price_for_defaultful_args_specified_by_keyword = \
                    2 * len(defaultful_args_to_specify_by_keyword) + \
                    sum(
                        dict_tools.get_list(
                            prices_of_keyword_prefixes,
                            defaultful_args_to_specify_by_keyword
                        )
                    ) + \
                    sum(
                        dict_tools.get_list(
                            prices_of_values,
                            defaultful_args_to_specify_by_keyword
                        )
                    )
                # The `2 * len(...)` addend is to account for the ", " parts
                # between the arguments.
                
                # Now we need to figure out if this candidate gets the "lonely
                # comma discount".
                if lonely_comma_discount_may_be_given and \
                   (defaultful_args_to_specify_by_keyword or \
                    defaultful_args_to_specify_positionally):
                    
                    lonely_comma_discount = -2
                    
                else:
                    lonely_comma_discount = 0
                
                price = price_for_positionally_specified_defaultful_args + \
                        price_for_defaultful_args_specified_by_keyword + \
                        lonely_comma_discount
                
                total_price_for_n_dasp_candidate[candidate] = price


            # Finished iterating on candidates! Time to pick our winner.
                
            minimum_price = min(total_price_for_n_dasp_candidate.values())
            
            leading_candidates = [
                candidate for candidate in 
                total_price_for_n_dasp_candidate.keys() if
                total_price_for_n_dasp_candidate[candidate] == minimum_price
            ]
            
            if len(leading_candidates) == 1:
                # We finished with one candidate which has the minimum price.
                # This is our winner.
                (winner,) = leading_candidates
                n_defaultful_args_to_specify_positionally = winner
            
            else:
                # We have a draw! We're gonna have to settle it by picking the
                # lowest candidate, because in our definition of "canonical
                # arguments profile", our second priority after "as few
                # characters as possible" is "as many keyword arguments as
                # possible".
                winner = leading_candidates[0]
                
            n_defaultful_args_to_specify_positionally = winner
            
        # We have a winner! Now we know exactly which defaultful args should
        # be specified positionally and which should be specified by
        # keyword.
            
        # First we add the positionally specified:
            
        defaultful_args_to_specify_positionally = \
            defaultful_args[:n_defaultful_args_to_specify_positionally]
        self.args += tuple(
            (getcallargs_result[defaultful_arg] for defaultful_arg
             in defaultful_args_to_specify_positionally)
        )
        
        # Now we add those specified by keyword:

        defaultful_args_to_specify_by_keyword = list(filter(
                defaultful_args_differing_from_defaults.__contains__,
                defaultful_args[n_defaultful_args_to_specify_positionally:]
            ))
        for defaultful_arg in defaultful_args_to_specify_by_keyword:
            self.kwargs[defaultful_arg] = getcallargs_result[defaultful_arg]
                
        
        #######################################################################
        # Phase 3: Add the star args:
        
        if s_star_args and getcallargs_result[s_star_args]:
            
            assert not self.kwargs
            # Just making sure that no non-star args were specified by keyword,
            # which would make it impossible for us to put stuff in `*args`.
            
            self.args += getcallargs_result[s_star_args]        

            
        #######################################################################
        # Phase 4: Add the star kwargs:
        
        if s_star_kwargs and getcallargs_result[s_star_kwargs]:
            
            # We can't just add the `**kwargs` as is; we need to add them
            # according to canonical ordering. So we need to sort them first.
            
            unsorted_star_kwargs_names = \
                list(getcallargs_result[s_star_kwargs].keys())
            sorted_star_kwargs_names = sorted(
                unsorted_star_kwargs_names,
                key=comparison_tools.underscore_hating_key
            )
            
            sorted_star_kwargs = OrderedDict(
                zip(
                    sorted_star_kwargs_names,
                    dict_tools.get_list(
                        getcallargs_result[s_star_kwargs],
                        sorted_star_kwargs_names
                    )
                )
            )
            
            
            self.kwargs.update(sorted_star_kwargs)
            
        # Our 4-phases algorithm is done! The argument profile is canonical.  #
        #######################################################################
        #######################################################################
        
        
        #######################################################################
        # Now a bit of post-processing:
        
        _arguments = OrderedDict()
        
        dict_of_positional_arguments = OrderedDict(
            dict_tools.filter_items(
                getcallargs_result,
                lambda key, value: ((key not in self.kwargs) and \
                                    (key != s_star_args) and \
                                    (key != s_star_kwargs))
            )
        )
        dict_of_positional_arguments.sort(key=s_args.index)
        _arguments.update(dict_of_positional_arguments)
        
        if s_star_args:
            _arguments['*'] = getcallargs_result[s_star_args]
            
        _arguments.update(self.kwargs)
        
        self._arguments = _arguments
        '''Ordered dict of arguments, both positional- and keyword-.'''
        
        # Caching the hash, since its computation can take a long time:
        self._hash = cheat_hashing.cheat_hash(
            (
                self.function,
                self.args,
                tuple(self.kwargs)
            )
        )
Esempio n. 28
0
def signature(obj):
    '''Get a signature object for the passed callable.'''

    if not callable(obj):
        raise TypeError('{0!r} is not a callable object'.format(obj))

    if isinstance(obj, types.MethodType):
        sig = signature(obj.__func__)
        if obj.__self__ is None:
            # Unbound method: the first parameter becomes positional-only
            if sig.parameters:
                first = sig.parameters.values()[0].replace(
                    kind=_POSITIONAL_ONLY)
                return sig.replace(parameters=(first, ) +
                                   tuple(sig.parameters.values())[1:])
            else:
                return sig
        else:
            # In this case we skip the first parameter of the underlying
            # function (usually `self` or `cls`).
            return sig.replace(parameters=tuple(sig.parameters.values())[1:])

    try:
        sig = obj.__signature__
    except AttributeError:
        pass
    else:
        if sig is not None:
            return sig

    try:
        # Was this function wrapped by a decorator?
        wrapped = obj.__wrapped__
    except AttributeError:
        pass
    else:
        return signature(wrapped)

    if isinstance(obj, types.FunctionType):
        return Signature.from_function(obj)

    if isinstance(obj, functools.partial):
        sig = signature(obj.func)

        new_params = OrderedDict(sig.parameters.items())

        partial_args = obj.args or ()
        partial_keywords = obj.keywords or {}
        try:
            ba = sig.bind_partial(*partial_args, **partial_keywords)
        except TypeError as ex:
            msg = 'partial object {0!r} has incorrect arguments'.format(obj)
            raise ValueError(msg)

        for arg_name, arg_value in ba.arguments.items():
            param = new_params[arg_name]
            if arg_name in partial_keywords:
                # We set a new default value, because the following code
                # is correct:
                #
                #   >>> def foo(a): print(a)
                #   >>> print(partial(partial(foo, a=10), a=20)())
                #   20
                #   >>> print(partial(partial(foo, a=10), a=20)(a=30))
                #   30
                #
                # So, with 'partial' objects, passing a keyword argument is
                # like setting a new default value for the corresponding
                # parameter
                #
                # We also mark this parameter with '_partial_kwarg'
                # flag.  Later, in '_bind', the 'default' value of this
                # parameter will be added to 'kwargs', to simulate
                # the 'functools.partial' real call.
                new_params[arg_name] = param.replace(default=arg_value,
                                                     _partial_kwarg=True)

            elif (param.kind not in (_VAR_KEYWORD, _VAR_POSITIONAL)
                  and not param._partial_kwarg):
                new_params.pop(arg_name)

        return sig.replace(parameters=new_params.values())

    sig = None
    if isinstance(obj, type):
        # obj is a class or a metaclass

        # First, let's see if it has an overloaded __call__ defined
        # in its metaclass
        call = _get_user_defined_method(type(obj), '__call__')
        if call is not None:
            sig = signature(call)
        else:
            # Now we check if the 'obj' class has a '__new__' method
            new = _get_user_defined_method(obj, '__new__')
            if new is not None:
                sig = signature(new)
            else:
                # Finally, we should have at least __init__ implemented
                init = _get_user_defined_method(obj, '__init__')
                if init is not None:
                    sig = signature(init)
    elif not isinstance(obj, _NonUserDefinedCallables):
        # An object with __call__
        # We also check that the 'obj' is not an instance of
        # _WrapperDescriptor or _MethodWrapper to avoid
        # infinite recursion (and even potential segfault)
        call = _get_user_defined_method(type(obj), '__call__', 'im_func')
        if call is not None:
            sig = signature(call)

    if sig is not None:
        # For classes and objects we skip the first parameter of their
        # __call__, __new__, or __init__ methods
        return sig.replace(parameters=tuple(sig.parameters.values())[1:])

    if isinstance(obj, types.BuiltinFunctionType):
        # Raise a nicer error message for builtins
        msg = 'no signature found for builtin function {0!r}'.format(obj)
        raise ValueError(msg)

    raise ValueError(
        'callable {0!r} is not supported by signature'.format(obj))