from typing import Union


def object_to_qualified_name(value, fail=False, default_module_name='builtins') -> Union[str, None]:
    """
    Get the fully qualified name of a Python object.
    It is true that ``qualified_name_to_object(object_to_qualified_name(obj)) is obj``.

    >>> from unittest import TestCase
    >>> object_to_qualified_name(TestCase)
    'unittest.case.TestCase'

    See also :py:func:`qualified_name_to_object`.

    :param value: some Python object
    :param fail: raise ``ValueError`` if name cannot be derived.
    :param default_module_name: if this is the *value*'s module name, no module name will be returned.
    :return: fully qualified name if it can be derived, otherwise ``None`` if *fail* is ``False``.
    :raise ValueError: if *fail* is ``True`` and the name cannot be derived.
    """

    try:
        module_name = value.__module__
    except AttributeError:
        module_name = None
    if module_name == default_module_name:
        module_name = None

    try:
        name = value.__name__
    except AttributeError:
        name = None

    if name:
        return module_name + '.' + name if module_name else name
    elif fail:
        raise ValueError("missing attribute '__name__'")
    else:
        return str(value)