def test_cheat_hash(): '''Test `cheat_hash` on various objects.''' things = [ 1, 7, 4.5, [1, 2, 3.4], (1, 2, 3.4), { 1: 2, 3: 4.5 }, {1, 2, 3.4}, [1, [1, 2], 3], [1, { frozenset((1, 2)): 'meow' }, 3], sum, None, (None, { None: None }) ] things_copy = copy.deepcopy(things) for thing, thing_copy in zip(things, things_copy): assert cheat_hash(thing) == cheat_hash(thing) == \ cheat_hash(thing_copy) == cheat_hash(thing_copy)
def __init__(self, containing_dict, function, *args, **kwargs): ''' Construct the `SleekCallArgs`. `containing_dict` is the `dict` we'll try to remove ourselves from when one of our sleekrefs dies. `function` is the function for which we calculate call args from `*args` and `**kwargs`. ''' self.containing_dict = containing_dict ''' `dict` we'll try to remove ourselves from when 1 of our sleekrefs dies. ''' args_spec = cute_inspect.getargspec(function) star_args_name, star_kwargs_name = \ args_spec.varargs, args_spec.keywords call_args = cute_inspect.getcallargs(function, *args, **kwargs) del args, kwargs self.star_args_refs = [] '''Sleekrefs to star-args.''' if star_args_name: star_args = call_args.pop(star_args_name, None) if star_args: self.star_args_refs = [SleekRef(star_arg, self.destroy) for star_arg in star_args] self.star_kwargs_refs = {} '''Sleerefs to star-kwargs.''' if star_kwargs_name: star_kwargs = call_args.pop(star_kwargs_name, {}) if star_kwargs: self.star_kwargs_refs = CuteSleekValueDict(self.destroy, star_kwargs) self.args_refs = CuteSleekValueDict(self.destroy, call_args) '''Mapping from argument name to value, sleek-style.''' # In the future the `.args`, `.star_args` and `.star_kwargs` attributes # may change, so we must record the hash now: self._hash = cheat_hashing.cheat_hash( ( self.args, self.star_args, self.star_kwargs ) )
def test_cheat_hash(): '''Test `cheat_hash` on various objects.''' things = [ 1, 7, 4.5, [1, 2, 3.4], (1, 2, 3.4), {1: 2, 3: 4.5}, {1, 2, 3.4}, [1, [1, 2], 3], [1, {frozenset((1, 2)): 'meow'}, 3], sum, None, (None, {None: None}) ] things_copy = copy.deepcopy(things) for thing, thing_copy in zip(things, things_copy): assert cheat_hash(thing) == cheat_hash(thing) == \ cheat_hash(thing_copy) == cheat_hash(thing_copy)
def __init__(self, function, *args, **kwargs): ''' Construct the arguments profile. `*args` and `**kwargs` are the arguments that go into the `function`. ''' if not callable(function): raise Exception('%s is not a callable object.' % function) self.function = function raw_args = args raw_kwargs = kwargs del args, kwargs self.args = () '''Tuple of positional arguments.''' self.kwargs = OrderedDict() '''Ordered dict of keyword arguments.''' args_spec = cute_inspect.getargspec(function) (s_args, s_star_args, s_star_kwargs, s_defaults) = args_spec # `getargspec` has a weird policy, when inspecting a function with no # defaults, to give a `defaults` of `None` instead of the more # consistent `()`. We fix that here: if s_defaults is None: s_defaults = () getcallargs_result = cute_inspect.getcallargs(function, *raw_args, **raw_kwargs) self.getcallargs_result = getcallargs_result # The number of args which have default values: n_defaultful_args = len(s_defaults) # The word "defaultful" means "something which has a default." ####################################################################### ####################################################################### # Now we'll create the arguments profile, using a 4-phases algorithm. # # # ####################################################################### # Phase 1: We specify all the args that don't have a default as # positional args: defaultless_args = s_args[:-n_defaultful_args] if n_defaultful_args \ else s_args[:] self.args += tuple( dict_tools.get_list(getcallargs_result, defaultless_args) ) ####################################################################### # Phase 2: We now have to deal with args that have a default. Some of # them, possibly none and possibly all of them, should be given # positionally. Some of them, possibly none, should be given by # keyword. And some of them, possibly none and possibly all of them, # should not be given at all. It is our job to figure out in which way # each argument should be given. # In this variable: n_defaultful_args_to_specify_positionally = None # We will put the number of defaultful arguments that should be # specified positionally. defaultful_args = s_args[-n_defaultful_args:] if n_defaultful_args \ else [] # `dict` that maps from argument name to default value: defaults = OrderedDict(zip(defaultful_args, s_defaults)) defaultful_args_differing_from_defaults = { defaultful_arg for defaultful_arg in defaultful_args if defaults[defaultful_arg] != getcallargs_result[defaultful_arg] } if s_star_args and getcallargs_result[s_star_args]: # We have some arguments that go into `*args`! This means that we # don't even need to think hard, we can already be sure that we're # going to have to specify *all* of the defaultful arguments # positionally, otherwise it will be impossible to put arguments in # `*args`. n_defaultful_args_to_specify_positionally = n_defaultful_args else: # `dict` mapping from each defaultful arg to the "price" of # specifying its value: prices_of_values = OrderedDict({ defaultful_arg: len(repr(getcallargs_result[defaultful_arg])) for defaultful_arg in defaultful_args }) # The price is simply the string length of the value's `repr`. # `dict` mapping from each defaultful arg to the "price" of # specifying it as a keyword (not including the length of the # value): prices_of_keyword_prefixes = OrderedDict({ defaultful_arg: len(defaultful_arg)+1 for defaultful_arg in defaultful_args }) # For example, if we have a defaultful arg "gravity_strength", then # specifiying it by keyword will require using the string # "gravity_strength=", which is 17 characters long, therefore the # price is 17. # Now we need to decide just how many defaultful args we are going # to specify positionally. The options are anything from `0` to # `n_defaultful_args`. We're going to go one by one, and calcluate # the price for each candidate, and put it in this dict: total_price_for_n_dasp_candidate = OrderedDict() # (The `n_dasp` here is an abbreivation of the # `n_defaultful_args_to_specify_positionally` variable defined # before.) # # After we have the price for each option, we'll select the one # with the lowest price. # One thing to do before iterating on the candidates is to find out # whether the "lonely comma discount" is in effect. # # The "lonely comma discount" is given when there's nothing but # defaultful arguments to this function, and therefore the number # of ", " strings needed here is not `candidate`, but `candidate - # 1`, unless of course candidate is zero. if not defaultless_args and \ (not s_star_args or not getcallargs_result[s_star_args]) and \ (not s_star_kwargs or not getcallargs_result[s_star_kwargs]): lonely_comma_discount_may_be_given = True else: lonely_comma_discount_may_be_given = False # Now we iterate on the candidates to find out which one has the # lowest price: for candidate in range(n_defaultful_args + 1): defaultful_args_to_specify_positionally = \ defaultful_args[:candidate] price_for_positionally_specified_defaultful_args = \ 2 * candidate + \ sum( dict_tools.get_list( prices_of_values, defaultful_args_to_specify_positionally ) ) # The `2 * candidate` addend is to account for the ", " parts # between the arguments. defaultful_args_to_specify_by_keyword = list(filter( defaultful_args_differing_from_defaults.__contains__, defaultful_args[candidate:] )) price_for_defaultful_args_specified_by_keyword = \ 2 * len(defaultful_args_to_specify_by_keyword) + \ sum( dict_tools.get_list( prices_of_keyword_prefixes, defaultful_args_to_specify_by_keyword ) ) + \ sum( dict_tools.get_list( prices_of_values, defaultful_args_to_specify_by_keyword ) ) # The `2 * len(...)` addend is to account for the ", " parts # between the arguments. # Now we need to figure out if this candidate gets the "lonely # comma discount". if lonely_comma_discount_may_be_given and \ (defaultful_args_to_specify_by_keyword or \ defaultful_args_to_specify_positionally): lonely_comma_discount = -2 else: lonely_comma_discount = 0 price = price_for_positionally_specified_defaultful_args + \ price_for_defaultful_args_specified_by_keyword + \ lonely_comma_discount total_price_for_n_dasp_candidate[candidate] = price # Finished iterating on candidates! Time to pick our winner. minimum_price = min(total_price_for_n_dasp_candidate.values()) leading_candidates = [ candidate for candidate in total_price_for_n_dasp_candidate.keys() if total_price_for_n_dasp_candidate[candidate] == minimum_price ] if len(leading_candidates) == 1: # We finished with one candidate which has the minimum price. # This is our winner. (winner,) = leading_candidates n_defaultful_args_to_specify_positionally = winner else: # We have a draw! We're gonna have to settle it by picking the # lowest candidate, because in our definition of "canonical # arguments profile", our second priority after "as few # characters as possible" is "as many keyword arguments as # possible". winner = leading_candidates[0] n_defaultful_args_to_specify_positionally = winner # We have a winner! Now we know exactly which defaultful args should # be specified positionally and which should be specified by # keyword. # First we add the positionally specified: defaultful_args_to_specify_positionally = \ defaultful_args[:n_defaultful_args_to_specify_positionally] self.args += tuple( (getcallargs_result[defaultful_arg] for defaultful_arg in defaultful_args_to_specify_positionally) ) # Now we add those specified by keyword: defaultful_args_to_specify_by_keyword = list(filter( defaultful_args_differing_from_defaults.__contains__, defaultful_args[n_defaultful_args_to_specify_positionally:] )) for defaultful_arg in defaultful_args_to_specify_by_keyword: self.kwargs[defaultful_arg] = getcallargs_result[defaultful_arg] ####################################################################### # Phase 3: Add the star args: if s_star_args and getcallargs_result[s_star_args]: assert not self.kwargs # Just making sure that no non-star args were specified by keyword, # which would make it impossible for us to put stuff in `*args`. self.args += getcallargs_result[s_star_args] ####################################################################### # Phase 4: Add the star kwargs: if s_star_kwargs and getcallargs_result[s_star_kwargs]: # We can't just add the `**kwargs` as is; we need to add them # according to canonical ordering. So we need to sort them first. unsorted_star_kwargs_names = \ list(getcallargs_result[s_star_kwargs].keys()) sorted_star_kwargs_names = sorted( unsorted_star_kwargs_names, key=comparison_tools.underscore_hating_key ) sorted_star_kwargs = OrderedDict( zip( sorted_star_kwargs_names, dict_tools.get_list( getcallargs_result[s_star_kwargs], sorted_star_kwargs_names ) ) ) self.kwargs.update(sorted_star_kwargs) # Our 4-phases algorithm is done! The argument profile is canonical. # ####################################################################### ####################################################################### ####################################################################### # Now a bit of post-processing: _arguments = OrderedDict() dict_of_positional_arguments = OrderedDict( dict_tools.filter_items( getcallargs_result, lambda key, value: ((key not in self.kwargs) and \ (key != s_star_args) and \ (key != s_star_kwargs)) ) ) dict_of_positional_arguments.sort(key=s_args.index) _arguments.update(dict_of_positional_arguments) if s_star_args: _arguments['*'] = getcallargs_result[s_star_args] _arguments.update(self.kwargs) self._arguments = _arguments '''Ordered dict of arguments, both positional- and keyword-.''' # Caching the hash, since its computation can take a long time: self._hash = cheat_hashing.cheat_hash( ( self.function, self.args, tuple(self.kwargs) ) )
def __init__(self, function, *args, **kwargs): ''' Construct the arguments profile. `*args` and `**kwargs` are the arguments that go into the `function`. ''' if not callable(function): raise Exception('%s is not a callable object.' % function) self.function = function raw_args = args raw_kwargs = kwargs del args, kwargs self.args = () '''Tuple of positional arguments.''' self.kwargs = OrderedDict() '''Ordered dict of keyword arguments.''' args_spec = cute_inspect.getargspec(function) (s_args, s_star_args, s_star_kwargs, s_defaults) = args_spec # `getargspec` has a weird policy, when inspecting a function with no # defaults, to give a `defaults` of `None` instead of the more # consistent `()`. We fix that here: if s_defaults is None: s_defaults = () getcallargs_result = cute_inspect.getcallargs(function, *raw_args, **raw_kwargs) self.getcallargs_result = getcallargs_result # The number of args which have default values: n_defaultful_args = len(s_defaults) # The word "defaultful" means "something which has a default." ####################################################################### ####################################################################### # Now we'll create the arguments profile, using a 4-phases algorithm. # # # ####################################################################### # Phase 1: We specify all the args that don't have a default as # positional args: defaultless_args = s_args[:-n_defaultful_args] if n_defaultful_args \ else s_args[:] self.args += tuple( dict_tools.get_list(getcallargs_result, defaultless_args)) ####################################################################### # Phase 2: We now have to deal with args that have a default. Some of # them, possibly none and possibly all of them, should be given # positionally. Some of them, possibly none, should be given by # keyword. And some of them, possibly none and possibly all of them, # should not be given at all. It is our job to figure out in which way # each argument should be given. # In this variable: n_defaultful_args_to_specify_positionally = None # We will put the number of defaultful arguments that should be # specified positionally. defaultful_args = s_args[-n_defaultful_args:] if n_defaultful_args \ else [] # `dict` that maps from argument name to default value: defaults = OrderedDict(zip(defaultful_args, s_defaults)) defaultful_args_differing_from_defaults = { defaultful_arg for defaultful_arg in defaultful_args if defaults[defaultful_arg] != getcallargs_result[defaultful_arg] } if s_star_args and getcallargs_result[s_star_args]: # We have some arguments that go into `*args`! This means that we # don't even need to think hard, we can already be sure that we're # going to have to specify *all* of the defaultful arguments # positionally, otherwise it will be impossible to put arguments in # `*args`. n_defaultful_args_to_specify_positionally = n_defaultful_args else: # `dict` mapping from each defaultful arg to the "price" of # specifying its value: prices_of_values = OrderedDict({ defaultful_arg: len(repr(getcallargs_result[defaultful_arg])) for defaultful_arg in defaultful_args }) # The price is simply the string length of the value's `repr`. # `dict` mapping from each defaultful arg to the "price" of # specifying it as a keyword (not including the length of the # value): prices_of_keyword_prefixes = OrderedDict({ defaultful_arg: len(defaultful_arg) + 1 for defaultful_arg in defaultful_args }) # For example, if we have a defaultful arg "gravity_strength", then # specifiying it by keyword will require using the string # "gravity_strength=", which is 17 characters long, therefore the # price is 17. # Now we need to decide just how many defaultful args we are going # to specify positionally. The options are anything from `0` to # `n_defaultful_args`. We're going to go one by one, and calcluate # the price for each candidate, and put it in this dict: total_price_for_n_dasp_candidate = OrderedDict() # (The `n_dasp` here is an abbreivation of the # `n_defaultful_args_to_specify_positionally` variable defined # before.) # # After we have the price for each option, we'll select the one # with the lowest price. # One thing to do before iterating on the candidates is to find out # whether the "lonely comma discount" is in effect. # # The "lonely comma discount" is given when there's nothing but # defaultful arguments to this function, and therefore the number # of ", " strings needed here is not `candidate`, but `candidate - # 1`, unless of course candidate is zero. if not defaultless_args and \ (not s_star_args or not getcallargs_result[s_star_args]) and \ (not s_star_kwargs or not getcallargs_result[s_star_kwargs]): lonely_comma_discount_may_be_given = True else: lonely_comma_discount_may_be_given = False # Now we iterate on the candidates to find out which one has the # lowest price: for candidate in range(n_defaultful_args + 1): defaultful_args_to_specify_positionally = \ defaultful_args[:candidate] price_for_positionally_specified_defaultful_args = \ 2 * candidate + \ sum( dict_tools.get_list( prices_of_values, defaultful_args_to_specify_positionally ) ) # The `2 * candidate` addend is to account for the ", " parts # between the arguments. defaultful_args_to_specify_by_keyword = list( filter( defaultful_args_differing_from_defaults.__contains__, defaultful_args[candidate:])) price_for_defaultful_args_specified_by_keyword = \ 2 * len(defaultful_args_to_specify_by_keyword) + \ sum( dict_tools.get_list( prices_of_keyword_prefixes, defaultful_args_to_specify_by_keyword ) ) + \ sum( dict_tools.get_list( prices_of_values, defaultful_args_to_specify_by_keyword ) ) # The `2 * len(...)` addend is to account for the ", " parts # between the arguments. # Now we need to figure out if this candidate gets the "lonely # comma discount". if lonely_comma_discount_may_be_given and \ (defaultful_args_to_specify_by_keyword or \ defaultful_args_to_specify_positionally): lonely_comma_discount = -2 else: lonely_comma_discount = 0 price = price_for_positionally_specified_defaultful_args + \ price_for_defaultful_args_specified_by_keyword + \ lonely_comma_discount total_price_for_n_dasp_candidate[candidate] = price # Finished iterating on candidates! Time to pick our winner. minimum_price = min(total_price_for_n_dasp_candidate.values()) leading_candidates = [ candidate for candidate in total_price_for_n_dasp_candidate.keys() if total_price_for_n_dasp_candidate[candidate] == minimum_price ] if len(leading_candidates) == 1: # We finished with one candidate which has the minimum price. # This is our winner. (winner, ) = leading_candidates n_defaultful_args_to_specify_positionally = winner else: # We have a draw! We're gonna have to settle it by picking the # lowest candidate, because in our definition of "canonical # arguments profile", our second priority after "as few # characters as possible" is "as many keyword arguments as # possible". winner = leading_candidates[0] n_defaultful_args_to_specify_positionally = winner # We have a winner! Now we know exactly which defaultful args should # be specified positionally and which should be specified by # keyword. # First we add the positionally specified: defaultful_args_to_specify_positionally = \ defaultful_args[:n_defaultful_args_to_specify_positionally] self.args += tuple( (getcallargs_result[defaultful_arg] for defaultful_arg in defaultful_args_to_specify_positionally)) # Now we add those specified by keyword: defaultful_args_to_specify_by_keyword = list( filter( defaultful_args_differing_from_defaults.__contains__, defaultful_args[n_defaultful_args_to_specify_positionally:])) for defaultful_arg in defaultful_args_to_specify_by_keyword: self.kwargs[defaultful_arg] = getcallargs_result[defaultful_arg] ####################################################################### # Phase 3: Add the star args: if s_star_args and getcallargs_result[s_star_args]: assert not self.kwargs # Just making sure that no non-star args were specified by keyword, # which would make it impossible for us to put stuff in `*args`. self.args += getcallargs_result[s_star_args] ####################################################################### # Phase 4: Add the star kwargs: if s_star_kwargs and getcallargs_result[s_star_kwargs]: # We can't just add the `**kwargs` as is; we need to add them # according to canonical ordering. So we need to sort them first. unsorted_star_kwargs_names = \ list(getcallargs_result[s_star_kwargs].keys()) sorted_star_kwargs_names = sorted( unsorted_star_kwargs_names, key=comparison_tools.underscore_hating_key) sorted_star_kwargs = OrderedDict( zip( sorted_star_kwargs_names, dict_tools.get_list(getcallargs_result[s_star_kwargs], sorted_star_kwargs_names))) self.kwargs.update(sorted_star_kwargs) # Our 4-phases algorithm is done! The argument profile is canonical. # ####################################################################### ####################################################################### ####################################################################### # Now a bit of post-processing: _arguments = OrderedDict() dict_of_positional_arguments = OrderedDict( dict_tools.filter_items( getcallargs_result, lambda key, value: ((key not in self.kwargs) and \ (key != s_star_args) and \ (key != s_star_kwargs)) ) ) dict_of_positional_arguments.sort(key=s_args.index) _arguments.update(dict_of_positional_arguments) if s_star_args: _arguments['*'] = getcallargs_result[s_star_args] _arguments.update(self.kwargs) self._arguments = _arguments '''Ordered dict of arguments, both positional- and keyword-.''' # Caching the hash, since its computation can take a long time: self._hash = cheat_hashing.cheat_hash( (self.function, self.args, tuple(self.kwargs)))