Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

**Announcement**: Compatibility with Python versions older than 3.7 (2.7, 3.4, 3.5, and 3.6) is deprecated and will be removed in getmac 1.0.0. If you are stuck on an unsupported Python, consider loosely pinning the version of this package in your dependency list, e.g. `getmac<1.0.0` or `getmac~=0.9.0`.

## 0.9.6 (06/04/2026)

### Changed
* Made library py.typed

## 0.9.5 (07/15/2024)

### Changed
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ The [Python Discord server](https://discord.gg/python) is a good place to ask qu
- Tomasz Duda (@tomaszduda23) - support for docker in network bridge mode
- Steven Looman (@StevenLooman) - Windows 11 testing
- Reimund Renner (@raymanP) - macOS fixes
- Simone Chemelli (@chemelli74) - py.typed the library

## Sources
Many of the methods used to acquire an address and the core logic framework are attributed to the CPython project's UUID implementation.
Expand Down
85 changes: 55 additions & 30 deletions getmac/getmac.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,35 @@
updated_mac = get_mac_address(ip="10.0.0.1", network_request=True)

"""

import ctypes
from io import BufferedWriter
import logging
import os
import platform
import re
import shlex
import socket
import subprocess
import struct
import sys
import traceback
import warnings
from subprocess import CalledProcessError, check_output

try: # Python 3
from subprocess import DEVNULL # type: ignore
except ImportError: # Python 2
DEVNULL = open(os.devnull, "wb") # type: ignore
DEVNULL: int | BufferedWriter
try:
DEVNULL = subprocess.DEVNULL
except AttributeError: # Python 2
DEVNULL = open(os.devnull, "wb")

# Used for mypy (a data type analysis tool)
# If you're copying the code, this section can be safely removed
try:
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Dict, List, Optional, Set, Tuple, Type, Union
from typing import Dict, IO, List, Optional, Set, Tuple, Type, Union
except ImportError:
pass

Expand Down Expand Up @@ -79,7 +83,7 @@
_SYST = _UNAME.system # type: str
if _SYST == "Java":
try:
import java.lang
import java.lang # type: ignore[import-not-found]

_SYST = str(java.lang.System.getProperty("os.name"))
except ImportError:
Expand Down Expand Up @@ -271,10 +275,11 @@ def _popen(command, args):

def _call_proc(executable, args):
# type: (str, str) -> str
cmd = "" # type: Union[str, List[str]]
if WINDOWS:
cmd = executable + " " + args # type: ignore
cmd = executable + " " + args
else:
cmd = [executable] + shlex.split(args) # type: ignore
cmd = [executable] + shlex.split(args)

output = check_output(cmd, stderr=DEVNULL, env=ENV)

Expand Down Expand Up @@ -303,7 +308,7 @@ def _fetch_ip_using_dns():
"""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("1.1.1.1", 53))
ip = s.getsockname()[0]
ip = str(s.getsockname()[0])
s.close() # NOTE: sockets don't have context manager in 2.7 :(
return ip

Expand Down Expand Up @@ -338,7 +343,7 @@ class Method:

def test(self): # type: () -> bool # noqa: T484
"""Low-impact test that the method is feasible, e.g. command exists."""
pass # pragma: no cover
raise NotImplementedError # pragma: no cover

# TODO: automatically clean MAC on return
def get(self, arg): # type: (str) -> Optional[str]
Expand All @@ -359,7 +364,7 @@ def get(self, arg): # type: (str) -> Optional[str]
Lowercase colon-separated MAC address, or None if one could
not be found.
"""
pass # pragma: no cover
raise NotImplementedError # pragma: no cover

@classmethod
def __str__(cls): # type: () -> str
Expand Down Expand Up @@ -466,7 +471,7 @@ class ArpVariousArgs(Method):
("-a", True), # "arp -a 192.168.1.1"
)
_args_tested = False # type: bool
_good_pair = () # type: Union[Tuple, Tuple[str, bool]]
_good_pair = None # type: Optional[Tuple[str, bool]]

def test(self): # type: () -> bool
return check_command("arp")
Expand Down Expand Up @@ -508,9 +513,12 @@ def get(self, arg): # type: (str) -> Optional[str]

if not command_output:
# if True, then include IP as a command argument
cmd_args = [self._good_pair[0]]
good_pair = self._good_pair
if good_pair is None:
return None
cmd_args = [good_pair[0]]

if self._good_pair[1]:
if good_pair[1]:
cmd_args.append(arg)

command_output = _popen("arp", " ".join(cmd_args))
Expand Down Expand Up @@ -638,36 +646,45 @@ class CtypesHost(Method):

def test(self): # type: () -> bool
try:
return ctypes.windll.wsock32.inet_addr(b"127.0.0.1") > 0 # noqa: T484
windll = getattr(ctypes, "windll", None)
if windll is None:
return False
result = windll.wsock32.inet_addr(b"127.0.0.1") # type: int
return result > 0
except Exception:
return False

def get(self, arg): # type: (str) -> Optional[str]
windll = getattr(ctypes, "windll", None)
if windll is None:
return None

arg_input = arg # type: Union[str, bytes]
if not PY2: # Convert to bytes on Python 3+ (Fixes GitHub issue #7)
arg = arg.encode() # type: ignore
arg_input = arg.encode()

try:
inetaddr = ctypes.windll.wsock32.inet_addr(arg) # type: ignore
inetaddr = windll.wsock32.inet_addr(arg_input)
if inetaddr in (0, -1):
raise Exception
except Exception:
# TODO: this assumes failure is due to arg being a hostname
# We should be explicit about only accepting ipv4 addresses
# and handle any hostname resolution in calling code
hostip = socket.gethostbyname(arg)
inetaddr = ctypes.windll.wsock32.inet_addr(hostip) # type: ignore
inetaddr = windll.wsock32.inet_addr(hostip)

buffer = ctypes.c_buffer(6)
addlen = ctypes.c_ulong(ctypes.sizeof(buffer))

# https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-sendarp
send_arp = ctypes.windll.Iphlpapi.SendARP # type: ignore
send_arp = windll.Iphlpapi.SendARP
if send_arp(inetaddr, 0, ctypes.byref(buffer), ctypes.byref(addlen)) != 0:
return None

# Convert binary data into a string.
macaddr = ""
for intval in struct.unpack("BBBBBB", buffer): # type: ignore
for intval in struct.unpack("BBBBBB", buffer):
if intval > 15:
replacestr = "0x"
else:
Expand Down Expand Up @@ -723,19 +740,24 @@ class UuidLanscan(Method):

def test(self): # type: () -> bool
try:
from uuid import _find_mac # noqa: T484
import uuid

return check_command("lanscan")
return bool(getattr(uuid, "_find_mac", None)) and check_command("lanscan")
except Exception:
return False

def get(self, arg): # type: (str) -> Optional[str]
from uuid import _find_mac # type: ignore
import uuid

_find_mac = getattr(uuid, "_find_mac", None)
if _find_mac is None:
return None

arg_input = arg # type: Union[str, bytes]
if not PY2:
arg = bytes(arg, "utf-8") # type: ignore
arg_input = bytes(arg, "utf-8")

mac = _find_mac("lanscan", "-ai", [arg], lambda i: 0)
mac = _find_mac("lanscan", "-ai", [arg_input], lambda i: 0)

if mac:
return _uuid_convert(mac)
Expand Down Expand Up @@ -787,7 +809,7 @@ class GetmacExe(Method):
# Network Adapter (the human-readable name)
(r"\r\n.*", r".*" + MAC_RE_DASH + r".*\r\n"),
] # type: List[Tuple[str, str]]
_champ = () # type: Union[tuple, Tuple[str, str]]
_champ = None # type: Optional[Tuple[str, str]]

def test(self): # type: () -> bool
# NOTE: the scripts from this library (getmac) are excluded from the
Expand Down Expand Up @@ -1379,7 +1401,8 @@ def _swap_method_fallback(method_type, swap_with):
curr = METHOD_CACHE[method_type]
FALLBACK_CACHE[method_type].remove(found)
METHOD_CACHE[method_type] = found
FALLBACK_CACHE[method_type].insert(0, curr) # noqa: T484
if curr is not None:
FALLBACK_CACHE[method_type].insert(0, curr)

return True

Expand Down Expand Up @@ -1527,8 +1550,9 @@ def initialize_method_cache(
)

# Populate fallback cache with all the tested methods, minus the currently active method
if METHOD_CACHE[method_type] and METHOD_CACHE[method_type] in tested_methods:
tested_methods.remove(METHOD_CACHE[method_type]) # noqa: T484
current_method = METHOD_CACHE[method_type]
if current_method and current_method in tested_methods:
tested_methods.remove(current_method)

FALLBACK_CACHE[method_type] = tested_methods

Expand Down Expand Up @@ -1871,7 +1895,8 @@ def get_mac_address( # noqa: C901
global DEFAULT_IFACE

if not DEFAULT_IFACE:
DEFAULT_IFACE = get_by_method("default_iface") # noqa: T484
default_iface = get_by_method("default_iface")
DEFAULT_IFACE = default_iface if default_iface else ""

if DEFAULT_IFACE:
DEFAULT_IFACE = str(DEFAULT_IFACE).strip()
Expand Down
2 changes: 2 additions & 0 deletions getmac/py.typed
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@


12 changes: 10 additions & 2 deletions getmac/shutilwhich.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@
import os
import sys

try:
from typing import List, Optional
except ImportError:
pass


# Everything below this point has been copied verbatim from the Python-3.3
# sources.
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
# type: (str, int, Optional[str]) -> Optional[str]
"""Given a command, mode, and a PATH string, return the path which
conforms to the given mode on the PATH, or None if there is no such
file.
Expand All @@ -28,14 +34,16 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
# type: (str, int) -> bool
return os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn)

# Short circuit. If we're given a full path which matches the mode
# and it exists, we're done here.
if _access_check(cmd, mode):
return cmd

path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep)
path_str = path if path is not None else os.environ.get("PATH", os.defpath)
path_parts = path_str.split(os.pathsep) # type: List[str]

if sys.platform == "win32":
# The current directory takes precedence on Windows.
Expand All @@ -56,7 +64,7 @@ def _access_check(fn, mode):
files = [cmd]

seen = set()
for dir in path:
for dir in path_parts:
dir = os.path.normcase(dir)
if dir not in seen:
seen.add(dir)
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
license="MIT",
data_files=[],
packages=["getmac"],
package_data={"getmac": ["py.typed"]},
zip_safe=True,
entry_points={"console_scripts": ["getmac2 = getmac.__main__:main"]}
if sys.version_info[:2] <= (2, 7)
Expand Down Expand Up @@ -82,6 +83,7 @@
"Programming Language :: Python :: Implementation :: PyPy",
"Programming Language :: Python :: Implementation :: IronPython",
"Programming Language :: Python :: Implementation :: Jython",
"Typing :: Typed",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"Intended Audience :: Information Technology",
Expand Down