Repository: postlund/pyatv
Pull request #1520 (2021-12-10 22:24:55)

dep: Migrate from netifaces to ifaddr
Relates to #1509

<a href="https://gitpod.io/#https://github.com/postlund/pyatv/pull/1520"><img src="https://gitpod.io/button/open-in-gitpod.svg"/></a>



codecov[bot]: # [Codecov](https://codecov.io/gh/postlund/pyatv/pull/1520?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) Report
> Merging [#1520](https://codecov.io/gh/postlund/pyatv/pull/1520?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) (5130d6f) into [master](https://codecov.io/gh/postlund/pyatv/commit/f0c4c33d1c4abb8d3ba69b13fa90996f5c1a97e6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) (f0c4c33) will **decrease** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/postlund/pyatv/pull/1520/graphs/tree.svg?width=650&height=150&src=pr&token=wb6azJjRSY&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl)](https://codecov.io/gh/postlund/pyatv/pull/1520?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl)

```diff
@@            Coverage Diff             @@
##           master    #1520      +/-   ##
==========================================
- Coverage   92.34%   92.34%   -0.01%     
==========================================
  Files         150      150              
  Lines        9734     9726       -8     
==========================================
- Hits         8989     8981       -8     
  Misses        745      745              
```


| [Impacted Files](https://codecov.io/gh/postlund/pyatv/pull/1520?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) | Coverage Δ | |
|---|---|---|
| [pyatv/protocols/dmap/pairing.py](https://codecov.io/gh/postlund/pyatv/pull/1520/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvcHJvdG9jb2xzL2RtYXAvcGFpcmluZy5weQ==) | `97.67% <100.00%> (+0.79%)` | :arrow_up: |
| [pyatv/support/net.py](https://codecov.io/gh/postlund/pyatv/pull/1520/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvc3VwcG9ydC9uZXQucHk=) | `92.06% <100.00%> (+0.26%)` | :arrow_up: |
| [pyatv/protocols/raop/raop.py](https://codecov.io/gh/postlund/pyatv/pull/1520/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvcHJvdG9jb2xzL3Jhb3AvcmFvcC5weQ==) | `91.41% <0.00%> (-0.26%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/postlund/pyatv/pull/1520?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/postlund/pyatv/pull/1520?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl). Last update [f0c4c33...5130d6f](https://codecov.io/gh/postlund/pyatv/pull/1520?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl).


Patch:
diff --git a/base_versions.txt b/base_versions.txt
index 4f86e56e..a8ee773e 100644
--- a/base_versions.txt
+++ b/base_versions.txt
@@ -1,9 +1,9 @@
 aiohttp==3.1.0,<5
 bitarray==2.1.2
 cryptography==2.6
+ifaddr==0.1.7
 mediafile==0.8.1
 miniaudio==1.45
-netifaces==0.10.0
 protobuf==3.18.0,<4
 srptools==0.2.0
 zeroconf==0.28.2
diff --git a/pyatv/protocols/dmap/pairing.py b/pyatv/protocols/dmap/pairing.py
index 8bf1ccf5..6e9f96cc 100644
--- a/pyatv/protocols/dmap/pairing.py
+++ b/pyatv/protocols/dmap/pairing.py
@@ -3,40 +3,26 @@
 import asyncio
 import hashlib
 from io import StringIO
-from ipaddress import IPv4Address, ip_address
+from ipaddress import IPv4Address
 import logging
 import random
 from typing import List, Optional
 
 from aiohttp import web
-import netifaces
 from zeroconf import Zeroconf
 
 from pyatv.core import mdns
 from pyatv.interface import BaseConfig, BaseService, PairingHandler
 from pyatv.protocols.dmap import tags
 from pyatv.support.http import ClientSessionManager
-from pyatv.support.net import unused_port
+from pyatv.support.net import get_private_addresses, unused_port
 
 _LOGGER = logging.getLogger(__name__)
 
 
-# Maybe replace with pyatv.net.get_local_address_reaching?
-def _get_private_ip_addresses():
-    for iface in netifaces.interfaces():
-        addresses = netifaces.ifaddresses(iface)
-        if netifaces.AF_INET not in addresses:
-            continue
-
-        for addr in addresses[netifaces.AF_INET]:
-            ipaddr = ip_address(addr["addr"])
-            if ipaddr.is_private and not ipaddr.is_loopback:
-                yield ipaddr
-
-
 def _get_zeroconf_addresses(addresses: Optional[List[str]]) -> List[IPv4Address]:
     if addresses is None:
-        return list(_get_private_ip_addresses())
+        return list(get_private_addresses(include_loopback=False))
 
     return [IPv4Address(address) for address in addresses]
 
diff --git a/pyatv/support/net.py b/pyatv/support/net.py
index fe4b362f..4e445ae4 100644
--- a/pyatv/support/net.py
+++ b/pyatv/support/net.py
@@ -7,7 +7,7 @@ import socket
 import struct
 from typing import List, Optional
 
-import netifaces
+from ifaddr import get_adapters
 
 from pyatv import exceptions
 
@@ -54,22 +54,25 @@ def mcast_socket(address: Optional[str], port: int = 0) -> socket.socket:
 
 def get_local_address_reaching(dest_ip: IPv4Address) -> Optional[IPv4Address]:
     """Get address of a local interface within same subnet as provided address."""
-    for iface in netifaces.interfaces():
-        for addr in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []):
-            iface = IPv4Interface(addr["addr"] + "/" + addr["netmask"])
+    for adapter in get_adapters():
+        for addr in [addr for addr in adapter.ips if addr.is_IPv4]:
+            iface = IPv4Interface(f"{addr.ip}/{addr.network_prefix}")
             if dest_ip in iface.network:
                 return iface.ip
     return None
 
 
-def get_private_addresses() -> List[IPv4Address]:
+def get_private_addresses(include_loopback=True) -> List[IPv4Address]:
     """Get private (RFC1918 + loopback) addresses from all interfaces."""
     addresses: List[IPv4Address] = []
-    for iface in netifaces.interfaces():
-        for addr in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []):
-            ipaddr = IPv4Address(addr["addr"])
+    for adapter in get_adapters():
+        for addr in [addr for addr in adapter.ips if addr.is_IPv4]:
+            ipaddr = IPv4Address(addr.ip)
+            if ipaddr.is_loopback and not include_loopback:
+                continue
             if ipaddr.is_private:
                 addresses.append(ipaddr)
+
     return addresses
 
 
diff --git a/pylintrc b/pylintrc
index f712a407..f0516c19 100644
--- a/pylintrc
+++ b/pylintrc
@@ -1,6 +1,5 @@
 [MASTER]
 reports=no
-extension-pkg-whitelist=netifaces
 
 [BASIC]
 good-names=
diff --git a/pyproject.toml b/pyproject.toml
index 0f8ffefe..cadbe52c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,7 +21,6 @@ forced_separate = [
 module = [
     "miniaudio",
     "audio_metadata",
-    "netifaces",
     "srptools",
     "bitarray",
 ]
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index e38d71d3..61c61573 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -1,9 +1,9 @@
 aiohttp==3.8.1
 bitarray==2.3.4
 cryptography==36.0.0
+ifaddr==0.1.7
 mediafile==0.9.0
 miniaudio==1.45
-netifaces==0.11.0
 protobuf==3.19.1
 srptools==1.0.1
 zeroconf==0.37.0
diff --git a/scripts/setup_dev_env.sh b/scripts/setup_dev_env.sh
index e1d36b38..b81876b5 100755
--- a/scripts/setup_dev_env.sh
+++ b/scripts/setup_dev_env.sh
@@ -43,12 +43,6 @@ pip install protobuf-setuptools
 echo "-> Installing dependencies..."
 pip install --upgrade pyyaml -r requirements/requirements_test.txt -r requirements/requirements_docs.txt
 
-if [[ $GITPOD_INSTANCE_ID ]]; then
-  echo "> Re-installing netifaces in GitPod due to bug"
-  pip uninstall -y netifaces
-  pip install netifaces
-fi
-
 echo "-> Running tests as verification..."
 python setup.py test
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index a46ded87..b3182aa8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,7 +3,7 @@ from types import SimpleNamespace
 import typing
 from unittest.mock import Mock, patch
 
-import netifaces
+from ifaddr import IP, Adapter
 import pytest
 
 import pyatv
@@ -29,27 +29,17 @@ def pytest_configure(config):
 
 
 @pytest.fixture(autouse=True)
-def stub_netifaces():
+def stub_ifaddr():
     methods = {
-        "interfaces": Mock(return_value=["eth0"]),
-        "ifaddresses": Mock(
-            return_value={
-                netifaces.AF_INET: [
-                    {
-                        "addr": "10.0.10.1",
-                        "netmask": "255.255.255.0",
-                        "broadcast": "10.0.10.255",
-                    },
-                    {
-                        "addr": "127.0.0.1",
-                        "netmask": "255.0.0.0",
-                    },
-                ]
-            }
-        ),
+        "get_adapters": Mock(
+            return_value=[
+                Adapter("eth0", "eth0", [IP("10.0.10.1", 24, "eth0")]),
+                Adapter("lo", "lo", [IP("127.0.0.1", 8, "lo")]),
+            ]
+        )
     }
-
-    with patch.multiple("netifaces", **methods):
+    with patch.multiple("pyatv.support.net", **methods):
+        print("patch")
         yield
 
 
diff --git a/tests/support/test_net.py b/tests/support/test_net.py
index eee4ae29..b04265aa 100644
--- a/tests/support/test_net.py
+++ b/tests/support/test_net.py
@@ -7,7 +7,7 @@ import sys
 from typing import Dict, List
 from unittest.mock import patch
 
-import netifaces
+from ifaddr import IP, Adapter
 import pytest
 
 from pyatv.exceptions import NotSupportedError
@@ -34,26 +34,15 @@ def skip_before_win_build(build_number: int):
 
 @pytest.fixture(autouse=True)
 def mock_address():
+    adapters: List[Adapter] = []
     addresses: Dict[str, List[str]] = {}
 
-    def _add(interface: str, address: IPv4Address):
-        addresses.setdefault(interface, []).append(address)
+    def _add(interface: str, address: str):
+        adapters.append(Adapter(interface, interface, [IP(address, 32, interface)]))
 
-    def _ifaddresses(interface: str):
-        iface_addresses = addresses.get(interface)
-        if not iface_addresses:
-            return {}
-
-        inet_addresses = [
-            {"addr": str(addr), "netmask": "255.255.255.0"} for addr in iface_addresses
-        ]
-        return {netifaces.AF_INET: inet_addresses}
-
-    with patch("netifaces.interfaces") as mock_interfaces:
-        with patch("netifaces.ifaddresses") as mock_ifaddr:
-            mock_interfaces.side_effect = lambda: list(addresses.keys())
-            mock_ifaddr.side_effect = _ifaddresses
-            yield _add
+    with patch("pyatv.support.net.get_adapters") as mock_adapters:
+        mock_adapters.side_effect = lambda: adapters
+        yield _add
 
 
 @pytest.fixture
@@ -90,6 +79,15 @@ def test_private_addresses(mock_address):
     ]
 
 
+def test_private_addresses(mock_address):
+    mock_address("wlan0", "10.0.0.1")
+    mock_address("lo", "127.0.0.1")
+
+    assert get_private_addresses(include_loopback=False) == [
+        ip_address("10.0.0.1"),
+    ]
+
+
 def test_public_addresses(mock_address):
     mock_address("eth0", "1.2.3.4")
     mock_address("eth1", "8.8.8.8")