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 get_address(obj, shorten=False, root=None, namespace={}): ''' Get the address of a Python object. This only works for objects that have addresses, like modules, classes, functions, methods, etc. It usually doesn't work on instances created during the program. (e.g. `[1, 2]` doesn't have an address.) ''' # todo: Support classes inside classes. Currently doesn't work because # Python doesn't tell us inside in which class an inner class was defined. # We'll probably have to do some kind of search. if not (isinstance(obj, types.ModuleType) or hasattr(obj, '__module__')): raise TypeError("`%s` is not a module, nor does it have a " "`.__module__` attribute, therefore we can't get its " "address." % (obj, )) if isinstance(obj, types.ModuleType): address = obj.__name__ elif isinstance(obj, types.MethodType): address = '.'.join( (obj.__module__, obj.im_class.__name__, obj.__name__)) else: address = '.'.join((obj.__module__, obj.__name__)) # Now our attempt at an address is in `address`. Let's `try` to resolve # that address to see if it's right and we get the same object: try: object_candidate = get_object_by_address(address) except Exception: confirmed_object_address = False else: is_same_object = \ (obj == object_candidate) if isinstance(obj, types.MethodType) \ else (obj is object_candidate) confirmed_object_address = is_same_object if not confirmed_object_address: # We weren't able to confirm that the `address` we got is the correct # one for this object, so we won't even try to shorten it in any way, # just return what we got and hoped we didn't disappoint the user too # badly: return address assert confirmed_object_address is True # We confirmed we got the right `address`! Now we can try to shorten it # some, if the user specified so in the arguments: ### Shortening the address using `root` and/or `namespace`: ############### # # if root or namespace: # Ensuring `root` and `namespace` are actual objects: if isinstance(root, basestring): root = get_object_by_address(root) if isinstance(namespace, basestring): namespace = get_object_by_address(namespace) if namespace: (_useless, original_namespace_dict) = \ _get_parent_and_dict_from_namespace(namespace) def my_filter(key, value): name = getattr(value, '__name__', '') return isinstance(name, basestring) and name.endswith(key) namespace_dict = dict_tools.filter_items(original_namespace_dict, my_filter) namespace_dict_keys = namespace_dict.keys() namespace_dict_values = namespace_dict.values() # Split to address parts: address_parts = address.split('.') # e.g., `['garlicsim', 'misc', 'step_copy', 'StepCopy']`. heads = [ '.'.join(address_parts[:i]) for i in xrange(1, len(address_parts) + 1) ] # `heads` is something like: `['garlicsim', 'garlicsim.misc', # 'garlicsim.misc.step_copy', 'garlicsim.misc.step_copy.StepCopy']` for head in reversed(heads): object_ = get_object_by_address(head) if root: if object_ is root: root_short_name = root.__name__.rsplit('.', 1)[-1] address = address.replace(head, root_short_name, 1) break if namespace: if object_ in namespace_dict_values: fitting_keys = [ key for key in namespace_dict_keys if namespace_dict[key] is object_ ] key = min(fitting_keys, key=len) address = address.replace(head, key, 1) # # ### Finshed shortening address using `root` and/or `namespace`. ########### # If user specified `shorten=True`, let the dedicated `shorten_address` # function drop redundant intermediate nodes: if shorten: address = shorten_address(address, root=root, namespace=namespace) # A little fix to avoid describing something like `list` as # `__builtin__.list`: if address.startswith('__builtin__.'): shorter_address = address.replace('__builtin__.', '', 1) if get_object_by_address(shorter_address) == obj: address = shorter_address return address
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 get_address(obj, shorten=False, root=None, namespace={}): ''' Get the address of a Python object. This only works for objects that have addresses, like modules, classes, functions, methods, etc. It usually doesn't work on instances created during the program. (e.g. `[1, 2]` doesn't have an address.) ''' # todo: Support classes inside classes. Currently doesn't work because # Python doesn't tell us inside in which class an inner class was defined. # We'll probably have to do some kind of search. if not (isinstance(obj, types.ModuleType) or hasattr(obj, '__module__')): raise TypeError("`%s` is not a module, nor does it have a " "`.__module__` attribute, therefore we can't get its " "address." % (obj,)) if isinstance(obj, types.ModuleType): address = obj.__name__ elif isinstance(obj, types.MethodType): address = '.'.join((obj.__module__, obj.__self__.__class__.__name__, obj.__name__)) else: address= '.'.join((obj.__module__, obj.__name__)) # Now our attempt at an address is in `address`. Let's `try` to resolve # that address to see if it's right and we get the same object: try: object_candidate = get_object_by_address(address) except Exception: confirmed_object_address = False else: is_same_object = \ (obj == object_candidate) if isinstance(obj, types.MethodType) \ else (obj is object_candidate) confirmed_object_address = is_same_object if not confirmed_object_address: # We weren't able to confirm that the `address` we got is the correct # one for this object, so we won't even try to shorten it in any way, # just return what we got and hoped we didn't disappoint the user too # badly: return address assert confirmed_object_address is True # We confirmed we got the right `address`! Now we can try to shorten it # some, if the user specified so in the arguments: ### Shortening the address using `root` and/or `namespace`: ############### # # if root or namespace: # Ensuring `root` and `namespace` are actual objects: if isinstance(root, str): root = get_object_by_address(root) if isinstance(namespace, str): namespace = get_object_by_address(namespace) if namespace: (_useless, original_namespace_dict) = \ _get_parent_and_dict_from_namespace(namespace) def my_filter(key, value): name = getattr(value, '__name__', '') return isinstance(name, str) and name.endswith(key) namespace_dict = dict_tools.filter_items( original_namespace_dict, my_filter ) namespace_dict_keys = list(namespace_dict.keys()) namespace_dict_values = list(namespace_dict.values()) # Split to address parts: address_parts = address.split('.') # e.g., `['garlicsim', 'misc', 'step_copy', 'StepCopy']`. heads = ['.'.join(address_parts[:i]) for i in range(1, len(address_parts) + 1)] # `heads` is something like: `['garlicsim', 'garlicsim.misc', # 'garlicsim.misc.step_copy', 'garlicsim.misc.step_copy.StepCopy']` for head in reversed(heads): object_ = get_object_by_address(head) if root: if object_ is root: root_short_name = root.__name__.rsplit('.', 1)[-1] address = address.replace(head, root_short_name, 1) break if namespace: if object_ in namespace_dict_values: fitting_keys = [key for key in namespace_dict_keys if namespace_dict[key] is object_] key = min(fitting_keys, key=len) address = address.replace(head, key, 1) # # ### Finshed shortening address using `root` and/or `namespace`. ########### # If user specified `shorten=True`, let the dedicated `shorten_address` # function drop redundant intermediate nodes: if shorten: address = shorten_address(address, root=root, namespace=namespace) # A little fix to avoid describing something like `list` as # `builtins.list`: if address.startswith('builtins.'): shorter_address = address.replace('builtins.', '', 1) if get_object_by_address(shorter_address) == obj: address = shorter_address return address