Repository navigation
v0.4.0
Added
-
bind(..., device=<interface>)restricts a socket to one device, so a
wildcard socket receives what arrives on that adapter only: the way to listen
for the broadcasts of one adapter on a multi-homed host, which a socket bound to
the adapter's address cannot hear on Linux.has_device_binding()says
whether the platform can, andDeviceBindingUnsupportedError(a
NetimpsErrorand anOSError) is whatbind(device=...)raises where it
cannot. Linux usesSO_BINDTODEVICE, measured unprivileged on kernel 6.18 for
IPv4 and IPv6; Windows has no option that restricts receive, so it is
unsupported there; macOS 15.7 and FreeBSD 16.0 were measured and are
unsupported too (macOS acceptsIP_BOUND_IF, which scopes what is sent but
does not restrict what is received).device=withinterface=raisesValueError. -
format_address(address) -> strwrites an address object as the same text
on every Python: a v4-mapped IPv6 address in the mixed form
(::ffff:1.2.3.4, RFC 5952 section 5), wherestr()writes the hex form
(::ffff:102:304) before 3.13; any other address asstr()does, a%zone
kept and an interface's/prefixkept. It takes address and interface
objects and raisesTypeErrorfor anything else. -
classify(text)reads text as aMACAddress, anIPNetwork, an
IPInterfaceor anIPAddress, in that order of preference (a/makes it
a network, or an interface when host bits are set), and raises
NetimpsValueErrorfor anything else. It never asks a resolver.
PortsLikeaccepts a comma-separated string (scan_ports(host, "22,https,8000"); an empty item raises), andget_default_portaccepts a
port number as text and returns it as anint(outside0-65535raises
NetimpsValueError); the text is ASCII digits and nothing else, and a
schemethat is not text raisesTypeError. The command line used to do each of these itself. -
get_interfaceanditer_interfaceslook an interface up by name and by
index:get_interface("eth0")for text that is no address, network or MAC,
andget_interface(index=3)(a keyword, since anintquery is an address).
Datagram.interface_indexandRoute.interface_indexnow have a way back to
anInterface. -
aretry(func, attempts=3, *, delay, multiplier, max_delay, jitter, retryable, on_retry, jitter_seconds, symmetric)andawait_for_port(dst, port, *, deadline=30.0, interval=0.1, timeout=None)areretryandwait_for_port
as coroutines: the same arguments, schedule, answers and exceptions, with the
waits asasyncio.sleepand the connect as a non-blocking connect on the
running loop, so no thread is used for an address (a name is looked up with
loop.getaddrinfo, the loop's executor). Cancelling leaves no socket and no
task behind, andawait_for_portends within a second after itsdeadline.
import netimpsstill does not import asyncio. The header's asyncio section
now lists by name what awaits without a thread and what needsto_threador
run_in_executor, with thefunctools.partialform for keyword-only options. -
Interface.is_multicastandInterface.is_point_to_point: the kernel's
IFF_MULTICASTandIFF_POINTOPOINTon POSIX, and on Windows the absence of
the adapter's no-multicast flag and a PPP, SLIP or tunnel adapter type;None
when the system did not say (the degraded enumeration, a hand-built value, a
POSIX platform whose flag value is not known). Part of equality, the hash and
the repr, and shown bynetimps interfaces([point-to-point], and both in
--json). Linux reports its loopback as not multicast-capable. -
cache=on the lookups that resolve an adapter:iter_addresses,
bind(forinterface=anddevice=),join_group,leave_group,
multicast_socketandping(forsrc=) takeget_interfaces's argument,
so a caller making many of them pays one enumeration. The default stays
False: nothing changes for a caller who does not ask.get_interface(mac)
returns the first adapter carrying a MAC, in enumeration order, and
iter_interfaces(mac)yields them all; the header says so. -
Interface.is_up:Truewhen the interface is usable (IFF_UPand
IFF_RUNNINGon POSIX, the operational status on Windows),Falsewhen it is
not,Nonewhen the system did not say; part of equality, the hash and the
repr, and shown bynetimps interfaces([down], andis_upin--json). -
UDPEndpoint.asend(data, dst, port, *, src=None):sendawaited, on
every loop type. It waits for writability, with the loop running, when the
kernel's send buffer is full. -
One exception base and four new exported classes.
NetimpsErroris the
base of everything netimps raises on its own account;
NetimpsValueError(NetimpsError, ValueError)is for text that is not the
value it was asked to become;ResolutionTimeoutError(ResolutionError, TimeoutError)is what the resolvers now raise when a deadline expires
(resolve_system,resolve_nslookup,resolve_wire,resolve_doh);
DNSDecodeError(NetimpsValueError)is the DNS codec's error, formerly the
privateWireError(ValueError). -
UDPEndpoint.aclose(),async with UDPEndpoint(...).aclose()is
close()for a coroutine: it waits for the reader threadarecvmay have
started to leave, with the loop running, whereclose()joins it on the
calling thread.close()andaclose()are each complete on return and
harmless when called again, in either order. -
Host.resolve()andFQDN.resolve()return the pair(fqdn, ip), and
Host.ip(),Host.fqdn()andFQDN.ip()take the resolver options
(ns,timeout,port,tcp,search,backends,source) and
check=;Host.ip()andFQDN.ip()also takeipv6=,Host.fqdn()does
not.check=Trueraises whereNonewould be returned:NoAnswerError
for an empty answer or an empty host,ResolutionErrorfor an outage.ip()of a name looks
it up and of an address does not;fqdn()of an address looks it up and of a
name does not. With none ofns,port,tcp,sourceorbackendsthe OS
resolver alone answers, asget_ipdid; naming one selectsresolve()'s own
chain.resolve()acceptsrdtype=("a", "aaaa")for both families at once. -
MACAddress.__format__and__bytes__:f"{mac:-X}"ismac.format("-", upper=True)andbytes(mac)is the six octets.
FQDN.encode(),bytes(name),FQDN.decode(data)andFQDN.decode_at(data, offset)
write and read the RFC 1035 wire form;decode_atfollows compression
pointers with loop detection and returns(name, end)for use inside a
message. Malformed data raisesDNSDecodeError. The package's own reply
reader shares the same label reader, and now also refuses a label over 63
octets and a name over 255. -
MACAddress.parse,FQDN.parseandHost.parsebuild the type from
text, raisingNetimpsValueErrorfor bad text andTypeErrorfor a
non-str;Host.try_parse(text, default=None)joins the other two, whose
try_parsegainsdefault=.Host.parserefuses only empty or blank text. -
is_unicast(address, interface=None, *, cache=False)andDatagram.is_unicast: false for the wildcard, a multicast group and a broadcast (limited or subnet), true otherwise.Datagram.is_unicastisNonewhen there was no pktinfo to say where the datagram went. -
WS-Management schemes in the registry:
wsman5985,wsmans5986 (IANA),winrm5985,winrms5986 andpsrp5985, soget_default_port("winrm")is no longerNone.get_default_scheme(5985)iswsmanandget_default_scheme(5986)iswsmans. -
cache=onresolve(),Host.ip/fqdn/resolveandFQDN.ip/resolve, withclear_resolution_cache()andRESOLUTION_CACHE_TTL(30 seconds).False(the default) never reads or writes the cache,Trueuses the TTL, a number is the TTL, as onget_interfaces. Keyed on the name and every option; an empty answer is cached like any other, an outage is not. A call that passescache=neither reads nor writes aHost's memo. -
split_zone(text),is_local_host(host, *, resolve=False, cache=False)andsplit_hostof a(host, port)pair.split_zone("fe80::1%eth0")is("fe80::1", "eth0");is_local_hostis true for a loopback or locally assigned literal,localhost, and this machine's own name, and resolves any other name only when asked;split_host(("h", None), default_port=69)is("h", 69). -
UDPEndpoint.datagrams(*, on_error=None): a callable given the exception from a failed receive; return true to carry on with the next datagram. Without it the loop still stops at the first error. -
Four aliases that already appeared in public signatures are exported:
InterfaceLike(what names a local interface),InterfaceQuery(what
get_interfacelooks up),PortsLikeandSocketAddress.
Renamed
Breaking. No alias is kept: the old name is gone. Acronyms are spelled in
capitals, an input alias is named for the type it becomes, and a predicate or
probe says so in its prefix.
| Before | Now |
|---|---|
UdpEndpoint |
UDPEndpoint |
Fqdn |
FQDN |
FqdnLike |
FQDNLike |
MACLike |
MACAddressLike |
AddressLike |
HostLike |
normalize_host |
split_host |
interface_for |
get_interface |
interfaces_for |
iter_interfaces |
hop_count |
count_hops |
supports_pktinfo |
has_pktinfo |
supports_recvmsg |
has_recvmsg |
socket_patched |
is_socket_patched |
APIPA |
LINK_LOCAL_V4 |
InterfaceSpec |
InterfaceLike (it was never exported, so importing it from the root already failed) |
HOST_DN |
get_hostname() (a constant becomes a call) |
UdpEndpoint.supports_pktinfo |
UDPEndpoint.has_pktinfo |
UdpEndpoint.supports_src_pinning |
UDPEndpoint.has_src_pinning |
Interface.loopback, Interface(loopback=) |
Interface.is_loopback, Interface(is_loopback=) |
MACAddress.as_str(sep, upper) |
MACAddress.format(sep, *, upper=), format(mac, "-X") |
Fqdn.wire (property) |
FQDN.encode(), bytes(name) |
Fqdn.unicode (property) |
FQDN.to_unicode() |
Fqdn.as_fully_qualified() |
FQDN.fully_qualified() |
Host.value = ... |
read-only; build a new Host |
Interface.ips (list) |
Interface.ips (tuple) |
MACAddress._VALID_MAC |
MACAddress.is_valid(text) |
get_ip(x) |
Host(x).ip() (removed; check=True raises where get_ip returned None) |
Host.fqdn (property) |
Host.fqdn() (method; reverse lookup for an address) |
Host.ip(refresh) |
Host.ip(*, check, ipv6, refresh, <resolver options>) |
Fqdn.resolve() -> records |
FQDN.resolve() -> (fqdn, ip); records come from netimps.resolve(name, rdtype) |
wait_for_port(timeout=) (whole wait) |
wait_for_port(deadline=) |
wait_for_port(connect_timeout=) (one attempt) |
wait_for_port(timeout=) |
(Fqdn/Host method) strict= |
check= |
UDPEndpoint.send(data, address=, port, src) |
UDPEndpoint.send(data, dst, port, *, src=) |
UDPEndpoint.recv/arecv/datagrams(bufsize, resolve_interface) positional |
resolve_interface= keyword-only |
UDPEndpoint.reply_socket(datagram, port, connreset) positional |
connreset= keyword-only |
Interface.primary_ip(ipv6, loopback_ok) positional |
loopback_ok= keyword-only |
retry/backoff_delays/Backoff _sleep= and _random= parameters |
removed; they were the package's own test hooks and have no replacement |
PingResult.rtt_ms (milliseconds) |
PingResult.rtt (seconds) |
PingResult.host, PingResult(host=) |
PingResult.dst, PingResult(dst=) |
Datagram.local_address, Datagram(local_address=) |
Datagram.destination, Datagram(destination=) (the address the datagram was sent to) |
NETIMPS_NO_SOCKET_PATCH |
NETIMPS_SOCKET_PATCH (sense inverted: a false spelling opts out; the old variable set at all is an error at import) |
netimps.cli:run |
netimps.cli:main |
Changed
-
The
cliextra needsduho0.7:duho>=0.7.0,<0.8, where it was
>=0.6.0,<0.7. Thenetimpscommand, its options and its output are the
same; the library itself still imports nothing of it. -
An address object is written the same on every Python.
join_hostof a
v4-mapped IPv6 address on Python before 3.13 is[::ffff:1.2.3.4]:80, where
it was[::ffff:102:304]:80; the same goes forstr(Host(address)),
split_hostandsplit_zoneof an address object, the repr ofRouteand the
addresses the command line prints (format_address). Text passed in as a
stris not re-rendered. -
The shipped API header is split.
netimps/AGENTS.mdkeeps every public
name with its signature and one sentence, the exceptions, the command line's
contract, the environment variables (NETIMPS_SOCKET_PATCHwith its accepted
spellings and its import-time error,NETIMPS_MCP,AGENT_HELP), the asyncio
rules and the cross-cutting gotchas; the detail of each large topic is in an
AGENTS.mdbeside the code that implements it (_dns,_ping,_sockets,
_ifaddrs,_ip,_fqdn,_msg,_udpandcli), listed in a table in the
top header. All of them ship inside the package. -
The command line is the
netimps.clipackage, and its entry point is
netimps.cli:main(argv=None) -> int.netimps.cli:runis gone (it could
returnNone); thenetimpsconsole script andpython -m netimpscall
main. -
netimps scanexits 1 when nothing answered (it exited 0), and
netimps port 99999exits 2 (it exited 1):0found or yes,1nothing
found or the answer was no,2an error, for every command. -
-qprints no result line and answers by the exit status alone, as
grep -q; it was only the log level. Errors still go to stderr and
--jsonis not affected. -
Method options are keyword-only, breaking.
UDPEndpoint.recv,arecv
anddatagramstakebufsizepositionally andresolve_interfaceby name;
UDPEndpoint.send(data, dst, port, *, src=None)names its destinationdst
(it wasaddress) and takessrcby name;reply_socket(datagram, port=0, *, connreset=False);Interface.primary_ip(ipv6=False, *, loopback_ok=True).
retry,backoff_delaysandBackoffno longer take_sleepand_random
parameters: they were the package's own test hooks.UDPEndpoint.datagrams()
is typed
AsyncIterator[Datagram]. -
bind()defaults, breaking.family=None(wasAF_INET) infers the
family from the address, sobind("::1")is an IPv6 socket; the wildcard
""stays IPv4, and a name is IPv4 when it has an IPv4 address.
connreset=None(wasTrue) means off for a datagram socket and
untouched for any other;TrueandFalsestill force it. On Windows an
unconnected UDP socket frombind()no longer raisesConnectionResetError
on a later receive after an ICMP port-unreachable. A failedbind()now
carries thebind_error_hinttext in the message of every failure that
function recognises, keeping itsOSErrorsubclass anderrno;
set_buffer_sizelogs aWARNINGwhen the kernel grants less than was
asked, once per process for each distinct shortfall. -
MACAddress._VALID_MACis gone. It was a private class attribute the class docstring invited callers to read; screen text withMACAddress.is_valid(text). -
UDPEndpoint.send(src=<address>)enumerates no interfaces. An addresssrcis used as given and the kernel picks the adapter (a%zonestill names one); a MAC or an adapter name still resolves, and anInterfacestill pins the adapter as well. -
Host,MACAddress,PingResult,RouteandInterfaceare read-only.
Assigning to or deleting any attribute raisesAttributeError; build a new
value instead. All six value types (FQDNalready was) copy and pickle.
Interface.ipsis a tuple rather than a list, soiface.ips.append(...)
and the other list mutators raise; the constructor still takes any iterable.
Interface.loopbackis gone: readis_loopback. A cached
get_interfaces(cache=...)call now hands back the storedInterface
objects in a fresh list, copying onlyraw. -
Host.fqdnis a method, andHost.iptakes keyword-only options.
host.fqdnbecomeshost.fqdn();host.ip(True)becomes
host.ip(refresh=True), which asks again and replaces the memo as before.
A call that passes any other option neither reads nor writes it.FQDN.resolve()no
longer returns DNS records: callnetimps.resolve(name, rdtype)for those. -
Options are keyword-only. 37 callables keep only their operands as
positional parameters, and every option after them must be named:
ping("h", 3)is nowping("h", tries=3). Parameter names, order and
defaults are unchanged. Positional parameters kept:
none forget_interfaces; one forBackoff,Route,UDPEndpoint,
count_hops,discover_mtu,get_free_port,get_interface,get_route,
iter_addresses,max_udp_payload,patch_socket_module,ping,
set_buffer_sizeandsplit_host; two forInterface(name,index),
PingResult(ok,dst),backoff_delays,get_pmtu,get_source_ip,
get_tcp_mss,join_group,leave_group,multicast_socket,
register_port,resolve,resolve_dnspython,resolve_doh,
resolve_nslookup,resolve_system,resolve_wire,retry,scan_hosts,
scan_ports,tcp_check,try_parseandwait_for_port.recvmsg,
sendmsg,CMSG_LEN,CMSG_SPACE,DatagramandSocketOptionkeep their
shapes.parse,try_parseandis_validnamestrict(the network
builders' option) instead of an untyped**kwargs;try_parse(value, type, default)is nowtry_parse(value, type, default=default). -
wait_for_portandPingResultsay which time they mean.timeout
is one attempt anddeadlinethe whole operation, as everywhere else in the
package;PingResult.rttis a duration in seconds, wherertt_mswas
milliseconds. A sub-millisecond reply is still0.0. The command line's
ping --jsonkeeps itshostandrtt_mskeys, converting for display. -
discover_mtunames the options it forwards.tries=,ipv6=and
ttl=replace**ping_kwargs; passingsize=ordont_fragment=, which the
search varies itself, is now Python's ownTypeErrorfor an unexpected
keyword rather than a bespoke one. -
Annotations say what the code accepts and returns.
resolve,
resolve_dnspython,resolve_system,resolve_nslookup,resolve_wireand
resolve_dohare overloaded onrdtype:"a"givesList[IPv4Address],
"aaaa"List[IPv6Address],"ptr"List[str], anything else
List[Any].method=onpinganddiscover_mtuis
Literal["icmp", "tcp", "udp"];is_broadcast(address: IPAddressLike, interface: Optional[Interface]);join_hostandsplit_hosttake
HostLike;parse,try_parseandis_validtakestrictand typed
**options. No parameter of a public callable is a bareAny. -
MACAddress.try_parseandFQDN.try_parsetake text only. They answer
None(or the newdefault=) for text that does not parse, and raise
TypeErrorfor anything that is not astr--MACAddress.try_parse(None),
FQDN.try_parse(3.5)-- where they returnedNone. The generic
netimps.try_parse(value, type)is unchanged and still answersdefault
for any object. ForMACAddress,FQDN,Hostand subclasses it now
builds astrthroughType.parse; anint,bytesor built value still
goes to the constructor. -
HOST_DNis replaced byget_hostname(*, fqdn=False). A constant
computed fromplatform.node()made everyimport netimpsask for the host
name, a WMI query on Windows; the function asks when called.fqdn=True
returnssocket.getfqdn(). -
NETIMPS_NO_SOCKET_PATCHis replaced byNETIMPS_SOCKET_PATCH, with
the sense turned round:0,false,nooroffopts out of the
import-timesocketpatch, and unset still means patched. A value that is
none of the documented spellings, or the old variable set to anything,
raisesValueErrorat import naming the variable, so that a stale setting
cannot silently re-enable the patch. -
HostLike(formerlyAddressLike) now includesHostandFQDN, which
every destination parameter already accepted at run time. -
Malformed text raises
NetimpsValueErrorfromparse,MACAddress,
FQDN,split_host,join_hostand the query check of
resolve_nslookup; message unchanged. It is aValueError, so
except ValueErrorstill catches it.parsenow also converts the
ipaddressbuilders' own errors, chained as__cause__. -
UDPEndpoint.recvandsendraise the builtinTimeoutErrorwhen the
socket's timeout expires. From Python 3.10 that is whatsocket.timeout
already is; on 3.9 it was only anOSError. -
ResolutionErrorandAddressInUseErrornow also derive from
NetimpsError. Every existingexceptclause keeps matching. -
resolve_wireandresolve_dohchain an unreadable reply's
DNSDecodeErroras the__cause__of theResolutionErrorthey raise. -
Every program netimps runs (
ping,nslookup,route,traceroute,
tracert) is started the same way. It is found onPATHbefore anything
runs, so a missing one is named in the error; standard input is closed;
LC_ALL=Cis added to the child's environment, so output no longer follows
the user's locale; and output is decoded explicitly with
errors="replace"(the OEM code page on Windows, UTF-8 elsewhere) instead of
the locale's default, so a byte that does not decode no longer raises
UnicodeDecodeErrorout ofcount_hopsorget_route. A deadline now kills
the program's children as well as the program. A program that resolves to a
.bator.cmdis refused. What each function reports for a missing or hung
program is unchanged. -
A resolver that could not be asked is no longer an empty answer.
resolve_dnspythonraisesResolutionTimeoutErrorfor a timeout and
ResolutionErrorfor every server failing (a SERVFAIL, a refused port) or no
resolver configuration, where it returned[];resolve_systemraises
ResolutionErrorfor a temporary failure (EAI_AGAIN,EAI_FAIL,herror
2 and 3) and keeps[]forEAI_NONAMEand no data;resolve_nslookup
reads the reason after the colon of "can't find : ", so
Windows'No response from server(exit 0) andSERVFAILraise while
Non-existent domainstays[].resolve()withoutstrictstill answers
[],strict=Truenow raises where it could not before, andcache=stores
nothing for an outage.resolve_dnspythonno longer turns every exception
intoValueError("invalid DNS query"): only a malformed name or an unknown
record type is aValueError, anOSErroris aResolutionError, and
anything else propagates; dnspython's missing-configuration error no longer
escapesresolve()raw. -
A missing
dnsextra is named.resolve_dnspythonraises
ResolutionErrorsayingpip install "netimps[dns]", andresolve()raises
the same for a request onlydnspythoncould serve (a record type the other
backends do not read) instead of returning[]. Newhas_dns() -> bool. -
resolve_doh(..., allow_http=False). A URL that is nothttps://is a
ValueErrorbefore a request is made;allow_http=Trueaccepts plain
http://. The default fetch follows no redirect (a 3xx is a
ResolutionError), reads at most 65,536 bytes of a reply, and names the URL
in messages without its credentials, query string or fragment. -
deadline=onresolve(),Host.ip/fqdn/resolveandFQDN.ip/resolve.
timeoutis one attempt and means different things per backend (the whole
call for dnspython and the wire backend, each candidate name for the OS
resolver and nslookup), so a backend chain, a record-type pair and a search
list multiplied it.deadlineis the total for the call, shared by all of
them; the header states what eachtimeoutbounds. -
ResolutionErroris anOSError, andNoAnswerErroris new.
ResolutionError(NetimpsError, OSError), as the standard library's own name
failuresocket.gaierroris, soexcept OSErroraround a connect catches it
andretry()retries it by default.NoAnswerError(ResolutionError)is what
check=TrueonHost.ip/fqdn/resolveandFQDN.ip/resolveraises when the
lookup completed with no such name or record; an outage stays a plain
ResolutionErrororResolutionTimeoutError.retry's docstring and the
header say how to leaveNoAnswerErrorout.
Removed
get_ip(address, ipv6=None).Host(x).ip()is the same
operation with the name kept, andHost(x).ip(check=True)raises
NoAnswerError(aResolutionError) whereget_ipreturnedNone.get_routeand the
addrcommand use it;Host(x).resolve()gives(fqdn, ip).
Fixed
-
is_wildcardtakes what its annotation says: aHostor anFQDN
(HostLike), read as its text. AHostholding a name raised
NetimpsValueError; it answersFalse, as the same text does. -
Host.ip,Host.fqdn,Host.resolve,FQDN.ipandFQDN.resolveare
annotated bycheck: withcheck=Truethey raise instead of returning
None, and the annotations say so (anIPAddress, anFQDN, a pair of
both), so a caller needs no assertion. Nothing else changes at run time. -
On Windows the
os.sysconfstand-in answersSC_OPEN_MAX. The socket
patch installsos.sysconfwhere the platform has none, and it answered
SC_IOV_MAXalone. A library that readsSC_OPEN_MAXbehind
hasattr(os, "sysconf"), as an SFTP server does to size its handle limit,
got aValueErroron every session. It is 8192, the size of the C runtime's
descriptor table. -
A name the IDNA codec refuses no longer escapes as
UnicodeEncodeError.
An empty label (a..b) or a label over 63 octets isFalsefrom
is_local_host(..., resolve=True)and aNetimpsValueErrorfrom
resolve_system. -
get_default_port(None)andget_default_port(22)raiseTypeError,
where anAttributeErrorcame out of the function. -
resolve_dohcloses the response of an HTTP error status. The error
urllib raises is itself an open response; it was left for the garbage
collector, which reports it as aResourceWarningat an unrelated moment. -
is_broadcastandis_unicastread an interface object as its address.
IPv4Interfaceis anIPv4Addresssubclass and was compared as address and
network, sois_broadcast(IPv4Interface("10.0.0.255/24"), nic)was false and
is_unicastof it true; every classifier now reduces an interface to its
.ipfirst. -
One zone rule for
interface_index,interface_addressand
get_interface.interface_index("::1%nosuchadapter")was the loopback
index with the zone ignored and("::1%999")was 999 unchecked, while
get_interfacefound nothing for both. A zone must name the adapter that
holds the address, and a strict lookup of one that does not raises. The
address in an interface spec must be held locally:fe80::1%12for an
address no adapter holds is no longer index 12. -
A family is
4orAF_INET,6orAF_INET6, wherever one is taken.
has_pktinfo(4)answeredFalseanditer_addresses(family=AF_INET)raised;
bind,get_free_port,has_pktinfoanditer_addressesnow share one
reader, and anything else raisesValueError. -
Windows no longer lists addresses it cannot bind. An address the system
marks tentative or duplicate is left out ofInterface.ips: the four
media-disconnected adapters on the review machine each held a169.254
address in the state Tentative, whichbindrefused withWSAEADDRNOTAVAIL
whileis_local_addressandprimary_ipreturned it. The adapters stay
listed, withis_upfalse. The Windows interface index falls back to
Ipv6IfIndexwhenIfIndexis 0, so an adapter with IPv4 unbound is not
reported as having no index. -
Interfaceis a value type that checks itself. The constructor raises
TypeErrorfor a wrong-typed field (it acceptedmac="notamac",ips="ab"
andname=5),ipv4andipv6are tuples likeips,rawis a read-only
mapping with tuples inside (a list inside a cachedrawwas shared between
callers), andrepris a constructor call that rebuilds an equal value
(it printed the addresses as text). -
clear_interface_cache()is not lost to an enumeration in flight. A
thread that began before the clear stored its pre-clear snapshot afterwards,
andget_interfaces(cache=math.inf)served it indefinitely; a clear now
invalidates every enumeration started before it. -
A defect in the native interface walk is not reported as a degraded host.
The dispatch caughtOSError,AttributeErrorandValueError, so a bad
pointer read became a one-interface answer; onlyOSErrordegrades, with the
reason inrawand logged once at debug. -
A BSD netmask is read no further than its own length. macOS trims a
netmask sockaddr after its last non-zero byte (255.0.0.0arrives as 5
bytes) and what follows belongs to the next structure. The bytes past
sa_lenare taken as zero instead of being read. -
retry,backoff_delaysandBackoffshare one set of argument rules,
checked when they are called.backoff_delayswas a generator, so
attempts=0or a negativedelayraised at the firstnext();
backoff_delays(3, 1.0, multiplier=0.5)yielded a shrinking[1.0, 0.5]
andbackoff_delays(2, 4.0, max_delay=1.0)cut the first wait to[1.0]
whileBackofffloored the multiplier and the ceiling instead. All three now
raiseValueErrorforattemptsbelow 1, a negativedelay, amultiplier
below 1, amax_delaybelowdelay, ajitteroutside 0 to 1 and a negative
jitter_seconds.retryis typedCallable[[], T] -> T(it returned
Any). -
scan_hostsbounds the work, not one factor of it, and does not build it
up front. The guard counted hosts only and every host-port pair was built
and queued before the first probe: a /22 with the common ports peaked at
63 MiB (about 1.7 kB a pair), and a /16 over all ports was 4.3 billion
tuples. A sweep of more than 4,194,304 probes now raisesValueErrorbefore
any probe (a /16 with the common ports is still allowed), andscan_hosts
andscan_portsfeed their pool as they run: a /22 with the common ports
peaks near 0.1 MiB. -
A destination of
NoneraisesTypeError.tcp_check(None, 80),
get_source_ip(None)and every otherdstparameter asked the resolver for
the host named"None"and answeredFalseorNone;dsttakes the same
allowlist assplit_host(a string, an address, an interface, aHostor an
FQDN), so anint,bytesorNoneraises.Host(None)is still the
empty host. -
bindreadsoptionsonce, andreuse_port=Trueshares a UDP port on
Windows. A generator passed asoptionswas used up by a scan for an
address-takeover request, so none of its options were applied. On Windows the
flag was a no-op and the second bind of a port raisedAddressInUseError; a
datagram socket now takes the address-sharing path, as on Linux and macOS two
sockets that both pass it bind one port (another process can take the port
over there too). A stream socket on Windows and every bind without the flag
stay exclusive. -
get_free_portinfers the family asbinddoes and takes any host:
get_free_port("::1")raisedgaierror. -
wait_for_portno longer shortens anintervalabove one second: it
backed off to a flat second after the first wait, sointerval=5polled
sixteen times in twenty seconds instead of four.is_local_hostreads the
inet_atonspellings (127.1,2130706433,0x7f.0.0.1) as the address
they spell. One timeout floor: the scanners floored to 1 ms and
tcp_checkagain to 50 ms; the scanners now pass the timeout on and the
floor istcp_check's 50 ms. -
discover_mtureports what it measured. It resolved the name a second
time for the header overhead and ignoredipv6=, so a 1480-byte IPv4 path
was reported as 1500 when the name's first record was IPv6; the target, the
family and the overhead now come from one resolution.method="tcp"ignored
ipv6=(an IPv4-only listener gave 65555 foripv6=True) and added the IPv6
header to an IPv4 segment size; it now uses the family that connected, and
get_tcp_mssgainsipv6=. A probe that was answered athighreturned
high(9000 on a loopback whose MTU is 65535); the search now continues to
the outgoing interface's MTU, andhighis only the ceiling when that MTU
cannot be read, where the result means "at least". On macOS the answer
stopped at 8192,net.inet.raw.maxdgram, and took 8.7 s; the limit is read,
a local destination is reported at the loopback MTU, and another path that
reaches it is retried withmethod="udp".discover_mtu("127.0.0.1")is the
loopback MTU (65535 on Windows). -
The
socketpatch survives a second import of the package, a bad call and
a stream socket. A second copy of the package in one process captured the
first copy's installedrecvmsgas the native one, so on Windows
UDPEndpoint.recv().destinationcame out as1.0.0.0with index 0 and
is_socket_patched()saidFalsewhile the method was installed; it now
recognises and takes over what the first installed.
patch_socket_module(iov_max=0)raised after installing the patch; it
validates first.recvmsgon a Windows stream socket failed with
WinError 10022; it usesWSARecv. -
IPv4 on FreeBSD reports the arrival and pins the source.
UDPEndpoint
there setIP_PKTINFO, which FreeBSD lacks, sohas_pktinfowas false and
destinationwasNone, whilehas_src_pinningwas true and
send(src=)raisedOSError42. It now usesIP_RECVDSTADDRand
IP_RECVIFto receive and anIP_SENDSRCADDRmessage to pin; the pin needs
a wildcard-bound, unconnected socket, sohas_src_pinningis false for an
endpoint bound to an address. Measured on FreeBSD 16.0; IPv6 is unchanged. -
Pinning a source on Windows. A wildcard-bound IPv6 endpoint pinned by an
interface with no IPv6 address, or by::, sent from::; both raise
ValueErrornow, as the IPv4 twin did. A dual-stack endpoint pins an IPv4
source (it failed withWinError 10022). A pinnedsendto a host name
resolves it (it failed withillegal IP address string passed to inet_pton). -
Closing a
UDPEndpointends a pendingarecv, andarecvleaves the
socket's timeout alone.close()andaclose()from another task now wake
a task inarecvwithRuntimeErrorand finish adatagrams()loop
(both hung on the Windows Proactor loop and on Linux). The loop's reader is
removed by the descriptor it was registered under, so closing the socket and
then cancelling the task no longer leaves it registered: a Windows selector
loop died withWinError 10038out ofrun_until_complete. Onearecvused
to leave a socket withsettimeout(5.0)at0.0, so the nextrecv()raised
BlockingIOErrorat once. -
pingbounds its own name lookup bytimeout(2.5 s fortimeout=0.5
with a 2 s lookup before), and takes the family ofsrcfrom the
destination:ping("::1", src=<loopback Interface>)pinned the interface's
IPv4 address on an IPv6 probe and was falsy on Windows, macOS and FreeBSD.
The header and docstring now say that a TCP refusal takes about two seconds
to arrive on Windows, soping(method="tcp")needs atimeoutof 3 there. -
A DNS reply is bounded. A short MX or SRV record is a
DNSDecodeError
(it escaped asstruct.error); a name follows at most 32 compression
pointers (one 65,000-byte reply cost 9.6 s of CPU); the UDP buffer is the
1,232 bytes the query advertises; a datagram with another id or question is
discarded instead of ending the query; the TCP read carries the whole
deadline (a byte every 0.3 s heldtimeout=0.5for 12 s); a nameserver port
outside 1-65535 is aValueError; and bytes outside letters, digits, hyphen
and underscore in a decoded name are written\DDD.resolve_nslookup
refuses annsor search domain thatnslookupwould read as an option. -
The programs the library runs are looked up in the absolute
PATH
entries only. On Windowsshutil.whichsearched the working directory
first, soping,nslookup,tracertandrouteran a same-named file
next to the caller; a relative or emptyPATHentry is now skipped on every
platform. The runner refuses a call without a timeout:
resolve_nslookup(timeout=None)allows 30 seconds instead of waiting
forever.count_hopsgivesNonewhentracerouteortracertexits
non-zero, and the header names the supported programs. -
recvmsgandsendmsghonour a socket timeout on Windows. A socket
with a timeout is non-blocking underneath, and the Winsock calls returned
at once:UDPEndpoint.recv()on a socket withsettimeout(0.3)raised
BlockingIOErrorafter 0.000 s instead of waiting. Both now wait for
readiness and raisesocket.timeoutwhen the time runs out, as the
stdlib methods do on POSIX. A blocking socket and a non-blocking one
behave as before. -
FQDNcompares by its case-folded key everywhere.is_subdomain_of
andrelative_towere case-sensitive where==andhashwere not
(FQDN("www.Example.com").is_subdomain_of("example.com")wasFalse), and
example.comagainstexample.com.answereda > bandb > atogether.
The four ordering operators now derive from one key, reversed folded labels
then absoluteness. -
FQDNvalidates after IDNA mapping, with one label rule shared by the
text and the wire entries. Fullwidth digits and the ideographic full stops
mapped to127.0.0.1and constructedFQDN('127.0.0.1'); U+3002 became a
dot inside one label. A label with a space, a control character or one of
: / ? # [ ] @(FQDN("http://example.com")) constructed, then failed
FQDN.decode(v.encode()). All now raiseNetimpsValueError.bytesand an
iterable item that is not astror anFQDN(FQDN(b"abc")was
FQDN('97.98.99'),FQDN(["www", None])waswww.None) raiseTypeError.
/,childandwith_hostnamerefuse a result over 253 octets, where
encode()used to emit 259;encode_namerefuses over 255 on the wire. -
split_host,split_zoneandjoin_hostshare one host rule and one
port rule.join_hostaccepted a network ('10.0.0.0/24:80'),bytes
("b'h':80"), afloatport ('h:80') andTrue('h:1'), and
split_hostread"h:8_0","h:+80","h: 80"and Arabic-Indic digits as
port 80; port text is now ASCII digits and every port goes through the one
range gate.split_host("[10.0.0.5]:80")and"[not an address]:80"raise,
asjoin_hostalready refused them. A value that is not a host type at all
(None, anint,bytes) raisesTypeErrorfrom all three, where
split_host/split_zone/join_hostraisedNetimpsValueError; so does a
non-intport. Text that does not parse staysNetimpsValueError. -
resolve(ns="host:port")answers the same with and without dnspython.
Thehost,host:port,[v6]and[v6]:portspellingsresolve_wire
documents are read by one parser for every backend:resolve_dnspython
hands dnspython each server's own port (it raisedValueErrorfor
host:port), and a malformednsis aNetimpsValueErrorbefore any
backend runs.resolve_nslookuptakeshost,[v6]or either with:53
and raisesNetimpsValueErrornamingport=for another port, which
nslookupcannot use; the chain leaves it out for such an entry. -
The classifiers answer the same on every Python and take one input rule.
is_link_scopedandis_wildcardansweredTrueon 3.13 and later and
Falsebefore for a v4-mapped address (::ffff:127.0.0.1,
::ffff:0.0.0.0); they unmap first.is_link_scoped,is_wildcard,
is_multicast,is_local_address,is_broadcast,is_unicastandunmap
takeIPAddressLike(is_link_scoped("127.0.0.1")raisedAttributeError,
unmap(2130706433)raised). All butis_wildcardraiseNetimpsValueError
for text that is no address, whereis_multicast,is_broadcastand
is_unicastansweredFalse.is_wildcardstill answersFalsefor a host
name: it is asked of a listen host, andbindtakes a name there. A network
or a value of another type is aTypeError(is_multicast(None)was
False).join_groupandleave_groupstill say "not a multicast group".
is_local_hoststill never raises. -
An empty query is refused by every resolver entry point.
resolve("")
and each backend raiseNetimpsValueError; the OS resolver and dnspython
applied the search list to it and answered with the search domain's own
records, andnslookuprefused it. -
try_parseandis_validraiseTypeErrorfor an option the builder
does not take.try_parse("10.0.0.5", IPAddress, strict=True)answered
Nonewhileparseraised. -
Host(...)reduces an interface to its address, as everydstparameter
does.Host(IPv4Interface("127.0.0.1/8"))kept the text127.0.0.1/8, so
.ip()wasNoneand the resolver was asked for it; a network is a
TypeError. -
Copy and pickle of a
MACAddress,HostorFQDNsubclass return the
subclass (they returned the base class), and theparse,try_parse,
FQDN.decodeandFQDN.decode_atclassmethods are typed to return the class
they were called on. The comparison methods ofMACAddress,FQDN.pingand
FQDN.__getitem__are annotated (bool,PingResult,stror a tuple of
labels, where a checker sawAny), and the named networks are typed with their
concrete classes, notIPv4Network | IPv6Network. -
Every
UDPEndpointresolves arrival interfaces from the one shared
enumeration. Each endpoint enumerated for itself, beside the process-wide
cache: twice a second for a server with one socket that also made cached
lookups, three times with two sockets. An endpoint's index is now built from
the enumerationget_interfaces(cache=True)shares, and is trusted for
INTERFACE_CACHE_TTLfrom the moment that enumeration began; an index it
lacks still enumerates at once. The UDP header gave that lifetime as 30
seconds; it isINTERFACE_CACHE_TTL, one second. -
On Windows a receive does not cost more for a larger
bufsize.
recvmsg, and soUDPEndpoint.recv, allocated a buffer ofbufsizefor
every call, and a new 64 KiB buffer is faulted in page by page: 34 against 12
microseconds per 300-octet datagram at the default size (one Windows machine,
local timing). Each thread now keeps one buffer of up to 64 KiB.
Full Changelog: v0.3.4...v0.4.0