def make_expressions(cls, callable_set, goal_type, tags_getter=None, non_reusable_type_set=None): """ Create a list of :class:`exekall.engine.Expression` out of the given ``callable_set``. """ op_set = { engine.Operator( callable_, tags_getter=tags_getter, non_reusable_type_set=non_reusable_type_set, ) for callable_ in callable_set } root_op_set = { op for op in op_set if issubclass(op.value_type, goal_type) } class_ctx = engine.ClassContext.from_op_set(op_set) expr_list = class_ctx.build_expr_list( root_op_set, non_produced_handler='raise', cycle_handler='raise', ) expr_list.sort(key=lambda expr: expr.get_id(full_qual=True, with_tags=True)) return expr_list
def sweep_param(callable_, param, start, stop, step=1): """ Used to generate a stream of numbers or strings to feed to a callable. :param callable_: Callable the numbers will be used by. :type callable_: collections.abc.Callable :param param: Name of the parameter of the callable the numbers will be providing values for. :type param: str :param start: Starting value. :type start: str :param stop: End value (inclusive) :type stop: str :param step: Increment step. :type step: str If ``start == stop``, only that value will be yielded, and it can be of any type. The type used will either be one that is annotated on the callable, or the one from the default value if no annotation is available, or float if no default value is found. The value will then be built by passing the string to the type as only parameter. """ op = engine.Operator(callable_) annot = op.get_prototype()[0] try: type_ = annot[param] except KeyError: sig = op.signature default = sig.parameters[param].default if default is not inspect.Parameter.default and default is not None: type_ = type(default) else: type_ = str if start == stop: yield type_(start) else: i = type_(start) step = type_(step) stop = type_(stop) while i <= stop: yield type_(i) i += step
def build_op_set(callable_pool, non_reusable_type_set, allowed_pattern_set, adaptor): op_set = { engine.Operator(callable_, non_reusable_type_set=non_reusable_type_set, tags_getter=adaptor.get_tags) for callable_ in callable_pool } filtered_op_set = adaptor.filter_op_set(op_set) # Make sure we have all the explicitely allowed operators filtered_op_set.update( op for op in op_set if utils.match_name(op.get_name(full_qual=True), allowed_pattern_set)) return filtered_op_set
def _get_callable_set(module, visited_obj_set, verbose): log_f = info if verbose else debug callable_pool = set() for name, obj in vars(module).items(): # skip internal classes that may end up being exposed as a global if inspect.getmodule(obj) is engine: continue if id(obj) in visited_obj_set: continue else: visited_obj_set.add(id(obj)) # If it is a class, get the list of methods if isinstance(obj, type): callable_list = [ callable_ for name, callable_ in inspect.getmembers(obj, predicate=callable) ] callable_list.append(obj) else: callable_list = [obj] callable_list = [c for c in callable_list if callable(c)] for callable_ in callable_list: try: op = engine.Operator(callable_) param_list, return_type = op.get_prototype() # If the callable is partially annotated, warn about it since it is # likely to be a mistake. except engine.PartialAnnotationError as e: log_f('Partially-annotated callable will not be used: {e}'. format( callable=get_name(callable_), e=e, )) continue # If some annotations fail to resolve except NameError as e: log_f( 'callable with unresolvable annotations will not be used: {e}' .format( callable=get_name(callable_), e=e, )) continue # If something goes wrong, that means it is not properly annotated # so we just ignore it except (AttributeError, ValueError, KeyError, engine.AnnotationError): continue # Swap-in a wrapper object, so we keep track on the class on which # the function was looked up if op.is_method: callable_ = engine.UnboundMethod(callable_, obj) # Also make sure we don't accidentally get callables that will # return a abstract base class instance, since that would not work # anyway. if inspect.isabstract(return_type): log_f( 'Instances of {} will not be created since it has non-implemented abstract methods' .format(get_name(return_type, full_qual=True))) else: callable_pool.add(callable_) return callable_pool
def _get_callable_set(namespace, visited_obj_set, verbose): """ :param namespace: Module or class """ log_f = info if verbose else debug callable_pool = set() if id(namespace) in visited_obj_set: return callable_pool else: visited_obj_set.add(id(namespace)) attributes = [ callable_ for name, callable_ in inspect.getmembers(namespace, predicate=callable) ] if isinstance(namespace, type): attributes.append(namespace) attributes = [ attr for attr in attributes if (id(attr) not in visited_obj_set and # skip internal classes that may end up being exposed as a global inspect.getmodule(attr) is not engine) ] visited_obj_set.update(attributes) for callable_ in attributes: # Explore the class attributes as well for nested types if (isinstance(callable_, type) and # Do not recurse into the "_type" class attribute of namedtuple, as # it has broken globals Python 3.6. The crazy checks are a # workaround the fact that there is no direct way to detect if a # class is a namedtuple. not (isinstance(namespace, type) and callable_.__name__ == '_type' and issubclass(callable_, tuple) and hasattr( callable_, '_make') and hasattr(callable_, '_asdict') and hasattr(callable_, '_replace'))): callable_pool.update( _get_callable_set(callable_, visited_obj_set, verbose)) try: op = engine.Operator(callable_) # Trigger exceptions if they have to be raised op.prototype # If the callable is partially annotated, warn about it since it is # likely to be a mistake. except engine.PartialAnnotationError as e: log_f( 'Partially-annotated callable "{callable}" will not be used: {e}' .format( callable=get_name(callable_), e=e, )) continue # If some annotations fail to resolve except NameError as e: log_f( 'callable "{callable}" with unresolvable annotations will not be used: {e}' .format( callable=get_name(callable_), e=e, )) continue # If something goes wrong, that means it is not properly annotated # so we just ignore it except (AttributeError, ValueError, KeyError, engine.AnnotationError): continue def has_typevar(op): return any( isinstance(x, typing.TypeVar) for x in {op.value_type, *op.prototype[0].values()}) # Swap-in a wrapper object, so we keep track on the class on which # the function was looked up if op.is_method: assert isinstance(namespace, type) callable_ = engine.UnboundMethod(callable_, namespace) # If the return annotation was a TypeVar, give a chance to # Operator to resolve it in case it was redefined in a # subclass, and we are inspecting that subclass if has_typevar(op): op = engine.Operator(callable_) def check_typevar_name(cls, name, var): if name != var.__name__: log_f( '__name__ of {cls}.{var.__name__} typing.TypeVar differs from the name it is bound to "{name}", which will prevent using it for polymorphic parameters or return annotation' .format( cls=get_name(cls, full_qual=True), var=var, name=name, )) return name type_vars = sorted( check_typevar_name(op.value_type, name, attr) for name, attr in inspect.getmembers( op.value_type, lambda x: isinstance(x, typing.TypeVar))) # Also make sure we don't accidentally get callables that will # return a abstract base class instance, since that would not work # anyway. if inspect.isabstract(op.value_type): log_f( 'Instances of {} will not be created since it has non-implemented abstract methods' .format(get_name(op.value_type, full_qual=True))) elif type_vars: log_f( 'Instances of {} will not be created since it has non-overridden TypeVar class attributes: {}' .format(get_name(op.value_type, full_qual=True), ', '.join(type_vars))) elif has_typevar(op): log_f( 'callable "{callable}" with non-resolved associated TypeVar annotations will not be used' .format(callable=get_name(callable_), )) else: callable_pool.add(callable_) return callable_pool