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 = set((
            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 xrange(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 = 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.itervalues())
            
            leading_candidates = [
                candidate for candidate in 
                total_price_for_n_dasp_candidate.iterkeys() 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 = 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 = \
                getcallargs_result[s_star_kwargs].keys()
            sorted_star_kwargs_names = sorted(
                unsorted_star_kwargs_names,
                cmp=cmp_tools.underscore_hating_cmp
            )
            
            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)
            )
        )
class ArgumentsProfile(object):
    '''
    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 = set((
            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 xrange(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 = 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.itervalues())
            
            leading_candidates = [
                candidate for candidate in 
                total_price_for_n_dasp_candidate.iterkeys() 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 = 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 = \
                getcallargs_result[s_star_kwargs].keys()
            sorted_star_kwargs_names = sorted(
                unsorted_star_kwargs_names,
                cmp=cmp_tools.underscore_hating_cmp
            )
            
            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 self._arguments.keys()
    
    
    def values(self):
        '''Get all the argument values.'''
        return self._arguments.values()
    
    
    def items(self):
        '''Get a tuple of all the `(argument_name, argument_value)` item.'''
        return 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 self._arguments.iterkeys()
    
    
    def itervalues(self):        
        '''Iterate on the argument value according to their order.'''
        return self._arguments.itervalues()
        
    
    def iteritems(self):
        '''Iterate on `(argument_name, argument_value)` items by order.'''
        return self._arguments.iteritems()
    
    
    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 __hash__(self):
        return self._hash
class SimpackGrokker(object, metaclass=caching.CachedType):
    '''Encapsulates a simpack and gives useful information and tools.'''

    @staticmethod
    def create_from_state(state):
        '''
        Create a simpack grokker from a state object, possibly using cached.
        '''
        simpack = simpack_tools.get_from_state(state)
        return SimpackGrokker(simpack)
    
    
    def __init__(self, simpack):
        self.simpack = simpack
        self.__init_analysis()
        self.__init_analysis_settings()
        self.__init_analysis_cruncher_types()

        
    def __init_analysis(self):
        '''Analyze the simpack.'''
        
        simpack = self.simpack
        
        try:
            State = simpack.State
        except AttributeError:
            raise InvalidSimpack("The `%s` simpack does not define a `State` "
                                 "class." % simpack.__name__.rsplit('.')[-1])
        
        if not misc_tools.is_subclass(State, garlicsim.data_structures.State):
            raise InvalidSimpack("The `%s` simpack defines a `State` class, "
                                 "but it's not a subclass of "
                                 "`garlicsim.data_structures.State`." % \
                                 simpack.__name__.rsplit('.')[-1])


        state_methods = dict(
            (name, value) for (name, value) in
            list(misc_tools.getted_vars(State).items()) if isinstance(value, collections.Callable)
        )

        self.step_functions_by_type = dict((step_type, []) for step_type in
                                           step_types.step_types_list)
        '''dict mapping from each step type to step functions of that type.'''
        
        
        for method in list(state_methods.values()):
            step_type = StepType.get_step_type(method)
            if step_type:
                self.step_functions_by_type[step_type].append(method)
            
                
        if self.step_functions_by_type[step_types.HistoryStep] or \
           self.step_functions_by_type[step_types.HistoryStepGenerator]:
            
            self.history_dependent = True

            self.all_step_functions = (
                self.step_functions_by_type[step_types.HistoryStepGenerator] +
                self.step_functions_by_type[step_types.HistoryStep]
            )
            
            if (self.step_functions_by_type[step_types.SimpleStep] or
                self.step_functions_by_type[step_types.StepGenerator] or
                self.step_functions_by_type[step_types.InplaceStep] or
                self.step_functions_by_type[step_types.InplaceStepGenerator]):
                
                raise InvalidSimpack("The `%s` simpack is defining both a "
                                     "history-dependent step and a "
                                     "non-history-dependent step - which "
                                     "is forbidden." % \
                                     simpack.__name__.rsplit('.')[-1])
        else: # No history step defined
            
            self.history_dependent = False
            
            self.all_step_functions = (
                self.step_functions_by_type[step_types.StepGenerator] + \
                self.step_functions_by_type[step_types.SimpleStep] + \
                self.step_functions_by_type[step_types.InplaceStepGenerator] +\
                self.step_functions_by_type[step_types.InplaceStep]
            )
            
            
        # (no-op assignments, just for docs:)
        
        self.history_dependent = self.history_dependent
        '''Flag saying whether the simpack looks at previous states.'''
        
        self.all_step_functions = self.all_step_functions
        '''
        All the step functions that the simpack provides, sorted by priority.
        '''
               
        if not self.all_step_functions:
            raise InvalidSimpack("The `%s` simpack has not defined any kind "
                                 "of step function." % \
                                 simpack.__name__.rsplit('.')[-1])
        
        self.default_step_function = self.all_step_functions[0]
        '''
        The default step function. Will be used if we don't specify another.
        '''
        
        
    def __init_analysis_settings(self):
        '''Analyze the simpack to produce a Settings object.'''
        # todo: consider doing this in `Settings.__init__`
        
        # We want to access the `.settings` of our simpack, but we don't know
        # if our simpack is a module or some other kind of object. So if it's a
        # module, we'll `try` to import `settings`.
        
        self.settings = Settings(self)
        
        if isinstance(self.simpack, types.ModuleType) and \
           not hasattr(self.simpack, 'settings'):
            
            # The `if` that we did here means: "If there's reason to suspect
            # that `self.simpack.settings` is a module that exists but hasn't
            # been imported yet."
            
            settings_module_name = ''.join((
                self.simpack.__name__,
                '.settings'
            ))
            
            import_tools.import_if_exists(settings_module_name,
                                          silent_fail=True)
            # This imports the `settings` submodule, if it exists, but it
            # does *not* keep a reference to it. We'll access `settings` as
            # an attribute of the simpack below.
            
        # Checking if there are original settings at all. If there aren't,
        # we're done.
        if hasattr(self.simpack, 'settings'):
            
            original_settings = getattr(self.simpack, 'settings')
            
            for setting_name in list(list(vars(self.settings).keys())):
                if hasattr(original_settings, setting_name):
                    value = getattr(original_settings, setting_name)
                    setattr(self.settings, setting_name, value)
            # todo: currently throws away unrecognized attributes from the
            # simpack's settings.
                

    def __init_analysis_cruncher_types(self):
        '''Figure out which crunchers this simpack can use.'''
        
        # todo: possibly fix `CRUNCHERS` to some canonical state in `.settings`
        from garlicsim.asynchronous_crunching import crunchers, BaseCruncher
        simpack = self.simpack
        
        self.cruncher_types_availability = OrderedDict()
        '''dict mapping from cruncher type to whether it can be used.'''
        
        self.available_cruncher_types = []
        '''The cruncher types that this simpack can use.'''
        
        CRUNCHERS = self.settings.CRUNCHERS

        
        if isinstance(CRUNCHERS, str):
            (cruncher_type,) = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if
                 cruncher_type_.__name__ == CRUNCHERS]
            self.available_cruncher_types = [cruncher_type]
            self.cruncher_types_availability[cruncher_type] = True
        
            ### Giving unavailability reasons: ################################
            #                                                                 #
            unavailable_cruncher_types = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if cruncher_type_ not in
                 self.available_cruncher_types]
            self.cruncher_types_availability.update(dict(
                (
                    unavailable_cruncher_type,
                    ReasonedBool(
                        False,
                        'The `%s` simpack specified `%s` as the only '
                        'available cruncher type.' % \
                        (simpack.__name__.rsplit('.')[-1],
                         cruncher_type.__name__)
                    )
                ) for unavailable_cruncher_type in unavailable_cruncher_types
            ))
            #                                                                 #
            ###################################################################

        
        elif misc_tools.is_subclass(CRUNCHERS, BaseCruncher):
            cruncher_type = CRUNCHERS
            self.available_cruncher_types = [cruncher_type]
            self.cruncher_types_availability[cruncher_type] = True
            
            ### Giving unavailability reasons: ################################
            #                                                                 #
            unavailable_cruncher_types = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if cruncher_type_ not in
                 self.available_cruncher_types]
            self.cruncher_types_availability.update(dict(
                (
                    unavailable_cruncher_type,
                    ReasonedBool(
                        False,
                        'The `%s` simpack specified `%s` as the only '
                        'available cruncher type.' % \
                        (simpack.__name__.rsplit('.')[-1],
                         cruncher_type.__name__)
                    )
                ) for unavailable_cruncher_type in unavailable_cruncher_types
            ))
            #                                                                 #
            ###################################################################
            
        
        elif cute_iter_tools.is_iterable(CRUNCHERS):
            self.available_cruncher_types = []
            for item in CRUNCHERS:
                if isinstance(item, str):
                    (cruncher_type,) = \
                        [cruncher_type_ for cruncher_type_ in
                         crunchers.cruncher_types_list if
                         cruncher_type_.__name__ == item]
                else:
                    assert misc_tools.is_subclass(item, BaseCruncher)
                    cruncher_type = item
                self.available_cruncher_types.append(cruncher_type)
                self.cruncher_types_availability[cruncher_type] = True

            ### Giving unavailability reasons: ################################
            #                                                                 #
            unavailable_cruncher_types = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if cruncher_type_ not in
                 self.available_cruncher_types]
            self.cruncher_types_availability.update(dict(
                (
                    unavailable_cruncher_type,
                    ReasonedBool(
                        False,
                        'The `%s` simpack specified a list of available '
                        'crunchers and `%s` is not in it.' % \
                        (simpack.__name__.rsplit('.')[-1],
                         unavailable_cruncher_type.__name__)
                    )
                    
                ) for unavailable_cruncher_type in unavailable_cruncher_types
            ))
            #                                                                 #
            ###################################################################
            
        
        elif isinstance(CRUNCHERS, collections.Callable):
            assert not isinstance(CRUNCHERS, BaseCruncher)
            self.available_cruncher_types = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if
                 CRUNCHERS(cruncher_type_)]
            for available_cruncher_type in self.available_cruncher_types:
                self.cruncher_types_availability[available_cruncher_type] = \
                    True
            
            ### Giving unavailability reasons: ################################
            #                                                                 #
            unavailable_cruncher_types = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if cruncher_type_ not in
                 self.available_cruncher_types]
            for unavailable_cruncher_type in unavailable_cruncher_types:
                reason = getattr(
                    CRUNCHERS(unavailable_cruncher_type),
                    'reason',
                    'No reason was given for `%s` not being accepted.' % \
                    unavailable_cruncher_type.__name__
                )
                self.cruncher_types_availability[
                    unavailable_cruncher_type
                    ] = ReasonedBool(False, reason)
            #                                                                 #
            ###################################################################
            
        #######################################################################
            
        else:
            raise InvalidSimpack("The `CRUNCHERS` setting must be either a "
                                 "cruncher type (or name string), a list of "
                                 "cruncher types, or a filter function for "
                                 "cruncher types. You supplied `%s`, which is "
                                 "neither." % CRUNCHERS)

        
    def step(self, state_or_history_browser, step_profile):
        '''
        Perform a step of the simulation.
        
        The step profile will specify which parameters to pass to the simpack's
        step function.
        '''
        # todo: probably inefficient, but this method is probably not used much
        # anyway.
        
        step_iterator = self.get_step_iterator(state_or_history_browser,
                                               step_profile)
        return next(step_iterator)
    
            
    def get_step_iterator(self, state_or_history_browser, step_profile):
        '''
        Get a step iterator for crunching states of the simulation.
        
        The step profile will specify which parameters to pass to the simpack's
        step function.
        '''
        
        step_function = step_profile.step_function
        step_type = StepType.get_step_type(step_function)
        
        return step_type.step_iterator_class(state_or_history_browser,
                                             step_profile)
        
    
    def get_inplace_step_iterator(self, state, step_profile):
        '''
        Get an inplace step iterator which modifies the state in place.
        
        On every iteration of the inplace step iterator, `state` will be
        changed to be the next moment in the simulation. No new state objects
        will be created.
        
        This can only be used with inplace step functions.
        '''
        
        step_function = step_profile.step_function
        step_type = StepType.get_step_type(step_function)
        
        if step_type not in (step_types.InplaceStep,
                             step_types.InplaceStepGenerator):
            raise Exception("Can't get an inplace step iterator for the step "
                            "function you're using, because it's not an "
                            "inplace step function, it's a %s." % 
                            step_type.verbose_name)
        
        return step_type.inplace_step_iterator_class(
            state,
            step_profile
        )
    
    
    def is_inplace_iterator_available(self, step_profile):
        '''
        Return whether `step_profile` allows using an inplace step iterator.
        
        Only step profiles that use an inplace step function (or generator)
        allow using inplace step iterators.
        '''
        step_function = step_profile.step_function
        step_type = StepType.get_step_type(step_function)
        
        return (step_type in (step_types.InplaceStep,
                              step_types.InplaceStepGenerator))
        
    

    def build_step_profile(self, *args, **kwargs):
        '''
        Build a step profile smartly.
        
        The canonical way to build a step profile is to provide it with a step
        function, `*args` and `**kwargs`. But in this function we're being a
        little smarter so the user will have less work.
        
        You do not need to enter a step function; we will use the default one,
        unless you specify a different one as `step_function`.
        
        You may also pass in a step profile as `step_profile`, and it will be
        noticed and used.
        '''
        parse_arguments_to_step_profile = \
            garlicsim.misc.StepProfile.build_parser(
                self.default_step_function
            )
        
        step_profile = parse_arguments_to_step_profile(*args, **kwargs)
        return step_profile
Beispiel #4
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 = set((
            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 xrange(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 = 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.itervalues())

            leading_candidates = [
                candidate
                for candidate in total_price_for_n_dasp_candidate.iterkeys()
                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 = 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 = \
                getcallargs_result[s_star_kwargs].keys()
            sorted_star_kwargs_names = sorted(
                unsorted_star_kwargs_names,
                cmp=cmp_tools.underscore_hating_cmp)

            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)))
Beispiel #5
0
class ArgumentsProfile(object):
    '''
    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 = set((
            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 xrange(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 = 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.itervalues())

            leading_candidates = [
                candidate
                for candidate in total_price_for_n_dasp_candidate.iterkeys()
                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 = 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 = \
                getcallargs_result[s_star_kwargs].keys()
            sorted_star_kwargs_names = sorted(
                unsorted_star_kwargs_names,
                cmp=cmp_tools.underscore_hating_cmp)

            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 self._arguments.keys()

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

    def items(self):
        '''Get a tuple of all the `(argument_name, argument_value)` item.'''
        return 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 self._arguments.iterkeys()

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

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

    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 __hash__(self):
        return self._hash
Beispiel #6
0
class SimpackGrokker(object):
    '''Encapsulates a simpack and gives useful information and tools.'''

    __metaclass__ = caching.CachedType

    @staticmethod
    def create_from_state(state):
        '''
        Create a simpack grokker from a state object, possibly using cached.
        '''
        simpack = simpack_tools.get_from_state(state)
        return SimpackGrokker(simpack)

    def __init__(self, simpack):
        self.simpack = simpack
        self.__init_analysis()
        self.__init_analysis_settings()
        self.__init_analysis_cruncher_types()

    def __init_analysis(self):
        '''Analyze the simpack.'''

        simpack = self.simpack

        try:
            State = simpack.State
        except AttributeError:
            raise InvalidSimpack("The `%s` simpack does not define a `State` "
                                 "class." % simpack.__name__.rsplit('.')[-1])

        if not misc_tools.is_subclass(State, garlicsim.data_structures.State):
            raise InvalidSimpack("The `%s` simpack defines a `State` class, "
                                 "but it's not a subclass of "
                                 "`garlicsim.data_structures.State`." % \
                                 simpack.__name__.rsplit('.')[-1])

        state_methods = dict(
            (name, value)
            for (name, value) in misc_tools.getted_vars(State).iteritems()
            if callable(value))

        self.step_functions_by_type = dict(
            (step_type, []) for step_type in step_types.step_types_list)
        '''dict mapping from each step type to step functions of that type.'''

        for method in state_methods.itervalues():
            step_type = StepType.get_step_type(method)
            if step_type:
                self.step_functions_by_type[step_type].append(method)


        if self.step_functions_by_type[step_types.HistoryStep] or \
           self.step_functions_by_type[step_types.HistoryStepGenerator]:

            self.history_dependent = True

            self.all_step_functions = (
                self.step_functions_by_type[step_types.HistoryStepGenerator] +
                self.step_functions_by_type[step_types.HistoryStep])

            if (self.step_functions_by_type[step_types.SimpleStep]
                    or self.step_functions_by_type[step_types.StepGenerator]
                    or self.step_functions_by_type[step_types.InplaceStep]
                    or self.step_functions_by_type[
                        step_types.InplaceStepGenerator]):

                raise InvalidSimpack("The `%s` simpack is defining both a "
                                     "history-dependent step and a "
                                     "non-history-dependent step - which "
                                     "is forbidden." % \
                                     simpack.__name__.rsplit('.')[-1])
        else:  # No history step defined

            self.history_dependent = False

            self.all_step_functions = (
                self.step_functions_by_type[step_types.StepGenerator] + \
                self.step_functions_by_type[step_types.SimpleStep] + \
                self.step_functions_by_type[step_types.InplaceStepGenerator] +\
                self.step_functions_by_type[step_types.InplaceStep]
            )

        # (no-op assignments, just for docs:)

        self.history_dependent = self.history_dependent
        '''Flag saying whether the simpack looks at previous states.'''

        self.all_step_functions = self.all_step_functions
        '''
        All the step functions that the simpack provides, sorted by priority.
        '''

        if not self.all_step_functions:
            raise InvalidSimpack("The `%s` simpack has not defined any kind "
                                 "of step function." % \
                                 simpack.__name__.rsplit('.')[-1])

        self.default_step_function = self.all_step_functions[0]
        '''
        The default step function. Will be used if we don't specify another.
        '''

    def __init_analysis_settings(self):
        '''Analyze the simpack to produce a Settings object.'''
        # todo: consider doing this in `Settings.__init__`

        # We want to access the `.settings` of our simpack, but we don't know
        # if our simpack is a module or some other kind of object. So if it's a
        # module, we'll `try` to import `settings`.

        self.settings = Settings(self)

        if isinstance(self.simpack, types.ModuleType) and \
           not hasattr(self.simpack, 'settings'):

            # The `if` that we did here means: "If there's reason to suspect
            # that `self.simpack.settings` is a module that exists but hasn't
            # been imported yet."

            settings_module_name = ''.join(
                (self.simpack.__name__, '.settings'))

            import_tools.import_if_exists(settings_module_name,
                                          silent_fail=True)
            # This imports the `settings` submodule, if it exists, but it
            # does *not* keep a reference to it. We'll access `settings` as
            # an attribute of the simpack below.

        # Checking if there are original settings at all. If there aren't,
        # we're done.
        if hasattr(self.simpack, 'settings'):

            original_settings = getattr(self.simpack, 'settings')

            for setting_name in vars(self.settings).keys():
                if hasattr(original_settings, setting_name):
                    value = getattr(original_settings, setting_name)
                    setattr(self.settings, setting_name, value)
            # todo: currently throws away unrecognized attributes from the
            # simpack's settings.

    def __init_analysis_cruncher_types(self):
        '''Figure out which crunchers this simpack can use.'''

        # todo: possibly fix `CRUNCHERS` to some canonical state in `.settings`
        from garlicsim.asynchronous_crunching import crunchers, BaseCruncher
        simpack = self.simpack

        self.cruncher_types_availability = OrderedDict()
        '''dict mapping from cruncher type to whether it can be used.'''

        self.available_cruncher_types = []
        '''The cruncher types that this simpack can use.'''

        CRUNCHERS = self.settings.CRUNCHERS

        if isinstance(CRUNCHERS, basestring):
            (cruncher_type,) = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if
                 cruncher_type_.__name__ == CRUNCHERS]
            self.available_cruncher_types = [cruncher_type]
            self.cruncher_types_availability[cruncher_type] = True

            ### Giving unavailability reasons: ################################
            #                                                                 #
            unavailable_cruncher_types = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if cruncher_type_ not in
                 self.available_cruncher_types]
            self.cruncher_types_availability.update(dict(
                (
                    unavailable_cruncher_type,
                    ReasonedBool(
                        False,
                        'The `%s` simpack specified `%s` as the only '
                        'available cruncher type.' % \
                        (simpack.__name__.rsplit('.')[-1],
                         cruncher_type.__name__)
                    )
                ) for unavailable_cruncher_type in unavailable_cruncher_types
            ))
            #                                                                 #
            ###################################################################

        elif misc_tools.is_subclass(CRUNCHERS, BaseCruncher):
            cruncher_type = CRUNCHERS
            self.available_cruncher_types = [cruncher_type]
            self.cruncher_types_availability[cruncher_type] = True

            ### Giving unavailability reasons: ################################
            #                                                                 #
            unavailable_cruncher_types = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if cruncher_type_ not in
                 self.available_cruncher_types]
            self.cruncher_types_availability.update(dict(
                (
                    unavailable_cruncher_type,
                    ReasonedBool(
                        False,
                        'The `%s` simpack specified `%s` as the only '
                        'available cruncher type.' % \
                        (simpack.__name__.rsplit('.')[-1],
                         cruncher_type.__name__)
                    )
                ) for unavailable_cruncher_type in unavailable_cruncher_types
            ))
            #                                                                 #
            ###################################################################

        elif cute_iter_tools.is_iterable(CRUNCHERS):
            self.available_cruncher_types = []
            for item in CRUNCHERS:
                if isinstance(item, basestring):
                    (cruncher_type,) = \
                        [cruncher_type_ for cruncher_type_ in
                         crunchers.cruncher_types_list if
                         cruncher_type_.__name__ == item]
                else:
                    assert misc_tools.is_subclass(item, BaseCruncher)
                    cruncher_type = item
                self.available_cruncher_types.append(cruncher_type)
                self.cruncher_types_availability[cruncher_type] = True

            ### Giving unavailability reasons: ################################
            #                                                                 #
            unavailable_cruncher_types = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if cruncher_type_ not in
                 self.available_cruncher_types]
            self.cruncher_types_availability.update(dict(
                (
                    unavailable_cruncher_type,
                    ReasonedBool(
                        False,
                        'The `%s` simpack specified a list of available '
                        'crunchers and `%s` is not in it.' % \
                        (simpack.__name__.rsplit('.')[-1],
                         unavailable_cruncher_type.__name__)
                    )

                ) for unavailable_cruncher_type in unavailable_cruncher_types
            ))
            #                                                                 #
            ###################################################################

        elif callable(CRUNCHERS):
            assert not isinstance(CRUNCHERS, BaseCruncher)
            self.available_cruncher_types = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if
                 CRUNCHERS(cruncher_type_)]
            for available_cruncher_type in self.available_cruncher_types:
                self.cruncher_types_availability[available_cruncher_type] = \
                    True

            ### Giving unavailability reasons: ################################
            #                                                                 #
            unavailable_cruncher_types = \
                [cruncher_type_ for cruncher_type_ in
                 crunchers.cruncher_types_list if cruncher_type_ not in
                 self.available_cruncher_types]
            for unavailable_cruncher_type in unavailable_cruncher_types:
                reason = getattr(
                    CRUNCHERS(unavailable_cruncher_type),
                    'reason',
                    'No reason was given for `%s` not being accepted.' % \
                    unavailable_cruncher_type.__name__
                )
                self.cruncher_types_availability[
                    unavailable_cruncher_type] = ReasonedBool(False, reason)
            #                                                                 #
            ###################################################################

        #######################################################################

        else:
            raise InvalidSimpack("The `CRUNCHERS` setting must be either a "
                                 "cruncher type (or name string), a list of "
                                 "cruncher types, or a filter function for "
                                 "cruncher types. You supplied `%s`, which is "
                                 "neither." % CRUNCHERS)

    def step(self, state_or_history_browser, step_profile):
        '''
        Perform a step of the simulation.
        
        The step profile will specify which parameters to pass to the simpack's
        step function.
        '''
        # todo: probably inefficient, but this method is probably not used much
        # anyway.

        step_iterator = self.get_step_iterator(state_or_history_browser,
                                               step_profile)
        return step_iterator.next()

    def get_step_iterator(self, state_or_history_browser, step_profile):
        '''
        Get a step iterator for crunching states of the simulation.
        
        The step profile will specify which parameters to pass to the simpack's
        step function.
        '''

        step_function = step_profile.step_function
        step_type = StepType.get_step_type(step_function)

        return step_type.step_iterator_class(state_or_history_browser,
                                             step_profile)

    def get_inplace_step_iterator(self, state, step_profile):
        '''
        Get an inplace step iterator which modifies the state in place.
        
        On every iteration of the inplace step iterator, `state` will be
        changed to be the next moment in the simulation. No new state objects
        will be created.
        
        This can only be used with inplace step functions.
        '''

        step_function = step_profile.step_function
        step_type = StepType.get_step_type(step_function)

        if step_type not in (step_types.InplaceStep,
                             step_types.InplaceStepGenerator):
            raise Exception("Can't get an inplace step iterator for the step "
                            "function you're using, because it's not an "
                            "inplace step function, it's a %s." %
                            step_type.verbose_name)

        return step_type.inplace_step_iterator_class(state, step_profile)

    def is_inplace_iterator_available(self, step_profile):
        '''
        Return whether `step_profile` allows using an inplace step iterator.
        
        Only step profiles that use an inplace step function (or generator)
        allow using inplace step iterators.
        '''
        step_function = step_profile.step_function
        step_type = StepType.get_step_type(step_function)

        return (step_type
                in (step_types.InplaceStep, step_types.InplaceStepGenerator))

    def build_step_profile(self, *args, **kwargs):
        '''
        Build a step profile smartly.
        
        The canonical way to build a step profile is to provide it with a step
        function, `*args` and `**kwargs`. But in this function we're being a
        little smarter so the user will have less work.
        
        You do not need to enter a step function; we will use the default one,
        unless you specify a different one as `step_function`.
        
        You may also pass in a step profile as `step_profile`, and it will be
        noticed and used.
        '''
        parse_arguments_to_step_profile = \
            garlicsim.misc.StepProfile.build_parser(
                self.default_step_function
            )

        step_profile = parse_arguments_to_step_profile(*args, **kwargs)
        return step_profile