Customizing methods at runtime

Warning

Most of what this page describes lives in getmac.getmac and is an internal API that may change between releases. Use it with care.

This page covers changing which methods getmac uses, from your own code: adding a method, removing a built-in one, and undoing your changes in tests. How methods are chosen and used explains how methods are chosen, which is worth reading first, and Architecture has a “Where things live” section on what to import from where.

Adding a custom method

You can add your own methods at runtime, for example to support a command getmac doesn’t know about. If the method would be useful to others, consider contributing it to getmac, see Adding a new method.

Writing the class

The examples below use this method, which always returns the same MAC. Save it as my_methods.py.

my_methods.py
from typing import Optional

from getmac.getmac import Method


class FixedMacIface(Method):
    """Always returns the same MAC address. Only useful as an example."""

    platforms = {"linux", "darwin", "windows"}
    method_type = "iface"

    def test(self) -> bool:
        return True

    def get(self, arg: str) -> Optional[str]:
        return "02:00:00:00:00:01"

Things the example doesn’t show:

  • Set network_request = True if get() can result in network traffic (e.g. an ARP request).

  • Keep test() cheap, such as checking that a command or file exists. If it raises, getmac treats it as a failed test.

  • Return None from get() when the MAC wasn’t found. If it can’t work on this system, raise an exception or set self.unusable = True instead, so getmac moves on to the next method (see Falling back during a lookup).

  • A default_iface method’s get() is called with an empty string, so define it as def get(self, arg: str = "").

The rest of the rules, like the platforms and method_type values, are in Platforms and Method types. The “Write the method” section of Adding a new method goes through them in more detail, and applies to methods kept in your own code too (outside getmac.py, you import utils and consts yourself, as the example below does).

getmac.utils has helpers for running commands (popen()), reading files (read_file()) and matching output (search()), and Constants has regular expressions for MAC addresses, such as MAC_RE_COLON.

Here’s a more realistic method. It parses the output of ip -brief link show dev <iface>, which looks like lo  UNKNOWN  00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>.

from typing import Optional

from getmac import utils
from getmac.getmac import Method
from getmac.variables import consts


class IpBriefLinkIface(Method):
    """Get the MAC of an interface from ``ip -brief link show dev <iface>``."""

    platforms = {"linux"}
    method_type = "iface"

    def test(self) -> bool:
        return utils.check_command("ip")

    def get(self, arg: str) -> Optional[str]:
        # If the interface doesn't exist, "ip" exits with code 1, which getmac
        # treats as "not found". Other exit codes make getmac try the next method.
        output = utils.popen("ip", f"-brief link show dev {arg}")
        return utils.search(r"^\S+\s+\S+\s+" + consts.MAC_RE_COLON, output)

Registering it

Add the class to METHODS before the first lookup of its type (e.g. "iface"). The order of METHODS is the order methods are tested in, so insert it at the front to make it the primary method:

from getmac import get_mac_address
from getmac.getmac import METHODS

from my_methods import FixedMacIface

METHODS.insert(0, FixedMacIface)  # Before the first "iface" lookup

print(get_mac_address(interface="eth0"))

Output:

02:00:00:00:00:01

If you append it instead, any built-in method of the same type that passes its test comes first, and yours becomes a fallback.

Change the list in place, as above, or replace the module attribute (getmac.getmac.METHODS = [...]).

If lookups already happened

Adding a method doesn’t affect a type whose cache is already built. To make getmac choose again, set that type’s entry in METHOD_CACHE to None and empty its FALLBACK_CACHE list. The next lookup of that type tests METHODS again.

from getmac import get_mac_address
from getmac.getmac import FALLBACK_CACHE, METHOD_CACHE, METHODS

from my_methods import FixedMacIface

print(get_mac_address(interface="lo"))  # Builds the "iface" cache

METHODS.insert(0, FixedMacIface)
print(get_mac_address(interface="lo"))  # Still uses the method chosen earlier

METHOD_CACHE["iface"] = None  # Forget the "iface" selection
FALLBACK_CACHE["iface"] = []
print(get_mac_address(interface="lo"))  # METHODS is tested again

Example output on Linux:

00:00:00:00:00:00
00:00:00:00:00:00
02:00:00:00:00:01

To start over completely, reset every type. Also clear gvars.DEFAULT_IFACE, where get_mac_address() remembers the default interface after the first call without arguments. Without that, a new default_iface method is never used by get_mac_address().

from getmac.getmac import FALLBACK_CACHE, METHOD_CACHE
from getmac.variables import gvars


def reset_method_caches() -> None:
    """Forget every method getmac has chosen, and the cached default interface."""
    for method_type in METHOD_CACHE:
        METHOD_CACHE[method_type] = None
        FALLBACK_CACHE[method_type] = []
    gvars.DEFAULT_IFACE = ""

check_command() also caches whether each command exists, in gvars.CHECK_COMMAND_CACHE. Clear that dict as well if commands were installed or removed since the first lookup.

To undo these changes in tests, see Undoing changes in tests.

Using a method without registering it

You can also put an instance straight into METHOD_CACHE. This skips METHODS, test() and the platform check. Fallbacks that are already cached are kept, but if that type’s cache hadn’t been built yet, there are none.

from getmac import get_mac_address
from getmac.getmac import METHOD_CACHE

from my_methods import FixedMacIface

METHOD_CACHE["iface"] = FixedMacIface()

print(get_mac_address(interface="eth0"))  # 02:00:00:00:00:01

The instance stays the primary method until it fails and getmac falls back.

Forcing a custom method

FORCE_METHOD only finds methods that are in METHODS, so register the method first. Where it is in the list doesn’t matter.

from getmac import get_mac_address, settings
from getmac.getmac import METHODS

from my_methods import FixedMacIface

METHODS.append(FixedMacIface)
settings.FORCE_METHOD = "FixedMacIface"

print(get_mac_address(interface="eth0"))  # 02:00:00:00:00:01

Removing a built-in method

To stop getmac from using a built-in method, for example one that misbehaves on your system, remove it from METHODS. As with adding a method, reset the type’s cache if a lookup of that type already happened. On the command line, use --exclude-method (see Command-Line Interface).

from getmac import get_mac_address
from getmac.getmac import FALLBACK_CACHE, METHOD_CACHE, METHODS, SysIfaceFile

METHODS.remove(SysIfaceFile)
METHOD_CACHE["iface"] = None  # Only needed if an "iface" lookup already happened
FALLBACK_CACHE["iface"] = []

get_mac_address(interface="lo")
print(METHOD_CACHE["iface"], [str(m) for m in FALLBACK_CACHE["iface"]])

Example output on Linux:

FcntlIface ['IpLinkIface']

Undoing changes in tests

Changes to METHODS, METHOD_CACHE, FALLBACK_CACHE and settings affect the whole process, so tests that make them should undo them afterwards. With pytest, the built-in monkeypatch fixture can give each test its own copy of the list and caches, and puts the originals back when the test ends:

import getmac.getmac as gm
import pytest
from getmac import get_mac_address
from getmac.variables import gvars

from my_methods import FixedMacIface


@pytest.fixture
def isolated_getmac(monkeypatch):
    """Give the test its own METHODS list and empty caches."""
    monkeypatch.setattr(gm, "METHODS", [FixedMacIface, *gm.METHODS])
    monkeypatch.setattr(gm, "METHOD_CACHE", dict.fromkeys(gm.METHOD_CACHE))
    monkeypatch.setattr(gm, "FALLBACK_CACHE", {key: [] for key in gm.FALLBACK_CACHE})
    monkeypatch.setattr(gvars, "DEFAULT_IFACE", "")


def test_fixed_mac_is_used(isolated_getmac):
    assert get_mac_address(interface="eth0") == "02:00:00:00:00:01"

Use monkeypatch.setattr(settings, "FORCE_METHOD", ...) in the same way to change a setting for one test.