class BuiltinException(CompositeObject):
    """A built-in exception instance, which may hold any kind of objects in its
    `args` attribute.

    It doesn't matter if an exception was generated by the interpreter or by
    the user code, it will be serialized as BuiltinException nevertheless. This
    means we can't assume anything about the contents of `args`.

    User-defined exceptions, both deriving from BaseException and those not,
    are serialized as UserObjects.
    """
    def __init__(self, obj, serialize):
        CompositeObject.__init__(self, obj)

        self.args = map(serialize, obj.args)
        self.constructor_format = "%s(%%s)" % class_name(obj)
        self.imports = set()

        if class_of(obj) in BUILTIN_ENVIRONMENT_ERROR_TYPES and obj.filename is not None:
            self.args.append(serialize(obj.filename)) class ImmutableObject(SerializedObject):
    """A serialized object which identity doesn't matter.

    Immutable objects (like strings or integers) and objects with well-defined
    location (like non-anonymous functions) are all considered ImmutableObjects.

    ImmutableObjects are certain to have a reconstructor.

    :IVariables:
      reconstructor : str
        A string representing code that will construct the object.
      imports : set
        A set of import descriptions needed for the reconstructor code to work.
    """
    def __init__(self, obj):
        SerializedObject.__init__(self, obj)

        self.reconstructor, self.imports = ImmutableObject.get_reconstructor_with_imports(obj)

    def __eq__(self, other):
        return isinstance(other, ImmutableObject) and \
            self.reconstructor == other.reconstructor and \
            self.imports == other.imports

    def __hash__(self):
        return hash(self.reconstructor)

    def __repr__(self):
        if self.imports:
            return "ImmutableObject(%r, imports=%r)" % (self.reconstructor, self.imports)
        return "ImmutableObject(%r)" % self.reconstructor

    # :: object -> (string, set)
    def get_reconstructor_with_imports(obj):
        """
        >>> reconstructor, imports = ImmutableObject.get_reconstructor_with_imports(re.compile('abcd'))
        >>> reconstructor
        "re.compile('abcd')"
        >>> imports == set(['re'])
        True
        >>> reconstructor, imports = ImmutableObject.get_reconstructor_with_imports(re.compile('abcd', re.I | re.M))
        >>> reconstructor
        "re.compile('abcd', re.IGNORECASE | re.MULTILINE)"
        >>> imports == set(['re'])
        True
        """
        if isinstance(obj, (int, long, float, str, unicode, types.NoneType)):
            # Bultin types has very convienient representation.
            return repr(obj), set()
        elif isinstance(obj, RePatternType):
            flags = regexp_flags_as_string(obj.flags)
            if flags:
                return ('re.compile(%r, %s)' % (obj.pattern, flags), set(['re']))
            else:
                return ('re.compile(%r)' % obj.pattern, set(['re']))
        elif isinstance(obj, types.FunctionType):
            function = obj.func_name
            module = obj.__module__
            return (function, set([(module, function)]))
        else:
            raise TypeError("Unknown type of an ImmutableObject: %r." % obj)
    get_reconstructor_with_imports = staticmethod(get_reconstructor_with_imports) class MapObject(CompositeObject):
    """A mutable object that contains unordered mapping of key/value pairs.
    """
    def __init__(self, obj, serialize):
        # Serialize the parts first and only after that call super, so that
        # the parts get a lower timestamp than the whole object.
        self.mapping = [(serialize(k), serialize(v)) for k,v in obj.items()]

        CompositeObject.__init__(self, obj)

        self.constructor_format = "{%s}"
        self.imports = set() class UnknownObject(SerializedObject):
    """A user object or a builtin value that we cannot recreate.

    :IVariables:
      partial_reconstructor : str
        A string representation of a partial object reconstructor. It doesn't
        have to be parsable, as it will be part of a comment. Partial
        reconstructor should give all possible hints about an object to help
        the user complete the code.
    """
    def __init__(self, obj):
        SerializedObject.__init__(self, obj)
        self.partial_reconstructor = get_partial_reconstructor(obj)

    def __repr__(self):
        return "UnknownObject(%r)" % self.partial_reconstructor class SequenceObject(CompositeObject):
    """A builtin object that contains an ordered sequence of other objects
    inside it.

    Tuples and other immutable builtin types are still serialized into
    a SequenceObject, because they may contain a mutable element inside them.
    """
    type_formats_with_imports = {
        list: ("[%s]", set()),
        frozenset: ("frozenset([%s])", set()),
        set: ("set([%s])", set()),
        sets.ImmutableSet: ("ImmutableSet([%s])", set([("sets", "ImmutableSet")])),
        sets.Set: ("Set([%s])", set([("sets", "Set")])),
        tuple: ("(%s)", set()),
    }

    def __init__(self, obj, serialize):
        # Serialize the parts first and only after that call super, so that
        # the parts get a lower timestamp than the whole object.
        self.contained_objects = map(serialize, obj)

        CompositeObject.__init__(self, obj)

        # Arrays constructor needs to include a typecode.
        if isinstance(obj, array.array):
            self.constructor_format = "array.array('%s', [%%s])" % obj.typecode
            self.imports = set(["array"])
        # Special case for tuples with a single element.
        elif isinstance(obj, tuple) and len(obj) == 1:
            self.constructor_format = "(%s,)"
            self.imports = set()
        else:
            self.constructor_format = self.type_formats_with_imports[type(obj)][0]
            self.imports = self.type_formats_with_imports[type(obj)][1]

    def __repr__(self):
        return "SequenceObject(%s)" % (self.constructor_format % self.contained_objects) class LibraryObject(SerializedObject):
    type_formats_with_imports = {
        ('xml.dom.minidom', 'Element'):
            ("Element(%s)",
             ["tagName", "namespaceURI", "prefix"],
             set([("xml.dom.minidom", "Element")])),
        ('datetime', 'datetime'):
            ("datetime.datetime(%s)",
             ["year", "month", "day", "hour", "minute", "second", "microsecond", "tzinfo"],
             set(["datetime"])),
    }

    def __init__(self, obj, serialize):
        con, argnames, imp = self.type_formats_with_imports[id_of_class_of(obj)]

        self.constructor_format = con
        self.arguments = map(serialize, [getattr(obj, a, None) for a in argnames])
        self.imports = imp

        # Arguments were serialized first, before a call to super, so that they
        # get a lower timestamp than the whole object.
        SerializedObject.__init__(self, obj) def is_immutable(obj):
    # Bool class is a subclass of int, so True and False are included in this
    # condition.
    if isinstance(obj, (float, int, long, str, unicode, types.NoneType, RePatternType)):
        return True
    elif isinstance(obj, types.FunctionType) and obj.func_name != '<lambda>':
        return True
    return False def is_sequence(obj):
    return type(obj) in [array.array, list, frozenset, set,
                         sets.ImmutableSet, sets.Set, tuple] def is_mapping(obj):
    return type(obj) in [dict] def is_builtin_exception(obj):
    """Return True if given object is an instance of a built-in exception, like
    NameError or EOFError. Return False for instances of user-defined
    exceptions.
    """
    return class_of(obj) in BUILTIN_EXCEPTION_TYPES def is_library_object(obj):
    return id_of_class_of(obj) in LibraryObject.type_formats_with_imports.keys() class Call(Event):
    """Stores information about a single function or method call.

    Includes reference to the caller, all call arguments, references to
    other calls made inside this one, references to side effects that occured
    during this call and finally an output value.

    __eq__ and __hash__ definitions provided for Function.get_unique_calls()
    and UserObject.get_external_calls().
    """
    def __init__(self, definition, args, output=None, exception=None):
        if [value for value in args.values() if not isinstance(value, SerializedObject)]:
            raise ValueError("Values of all arguments should be instances of SerializedObject class.")
        if output and exception:
            raise ValueError("Call should have a single point of return.")
        if not isinstance(definition, Definition):
            raise ValueError("Call definition object should be an instance of Definition.")

        super(Call, self).__init__()

        self.definition = definition
        self.input = args
        self.output = output
        self.exception = exception

        self.caller = None
        self.subcalls = []
        self.side_effects = []

    def add_subcall(self, call):
        if call.caller is not None:
            raise TypeError("This %s of %s already has a caller." % \
                                (class_name(call), call.definition.name))
        call.caller = self
        self.subcalls.append(call)

    def raised_exception(self):
        return self.exception is not None

    def set_output(self, output):
        self.output = output

    def set_exception(self, exception):
        self.exception = exception

    def clear_exception(self):
        self.exception = None

    def add_side_effect(self, side_effect):
        self.side_effects.append(side_effect)

    def __eq__(self, other):
        if type(self) != type(other):
            return False
        return self.definition == other.definition and \
               self.input == other.input and \
               self.output == other.output and \
               self.exception == other.exception and \
               self.side_effects == other.side_effects

    def __hash__(self):
        return hash((self.definition.name,
                     tuple(self.input.iteritems()),
                     self.output,
                     self.exception,
                     tuple(self.side_effects)))

    def __repr__(self):
        return "%s(definition=%s, input=%r, output=%r, exception=%r, side_effects=[..(%d)..])" % \
            (class_name(self), self.definition.name, self.input, self.output,
             self.exception, len(self.side_effects)) class Class(ObjectInModule):
    def __init__(self, name, methods=[], bases=[], code=None, module=None):
        ObjectInModule.__init__(self, name, code)
        self.methods = []
        self.bases = bases
        self.module = module
        self.user_objects = []

        self.add_methods(methods)

    def _set_class_for_method(self, method):
        if method.klass is not None:
            raise TypeError("Trying to add %r to class %r, while the "
                            "method is already inside %r." % \
                                (method, self.name, method.klass.name))
        method.klass = self

    def add_methods(self, methods):
        for method in methods:
            self._set_class_for_method(method)
            self.methods.append(method)

    def add_user_object(self, user_object):
        self.user_objects.append(user_object)

    def get_traced_method_names(self):
        traced_method_names = set()
        for user_object in self.user_objects:
            for call in user_object.calls:
                traced_method_names.add(call.definition.name)
        return traced_method_names

    def get_untraced_methods(self):
        traced_method_names = self.get_traced_method_names()
        def is_untraced(method):
            return method.name not in traced_method_names
        return filter(is_untraced, self.methods)

    def find_method_by_name(self, name):
        for method in self.methods:
            if method.name == name:
                return method

    def get_creational_method(self):
        """Return either __new__ or __init__ method of this class, with __new__
        taking precedence.
        """
        method = self.find_method_by_name('__new__')
        if method:
            return method
        return self.find_method_by_name('__init__')

    def __repr__(self):
        return "Class(name=%s)" % self.name class Function(Definition, Callable):
    def __init__(self, name, args=None, code=None, calls=None, is_generator=False, module=None):
        Definition.__init__(self, name, args=args, code=code, is_generator=is_generator)
        Callable.__init__(self, calls)
        self.module = module
        if is_generator:
            self.calls_type = GeneratorObject
        else:
            self.calls_type = FunctionCall

    def get_unique_calls(self):
        return set(self.calls)

    def __repr__(self):
        return "Function(name=%s, args=%r, calls=%r)" % (self.name, self.args, self.calls) class FunctionCall(Call):
    pass class GeneratorObject(Callable, SerializedObject):
    """Representation of a generator object - a callable with an input and many
    outputs (here called "yields").
    """
    calls_type = GeneratorObjectInvocation

    def __init__(self, obj, generator=None, args=None, callable=None):
        Callable.__init__(self)
        SerializedObject.__init__(self, obj)
        if generator is not None and args is not None and callable is not None:
            self.activate(generator, args, callable)

    def activate(self, generator, args, callable):
        assert_argument_type(generator, (Function, Method))
        assert_argument_type(callable, (Function, UserObject))
        if self.is_activated():
            raise ValueError("This generator has already been activated.")
        if not generator.is_generator:
            raise TypeError("Tried to activate GeneratorObject with %r as a generator definition." % generator)
        self.definition = generator
        self.args = args
        # Finally register this GeneratorObject with its callable context
        # (which will be a Function or an UserObject). This has to be
        # done only once for each GeneratorObject.
        callable.add_call(self)

    def is_activated(self):
        return hasattr(self, 'args')

    def raised_exception(self):
        return any([c.raised_exception() for c in self.calls])

    def _get_exception(self):
        for invocation in self.calls:
            if invocation.raised_exception():
                return invocation.exception
    exception = property(_get_exception)

    def __repr__(self):
        if self.is_activated():
            return "GeneratorObject(generator=%r, args=%r)" % \
                (self.definition.name, self.args)
        else:
            return "GeneratorObject()" class GeneratorObjectInvocation(Call):
    """Representation of a single generator invocation.

    Each time a generator is resumed a new GeneratorObjectInvocation is created.
    """ class MethodCall(Call):
    pass class Project(object):
    """Object representing the whole project under Pythoscope wings.

    No modifications are final until you call save().
    """
    def from_directory(cls, project_path):
        """Read the project information from the .pythoscope/ directory of
        the given project.

        The pickle file may not exist for project that is analyzed the
        first time and that's OK.
        """
        project_path = os.path.realpath(project_path)
        try:
            project = load_pickle_from(get_pickle_path(project_path))
            # Update project's path, as the directory could've been moved.
            project.path = project_path
        except IOError:
            project = Project(project_path)
        return project
    from_directory = classmethod(from_directory)

    def __init__(self, path, code_trees_manager_class=FilesystemCodeTreesManager):
        """Initialize a Project instance using the given path as the project's
        top directory.

        During normal operation code_trees_manager_class is the class that
        the Project delegates to all CodeTree management tasks, but during
        testing this can be replaced with something else, possibly a class
        that doesn't touch the file system.
        """
        self.path = path
        self.new_tests_directory = "tests"
        self.points_of_entry = {}
        self.snippet_executions = []
        self._modules = {}
        self.code_trees_manager = code_trees_manager_class(get_code_trees_path(path))

        self._find_new_tests_directory()

    def _get_pickle_path(self):
        return get_pickle_path(self.path)

    def get_points_of_entry_path(self):
        return get_points_of_entry_path(self.path)

    def path_for_point_of_entry(self, name):
        return os.path.join(self.path, self.subpath_for_point_of_entry(name))

    def subpath_for_point_of_entry(self, name):
        return os.path.join(POINTS_OF_ENTRY_SUBPATH, name)

    def _find_new_tests_directory(self):
        for path in directories_under(self.path):
            if re.search(r'[_-]?tests?([_-]|$)', path):
                self.new_tests_directory = path

    def save(self):
        # To avoid inconsistencies try to save all project's modules first. If
        # any of those saves fail, the pickle file won't get updated.
        for module in self.get_modules():
            log.debug("Calling save() on module %r" % module.subpath)
            module.save()

        # We don't want to have a single AST in a Project instance.
        self.code_trees_manager.clear_cache()

        # Pickling the project after saving all of its modules, so any changes
        # made by Module instances during save() will be preserved as well.
        pickled_project = cPickle.dumps(self, cPickle.HIGHEST_PROTOCOL)

        log.debug("Writing project pickle to disk...")
        write_content_to_file(pickled_project, self._get_pickle_path(), binary=True)

    def find_module_by_full_path(self, path):
        subpath = self._extract_subpath(path)
        return self[subpath]

    def contains_point_of_entry(self, name):
        return name in self.points_of_entry

    def get_point_of_entry(self, name):
        return self.points_of_entry[name]

    def add_point_of_entry(self, poe):
        self.points_of_entry[poe.name] = poe

    def remove_point_of_entry(self, name):
        poe = self.points_of_entry.pop(name)
        poe.clear_previous_run()

    def create_module(self, path, **kwds):
        """Create a module for this project located under given path.

        If there already was a module with given subpath, it will get replaced
        with a new instance using the _replace_references_to_module method.

        Returns the new Module object.
        """
        module = Module(subpath=self._extract_subpath(path), project=self, **kwds)

        if module.subpath in self._modules.keys():
            self._replace_references_to_module(module)
            # Don't need to forget the old CallTree, as the creation of
            # the Module instance above overwrites it anyway.

        self._modules[module.subpath] = module

        return module

    def create_test_module_from_name(self, test_name, **kwds):
        """Create a module with given name in project tests directory.

        If the test module already exists, ModuleNeedsAnalysis exception will
        be raised.
        """
        test_path = self._path_for_test(test_name)
        if os.path.exists(test_path):
            raise ModuleNeedsAnalysis(test_path)
        return self.create_module(test_path, **kwds)

    def remove_module(self, subpath):
        """Remove a module from this Project along with all references to it
        from other modules and its CodeTree.
        """
        module = self[subpath]
        for test_case in self.iter_test_cases():
            try:
                test_case.associated_modules.remove(module)
            except ValueError:
                pass
        del self._modules[subpath]
        self.code_trees_manager.forget_code_tree(module.subpath)

    # :: (CodeTree, Module) -> None
    def remember_code_tree(self, code_tree, module):
        self.code_trees_manager.remember_code_tree(code_tree, module.subpath)

    # :: Module -> CodeTree
    def recall_code_tree(self, module):
        return self.code_trees_manager.recall_code_tree(module.subpath)

    def remember_execution_from_snippet(self, execution):
        self.snippet_executions.append(execution)

    def _replace_references_to_module(self, module):
        """Remove a module with the same subpath as given module from this
        Project and replace all references to it with the new instance.
        """
        old_module = self[module.subpath]
        for test_case in self.iter_test_cases():
            try:
                test_case.associated_modules.remove(old_module)
                test_case.associated_modules.append(module)
            except ValueError:
                pass

    def _extract_point_of_entry_subpath(self, path):
        """Takes the file path and returns subpath relative to the
        points of entry path.

        Assumes the given path is under points of entry path.
        """
        return extract_subpath(path, self.get_points_of_entry_path())

    def _extract_subpath(self, path):
        """Takes the file path and returns subpath relative to the
        project.

        Assumes the given path is under Project.path.
        """
        return extract_subpath(path, self.path)

    def contains_path(self, path):
        """Returns True if given path is under this project's path and False
        otherwise.
        """
        return starts_with_path(path, self.path)

    def iter_test_cases(self):
        for module in self.iter_modules():
            for test_case in module.test_cases:
                yield test_case

    def _path_for_test(self, test_module_name):
        """Return a full path to test module with given name.
        """
        return os.path.join(self.path, self.new_tests_directory, test_module_name)

    def __getitem__(self, module):
        for mod in self.iter_modules():
            if module in [mod.subpath, mod.locator]:
                return mod
        raise ModuleNotFound(module)

    def get_modules(self):
        return self._modules.values()

    def iter_modules(self):
        return self._modules.values()

    def iter_classes(self):
        for module in self.iter_modules():
            for klass in module.classes:
                yield klass

    def iter_functions(self):
        for module in self.iter_modules():
            for function in module.functions:
                yield function

    def find_object(self, type, name, modulename):
        try:
            return self[modulename].find_object(type, name)
        except ModuleNotFound:
            pass class UserObject(Callable, SerializedObject):
    """Serialized instance of a user-defined type.

    UserObjects are also callables that aggregate MethodCall instances,
    capturing the whole life of an object, from initialization to destruction.
    """
    calls_type = (MethodCall, GeneratorObject)

    def __init__(self, obj, klass):
        Callable.__init__(self)
        SerializedObject.__init__(self, obj)
        self.klass = klass
        self.type_name = self.klass.name

    # Defined lazily to ease testing - classes may be assigned to modules after
    # creation of UserObject, or never at all.
    module_name = property(lambda self: self.klass.module.locator, lambda s,v: None)

    def get_init_call(self):
        """Return a call to __init__ or None if it wasn't called.
        """
        return findfirst(lambda call: call.definition.name == '__init__', self.calls)

    def get_external_calls(self):
        """Return all calls to this object made from the outside.

        Note: __init__ is considered an internal call.
        """
        def is_not_init_call(call):
            return call.definition.name != '__init__'
        return filter(is_not_init_call, filter(self.is_external_call, self.calls))

    def get_init_and_external_calls(self):
        return filter(self.is_external_call, self.calls)

    def __repr__(self):
        return "UserObject(id=%s, klass=%r)" % (id(self), self.klass.name)

    def is_external_call(self, call):
        if isinstance(call, GeneratorObject):
            return True
        return (not call.caller) or (call.caller not in self.calls) def all_of_type(objects, type):
    """Return all objects that are instances of a given type.
    """
    return [o for o in objects if isinstance(o, type)] def assert_argument_type(obj, expected_type):
    if not isinstance(obj, expected_type):
        raise TypeError("%s() should be called with a %s argument, not %s" %
            (callers_name(), type_names(expected_type), obj)) def class_name(obj):
    return class_of(obj).__name__ def generator_has_ended(generator):
    """Return True if the generator has been exhausted and False otherwise.

    >>> generator_has_ended(1)
    Traceback (most recent call last):
      ...
    TypeError: argument is not a generator
    """
    if not isinstance(generator, types.GeneratorType):
        raise TypeError("argument is not a generator")
    return _generator_has_ended(generator) def get_generator_from_frame(frame):
    generators = all_of_type(gc.get_referrers(frame), types.GeneratorType)
    if generators:
        return generators[0] def is_generator_code(code):
    return code.co_flags & 0x20 != 0 def map_values(function, dictionary):
    new_dictionary = {}
    for key, value in dictionary.iteritems():
        new_dictionary[key] = function(value)
    return new_dictionary def module_name(obj):
    return class_of(obj).__module__
import itertools
import time
import types
from pythoscope.serializer import BuiltinException, ImmutableObject, MapObject,\
    UnknownObject, SequenceObject, LibraryObject, is_immutable, is_sequence,\
    is_mapping, is_builtin_exception, is_library_object
from pythoscope.store import Call, Class, Function, FunctionCall,\
    GeneratorObject, GeneratorObjectInvocation, MethodCall, Project, UserObject
from pythoscope.util import all_of_type, assert_argument_type, class_name,\
    generator_has_ended, get_generator_from_frame, is_generator_code,\
    map_values, module_name
    def create_serialized_user_object(self, obj):
        klass = self.project.find_object(Class, class_name(obj), module_name(obj))
        if klass:
            serialized = UserObject(obj, klass)
            klass.add_user_object(serialized)
            return serialized

    # :: object -> SerializedObject
    def create_serialized_object(self, obj):
        # Generator object has been passed as a value. We don't have enough
        # information to create a complete GeneratorObject instance here, so
        # we create a stub to be activated later.
        if isinstance(obj, types.GeneratorType):
            return GeneratorObject(obj)
        user_object = self.create_serialized_user_object(obj)
        if user_object:
            return user_object
        elif is_immutable(obj):
            return ImmutableObject(obj)
        elif is_sequence(obj):
            return SequenceObject(obj, self.serialize)
        elif is_mapping(obj):
            return MapObject(obj, self.serialize)
        elif is_builtin_exception(obj):
            return BuiltinException(obj, self.serialize)
        elif is_library_object(obj):
            return LibraryObject(obj, self.serialize)
        else:
            return UnknownObject(obj)

    # :: (type, Definition, Callable, args, code, frame) -> Call
    def create_call(self, call_type, definition, callable, args, code, frame):
        sargs = self.serialize_call_arguments(args)
        if is_generator_code(code):
            generator = get_generator_from_frame(frame)
            # Each generator invocation is related to some generator object,
            # so we have to create one if it wasn't captured yet.
            def create_generator_object(_):
                gobject = GeneratorObject(generator, definition, sargs, callable)
                save_generator_inside(gobject, generator)
                return gobject
            gobject = self._retrieve_or_capture(generator, create_generator_object)
            # It may have been captured, but not necessarily invoked yet, so
            # we activate it if that's the case.
            if not gobject.is_activated():
                gobject.activate(definition, sargs, callable)
                save_generator_inside(gobject, generator)
            # In case of generators the call is really an invocation (resume) of
            # a specific generator object. Input arguments were already saved
            # in the GeneratorObject, and there's no need for duplicating them.
            call_type = GeneratorObjectInvocation
            callable = gobject
            sargs = {}
        call = call_type(definition, sargs)
        self.captured_calls.append(call)
        callable.add_call(call)
        return call

    # :: (str, object, dict, code, frame) -> MethodCall | None
    def create_method_call(self, name, obj, args, code, frame):
        user_object = self.try_serializing_as_user_object(obj)

        # We ignore the call if we can't find the class of this object.
        if user_object:
            method = user_object.klass.find_method_by_name(name)
            if method:
                return self.create_call(MethodCall, method, user_object, args, code, frame)
            else:
                # TODO: We're lacking a definition of a method in a known class,
                # so at least issue a warning.
                pass

    # :: (str, dict, code, frame) -> FunctionCall | None
    def create_function_call(self, name, args, code, frame):
        if self.project.contains_path(code.co_filename):
            modulename = self.project._extract_subpath(code.co_filename)
            function = self.project.find_object(Function, name, modulename)
            if function:
                return self.create_call(FunctionCall, function, function,
                                        args, code, frame)

    # :: (str, *object) -> SideEffect
    def create_side_effect(self, klass, *args):
        return klass(*map(self.serialize, args))

    # :: (object, callable) -> SerializedObject | None
    def _retrieve_or_capture(self, obj, capture_callback):
        """Return existing description of the given object or create and return
        new one if the description wasn't captured yet.

        Preserves identity of objects, by storing them in `captured_objects`
        list.

        Returns None, when an obj wasn't serialized earlier and capture_callback
        returns None:
            >>> e = Execution(Project("."))
            >>> e._retrieve_or_capture(123, lambda x: None) is None
            True
            >>> e._preserved_objects
            []
        """
        try:
            return self.captured_objects[object_id(obj)]
        except KeyError:
            captured = capture_callback(obj)
            if captured:
                self._preserve(obj)
                self.captured_objects[object_id(obj)] = captured
                return captured

    def _preserve(self, obj):
        """Preserve an object from garbage collection, so its id won't get
        occupied by any other object.
        """
        self._preserved_objects.append(obj)

    def iter_captured_generator_objects(self):
        return all_of_type(self.captured_objects.values(), GeneratorObject)

    def remove_call_from_call_graph(self, call_to_remove):

        assert_argument_type(call_to_remove, Call)