def test_unhashable_star_empty(): '''Test `ArgumentsProfile` hashing and handling of `*()`.''' def func(a, b, c=3, d=4, **kwargs): pass assert sys.version_info[0] == 2 assert sys.version_info[1] >= 5 if sys.version_info[1] == 5: raise nose.SkipTest("Python 2.5 can't compile this test.") a2 = ArgumentsProfile(func, 7, ({'a': 'b'},), set([1, (3, 4)]), meow=[1, 2, {1: [1, 2]}]) assert a2.args == (7, ({'a': 'b'},), set([1, (3, 4)])) assert a2.kwargs == OrderedDict( (('meow', [1, 2, {1: [1, 2]}]),) ) # Python 2.5 can't compile the following, so we're compiling it dynamically # so as to not prevent Python 2.5 from being able to compile this module: exec("a3 = ArgumentsProfile(func, *(), b=({'a': 'b'},)," "c=set([1, (3, 4)]), a=7," "meow=[1, 2, {1: [1, 2]}])") assert a3.args == (7, ({'a': 'b'},), set([1, (3, 4)])) assert a3.kwargs == OrderedDict( (('meow', [1, 2, {1: [1, 2]}]),) ) assert hash(a2) == hash(a3)
def _recalculate(self): gui_project = self.frame.gui_project if not gui_project: return step_profiles = gui_project.step_profiles items = self._get_step_profile_items() def find_item_of_step_profile(step_profile): '''Find the menu item corresponding to `step_profile`.''' matching_items = \ [item for item in items if self.item_ids_to_step_profiles[item.Id] == step_profile] assert len(matching_items) in [0, 1] if matching_items: (matching_item, ) = matching_items return matching_item else: return None step_profiles_to_items = OrderedDict( ((step_profile, find_item_of_step_profile(step_profile)) for step_profile in step_profiles)) needed_items = filter(None, step_profiles_to_items.values()) unneeded_items = [item for item in items if (item not in needed_items)] for unneeded_item in unneeded_items: self.frame.Unbind(wx.EVT_MENU, unneeded_item) for item in items: self.RemoveItem(item) itemless_step_profiles = [ step_profile for step_profile in step_profiles if (step_profiles_to_items[step_profile] is None) ] for step_profile in itemless_step_profiles: step_profile_text = step_profile.__repr__( short_form=True, root=gui_project.simpack, namespace=gui_project.namespace) new_item = wx.MenuItem( self, -1, step_profile_text, 'Fork by crunching using %s' % step_profile_text) self.item_ids_to_step_profiles[new_item.Id] = step_profile step_profiles_to_items[step_profile] = new_item self.frame.Bind( wx.EVT_MENU, lambda event: gui_project.fork_by_crunching( step_profile=step_profile), new_item) for i, item in enumerate(step_profiles_to_items.itervalues()): self.InsertItem(i, item) updated_items = self._get_step_profile_items() for item, step_profile in izip(updated_items, step_profiles): assert self.item_ids_to_step_profiles[item.Id] == step_profile
def get_default_args_dict(function): ''' Get ordered dict from arguments which have a default to their default. Example: >>> def f(a, b, c=1, d='meow'): pass >>> get_default_args_dict(f) OrderedDict([('c', 1), ('d', 'meow')]) ''' arg_spec = cute_inspect.getargspec(function) (s_args, s_star_args, s_star_kwargs, s_defaults) = arg_spec # `getargspec` has a weird policy, when inspecting a function with no # defaults, to give a `defaults` of `None` instead of the more consistent # `()`. We fix that here: if s_defaults is None: s_defaults = () # The number of args which have default values: n_defaultful_args = len(s_defaults) defaultful_args = s_args[-n_defaultful_args:] if n_defaultful_args \ else [] return OrderedDict(zip(defaultful_args, s_defaults))
def test_generator(): '''Test `get_default_args_dict` on a generator function.''' def f(a, meow='frr', d={}): yield None assert get_default_args_dict(f) == \ OrderedDict((('meow', 'frr'), ('d', {})))
def test(): '''Test the basic workings of `get_default_args_dict`.''' def f(a, b, c=3, d=4): pass assert get_default_args_dict(f) == \ OrderedDict((('c', 3), ('d', 4)))
def test_empty(): '''Test `get_default_args_dict` on a function with no defaultful args.''' def f(a, b, c, *args, **kwargs): pass assert get_default_args_dict(f) == \ OrderedDict()
def test_many_defaultfuls_some_long(): ''' Test `ArgumentsProfile` with many defaultful arguments, some of them long. ''' def func(a, b, c=3, dragon=4, e=5, f=6, glide=7, human=8): pass a1 = ArgumentsProfile(func, 1, 2, glide='boom') assert a1.args == (1, 2) assert a1.kwargs == OrderedDict((('glide', 'boom'),)) a2 = ArgumentsProfile(func, 1, 2, 3, 4, 5, 6, 'boom') a3 = ArgumentsProfile(func, 1, 2, 3, glide='boom') assert a1 == a2 == a3 a4 = ArgumentsProfile(func, 1, 2, glide='boom', human='pow') a5 = ArgumentsProfile(func, 1, 2, 3, 4, 5, 6, 'boom', 'pow') # edge case, second priority assert a4.args == (1, 2) assert a4.kwargs == OrderedDict((('glide', 'boom'), ('human', 'pow'))) assert a4 == a5
def test_only_defaultless(): ''' Test `ArgumentsProfile` on a function with defaultless arguments only. ''' def func(a, b, c): pass a1 = ArgumentsProfile(func, 1, 2, 3) assert a1.args == (1, 2, 3) assert not a1.kwargs a2 = ArgumentsProfile(func, 1, c=3, b=2) a3 = ArgumentsProfile(func, c=3, a=1, b=2) a4 = ArgumentsProfile(func, 1, **{'c': 3, 'b': 2}) a5 = ArgumentsProfile(func, **OrderedDict((('c', 3), ('b', 2), ('a', 1)))) assert a1 == a2 == a3 == a4 == a5 for arg_prof in [a1, a2, a3, a4, a5]: ### Testing `.iteritems`: ############################################# # # assert dict(arg_prof) == {'a': 1, 'b': 2, 'c': 3} assert OrderedDict(arg_prof) == \ OrderedDict((('a', 1), ('b', 2), ('c', 3))) # # ### Finished testing `.iteritems`. #################################### ### Testing `.__getitem__`: ########################################### # # assert (arg_prof['a'], arg_prof['b'], arg_prof['c']) == (1, 2, 3) with cute_testing.RaiseAssertor(KeyError): arg_prof['non_existing_key'] # # ### Finished testing `.__getitem__`. ################################## ### Testing `.get`: ################################################### # # assert arg_prof.get('a') == arg_prof.get('a', 'asdfasdf') == 1 assert arg_prof.get('non_existing_key', 7) == 7 assert arg_prof.get('non_existing_key') is None
def decorator(function): # In case we're being given a function that is already cached: if getattr(function, 'is_cached', False): return function if max_size == infinity: cache_dict = {} def cached(function, *args, **kwargs): sleek_call_args = \ SleekCallArgs(cache_dict, function, *args, **kwargs) try: return cached._cache[sleek_call_args] except KeyError: cached._cache[sleek_call_args] = value = \ function(*args, **kwargs) return value else: # max_size < infinity cache_dict = OrderedDict() def cached(function, *args, **kwargs): sleek_call_args = \ SleekCallArgs(cache_dict, function, *args, **kwargs) try: result = cached._cache[sleek_call_args] cached._cache.move_to_end(sleek_call_args) return result except KeyError: cached._cache[sleek_call_args] = value = \ function(*args, **kwargs) if len(cached._cache) > max_size: cached._cache.popitem(last=False) return value cached._cache = cache_dict result = decorator_tools.decorator(cached, function) def cache_clear(): '''Clear the cache, deleting all saved results.''' cached._cache.clear() result.cache_clear = cache_clear result.is_cached = True return result
def test_unhashable(): '''Test hashing of `ArgumentsProfile` that has unhashable arguments.''' def func(a, b, c=3, d=4, **kwargs): pass a1 = ArgumentsProfile(func, 7, {1: 2}) assert a1.args == (7, {1: 2}) assert not a1.kwargs hash(a1) a2 = ArgumentsProfile(func, 7, ({'a': 'b'},), set([1, (3, 4)]), meow=[1, 2, {1: [1, 2]}]) assert a2.args == (7, ({'a': 'b'},), set([1, (3, 4)])) assert a2.kwargs == OrderedDict( (('meow', [1, 2, {1: [1, 2]}]),) ) d = {a1: 1, a2: 2} assert d[a1] == 1 assert d[a2] == 2
def test_many_defaultfuls_some_long_2(): ''' Test `ArgumentsProfile` with many defaultful arguments, some of them long. ''' def func(a, b, c=3, dragon=4, e=5, f=6, glide=7, human=8, iris=9): pass a1 = ArgumentsProfile(func, 1, 2, glide='boom') assert a1.args == (1, 2) assert a1.kwargs == OrderedDict((('glide', 'boom'),)) a2 = ArgumentsProfile(func, 1, 2, 3, 4, 5, 6, 'boom') a3 = ArgumentsProfile(func, 1, 2, 3, glide='boom') assert a1 == a2 == a3 a4 = ArgumentsProfile(func, 1, 2, glide='boom', human='pow', iris='badabang') a5 = ArgumentsProfile(func, 1, 2, 3, 4, 5, 6, 'boom', 'pow', 'badabang') assert a4 == a5 assert a4.args == (1, 2, 3, 4, 5, 6, 'boom', 'pow', 'badabang') assert not a4.kwargs
def test_simplest_defaultful(): ''' Test `ArgumentsProfile` on a function with defaultful arguments. ''' def func(a, b, c='three', d='four'): pass a1 = ArgumentsProfile(func, 'one', 'two') assert a1.args == ('one', 'two') assert not a1.kwargs a2 = ArgumentsProfile(func, 'one', 'two', 'three') a3 = ArgumentsProfile(func, 'one', 'two', 'three', 'four') assert a1 == a2 == a3 a4 = ArgumentsProfile(func, 'one', 'two', 'dynamite') assert a1 != a4 assert a4.args == ('one', 'two', 'dynamite') assert not a4.kwargs a5 = ArgumentsProfile(func, 'one', 'two', c='dynamite') a6 = ArgumentsProfile(func, 'one', 'two', 'dynamite', 'four') a7 = ArgumentsProfile(func, 'one', 'two', c='dynamite', d='four') a8 = ArgumentsProfile(func, 'one', 'two', 'dynamite', d='four') a9 = ArgumentsProfile(func, a='one', b='two', c='dynamite', d='four') a10 = ArgumentsProfile(func, d='four', c='dynamite', b='two', a='one') a11 = ArgumentsProfile(func, 'one', c='dynamite', d='four', b='two') assert a4 == a5 == a6 == a7 == a8 == a9 == a10 == a11 a12 = ArgumentsProfile(func, 'one', 'two', d='bang') assert a12.args == ('one', 'two') assert a12.kwargs == OrderedDict((('d', 'bang'),)) a13 = ArgumentsProfile(func, 'one', 'two', 'three', d='bang') a14 = ArgumentsProfile(func, 'one', 'two', c='three', d='bang') a15 = ArgumentsProfile(func, 'one', 'two', 'three', 'bang') a16 = ArgumentsProfile(func, a='one', b='two', c='three', d='bang') a17 = ArgumentsProfile(func, b='two', c='three', d='bang', a='one') assert a13 == a14 == a15 == a16 == a17
def test_defaultful_long_first(): ''' Test `ArgumentsProfile` on function with long first defaultful argument. ''' def func(a, b, creativity=3, d=4): pass a1 = ArgumentsProfile(func, 1, 2) assert a1.args == (1, 2) assert not a1.kwargs a2 = ArgumentsProfile(func, 1, 2, 3, 4) a3 = ArgumentsProfile(func, a=1, b=2, creativity=3, d=4) a4 = ArgumentsProfile(func, creativity=3, d=4, a=1, b=2) a5 = ArgumentsProfile(func, 1, 2, creativity=3, d=4) assert a1 == a2 == a3 == a4 == a5 a6 = ArgumentsProfile(func, 1, 2, d='booyeah') assert a6.args == (1, 2) assert a6.kwargs == OrderedDict((('d', 'booyeah'),)) a7 = ArgumentsProfile(func, 1, 2, 3, 'booyeah') a8 = ArgumentsProfile(func, 1, 2, creativity=3, d='booyeah') assert a6 == a7 == a8
def test_many_defaultfuls_and_star_args(): '''Test `ArgumentsProfile` with many defaultful arguments and `*args`.''' def func(a, b, c='three', d='four', e='five', f='six', *args): pass a1 = ArgumentsProfile(func, 'one', 'two', f='roar') assert a1.args == ('one', 'two') assert a1.kwargs == OrderedDict((('f', 'roar'),)) a2 = ArgumentsProfile(func, 'one', 'two', 'three', 'four', 'five', 'roar') assert a1 == a2 # Specifying `*args`, so can't specify pre-`*args` arguments by keyword: a3 = ArgumentsProfile(func, 'one', 'two', 'three', 'four', 'five', 'roar', 'meow_frr') assert a3.args == ('one', 'two', 'three', 'four', 'five', 'roar', 'meow_frr') assert not a3.kwargs a4 = ArgumentsProfile(func, 'one', 'two', 'three', 'four', 'five', 'six', 3, 1, 4, 1, 5, 9, 2) assert a4.args == ('one', 'two', 'three', 'four', 'five', 'six', 3, 1, 4, 1, 5, 9, 2) assert not a4.kwargs assert a4['*'] == (3, 1, 4, 1, 5, 9, 2)
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 test_defaultfuls_and_star_kwargs(): '''Test `ArgumentsProfile` with defaultful arguments and `**kwargs`.''' def func(a, b, c=3, d=4, **kwargs): pass a1 = ArgumentsProfile(func, 1, 2) assert a1.args == (1, 2) assert not a1.kwargs # Alphabetic ordering among the `**kwargs`, but `d` is first because it's a # non-star: a2 = ArgumentsProfile(func, 1, 2, d='bombastic', zany=True, blue=True) assert a2.args == (1, 2) assert a2.kwargs == OrderedDict( (('d', 'bombastic'), ('blue', True), ('zany', True)) ) a3 = ArgumentsProfile(func, 1, b=2, blue=True, d='bombastic', zany=True) a4 = ArgumentsProfile(func, zany=True, a=1, b=2, blue=True, d='bombastic') a5 = ArgumentsProfile(func, 1, 2, 3, 'bombastic', zany=True, blue=True) assert a2 == a3 == a4 == a5 for arg_prof in [a2, a3, a4, a5]: # Testing `.iteritems`: assert OrderedDict(arg_prof) == OrderedDict( (('a', 1), ('b', 2), ('c', 3), ('d', 'bombastic'), ('blue', True), ('zany', True)) ) ### Testing `.__getitem__`: ########################################### # # assert (arg_prof['a'], arg_prof['b'], arg_prof['c'], arg_prof['d'], arg_prof['blue'], arg_prof['zany']) == \ (1, 2, 3, 'bombastic', True, True) with cute_testing.RaiseAssertor(KeyError): arg_prof['non_existing_key'] # # ### Finished testing `.__getitem__`. ################################## ### Testing `.get`: ################################################### # # assert arg_prof.get('d') == arg_prof.get('d', 7) == 'bombastic' assert arg_prof.get('non_existing_key', 7) == 7 assert arg_prof.get('non_existing_key') is None # # ### Finished testing `.get`. ########################################## ### Testing `.iterkeys`, `.keys` and `__iter__`: ###################### # # assert list(arg_prof.iterkeys()) == list(arg_prof.keys()) == \ list(arg_prof) == ['a', 'b', 'c', 'd', 'blue', 'zany'] # # ### Finished testing `.iterkeys`, `.keys` and `__iter__`. ############# ### Testing `.itervalues` and `.values`: ############################## # # assert list(arg_prof.itervalues()) == list(arg_prof.values()) == \ [1, 2, 3, 'bombastic', True, True] # # ### Finished testing `.itervalues` and `.values`. ##################### ### Testing `.__contains__`: ########################################## # # for key in arg_prof: assert key in arg_prof assert 'agaofgnafgadf' not in arg_prof assert '**' not in arg_prof
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
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
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 __init__(self, cruncher_controls): CuteDialog.__init__( self, cruncher_controls.GetTopLevelParent(), title='Choose a cruncher type', size=(700, 300) ) self.frame = cruncher_controls.frame self.gui_project = cruncher_controls.gui_project self.selected_cruncher_type = None self.main_v_sizer = wx.BoxSizer(wx.VERTICAL) self.general_text = wx.StaticText( self, label=("Choose a cruncher type to be used when crunching the " "simulation. Your simulation will use the same algorithm " "regardless of which cruncher you'll choose; the choice of " "cruncher will affect how and where that algorithm will be " "run.") ) #self.general_text.SetSize((self.ClientSize[0] - 20, -1)) self.general_text.Wrap(self.ClientSize[0] - 20) self.general_text.Wrap(self.general_text.Size[0]) self.main_v_sizer.Add(self.general_text, 0, wx.EXPAND | wx.ALL, border=10) self.h_sizer = wx.BoxSizer(wx.HORIZONTAL) self.main_v_sizer.Add(self.h_sizer, 0, wx.EXPAND) self.cruncher_types_availability = cruncher_types_availability = \ self.gui_project.project.simpack_grokker.\ cruncher_types_availability self.cruncher_titles = cruncher_titles = OrderedDict() for cruncher_type, availability in cruncher_types_availability.items(): if availability == True: title = cruncher_type.__name__ else: assert availability == False title = '%s (not available)' % cruncher_type.__name__ cruncher_titles[title] = cruncher_type self.cruncher_list_box = wx.ListBox( self, choices=cruncher_titles.keys() ) self.cruncher_list_box.SetMinSize((250, 100)) self.cruncher_list_box.Select( cruncher_titles.values().index( self.gui_project.project.crunching_manager.cruncher_type ) ) self.h_sizer.Add(self.cruncher_list_box, 2*0, wx.EXPAND | wx.ALL, border=10) self.cruncher_text_scrolled_panel = CruncherTextScrolledPanel(self) self.h_sizer.Add(self.cruncher_text_scrolled_panel, 3*0, wx.EXPAND | wx.ALL, border=10) self.dialog_button_sizer = wx.StdDialogButtonSizer() self.main_v_sizer.Add(self.dialog_button_sizer, 0, wx.ALIGN_CENTER | wx.ALL, border=10) self.ok_button = wx.Button(self, wx.ID_OK, 'Switch cruncher type') self.dialog_button_sizer.AddButton(self.ok_button) self.ok_button.SetDefault() self.dialog_button_sizer.SetAffirmativeButton(self.ok_button) self.Bind(wx.EVT_BUTTON, self.on_ok, source=self.ok_button) self.cancel_button = wx.Button(self, wx.ID_CANCEL, 'Cancel') self.dialog_button_sizer.AddButton(self.cancel_button) self.Bind(wx.EVT_BUTTON, self.on_cancel, source=self.cancel_button) self.dialog_button_sizer.Realize() self.Bind(wx.EVT_LISTBOX, self.on_list_box_change, self.cruncher_list_box) self.Bind(wx.EVT_LISTBOX_DCLICK, self.on_list_box_double_click, self.cruncher_list_box) self.SetSizer(self.main_v_sizer) self.Layout() self.general_text.Wrap(self.general_text.Size[0]) self.main_v_sizer.Fit(self) self.update()
def _asdict(self): '''Return a new OrderedDict which maps field names to their values.''' return OrderedDict(zip(self._fields, self))
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)
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
def test_many_defaultfuls_and_star_args_and_star_kwargs(): ''' Test `ArgumentsProfile` with defaultful arguments, `*args` and `**kwargs`. ''' def func(a, b, c='three', d='four', e='five', f='six', *args, **kwargs): pass func(None, None) a1 = ArgumentsProfile(func, 'one', 'two', f='boomboomboom', __awesome=True, big=True) assert a1.args == ('one', 'two') assert a1.kwargs == OrderedDict( (('f', 'boomboomboom'), ('big', True), ('__awesome', True)) ) a2 = ArgumentsProfile(func, 'one', 'two', 'three', 'four', 'five', 'bombastic', 'meow_frr', __funky=None, zany=True, _wet=False, blue=True) assert a2.args == ('one', 'two', 'three', 'four', 'five', 'bombastic', 'meow_frr') assert a2.kwargs == OrderedDict( (('blue', True), ('zany', True), ('_wet', False), ('__funky', None)) ) a3 = ArgumentsProfile(func, 'one', 'two', 'three', 'four', 'five', 'bombastic', 'meow_frr', zany=True, __funky=None, blue=True, _wet=False, **OrderedDict()) assert a2 == a3 for arg_prof in [a2, a3]: # Testing `.iteritems`: assert OrderedDict(arg_prof) == OrderedDict( (('a', 'one'), ('b', 'two'), ('c', 'three'), ('d', 'four'), ('e', 'five'), ('f', 'bombastic'), ('*', ('meow_frr',)), ('blue', True), ('zany', True), ('_wet', False), ('__funky', None)) ) ### Testing `.__getitem__`: ########################################### # # assert (arg_prof['a'], arg_prof['b'], arg_prof['c'], arg_prof['d'], arg_prof['e'], arg_prof['f'], arg_prof['*'], arg_prof['blue'], arg_prof['zany'], arg_prof['_wet'], arg_prof['__funky']) == \ ('one', 'two', 'three', 'four', 'five', 'bombastic', ('meow_frr',), True, True, False, None) with cute_testing.RaiseAssertor(KeyError): arg_prof['non_existing_key'] # # ### Finished testing `.__getitem__`. ################################## ### Testing `.get`: ################################################### # # assert arg_prof.get('d') == arg_prof.get('d', 7) == 'four' assert arg_prof.get('non_existing_key', 7) == 7 assert arg_prof.get('non_existing_key') is None # # ### Finished testing `.get`. ########################################## ### Testing `.iterkeys`, `.keys` and `__iter__`: ###################### # # assert list(arg_prof.iterkeys()) == list(arg_prof.keys()) == \ list(arg_prof) == \ ['a', 'b', 'c', 'd', 'e', 'f', '*', 'blue', 'zany', '_wet', '__funky'] # # ### Finished testing `.iterkeys`, `.keys` and `__iter__`. ############# ### Testing `.itervalues` and `.values`: ############################## # # assert list(arg_prof.itervalues()) == list(arg_prof.values()) == \ ['one', 'two', 'three', 'four', 'five', 'bombastic', ('meow_frr',), True, True, False, None] # # ### Finished testing `.itervalues` and `.values`. ##################### ### Testing `.iteritems` and `.items`: ################################ # # items_1 = list(arg_prof.iteritems()) items_2 = arg_prof.items() assert items_1 == items_2 == zip(arg_prof.keys(), arg_prof.values()) # # ### Finished testing `.iteritems` and `.items`. ####################### ### Testing `.__contains__`: ########################################## # # for key in arg_prof: assert key in arg_prof
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 _recalculate(self): gui_project = self.frame.gui_project if not gui_project: return step_profiles = gui_project.step_profiles items = self._get_step_profile_items() def find_item_of_step_profile(step_profile): '''Find the menu item corresponding to `step_profile`.''' matching_items = \ [item for item in items if self.item_ids_to_step_profiles[item.Id] == step_profile] assert len(matching_items) in [0, 1] if matching_items: (matching_item,) = matching_items return matching_item else: return None step_profiles_to_items = OrderedDict( ((step_profile, find_item_of_step_profile(step_profile)) for step_profile in step_profiles) ) needed_items = filter(None, step_profiles_to_items.values()) unneeded_items = [item for item in items if (item not in needed_items)] for unneeded_item in unneeded_items: self.frame.Unbind(wx.EVT_MENU, unneeded_item) for item in items: self.RemoveItem(item) itemless_step_profiles = [ step_profile for step_profile in step_profiles if (step_profiles_to_items[step_profile] is None) ] for step_profile in itemless_step_profiles: step_profile_text = step_profile.__repr__( short_form=True, root=gui_project.simpack, namespace=gui_project.namespace ) new_item = wx.MenuItem( self, -1, step_profile_text, 'Fork by crunching using %s' % step_profile_text ) self.item_ids_to_step_profiles[new_item.Id] = step_profile step_profiles_to_items[step_profile] = new_item self.frame.Bind( wx.EVT_MENU, lambda event: gui_project.fork_by_crunching(step_profile=step_profile), new_item ) for i, item in enumerate(step_profiles_to_items.itervalues()): self.InsertItem(i, item) updated_items = self._get_step_profile_items() for item, step_profile in izip(updated_items, step_profiles): assert self.item_ids_to_step_profiles[item.Id] == step_profile