netimps 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {netimps-0.2.0 → netimps-0.2.2}/AGENTS.md +35 -14
  2. {netimps-0.2.0 → netimps-0.2.2}/CHANGELOG.md +88 -1
  3. {netimps-0.2.0 → netimps-0.2.2}/PKG-INFO +8 -4
  4. {netimps-0.2.0 → netimps-0.2.2}/README.md +5 -2
  5. {netimps-0.2.0 → netimps-0.2.2}/pyproject.toml +2 -1
  6. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/AGENTS.md +108 -42
  7. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/__init__.py +3 -1
  8. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_dns.py +180 -70
  9. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_iface_spec.py +85 -2
  10. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_ip.py +81 -17
  11. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_multicast.py +62 -34
  12. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_ping.py +172 -57
  13. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_scan.py +17 -8
  14. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_sockets.py +66 -22
  15. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_udp.py +12 -3
  16. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/cli.py +2 -2
  17. {netimps-0.2.0 → netimps-0.2.2}/tests/test_ip.py +66 -0
  18. {netimps-0.2.0 → netimps-0.2.2}/tests/test_net.py +322 -8
  19. {netimps-0.2.0 → netimps-0.2.2}/tests/test_scan.py +139 -1
  20. {netimps-0.2.0 → netimps-0.2.2}/tests/test_sockets.py +242 -0
  21. {netimps-0.2.0 → netimps-0.2.2}/.gitignore +0 -0
  22. {netimps-0.2.0 → netimps-0.2.2}/LICENSE +0 -0
  23. {netimps-0.2.0 → netimps-0.2.2}/docs/api/reference.md +0 -0
  24. {netimps-0.2.0 → netimps-0.2.2}/docs/changelog.md +0 -0
  25. {netimps-0.2.0 → netimps-0.2.2}/docs/index.md +0 -0
  26. {netimps-0.2.0 → netimps-0.2.2}/mkdocs.yml +0 -0
  27. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/__main__.py +0 -0
  28. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_ifaddrs.py +0 -0
  29. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_mac.py +0 -0
  30. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_retry.py +0 -0
  31. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/_scheme.py +0 -0
  32. {netimps-0.2.0 → netimps-0.2.2}/src/netimps/py.typed +0 -0
  33. {netimps-0.2.0 → netimps-0.2.2}/tests/test_centralized.py +0 -0
  34. {netimps-0.2.0 → netimps-0.2.2}/tests/test_cli.py +0 -0
  35. {netimps-0.2.0 → netimps-0.2.2}/tests/test_interfaces.py +0 -0
  36. {netimps-0.2.0 → netimps-0.2.2}/tests/test_mac.py +0 -0
  37. {netimps-0.2.0 → netimps-0.2.2}/tests/typing/api.py +0 -0
@@ -62,19 +62,22 @@ Requires Python 3.9+. No hard runtime dependencies.
62
62
 
63
63
  ```
64
64
  src/netimps/
65
- ├── __init__.py # the public surface: generic parse/try_parse/is_valid
66
- ├── _ip.py # private: IP type aliases, builder tables, IP helpers
67
- ├── _mac.py # private: MACAddress value type
68
- ├── _scheme.py # private: scheme <-> port registry
69
- ├── cli.py # public: duho-backed CLI (needs the `cli` extra)
70
- ├── __main__.py # `python -m netimps`
71
- ├── _scan.py # private: concurrent port/host scanning
72
- ├── _multicast.py # private: group membership and socket setup
73
- ├── _ifaddrs.py # private: ctypes getifaddrs/GetAdaptersAddresses bindings
74
- ├── _sockets.py # private: source IP, free port, tcp/wait, route, hops, MTU
75
- ├── _dns.py # private: resolve() chaining dnspython/system/nslookup backends
76
- ├── _ping.py # private: ping() over the platform binary
77
- └── py.typed # PEP 561 marker — the package ships inline type hints
65
+ ├── __init__.py # the public surface: generic parse/try_parse/is_valid
66
+ ├── _ip.py # private: IP type aliases, builder tables, IP helpers
67
+ ├── _mac.py # private: MACAddress value type
68
+ ├── _scheme.py # private: scheme <-> port registry
69
+ ├── cli.py # public: duho-backed CLI (needs the `cli` extra)
70
+ ├── __main__.py # `python -m netimps`
71
+ ├── _scan.py # private: concurrent port/host scanning
72
+ ├── _multicast.py # private: group membership and socket setup
73
+ ├── _ifaddrs.py # private: ctypes getifaddrs/GetAdaptersAddresses bindings
74
+ ├── _sockets.py # private: source IP, free port, tcp/wait, route, hops, MTU
75
+ ├── _dns.py # private: resolve() chaining dnspython/system/nslookup backends
76
+ ├── _ping.py # private: ping() over the platform binary
77
+ ├── _retry.py # private: bounded retry with exponential backoff
78
+ ├── _udp.py # private: UDP receive with arrival interface (IP_PKTINFO)
79
+ ├── _iface_spec.py # private: shared InterfaceSpec coercion (MAC/name/Interface -> address)
80
+ └── py.typed # PEP 561 marker — the package ships inline type hints
78
81
  ```
79
82
 
80
83
  **The import surface is still flat** — everything is re-exported from
@@ -96,6 +99,7 @@ map:
96
99
  | --- | --- |
97
100
  | `IPAddress`, `IPInterface`, `IPNetwork` | v4/v6 **union aliases** for annotations |
98
101
  | `IPAddressLike`, `IPInterfaceLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
102
+ | `AddressLike` | accepted-input union for a single destination (hostname, address, or interface object) |
99
103
  | `IPv4Address`, `IPv4Interface`, `IPv4Network`, `IPv6Address`, `IPv6Interface`, `IPv6Network` | stdlib concrete-type re-exports |
100
104
  | `parse`, `try_parse`, `is_valid` | build a type from a value (raising / `None` / `bool`) |
101
105
  | `MACAddress` | parse / classify / render MAC addresses |
@@ -104,7 +108,7 @@ map:
104
108
  | `collapse`, `subtract` | CIDR set maths |
105
109
  | `normalize_host` | `host:port` splitting, IPv6-aware |
106
110
  | `get_default_port`, `get_default_scheme`, `register_port` | scheme ↔ port registry |
107
- | `resolve` | DNS lookup → native records (`[]` on failure) |
111
+ | `resolve`, `resolve_dnspython`, `resolve_system`, `resolve_nslookup` | DNS lookup → native records (`[]` on failure); `resolve` chains the three backends, each independently callable |
108
112
  | `ping`, `PingResult` | reachability with RTT and TTL |
109
113
  | `bind`, `bind_error_hint`, `interface_for`, `interfaces_for`, `is_local_address` | socket creation and local membership |
110
114
  | `get_source_ip`, `get_free_port`, `tcp_check`, `wait_for_port` | socket helpers |
@@ -144,6 +148,23 @@ map:
144
148
  - **Check for silent platform gaps before adding a socket option.** `IP_MTU`,
145
149
  `IP_MTU_DISCOVER`, `IP_DONTFRAG` and `SO_REUSEPORT` do not exist on Windows;
146
150
  binding a multicast socket to the group address fails there too.
151
+ - **IPv6 multicast names an adapter by *index*, IPv4 by *address*.** They are
152
+ not two spellings of one thing: feeding an address to the v6 side does not
153
+ raise, it lands as index `0`, which is "kernel's choice". Use
154
+ `_iface_spec.interface_index()` for anything v6, `interface_address()` for
155
+ v4.
156
+ - **POSIX delivers asynchronous ICMP errors only to *connected* UDP sockets.**
157
+ An unconnected probe never sees a port-unreachable and just times out, while
158
+ Windows reports it either way — so the unconnected version tests green here
159
+ and under-reports on Linux CI. Also note `_discover_mtu_udp` is still
160
+ unconnected by design; it treats such errors as failure anyway.
161
+ - **`ThreadPoolExecutor.__exit__` calls `shutdown(wait=True)`,** and its atexit
162
+ hook joins worker threads too. It is therefore the wrong tool for bounding a
163
+ blocking call: a daemon `threading.Thread` joined through a queue is what
164
+ `_dns._resolve_system_once` uses, and why.
165
+ - **Match a ping reply by address token, and remember hostnames are plural.**
166
+ `gethostbyname` is IPv4-only — use `getaddrinfo` with an explicit family, or
167
+ `ipv6=` silently does nothing.
147
168
  - **Windows exposes no cached path MTU.** Already investigated, so do not
148
169
  re-derive it: `MIB_IPFORWARDROW.dwForwardMtu` reads 0 (unsupported), and
149
170
  `MIB_IPFORWARD_ROW2` has no MTU field. `Interface.mtu` is the link MTU;
@@ -7,6 +7,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.2] - 2026-08-16
11
+
12
+ ### Fixed
13
+
14
+ - **`resolve_system(timeout=)` now bounds wall time.** The lookup ran inside a
15
+ `with ThreadPoolExecutor(...)` block, whose `__exit__` joins the worker
16
+ still blocked in `getaddrinfo` -- so the timeout changed *what* was raised
17
+ but not *when*, and a 30s resolver hang still cost the caller 30s despite
18
+ `timeout=5.0`. `resolve()`'s chain consequently never reached `nslookup` at
19
+ the promised deadline either.
20
+ - **`interface=` is honoured for IPv6 multicast.** The spec was reduced to an
21
+ address and then passed to `if_nametoindex()`, which always fails for an
22
+ address string, so the `IPV6_JOIN_GROUP` index silently stayed `0` --
23
+ "kernel's choice", the exact default `interface=` exists to override.
24
+ `IPV6_MULTICAST_IF` was never set at all, so sends left by the default
25
+ route while joins listened elsewhere. Both now resolve the adapter to its
26
+ interface index. IPv4 behaviour is unchanged.
27
+ - **`ping(hostname, ipv6=True)` is no longer always false.** The reply address
28
+ was resolved with the IPv4-only `gethostbyname`, so a v6 reply was checked
29
+ against a v4 expectation and never matched. The expectation now comes from
30
+ `getaddrinfo` honouring `ipv6=`, and a name resolving to several addresses
31
+ counts as answered if the reply came from any of them.
32
+ - **`ping(..., method="tcp"/"udp")` reaches IPv6 destinations.** Both probes
33
+ opened `AF_INET` sockets unconditionally, so a v6 destination failed inside
34
+ `connect`/`sendto` and was reported as unreachable -- a wrong falsy answer
35
+ rather than an error. `ipv6=` now applies to all three methods.
36
+ - **`ping(..., method="udp")` detects ICMP port-unreachable on POSIX.** The
37
+ probe socket was never connected, and POSIX delivers asynchronous ICMP
38
+ errors only to connected UDP sockets -- so the documented "host answered,
39
+ nothing listening" signal worked on Windows alone and the probe just timed
40
+ out on Linux/macOS. `ECONNREFUSED` and `ECONNRESET` both now count.
41
+
42
+ ### Documentation
43
+
44
+ - The shipped API header named `PingResult.source` and `Route.source`; both
45
+ attributes are spelled `.src` (and `PingResult.host` was unlisted).
46
+ - Recorded the per-family multicast interface selection, `ping`'s `ipv6=`
47
+ reach across all three methods, and `resolve_system`'s real wall-time
48
+ deadline in the shipped header.
49
+ - Added the known, still-unverified macOS/BSD `ping6` reply-shape gap
50
+ (`from <addr>,` rather than `from <addr>:`) to the header rather than
51
+ guessing a parser change without a macOS runner.
52
+
53
+ ## [0.2.1] - 2026-07-30
54
+
55
+ ### Added
56
+
57
+ - **`AddressLike`**, a new type alias (`str | IPv4Address | IPv6Address |
58
+ IPv4Interface | IPv6Interface`) accepted by every `dst`-typed parameter:
59
+ `ping`, `tcp_check`, `wait_for_port`, `get_route`, `hop_count`, `get_pmtu`,
60
+ `discover_mtu`, `get_tcp_mss`, `scan_ports(host)`, `get_ip`,
61
+ `UdpEndpoint.send`, and `resolve`'s (and its backends') `query`. An
62
+ `IPv4Interface`/`IPv6Interface` unwraps to its `.ip` -- previously passing
63
+ one stringified with its `/prefix` intact, which every consumer
64
+ (subprocess argument, socket call, DNS query) read as garbage. A network
65
+ (`IPv4Network`/`IPv6Network`) raises `TypeError`, since it has no single
66
+ address to use.
67
+ - **`resolve()` (and all three backends) auto-select `rdtype`.** It now
68
+ defaults to `None`, which picks `"ptr"` when `query` is an address literal
69
+ and `"a"` otherwise -- `resolve("8.8.8.8")` now returns `['dns.google']`
70
+ instead of attempting a nonsensical A lookup on a literal address. Pass an
71
+ explicit `rdtype` to opt out. `resolve_system()` gains `"ptr"` support (via
72
+ `socket.gethostbyaddr()`) to make this work across every backend.
73
+
74
+ ### Fixed
75
+
76
+ - **`ping(src=...)` crashed with `NameError` instead of returning a falsy
77
+ result** when `src` named an interface with no usable address (e.g. an
78
+ unknown adapter name, or a MAC not currently present) -- a leftover
79
+ reference to an undefined `hostname` variable instead of `dst`. Found via
80
+ a `mypy` pass while auditing type annotations; a regression test now
81
+ covers the path.
82
+
83
+ ### Changed
84
+
85
+ - Public functions across the package now carry complete parameter and
86
+ return type annotations (previously missing on, among others, `collapse`,
87
+ `subtract`, `get_ip`, `PingResult`, `Route`, `scan_hosts`, `is_multicast`,
88
+ `join_group`/`leave_group`, `multicast_socket`, `UdpEndpoint`, and `bind`).
89
+ The recurring "loose interface spec" parameter (`ping(src=)`,
90
+ `bind(interface=)`, `discover_mtu(src=)`, `multicast_socket(interface=)`,
91
+ etc.) now shares one internal type alias instead of being unannotated at
92
+ each call site.
93
+
10
94
  ## [0.2.0] - 2026-07-29
11
95
 
12
96
  ### Added
@@ -155,7 +239,10 @@ below is simply what the package contains.
155
239
  - **`Host`**, **`retry()`/`backoff_delays()`**, and the named networks `APIPA`,
156
240
  `LOOPBACK_V4`, `LOOPBACK_V6`, `LINK_LOCAL_V6`.
157
241
 
158
- [Unreleased]: https://github.com/jose-pr/netimps/compare/v0.1.0...HEAD
242
+ [Unreleased]: https://github.com/jose-pr/netimps/compare/v0.2.2...HEAD
243
+ [0.2.2]: https://github.com/jose-pr/netimps/compare/v0.2.1...v0.2.2
244
+ [0.2.1]: https://github.com/jose-pr/netimps/compare/v0.2.0...v0.2.1
245
+ [0.2.0]: https://github.com/jose-pr/netimps/compare/v0.1.0...v0.2.0
159
246
  [0.1.0]: https://github.com/jose-pr/netimps/compare/v0.0.2...v0.1.0
160
247
  [0.0.2]: https://github.com/jose-pr/netimps/releases/tag/v0.0.2
161
248
  [0.0.1]: https://github.com/jose-pr/netimps/releases/tag/v0.0.1
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: netimps
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Small, self-contained network utilities: IP/MAC types, DNS lookup, ping, NIC discovery
5
5
  Project-URL: Homepage, https://github.com/jose-pr/netimps
6
6
  Project-URL: Documentation, https://jose-pr.github.io/netimps/
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
19
20
  Classifier: Topic :: System :: Networking
20
21
  Classifier: Typing :: Typed
21
22
  Requires-Python: >=3.9
@@ -86,8 +87,9 @@ nothing to compile and no wheel to miss for your platform.
86
87
  - **Multicast** — `multicast_socket` handling the join dance whose failure
87
88
  modes are otherwise silent.
88
89
  - **DNS and ping** — `resolve()` chaining `dnspython`/OS resolver/`nslookup`
89
- backends and returning native types; `ping()` returning round-trip time and
90
- TTL, not just a boolean.
90
+ backends and returning native types, accepting an IP/interface object
91
+ directly and auto-selecting a reverse lookup for an address query; `ping()`
92
+ returning round-trip time and TTL, not just a boolean.
91
93
 
92
94
  ## Installation
93
95
 
@@ -106,6 +108,7 @@ library never requires either.
106
108
  netimps interfaces # names, MACs, MTU, addresses
107
109
  netimps ping 8.8.8.8 -m tcp -p 443 # icmp | tcp | udp
108
110
  netimps resolve example.com aaaa
111
+ netimps resolve 8.8.8.8 # no rdtype -> auto ptr -> dns.google
109
112
  netimps check example.com https # port number or scheme name
110
113
  netimps mtu 8.8.8.8 # measured, not guessed
111
114
  netimps scan 192.0.2.0/29 -p common
@@ -193,6 +196,7 @@ netimps.retry(lambda: netimps.tcp_check("example.com", 443), attempts=3)
193
196
  | --- | --- |
194
197
  | `IPAddress`, `IPInterface`, `IPNetwork` | v4/v6 **union aliases** for annotations |
195
198
  | `IPAddressLike`, `IPInterfaceLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
199
+ | `AddressLike` | accepted-input union for a single destination (hostname, address, or `IPv4Interface`/`IPv6Interface` — its `.ip` is used) |
196
200
  | `IPv4Address`, `IPv4Interface`, ... | stdlib concrete-type re-exports |
197
201
  | `parse`, `try_parse`, `is_valid` | build a type from a value (raising / `None` / `bool`) |
198
202
  | `MACAddress` | parse / classify / render MAC addresses |
@@ -46,8 +46,9 @@ nothing to compile and no wheel to miss for your platform.
46
46
  - **Multicast** — `multicast_socket` handling the join dance whose failure
47
47
  modes are otherwise silent.
48
48
  - **DNS and ping** — `resolve()` chaining `dnspython`/OS resolver/`nslookup`
49
- backends and returning native types; `ping()` returning round-trip time and
50
- TTL, not just a boolean.
49
+ backends and returning native types, accepting an IP/interface object
50
+ directly and auto-selecting a reverse lookup for an address query; `ping()`
51
+ returning round-trip time and TTL, not just a boolean.
51
52
 
52
53
  ## Installation
53
54
 
@@ -66,6 +67,7 @@ library never requires either.
66
67
  netimps interfaces # names, MACs, MTU, addresses
67
68
  netimps ping 8.8.8.8 -m tcp -p 443 # icmp | tcp | udp
68
69
  netimps resolve example.com aaaa
70
+ netimps resolve 8.8.8.8 # no rdtype -> auto ptr -> dns.google
69
71
  netimps check example.com https # port number or scheme name
70
72
  netimps mtu 8.8.8.8 # measured, not guessed
71
73
  netimps scan 192.0.2.0/29 -p common
@@ -153,6 +155,7 @@ netimps.retry(lambda: netimps.tcp_check("example.com", 443), attempts=3)
153
155
  | --- | --- |
154
156
  | `IPAddress`, `IPInterface`, `IPNetwork` | v4/v6 **union aliases** for annotations |
155
157
  | `IPAddressLike`, `IPInterfaceLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
158
+ | `AddressLike` | accepted-input union for a single destination (hostname, address, or `IPv4Interface`/`IPv6Interface` — its `.ip` is used) |
156
159
  | `IPv4Address`, `IPv4Interface`, ... | stdlib concrete-type re-exports |
157
160
  | `parse`, `try_parse`, `is_valid` | build a type from a value (raising / `None` / `bool`) |
158
161
  | `MACAddress` | parse / classify / render MAC addresses |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "netimps"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  authors = [{ name = "Jose A." }]
9
9
  description = "Small, self-contained network utilities: IP/MAC types, DNS lookup, ping, NIC discovery"
10
10
  readme = "README.md"
@@ -19,6 +19,7 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3.11",
20
20
  "Programming Language :: Python :: 3.12",
21
21
  "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
22
23
  "Operating System :: OS Independent",
23
24
  "Topic :: System :: Networking",
24
25
  "Typing :: Typed",
@@ -16,7 +16,7 @@ is self-contained: it references nothing outside the installed distribution.
16
16
  testing, releasing) is not shipped; it lives with the source at
17
17
  <https://github.com/jose-pr/netimps>.
18
18
 
19
- `netimps.__version__` — the package version string (currently `"0.2.0"`).
19
+ `netimps.__version__` — the package version string (currently `"0.2.2"`).
20
20
 
21
21
  ## Argument naming
22
22
 
@@ -24,7 +24,7 @@ The package is consistent about what the first argument means:
24
24
 
25
25
  | Name | Meaning | Examples |
26
26
  | --- | --- | --- |
27
- | `dst` | where traffic is **sent** | `ping`, `tcp_check`, `wait_for_port`, `get_route`, `hop_count`, `discover_mtu` |
27
+ | `dst` | where traffic is **sent** | `ping`, `tcp_check`, `wait_for_port`, `get_route`, `hop_count`, `discover_mtu`, `get_tcp_mss`, `get_pmtu`, `scan_ports(host)` |
28
28
  | `src` | where traffic is **sent from** | `ping(src=)`, `get_free_port(src=)`, `discover_mtu(src=)` |
29
29
  | `host` / `network` | the thing being **examined** | `scan_ports(host)`, `scan_hosts(network)` |
30
30
  | `address` / `ip` | an address being **classified** (no DNS) | `get_ip`, `interface_for`, `interfaces_for`, `is_local_address`, `is_multicast`, `is_link_scoped` |
@@ -32,6 +32,14 @@ The package is consistent about what the first argument means:
32
32
  `dst`/`src` are abbreviated symmetrically, matching packet-header convention.
33
33
  A `dst` accepts a hostname; an `address` does not.
34
34
 
35
+ **Every `dst`-typed parameter accepts `AddressLike`** — a hostname string, an
36
+ address string, an existing `IPv4Address`/`IPv6Address`, or an
37
+ `IPv4Interface`/`IPv6Interface` (its `.ip` is used, dropping the `/prefix`,
38
+ which every consumer of a destination -- a subprocess argument, a socket
39
+ call, a DNS query -- would otherwise read as garbage). A network
40
+ (`IPv4Network`/`IPv6Network`) raises `TypeError`, since it has no single
41
+ address to send to. `get_ip` and `resolve`'s `query` accept the same forms.
42
+
35
43
  ## Types vs parsing — read this first
36
44
 
37
45
  The noun names are **types you annotate with**; you turn values into them with
@@ -57,6 +65,7 @@ The union aliases are **not callable** — `IPAddress("10.0.0.5")` is a
57
65
  | `IPAddressLike` | anything accepted *as input* for an address |
58
66
  | `IPInterfaceLike` | anything accepted *as input* for an address + prefix |
59
67
  | `IPNetworkLike` | anything accepted *as input* for a network |
68
+ | `AddressLike` | `str \| IPv4Address \| IPv6Address \| IPv4Interface \| IPv6Interface` -- any `dst`-typed parameter |
60
69
  | `MACLike` | `str \| int \| bytes \| MACAddress` |
61
70
 
62
71
  Plus the stdlib concretes re-exported so callers need not import `ipaddress`:
@@ -212,7 +221,22 @@ failure** (NXDOMAIN, NODATA, timeout) — never `None` — with **native
212
221
  types**: `A`/`AAAA` records are `ipaddress` objects, everything else is
213
222
  `str` (trailing root dot stripped, TXT strings unquoted).
214
223
 
215
- **`resolve(query, rdtype="a", ns=None, timeout=5.0, port=53, tcp=False, search=True, backends=None)`**
224
+ **`resolve(query, rdtype=None, ns=None, timeout=5.0, port=53, tcp=False, search=True, backends=None)`**
225
+
226
+ `query` accepts `AddressLike` (a hostname string, an address string, an
227
+ `IPv4Address`/`IPv6Address`, or an `IPv4Interface`/`IPv6Interface` -- its
228
+ `.ip` is used), not just a plain string.
229
+
230
+ `rdtype=None` (default) **auto-selects**: `"ptr"` when `query` is an address
231
+ literal (an `"a"`/`"aaaa"` lookup *of* an address makes no sense), `"a"`
232
+ otherwise -- the same default as before this was configurable::
233
+
234
+ resolve("example.com") # rdtype auto -> "a" -> ['93.184.216.34']
235
+ resolve("8.8.8.8") # rdtype auto -> "ptr" -> ['dns.google']
236
+
237
+ Pass an explicit `rdtype` to opt out -- `rdtype="a"` on an address still
238
+ attempts a literal (and empty) A lookup rather than being silently
239
+ overridden.
216
240
 
217
241
  Tries each backend in `backends` (default `["dnspython", "system",
218
242
  "nslookup"]`) until one gives a **definitive** answer — records, or a real
@@ -224,16 +248,21 @@ the last such error is raised. `backends` also accepts a single name as a
224
248
  plain string (`backends="system"`), or a custom order/subset
225
249
  (`backends=["nslookup", "dnspython"]`).
226
250
 
227
- - **`system`** is skipped automatically for a non-address `rdtype` or an
228
- explicit `ns=`/`port=` — it has no per-call nameserver override, so running
229
- it anyway would silently ignore the caller's choice.
251
+ - **`system`** is skipped automatically for a `rdtype` outside
252
+ `"a"`/`"aaaa"`/`"ptr"`, or an explicit `ns=`/`port=` — it has no per-call
253
+ nameserver override, so running it anyway would silently ignore the
254
+ caller's choice.
230
255
  - A malformed query or unknown record type raises `ValueError` immediately,
231
256
  without trying every backend — that's a caller bug, not a resolution
232
257
  outcome.
233
258
 
234
- **`resolve_dnspython(query, rdtype="a", ns=None, timeout=5.0, port=53, tcp=False, search=True)`**
259
+ **`resolve_dnspython(query, rdtype=None, ns=None, timeout=5.0, port=53, tcp=False, search=True)`**
235
260
 
236
- The original backend: `dnspython`, structured records, every `rdtype`.
261
+ The original backend: `dnspython`, structured records, every `rdtype`. Same
262
+ `AddressLike` `query` and auto-`rdtype` behavior as `resolve()`. A `"ptr"`
263
+ lookup (explicit or auto-selected) uses dnspython's `resolve_address()`,
264
+ which builds the reverse (`in-addr.arpa`/`ip6.arpa`) name from the literal
265
+ address itself -- the caller never constructs that name by hand.
237
266
 
238
267
  - **`ns=None` (default) uses the system resolver configuration** —
239
268
  `/etc/resolv.conf` on POSIX, the registry on Windows. Pass `ns=` (a string
@@ -249,25 +278,30 @@ The original backend: `dnspython`, structured records, every `rdtype`.
249
278
  already-qualified (trailing-dot) `query`.
250
279
  - `timeout` bounds the **whole resolution including retries**, so a list of
251
280
  dead nameservers cannot run past it.
252
- - Requires `dnspython` — the package's only runtime dependency. Raises
253
- `ResolutionError` (not `ValueError`) if it isn't installed, so `resolve()`'s
254
- chain falls through to the next backend instead of erroring outright.
255
-
256
- **`resolve_system(query, rdtype="a", timeout=5.0, search=True)`**
257
-
258
- The OS resolver, via `socket.getaddrinfo()` — **hosts file, NSS
259
- (`nsswitch.conf`) and DNS, in the order the OS applies them**, including any
260
- OS-level resolver cache. This is what `resolve_dnspython` cannot see (its own
261
- DNS query bypasses all of that).
262
-
263
- - **Address records only**: `rdtype` must be `"a"` or `"aaaa"`; anything else
264
- raises `ResolutionError` immediately, no query attempted.
265
- - **No `ns=` override** — `getaddrinfo` always asks whatever resolver the OS
266
- is configured with; there's no per-call nameserver parameter at that layer
267
- (not even via `ctypes` — reaching a specific nameserver without shelling
268
- out means speaking DNS wire protocol yourself, which is what `dnspython`
269
- already does).
270
- - **`search`**: `getaddrinfo` itself takes no search-list parameter either, so
281
+ - `dnspython` is an **optional** dependency (`pip install netimps[dns]`).
282
+ Raises `ResolutionError` (not `ValueError`) if it isn't installed, so
283
+ `resolve()`'s chain falls through to the next backend instead of erroring
284
+ outright.
285
+
286
+ **`resolve_system(query, rdtype=None, timeout=5.0, search=True)`**
287
+
288
+ The OS resolver, via `socket.getaddrinfo()`/`socket.gethostbyaddr()` — **hosts
289
+ file, NSS (`nsswitch.conf`) and DNS, in the order the OS applies them**,
290
+ including any OS-level resolver cache. This is what `resolve_dnspython`
291
+ cannot see (its own DNS query bypasses all of that). Same `AddressLike`
292
+ `query` and auto-`rdtype` behavior as `resolve()`.
293
+
294
+ - **Address and reverse records only**: `rdtype` must be `"a"`, `"aaaa"` or
295
+ `"ptr"`; anything else raises `ResolutionError` immediately, no query
296
+ attempted. `"ptr"` goes through `gethostbyaddr()` rather than
297
+ `getaddrinfo()` and returns `[hostname]`.
298
+ - **No `ns=` override** — the OS resolver functions always ask whatever
299
+ nameserver the OS is configured with; there's no per-call parameter at that
300
+ layer (not even via `ctypes` — reaching a specific nameserver without
301
+ shelling out means speaking DNS wire protocol yourself, which is what
302
+ `dnspython` already does).
303
+ - **`search`** (ignored for `"ptr"`, which has no suffix to expand):
304
+ `getaddrinfo` itself takes no search-list parameter either, so
271
305
  `search=True` (default) just leaves `query` as given and the OS resolver's
272
306
  own configured search list (glibc `ndots`/`search`, Windows per-adapter DNS
273
307
  suffix) applies as it normally would. `search=False` appends a trailing
@@ -275,12 +309,18 @@ DNS query bypasses all of that).
275
309
  trick `host`/`getent` scripts use. A **list of domain names** tries `query`
276
310
  qualified with each, in order, one `getaddrinfo` call per candidate,
277
311
  independent of (and untouched by) the OS's own search list.
312
+ - **`timeout` bounds wall time, per candidate name tried.** `getaddrinfo` has
313
+ no timeout of its own, so each attempt runs in a daemon helper thread that
314
+ is abandoned at the deadline; the underlying call is not cancelled, but
315
+ neither the caller nor interpreter exit waits for it. A broken resolver
316
+ therefore costs `timeout`, not however long it takes to give up — which is
317
+ also what lets `resolve()`'s chain reach `nslookup` on schedule.
278
318
 
279
- **`resolve_nslookup(query, rdtype="a", ns=None, timeout=5.0, search=True)`**
319
+ **`resolve_nslookup(query, rdtype=None, ns=None, timeout=5.0, search=True)`**
280
320
 
281
321
  Shells out to the `nslookup` binary — a fallback for when neither Python-level
282
322
  path is usable. Address records only: `rdtype` must be `"a"`, `"aaaa"` or
283
- `"ptr"`.
323
+ `"ptr"`. Same `AddressLike` `query` and auto-`rdtype` behavior as `resolve()`.
284
324
 
285
325
  - Parses **both BIND-style** (`Address: 1.2.3.4`, one line per address) **and
286
326
  Windows-style** (`Addresses:` with continuation lines, and NODATA as a bare
@@ -288,12 +328,13 @@ path is usable. Address records only: `rdtype` must be `"a"`, `"aaaa"` or
288
328
  its NXDOMAIN message on **stderr**, not stdout — both streams are checked.
289
329
  - `ns=` is passed as `nslookup`'s trailing `server` argument (a single
290
330
  nameserver, not a list).
291
- - **`search`** has the same three-way contract as the other backends, but
292
- `nslookup` has no built-in search-list handling — this issues one
293
- `nslookup` call per candidate name, in order, stopping at the first with
294
- actual records. `search=True` (default) draws the candidate list from the
295
- system resolver's search config (reusing `dnspython`'s `resolv.conf`/
296
- registry parsing if it's installed; `[]` — literal name only — if not).
331
+ - **`search`** has the same three-way contract as the other backends
332
+ (ignored for `"ptr"`), but `nslookup` has no built-in search-list
333
+ handling — this issues one `nslookup` call per candidate name, in order,
334
+ stopping at the first with actual records. `search=True` (default) draws
335
+ the candidate list from the system resolver's search config (reusing
336
+ `dnspython`'s `resolv.conf`/registry parsing if it's installed; `[]` —
337
+ literal name only — if not).
297
338
  - Raises `ResolutionError` (not `ValueError`) for a missing binary, a
298
339
  timeout, or an unparseable output shape — a genuine "no such name" is
299
340
  still `[]`.
@@ -303,11 +344,12 @@ path is usable. Address records only: `rdtype` must be `"a"`, `"aaaa"` or
303
344
  **`ping(dst, tries=1, timeout=1.0, ipv6=None, src=None, size=None, ttl=None, dont_fragment=False, method="icmp", port=None) -> PingResult`**
304
345
 
305
346
  `PingResult` is **truthy on success** and compares equal to `bool`, so
306
- `if ping(host):` and `== True` keep working, while carrying `.ok`, `.rtt_ms`,
307
- `.ttl`, `.source`, `.attempts`.
347
+ `if ping(host):` and `== True` keep working, while carrying `.ok`, `.host`,
348
+ `.rtt_ms`, `.ttl`, `.src`, `.attempts`.
308
349
 
309
350
  | Argument | Notes |
310
351
  | --- | --- |
352
+ | `dst` | `AddressLike` (hostname, address string, address object, or `IPv4Interface`/`IPv6Interface` -- its `.ip` is pinged). `ping(get_interfaces()[0].ipv4[0])` works directly. |
311
353
  | `src` | `Interface`, address, **MAC**, adapter name or string. A MAC is resolved to the adapter holding it. |
312
354
  | `size` | ICMP payload bytes. The wire packet is **28 bytes larger** (20 IP + 8 ICMP). |
313
355
  | `ttl` | initial hop limit — `-i` on Windows, `-t` on POSIX (the letters are **swapped**). |
@@ -324,6 +366,19 @@ failure. `tcp` and `udp` also report `rtt_ms`; only ICMP reports `ttl`.
324
366
  "TTL expired in transit", so the reply address is verified rather than
325
367
  trusting the exit code. Locale-independent — it matches on addresses, never
326
368
  prose.
369
+ - **`ipv6=` applies to all three methods**, not just the ICMP binary: it picks
370
+ the `-6`/`-4` flag, the family the reply address is resolved in, and the
371
+ family the `tcp`/`udp` probe sockets use. `ipv6=None` accepts either. A
372
+ hostname with several addresses counts as answered if the reply came from
373
+ any of them.
374
+ - **The `udp` probe connects its socket before sending**, which is why the ICMP
375
+ port-unreachable is seen on POSIX and not only on Windows — an unconnected
376
+ UDP socket is never delivered an asynchronous ICMP error on Linux/BSD.
377
+ `ECONNREFUSED` and `ECONNRESET` both count as "the host answered".
378
+ - **Known gap: macOS/BSD `ping6` reply lines.** Verification looks for
379
+ `from <addr>:`; BSD `ping6` is documented as printing `from <addr>,`. If
380
+ that holds, a v6 *literal* ping on macOS verifies as falsy despite a healthy
381
+ reply. Unverified on real hardware, so deliberately not "fixed" by guess.
327
382
  - An unusable `src` (unknown MAC, adapter with no address, foreign address)
328
383
  gives a falsy result — it **never silently falls back** to the default route.
329
384
  - Never raises: missing binary, hung subprocess and non-zero exit are all falsy.
@@ -363,9 +418,11 @@ failure. `tcp` and `udp` also report `rtt_ms`; only ICMP reports `ttl`.
363
418
  or reachable alone do not count. Malformed input raises like `parse`;
364
419
  loopback answers before interface discovery.
365
420
  - **`get_source_ip(dst="8.8.8.8", port=80)`** — which local address the kernel
366
- would use to reach `dst`. **Sends no packets.** The answer depends on
367
- `dst`: with a VPN up, a public probe returns the tunnel address and a LAN
368
- probe the physical one. Correct where hostname resolution picks a VM adapter.
421
+ would use to reach `dst`. `dst` accepts `AddressLike` (an address object or
422
+ `IPv4Interface`/`IPv6Interface`, not just a string). **Sends no packets.**
423
+ The answer depends on `dst`: with a VPN up, a public probe returns the
424
+ tunnel address and a LAN probe the physical one. Correct where hostname
425
+ resolution picks a VM adapter.
369
426
  - **`get_free_port(src="127.0.0.1", family=AF_INET) -> int`** — bind port 0 and
370
427
  read it back. **Inherently racy** — the port frees the instant it returns; if
371
428
  you can, bind port 0 in the server itself instead. `SO_REUSEADDR` is
@@ -379,7 +436,7 @@ failure. `tcp` and `udp` also report `rtt_ms`; only ICMP reports `ttl`.
379
436
 
380
437
  ## Routing, hops and MTU
381
438
 
382
- - **`get_route(dst="8.8.8.8") -> Route`** — `.source`, `.gateway`,
439
+ - **`get_route(dst="8.8.8.8") -> Route`** — `.dst`, `.src`, `.gateway`,
383
440
  `.interface_index`, `.on_link`. **First hop only, deliberately** — that is
384
441
  available unprivileged everywhere, unlike the full path. Never raises;
385
442
  unknown pieces are `None`/`0`. The gateway resolves on Windows and Linux only.
@@ -463,6 +520,12 @@ receives nothing, and looks fine:
463
520
  *both* send and receive. Without it the kernel picks by routing table, which
464
521
  on a multi-homed host is regularly the wrong adapter. An unknown interface
465
522
  **raises** rather than falling back.
523
+ - **The two families identify an adapter differently**, and the spec is
524
+ resolved accordingly: IPv4 by local *address* (`IP_ADD_MEMBERSHIP`,
525
+ `IP_MULTICAST_IF`), IPv6 by interface *index* (`IPV6_JOIN_GROUP`,
526
+ `IPV6_MULTICAST_IF`). An adapter the platform reports **no index** for
527
+ raises for an IPv6 group, because index `0` means "kernel's choice" — the
528
+ default `interface=` was passed to override.
466
529
 
467
530
  ## UDP with arrival interface
468
531
 
@@ -473,7 +536,9 @@ network a request came from.
473
536
 
474
537
  `recv(bufsize, resolve_interface=True) -> Datagram`, with `.data`, `.sender`,
475
538
  `.local_address`, `.interface_index` and `.interface`.
476
- `send(data, address, port, source=None)` pins the outgoing interface.
539
+ `send(data, address, port, src=None)` pins the outgoing interface. `address`
540
+ accepts `AddressLike`; `src` the usual loose interface spec (`Interface`,
541
+ MAC, adapter name or address).
477
542
 
478
543
  - **Degrades rather than failing.** `recvmsg` does not exist on Windows and
479
544
  `IP_PKTINFO` is not universal; there the interface fields are simply empty.
@@ -526,6 +591,7 @@ dependency on the CLI half.
526
591
  netimps interfaces # names, MACs, MTU, addresses
527
592
  netimps ping 8.8.8.8 -m tcp -p 443 # icmp | tcp | udp
528
593
  netimps resolve example.com aaaa
594
+ netimps resolve 8.8.8.8 # no rdtype -> auto ptr -> dns.google
529
595
  netimps check example.com https # port or scheme name
530
596
  netimps route 8.8.8.8 --hops
531
597
  netimps mtu 8.8.8.8 -m udp -p 9999
@@ -74,6 +74,7 @@ from ._ip import (
74
74
  _BUILDERS,
75
75
  _BUILDER_DEFAULTS,
76
76
  _CONCRETE,
77
+ AddressLike,
77
78
  IPAddress,
78
79
  IPAddressLike,
79
80
  IPInterface,
@@ -102,6 +103,7 @@ __all__ = [
102
103
  "IPAddressLike",
103
104
  "IPInterfaceLike",
104
105
  "IPNetworkLike",
106
+ "AddressLike",
105
107
  "MACLike",
106
108
  # Parsing.
107
109
  "parse",
@@ -161,7 +163,7 @@ __all__ = [
161
163
  "HOST_DN",
162
164
  ]
163
165
 
164
- __version__ = "0.2.0"
166
+ __version__ = "0.2.2"
165
167
 
166
168
  #: Fully-qualified (or short) name of the host running this process.
167
169
  HOST_DN = _platform.node()