Skip to content

Commit

Permalink
docstrings!
Browse files Browse the repository at this point in the history
  • Loading branch information
GhostofGoes committed Jul 18, 2024
1 parent ac38cd1 commit e994814
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 21 deletions.
10 changes: 6 additions & 4 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
- [ ] Add guide on using the modules API, e.g. registering a new method in `getmac.getmac.METHODS`, etc.
- [ ] Single page on RTD/publish with GitHub actions built with Sphinx and Furo
- [ ] Update docs/usage examples for `get_mac_address()`
- [ ] Document possible values for `PLATFORM` variable
- [ ] Document Method (and subclass) attributes (use Sphinx "#:" comments)
- [x] Document possible values for `PLATFORM` variable
- [x] Document Method (and subclass) attributes (use Sphinx "#:" comments)
- [ ] Re-add Man pages (and auto-build them in CI and include in releases and the distributions)
- [ ] Document `get_by_method()`
- [ ] Document `initialize_method_cache()`
- [ ] Auto-generated API docs
- [ ] Add docstrings to all util methods
- [x] Add docstrings to all util methods
- Furo, sphinx-autodoc-typehints, sphinx-argparse-cli, sphinx-automodapi, sphinx-copybutton, recommonmark

## Tests
Expand Down Expand Up @@ -43,7 +43,7 @@


## Breaking changes (or potentially breaking)
- [ ] Split getmac.py into separate files for methods, utils, etc.
- [x] Split getmac.py into separate files for methods, utils, etc.
- [ ] Replace the `UuidArpGetNode` method. It calls 3 commands and is quite inefficient, and doesn't exist in Python 3.9+. We should just take the methods and use directly.
- [ ] **Consolidate `ip6` argument into `ip` argument.**. Parse based on `::` character vs `.` character if `str` or via `.version == 4`/`.version == 6` for `ipaddress` objects.
- Combine `--ip` and `--ip6` CLI arguments into `--ip` output. this would make it *much* easier to test methods.
Expand Down Expand Up @@ -104,6 +104,8 @@


# Etc
- [ ] Add [isort](https://pycqa.github.io/isort/) (requires python 3.8+)
- [ ] cache the result of executable checks in `getmac.utils.popen()`
- [ ] Refactor the default interface code. Combine the functions into
one, move the default fallback logic into the function.
- TODO: MAC -> IP. "to_find='mac'"? (create GitHub issue?)
Expand Down
40 changes: 25 additions & 15 deletions getmac/getmac.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
.. code-block:: python
:caption: Examples
from getmac import get_mac_address
eth_mac = get_mac_address(interface="eth0")
win_mac = get_mac_address(interface="Ethernet 3")
ip_mac = get_mac_address(ip="192.168.0.1")
ip6_mac = get_mac_address(ip6="::1")
host_mac = get_mac_address(hostname="localhost")
updated_mac = get_mac_address(ip="10.0.0.1", network_request=True)
from getmac import get_mac_address
eth_mac = get_mac_address(interface="eth0")
win_mac = get_mac_address(interface="Ethernet 3")
ip_mac = get_mac_address(ip="192.168.0.1")
ip6_mac = get_mac_address(ip6="::1")
host_mac = get_mac_address(hostname="localhost")
updated_mac = get_mac_address(ip="10.0.0.1", network_request=True)
"""

Expand Down Expand Up @@ -83,7 +83,7 @@ def get(self, arg: str) -> Optional[str]: # noqa: ARG002
internal error with the command, or a bug in the code).
Args:
arg (str): What the method should get, such as an IP address
arg: What the method should get, such as an IP address
or interface name. In the case of default_iface methods,
this is not used and defaults to an empty string.
Expand Down Expand Up @@ -1068,15 +1068,19 @@ def get_method_by_name(method_name: str) -> Optional[Type[Method]]:

def get_instance_from_cache(method_type: str, method_name: str) -> Optional[Method]:
"""
Get the class for a named Method from the caches.
Get the class for a named :class:`~getmac.getmac.Method` from the caches.
METHOD_CACHE is checked first, and if that fails,
then any entries in FALLBACK_CACHE are checked.
If both fail, None is returned.
:data:`~getmac.getmac.METHOD_CACHE` is checked first, and if that fails,
then any entries in :data:`~getmac.getmac.FALLBACK_CACHE` are checked.
If both fail, :obj:`None` is returned.
Args:
method_type: method type to initialize the cache for.
method_type: what cache should be checked.
Allowed values are: ``ip4`` | ``ip6`` | ``iface`` | ``default_iface``
method_name: name of the method to look for
Returns:
The cached method, or :obj:`None` if the method was not found
"""

if str(METHOD_CACHE[method_type]) == method_name:
Expand Down Expand Up @@ -1126,7 +1130,10 @@ def initialize_method_cache(method_type: str, network_request: bool = True) -> b
method_type: method type to initialize the cache for.
Allowed values are: ``ip4`` | ``ip6`` | ``iface`` | ``default_iface``
network_request: if methods that make network requests should be included
(those methods that have the attribute ``network_request`` set to ``True``)
(those methods that have the attribute ``network_request`` set to :obj:`True`)
Returns:
If the cache was initialized successfully
"""
if METHOD_CACHE.get(method_type):
if settings.DEBUG:
Expand Down Expand Up @@ -1334,7 +1341,10 @@ def get_by_method(
Allowed values are: ``ip4``, ``ip6``, ``iface``, ``default_iface``
arg: Argument to pass to the method, e.g. an interface name or IP address
network_request: if methods that make network requests should be included
(those methods that have the attribute ``network_request`` set to ``True``)
(those methods that have the attribute ``network_request`` set to :obj:`True`)
Returns:
The MAC address string, or :obj:`None` if the operation failed
"""
if not arg and method_type != "default_iface":
gvars.log.error(f"Empty arg for method '{method_type}' (raw value: {arg!r})")
Expand Down
101 changes: 100 additions & 1 deletion getmac/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@

def check_command(command: str) -> bool:
"""
Check if a command exists using `shutil.which()`. The result of the check
Check if a command exists using :func:`shutil.which`. The result of the check
is cached in a global dict to speed up subsequent lookups.
Args:
command: command to check
Returns:
If the command exists
"""
if command not in gvars.CHECK_COMMAND_CACHE:
gvars.CHECK_COMMAND_CACHE[command] = bool(which(command, path=gvars.PATH_STR))
Expand All @@ -28,13 +34,32 @@ def check_command(command: str) -> bool:
def check_path(filepath: str) -> bool:
"""
Check if the file pointed to by `filepath` exists and is readable.
Args:
filepath: absolute path of file to check
Returns:
If the filepath exists and is readable
"""
return os.path.exists(filepath) and os.access(filepath, os.R_OK)


def clean_mac(mac: Optional[str]) -> Optional[str]:
"""
Check and format a string result to be lowercase colon-separated MAC.
It will clean out any garbage and ensure the length and colons are correct,
and replace ``-`` characters with ``:`` characters.
If string is invalid after as much cleanup as possible, then :obj:`None`
is returned. The specific issue is logged as a warning.
Args:
mac: MAC address string to clean
Returns:
Cleaned and formatted MAC address string, or :obj:`None` if
validation failed.
"""
if mac is None:
return None
Expand Down Expand Up @@ -81,6 +106,16 @@ def clean_mac(mac: Optional[str]) -> Optional[str]:


def read_file(filepath: str) -> Optional[str]:
"""
Open and read a file.
Args:
filepath: Absolute path of the file to read
Returns:
Text contents of the file, or :obj:`None` if opening
the file failed.
"""
try:
with open(filepath) as f:
return f.read()
Expand All @@ -92,6 +127,21 @@ def read_file(filepath: str) -> Optional[str]:
def search(
regex: str, text: str, group_index: int = 0, flags: int = 0
) -> Optional[str]:
"""
Search for a regular expression in a string, and return the specified group.
This is thin wrapper around :func:`re.search` with some error handling.
Args:
regex: regular expression
text: data to search
group_index: what index in the ``groupdict`` to return,
if there are more than 1
flags: :mod:`re` flags
Returns:
The result, or :obj:`None` if the parsing failed
or nothing was specified to search.
"""
if not text:
if settings.DEBUG:
gvars.log.debug("No text to _search()")
Expand All @@ -105,8 +155,29 @@ def search(


def popen(command: str, args: str) -> str:
"""
Execute a command with arguments and return the stdout (stderr is discarded).
Wrapper around :func:`~getmac.utils.call_proc`, with checks
to ensure the command exists and is executable and some debug
logging. This should be used instead of
:func:`~getmac.utils.call_proc`.
Args:
command: command to run, e.g. ``ping`` or ``ping.exe``
args: arguments to pass to the command, or empty string
if there are no arguments.
Returns:
stdout from the command (stderr is discarded)
Raises:
CalledProcessError: the command failed to execute
"""
for directory in gvars.PATH:
executable = os.path.join(directory, command)
# TODO: cache the result of these checks? these are system calls
# and they can add up.
if (
os.path.exists(executable)
and os.access(executable, os.F_OK | os.X_OK)
Expand All @@ -123,6 +194,22 @@ def popen(command: str, args: str) -> str:


def call_proc(executable: str, args: str) -> str:
"""
Wrapper around :func:`subprocess.check_output` with some
logging and type conversion. The reason this and
:func:`~getmac.utils.popen` are separate functions is
for testability.
Args:
executable: command to run
args: arguments to the command
Returns:
stdout from the command (stderr is discarded)
Raises:
CalledProcessError: the command failed to execute
"""
if consts.WINDOWS:
cmd = executable + " " + args # type: ignore
else:
Expand All @@ -142,6 +229,15 @@ def call_proc(executable: str, args: str) -> str:


def uuid_convert(mac: int) -> str:
"""
Convert value output from ``uuid`` internal function into a string.
Args:
mac: integer value returned from a ``uuid`` function
Returns:
String with colon-separated MAC address
"""
return ":".join(("%012X" % mac)[i : i + 2] for i in range(0, 12, 2))


Expand All @@ -152,6 +248,9 @@ def fetch_ip_using_dns() -> str:
Sends a UDP packet to Cloudflare's DNS (``1.1.1.1``), which should go through
the default interface. This populates the source address of the socket,
which we then inspect and return.
Returns:
IP address of this system's default network interface as a string
"""
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(("1.1.1.1", 53))
Expand Down
19 changes: 18 additions & 1 deletion getmac/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,29 @@ class Constants(VarsClass):
hasattr(sys, "getandroidapilevel") or "ANDROID_STORAGE" in os.environ
)

#: Generic platform identifier used for filtering methods
#: Generic platform identifier used for filtering methods.
#:
#: Possible values:
#:
#: - wsl
#: - linux
#: - windows
#: - darwin
#: - openbsd
#: - freebsd
#: - netbsd
#: - sunos
#: - any other values that can be returned by :func:`platform.uname`,
#: converted to lowercase.
# TODO: change to "wsl1", since WSL2 method should just work like normal linux
PLATFORM: str = "wsl" if (LINUX and WSL1) else _SYST.lower()

#: Regular expression pattern for MAC addresses with ':' characters
MAC_RE_COLON: str = r"([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})"

#: Regular expression pattern for MAC addresses with '-' characters
MAC_RE_DASH: str = r"([0-9a-fA-F]{2}(?:-[0-9a-fA-F]{2}){5})"

#: On OSX, some MACs in arp output may have a single digit instead of two
#: Examples: "18:4f:32:5a:64:5", "14:cc:20:1a:99:0"
#: This can also happen on other platforms, like Solaris
Expand Down

0 comments on commit e994814

Please sign in to comment.