"""
This module contains fixups for using nose under different versions of Python.
For Python 3.11+, most of these are simplified.
"""
import sys
import os
import traceback
import types
import inspect
import nose.util

__all__ = ['make_instancemethod', 'cmp_to_key', 'sort_list', 'ClassType',
           'TypeType', 'UNICODE_STRINGS', 'unbound_method', 'ismethod',
           'bytes_', 'is_base_exception', 'force_unicode', 'exc_to_unicode',
           'format_exception']

# In Python 3.x, all strings are unicode
UNICODE_STRINGS = True


def force_unicode(s, encoding='UTF-8'):
    return str(s)


def make_instancemethod(function, instance):
    """Create an instance method from a function using descriptor protocol."""
    return function.__get__(instance, instance.__class__)


def cmp_to_key(mycmp):
    """Convert a cmp= function into a key= function"""
    class Key:
        def __init__(self, obj):
            self.obj = obj
        def __lt__(self, other):
            return mycmp(self.obj, other.obj) < 0
        def __gt__(self, other):
            return mycmp(self.obj, other.obj) > 0
        def __eq__(self, other):
            return mycmp(self.obj, other.obj) == 0
    return Key


def sort_list(l, key, reverse=False):
    return l.sort(key=key, reverse=reverse)


# In Python 3.x, all objects are "new style" objects descended from 'type'
ClassType = type
TypeType = type


class UnboundMethod:
    """Emulates the behavior of an 'unbound method' under Python 3.x.

    Provides the ability to have a class associated with a function
    definition so that things can do stuff based on its associated class.
    """
    def __init__(self, cls, func):
        # Make sure we have all the same attributes as the original function,
        # so that the AttributeSelector plugin will work correctly...
        self.__dict__ = func.__dict__.copy()
        self._func = func
        self.__self__ = UnboundSelf(cls)
        self.__doc__ = getattr(func, '__doc__', None)

    def address(self):
        cls = self.__self__.cls
        modname = cls.__module__
        module = sys.modules[modname]
        filename = getattr(module, '__file__', None)
        if filename is not None:
            filename = os.path.abspath(filename)
        return (nose.util.src(filename), modname, "{}.{}".format(cls.__name__,
                                                        self._func.__name__))

    def __call__(self, *args, **kwargs):
        return self._func(*args, **kwargs)

    def __getattr__(self, attr):
        return getattr(self._func, attr)

    def __repr__(self):
        return '<unbound method {}.{}>'.format(self.__self__.cls.__name__,
                                           self._func.__name__)


class UnboundSelf:
    def __init__(self, cls):
        self.cls = cls

    def __getattribute__(self, attr):
        if attr == '__class__':
            return self.cls
        else:
            return object.__getattribute__(self, attr)


def unbound_method(cls, func):
    if inspect.ismethod(func):
        return func
    if not inspect.isfunction(func):
        raise TypeError('{} is not a function'.format(repr(func)))
    return UnboundMethod(cls, func)


def ismethod(obj):
    return inspect.ismethod(obj) or isinstance(obj, UnboundMethod)


def bytes_(s, encoding='utf8'):
    """Make a pseudo-bytes function that can be called without the encoding arg."""
    if isinstance(s, bytes):
        return s
    return bytes(s, encoding)


def isgenerator(o):
    if isinstance(o, UnboundMethod):
        o = o._func
    return inspect.isgeneratorfunction(o) or inspect.isgenerator(o)


def is_base_exception(exc):
    return isinstance(exc, BaseException)


def exc_to_unicode(ev, encoding='utf-8'):
    return str(ev)


def format_exception(exc_info, encoding='UTF-8'):
    ec, ev, tb = exc_info

    # Our exception object may have been turned into a string, and Python 3's
    # traceback.format_exception() doesn't take kindly to that (it expects an
    # actual exception object).  So we work around it, by doing the work
    # ourselves if ev is not an exception object.
    if not is_base_exception(ev):
        tb_data = force_unicode(
                ''.join(traceback.format_tb(tb)),
                encoding)
        ev = exc_to_unicode(ev)
        return tb_data + ev
    else:
        return force_unicode(
                ''.join(traceback.format_exception(*exc_info)),
                encoding)
