From 12763e3e61b9dc512bc20a61d48ed087c2192dc6 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 6 Apr 2026 15:01:52 +0000 Subject: [PATCH 1/3] Make library py.typed --- getmac/getmac.py | 83 +++++++++++++++++++++++++++---------------- getmac/py.typed | 2 ++ getmac/shutilwhich.py | 12 +++++-- setup.py | 2 ++ 4 files changed, 67 insertions(+), 32 deletions(-) create mode 100644 getmac/py.typed diff --git a/getmac/getmac.py b/getmac/getmac.py index 7587ec1..f053b4f 100644 --- a/getmac/getmac.py +++ b/getmac/getmac.py @@ -30,16 +30,18 @@ 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 = subprocess.DEVNULL # type: Union[int, IO[bytes]] +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 @@ -47,7 +49,7 @@ 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 @@ -79,7 +81,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: @@ -271,10 +273,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) @@ -303,7 +306,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 @@ -338,7 +341,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] @@ -359,7 +362,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 @@ -466,7 +469,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") @@ -508,9 +511,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)) @@ -638,16 +644,25 @@ 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: @@ -655,19 +670,19 @@ def get(self, arg): # type: (str) -> Optional[str] # 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: @@ -723,19 +738,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) @@ -787,7 +807,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 @@ -1379,7 +1399,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 @@ -1527,8 +1548,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 @@ -1871,7 +1893,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() diff --git a/getmac/py.typed b/getmac/py.typed new file mode 100644 index 0000000..139597f --- /dev/null +++ b/getmac/py.typed @@ -0,0 +1,2 @@ + + diff --git a/getmac/shutilwhich.py b/getmac/shutilwhich.py index 771335a..589cbb7 100644 --- a/getmac/shutilwhich.py +++ b/getmac/shutilwhich.py @@ -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. @@ -28,6 +34,7 @@ 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 @@ -35,7 +42,8 @@ def _access_check(fn, mode): 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. @@ -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) diff --git a/setup.py b/setup.py index 938d7e7..6897dd1 100644 --- a/setup.py +++ b/setup.py @@ -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) @@ -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", From 0b4af4ee0a6969f5f3b81ad6c384a50c2ad6eedd Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 6 Apr 2026 15:08:56 +0000 Subject: [PATCH 2/3] tweak --- CHANGELOG.md | 5 +++++ README.md | 1 + getmac/getmac.py | 1 + 3 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d9211f..b0610c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 7aad003..31c399a 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/getmac/getmac.py b/getmac/getmac.py index f053b4f..e8aa54e 100644 --- a/getmac/getmac.py +++ b/getmac/getmac.py @@ -23,6 +23,7 @@ updated_mac = get_mac_address(ip="10.0.0.1", network_request=True) """ + import ctypes import logging import os From 5c1c8e888f6e446b7c8e9d7b28b5d7bf3758b1fe Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 6 Apr 2026 15:18:25 +0000 Subject: [PATCH 3/3] tweak --- getmac/getmac.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/getmac/getmac.py b/getmac/getmac.py index e8aa54e..a0323c5 100644 --- a/getmac/getmac.py +++ b/getmac/getmac.py @@ -25,6 +25,7 @@ """ import ctypes +from io import BufferedWriter import logging import os import platform @@ -38,7 +39,7 @@ import warnings from subprocess import CalledProcessError, check_output -DEVNULL = subprocess.DEVNULL # type: Union[int, IO[bytes]] +DEVNULL: int | BufferedWriter try: DEVNULL = subprocess.DEVNULL except AttributeError: # Python 2