Best practice: ABC vs Union?

I’m exposing a function that takes a Buffer as an argument. However, the Buffer type only exists in Python 3.12+.

I had considered using one of these “ponyfills” for it:

Code

Option 1: typing.Union

try:
    # version_info >= (3, 12)
    from collections.abc import Buffer as BufferLike
except ImportError:
    # version_info < (3, 12)
    import abc
    import array
    import collections.abc
    import ctypes
    import mmap
    from typing import Union

    BufferLike = Union[
        collections.abc.ByteString,
        array.array,
        ctypes.Array,
        memoryview,
        mmap.mmap
    ]

    # * CFFI:
    try:
        import cffi
    except ImportError:
        pass
    else:
        if hasattr(cffi.FFI, 'buffer') and isinstance(cffi.FFI.buffer, type):
            BufferLike = Union[BufferLike, cffi.FFI.buffer]
        else:
            _ffi = cffi.FFI()
            if isinstance(_ffi.buffer, type):
                BufferLike = Union[BufferLike, _ffi.buffer]
            else:
                with _ffi.new('uint8_t[0]') as _cdata:
                    _cdata_buf = _ffi.buffer(_cdata)
                    _ffi_buffer = _cdata_buf.__class__
                BufferLike = Union[BufferLike, _ffi_buffer]
 
    # * NumPy:
    try:
        import numpy
    except ImportError:
        pass
    else:
        BufferLike = Union[BufferLike, numpy.ndarray]
        
    # * bitarray
    try:
        import bitarray
    except ImportError:
        pass
    else:
        BufferLike = Union[BufferLike, bitarray.bitarray]

Option 2: collections.abc.ABC

try:
    # version_info >= (3, 12)
    from collections.abc import Buffer as BufferLike
except ImportError:
    # version_info < (3, 12)
    import abc
    import array
    import collections.abc
    import ctypes
    import mmap

    class BufferLike(collections.abc.ByteString):
        pass

    BufferLike.register(array.array)
    BufferLike.register(ctypes.Array)
    BufferLike.register(memoryview)
    BufferLike.register(mmap.mmap)

    # Bonus Library Support:

    # * CFFI:
    try:
        import cffi
    except ImportError:
        pass
    else:
        if hasattr(cffi.FFI, 'buffer') and isinstance(cffi.FFI.buffer, type):
            BufferLike.register(cffi.FFI.buffer)
        else:
            _ffi = cffi.FFI()
            if isinstance(_ffi.buffer, type):
                BufferLike.register(_ffi.buffer)
            else:
                with _ffi.new('uint8_t[0]') as _cdata:
                    _cdata_buf = _ffi.buffer(_cdata)
                    _ffi_buffer = _cdata_buf.__class__
                BufferLike.register(_ffi_buffer)
 
    # * NumPy:
    try:
        import numpy
    except ImportError:
        pass
    else:
        BufferLike.register(numpy.ndarray)
        
    # * bitarray
    try:
        import bitarray
    except ImportError:
        pass
    else:
        BufferLike.register(bitarray.bitarray)

But I’m unsure what the considerations are between these options. As far as I can tell, they both do “work”, as in function correctly (at least when the user isn’t sticking in Buffer-like objects from 3rd-party libraries I don’t yet know about…)

The code does use isinstance(buffer_or_file, BufferLike) internally to narrow typing on conditional codepaths in functions that take Union[BufferLike, BinaryIO].

Is this just a matter of personal preference, or am I missing something major?

I would prefer a Protocol over an ABC for any application that doesn’t intend the user to subclass something themselves, and calling .register always feels like applying a sticking plaster to me.

The Union would be fine as it is, you’ve got a nice maintainable list of types at the start. But I wonder if a Protocol defining an explicit interface, the methods you’ll actually use, would require much less code (would it allow you to get rid of the attempts to import numpy etc. and update the union?). Otherwise I’d just live with the verbosity. The union much better communicates the intention that you’re writing that code to use it as a type annotation.

Is there no good Babel like module on Pypi with polyfills such as yours for the most recent types?

3 Likes

typing_extensions does backport Buffer, but the problem here is that there is no API. Before Python 3.12, the only way to know would be to try passing it to memoryview() or something.