Source code for xmen.functional

"""A collection of function and variables used to define experiments using the functional api"""

from xmen import Experiment


[docs]class Root(Experiment): """The first argument passed to a functional experiment and principally used to root an experiment instance to a particular directory:: def functional_experiment(root: Root, ...): with open(root.directory, 'w') as f: f.write('Running experiment) root.message({'time': time.time()}) Note: Root is nothing more than Experiment with a different name. Whilst principally offering exactly the same functionality, primarily the purpose of Root is to expose the directory property and messaging protocol of the Experiment class to functional experiment definitions. However, there is nothing stopping the user form using the full functionality of the Experiment class if they wish. Please consult the Experiment class documentation in this case. """
[docs]def read_comments(fn): """A helper function for reading comments from the function definition. This should not be generally needed as xmen takes care of this for you. Args: fn: A functional experiment definition conforming to the xmen api Returns: docs (str): A help string generated for each function argument. """ import inspect signature = inspect.signature(fn) src = '('.join(inspect.getsource(fn).split('(')[1:]) lines = [] params = {} for i, k in enumerate(signature.parameters): p = signature.parameters[k] if i > 0: ty = p.annotation if ty == inspect.Parameter.empty: ty = None default = p.default if default == inspect.Parameter.empty: default = None if ty is not None: if not isinstance(ty, str): string = getattr(ty, '__name__', None) if string is not None: string = str(string).replace('.typing', '') ty = string # find first comment comments = [] for ii, l in enumerate(src.splitlines()): l = l.split('#')[0] l = l.replace(' ', '') if ':' in l: l = l.split(':')[0] elif '=' in l: l = l.split('=')[0] if l == p.name: comments = src.splitlines()[ii:] break # comments = p.name.join(src.split(p.name)[1:]).split('\n') help = None for k, c in enumerate(comments): c = c.split('#') if k == 0 and len(c) == 2: help = c[-1].strip() elif k > 0 and c[0].strip() == '' and len(c) == 2: help += ' ' + c[1].strip() else: break # Generate attribute lines help_string = f'{p.name}' if ty is not None: help_string += f': {ty}' if default is not None: help_string += f'={default}' if help is not None: help_string += f' ~ {help.strip()}' # wrap text import textwrap help_string_wrapped = textwrap.wrap(help_string, break_long_words=False) for i in range(len(help_string_wrapped)): help_string_wrapped[i] = textwrap.indent(help_string_wrapped[i], ' ' * (4 + (i > 0) * 2)) lines += ['\n'.join(help_string_wrapped)] params[p.name] = (default, ty, help.strip() if help is not None else None, help_string, fn.__name__) return '\n'.join(lines), params
[docs]def functional_experiment(fn): """Convert a functional experiment to a class definition. Generally this should not be needed as xmen takes care of this for you. Specifically: - The parameters of the experiment are added from the argument of the function - Comments next to each argument will be automatically added to the doc string of the experiment - The experiments run method will be set to ``fn`` Args: fn (xmen.Root): An experiment definition conforming to the xmen functioanl api (the function must take as its first argument an object inheriting from experiment) Returns: Exp (class): A class equivalent definition of `fn` """ import inspect # Generate new class instance with name of the function inheriting from Experiment cls = type(fn.__name__, (Experiment,), {}) # Add parameters and get helps from the function definition signature = inspect.signature(fn) docs, params = read_comments(fn) for i, k in enumerate(signature.parameters): p = signature.parameters[k] if i > 0: default = p.default if default == inspect.Parameter.empty: default = None # Add attribute to class setattr(cls, p.name, default) # add parameters to _params cls._params = {**cls._params, **params} # Add parameters to __doc__ of the function cls.__doc__ = '' if fn.__doc__ is not None: cls.__doc__ = fn.__doc__ if not hasattr(fn, 'autodocs'): cls.__doc__ += '\n\nParameters:\n' + docs cls.fn = (fn.__module__, fn.__name__) # generate run method from the function def run(self): params = {k: v for k, v in self.__dict__.items() if not k.startswith('_')} return fn(self, **params) cls.run = run return cls
[docs]def autodoc(func): """A decorator used to add parameter comments to the docstring of func.""" from functools import wraps _docs, _ = read_comments(func) if func.__doc__ is None: func.__doc__ = '' func.__doc__ += '\n\nParameters:\n' func.__doc__ += _docs setattr(func, 'autodocs', _docs) return func