[Python-ideas] IPLike objects

Damla Altun initalize.damla at gmail.com
Sun Oct 21 06:58:19 EDT 2018


Hi,

Python's ipaddress module is really good for working with network / IP
related thing.
I would like to use ipaddress.IPv4/v6Address in creating network
interfaces. Like a socket but i can't,
the internal things and some standard libraries doesn't support them (even
socket).

    >>> server_address = (ipaddress.IPv4Address('127.0.0.1'), 10000)
    >>> sock.bind(server_address)
    Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
    TypeError: str, bytes or bytearray expected, not IPv4Address


Proposal
========

The best solution we've got[0] is implementing something like PathLike[1]
for IP Address objects. For PoC[2]
we called it IPLike. It is a protocol (some kind of interface) created with
Abstract Base Classes and
it forces class to implement __ipaddr__ method (like __fspath__ method).
The __ipaddr__ method should return
string or bytes formed notation of IP address. It is dotted notation in
IPv4, long formed notation in IPv6.

The __ipaddr__ method is also an identifier for IPLike classes. IPLike
interface's subclasshook
checks the __ipaddr__ method of given class when issubclass called.

    >>> class SomethingRepresentIP:
    ...     def __ipaddr__(self) -> str:
    ...         return '127.0.0.1'
    ...
    >>> issubclass(SomethingRepresentIP, IPLike)
    True
    >>> isinstance(SomethingRepresentIP(), IPLike)
    True

For PoC[2] we implented __ipaddr__ method to ipaddress._BaseAddress class.
It is parent class of IPv4/v6Adress.

    >>> issubclass(IPv4Address, IPLike)
    True
    >>> IPv4Address('127.0.0.1').__ipaddr__()
    '127.0.0.1'

Like os.fspath we need a general-purpose ip-getter. So we implemented
ipaddress.get_ipaddr(address) function.
When you give it an valid IP like object it returns the value of __ipaddr__
method.

    >>> get_ipaddr(IPv4Address('127.0.0.1'))
    '127.0.0.1'

When you give it string or bytes or integer object it calls
ipaddr.ip_address(address) and re-calls itself with
the value it got from ip_address. It helps get_ipaddr to convert
integer-formed / byte-formed IPs to string formed IPs
with a valid notation.

    >>> get_ipaddr('127.0.0.1')
    '127.0.0.1'
    >>> get_ipaddr(3232235521)
    '192.168.0.1'
    >>> get_ipaddr(b'\xC0\xA8\x00\x01')
    '192.168.0.1'

If you give any type else it raises TypeError. Also if you give it a IP
like object (an object that has __ipaddr__ method),
but the object's __ipaddr__ method returns something that is not
string/bytes it raises TypeError.

For PoC[2] i added socket to support IP like objects.

    >>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    >>> server_address = (SomethingRepresentIP(), 10000)
    >>> sock.bind(server_address)
    >>> sock.listen(1)
    >>> conn, client = sock.accept()
    >>> conn
    <socket.socket fd=4, family=AddressFamily.AF_INET,
type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 10000),
raddr=('127.0.0.1', 49452)>

I didn't implement a C API for ipaddress.get_ipaddr. The function is too
small for creating a new module, new header and the ipaddress library
currently has no
C-side module. If needed i can write a function inside the socketmodule.c .

Proof-of-Concept (Implementation)
=================================

The implementation can be found on github (DamlaAltun/cpython[2]) We have a
Abstract Base Class for IP Address protocol called IPLike.

    (ipaddress.py[3])

    class IPLike(abc.ABC):
    """
    Abstract base class for IP (address) like objects.
    """

    @abc.abstractmethod
    def __ipaddr__(self):
        """
        If it is a IPv4 return in the dotted form,
        If it is a IPv6 return in the long form.
        """
        raise NotImplementedError

    @classmethod
    def __subclasshook__(cls, subclass):
        return hasattr(subclass, '__ipaddr__')

It has a abstract method __ipaddr__ and a subclasshook. The __ipaddr__
method returns ip representation of object.
The subclasshook checks for __ipaddr__ method.

Then there is a ip-getter called get_ipaddr

    (ipaddress.py[4])

    def get_ipaddr(address):
        """Return the representation of a ip address like object.

        If bytes or string or int passed in, get_ipaddr converts it to
        a IPv4/v6 Address and calls it self again for getting,
        IP address of IPv4/v6 object. If a IPLike passed it uses IPLike
        interface to get the value.
        """

        if isinstance(address, (str, bytes, int)):
            return get_ipaddr(ip_address(address))

        addr_type = type(address)
        try:
            ip_addr = address.__ipaddr__()
        except AttributeError:
            if hasattr(address, '__ipaddr__'):
                raise
            else:
                raise TypeError("expected str, bytes or ipaddress.IPLike
object, "
                                "not {}".format(addr_type.__name__))

        if isinstance(ip_addr, (str, bytes)):
            return ip_addr
        else:
            raise TypeError("expected {}.__ipaddr__() to return str or
bytes, "
                            "not {}".format(addr_type.__name__,
                                            type(addr_type).__name__))


It takes one parameter, address. The address might be string, integer,
bytes or ipaddress.IPLike
If it is string or bytes or integer the get_ipaddr calls
ipaddress.ip_address for getting IPv4/v6Address
object. The purpose of that is converting bytes/int IP addresses to valid
formed string IP addresses. And calls
it self again with result of ipaddress.ip_address

It tries to get address' __ipaddr__ method, if it can successfully perform
it checks the return value of __ipaddr__ method.
If return value is string or bytes it returns the ip address. If it is not
it raises a TypeError.

In the C-Side we can check via _PyObject_LookupSpecial.

    (socketmodule.c[5])

    idna_converter(PyObject *obj, struct maybe_idna *data)
    {
        ...
        _Py_IDENTIFIER(__ipaddr__);
        ...
        func = _PyObject_LookupSpecial(obj, &PyId___ipaddr__);
        if (func != NULL) {
            obj = PyObject_CallFunctionObjArgs(func, NULL);
            Py_DECREF(func);
        }
    }


Thanks for your attention,

Damla

[0] https://github.com/python/cpython/pull/9956#pullrequestreview-166756568
[1] https://www.python.org/dev/peps/pep-0519
[2] https://github.com/DamlaAltun/cpython/tree/iplike-object
[3]
https://github.com/DamlaAltun/cpython/blob/iplike-object/Lib/ipaddress.py#L413
[4]
https://github.com/DamlaAltun/cpython/blob/iplike-object/Lib/ipaddress.py#L384
[5]
https://github.com/DamlaAltun/cpython/blob/iplike-object/Modules/socketmodule.c#L1537
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20181021/53149f0b/attachment-0001.html>


More information about the Python-ideas mailing list