diff --git a/CHANGELOG.md b/CHANGELOG.md index 381d7def1..90dfdb97c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog. If you were calling it directly, please use `Record.__getitem__(slice(...))` or simply `record[...]` instead. - Remove deprecated class `neo4j.Bookmark` in favor of `neo4j.Bookmarks`. - Remove deprecated class `session.last_bookmark()` in favor of `last_bookmarks()`. +- Make undocumented classes `ResolvedAddress`, `ResolvedIPv4Address`, and `ResolvedIPv6Address` private. ## Version 5.28 diff --git a/src/neo4j/__init__.py b/src/neo4j/__init__.py index 0f8b9bba7..40e4b845d 100644 --- a/src/neo4j/__init__.py +++ b/src/neo4j/__init__.py @@ -81,7 +81,7 @@ NotificationClassification, # noqa: TCH004 false positive (dynamic attribute) ) -from .addressing import ( +from ._addressing import ( Address, IPv4Address, IPv6Address, diff --git a/src/neo4j/_addressing.py b/src/neo4j/_addressing.py new file mode 100644 index 000000000..4cbe2597f --- /dev/null +++ b/src/neo4j/_addressing.py @@ -0,0 +1,373 @@ +# Copyright (c) "Neo4j" +# Neo4j Sweden AB [https://neo4j.com] +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import annotations + +import typing as t +from contextlib import suppress as _suppress +from socket import ( + AddressFamily, + AF_INET, + AF_INET6, + getservbyname, +) + + +if t.TYPE_CHECKING: + import typing_extensions as te + + +_T = t.TypeVar("_T") + + +if t.TYPE_CHECKING: + + class _WithPeerName(te.Protocol): + def getpeername(self) -> tuple: ... + + +__all__ = [ + "Address", + "IPv4Address", + "IPv6Address", + "ResolvedAddress", + "ResolvedIPv4Address", + "ResolvedIPv6Address", +] + + +class _AddressMeta(type(tuple)): # type: ignore[misc] + def __init__(cls, *args, **kwargs): + super().__init__(*args, **kwargs) + cls._ipv4_cls = None + cls._ipv6_cls = None + + def _subclass_by_family(cls, family): + subclasses = [ + sc + for sc in cls.__subclasses__() + if ( + sc.__module__ == cls.__module__ + and getattr(sc, "family", None) == family + ) + ] + if len(subclasses) != 1: + raise ValueError( + f"Class {cls} needs exactly one direct subclass with " + f"attribute `family == {family}` within this module. " + f"Found: {subclasses}" + ) + return subclasses[0] + + @property + def ipv4_cls(cls): + if cls._ipv4_cls is None: + cls._ipv4_cls = cls._subclass_by_family(AF_INET) + return cls._ipv4_cls + + @property + def ipv6_cls(cls): + if cls._ipv6_cls is None: + cls._ipv6_cls = cls._subclass_by_family(AF_INET6) + return cls._ipv6_cls + + +class Address(tuple, metaclass=_AddressMeta): + """ + Base class to represent server addresses within the driver. + + A tuple of two (IPv4) or four (IPv6) elements, representing the address + parts. See also python's :mod:`socket` module for more information. + + >>> Address(("example.com", 7687)) + IPv4Address(('example.com', 7687)) + >>> Address(("127.0.0.1", 7687)) + IPv4Address(('127.0.0.1', 7687)) + >>> Address(("::1", 7687, 0, 0)) + IPv6Address(('::1', 7687, 0, 0)) + + :param iterable: A collection of two or four elements creating an + :class:`.IPv4Address` or :class:`.IPv6Address` instance respectively. + """ + + #: Address family (:data:`socket.AF_INET` or :data:`socket.AF_INET6`). + family: AddressFamily | None = None + + def __new__(cls, iterable: t.Collection) -> Address: + if isinstance(iterable, cls): + return iterable + n_parts = len(iterable) + inst = tuple.__new__(cls, iterable) + if n_parts == 2: + inst.__class__ = cls.ipv4_cls + elif n_parts == 4: + inst.__class__ = cls.ipv6_cls + else: + raise ValueError( + "Addresses must consist of either " + "two parts (IPv4) or four parts (IPv6)" + ) + return inst + + @classmethod + def from_socket(cls, socket: _WithPeerName) -> Address: + """ + Create an address from a socket object. + + Uses the socket's ``getpeername`` method to retrieve the remote + address the socket is connected to. + """ + address = socket.getpeername() + return cls(address) + + @classmethod + def parse( + cls, + s: str, + default_host: str | None = None, + default_port: int | None = None, + ) -> Address: + """ + Parse a string into an address. + + The string must be in the format ``host:port`` (IPv4) or + ``[host]:port`` (IPv6). + If no port is specified, or is empty, ``default_port`` will be used. + If no host is specified, or is empty, ``default_host`` will be used. + + >>> Address.parse("localhost:7687") + IPv4Address(('localhost', 7687)) + >>> Address.parse("[::1]:7687") + IPv6Address(('::1', 7687, 0, 0)) + >>> Address.parse("localhost") + IPv4Address(('localhost', 0)) + >>> Address.parse("localhost", default_port=1234) + IPv4Address(('localhost', 1234)) + + :param s: The string to parse. + :param default_host: The default host to use if none is specified. + :data:`None` indicates to use ``"localhost"`` as default. + :param default_port: The default port to use if none is specified. + :data:`None` indicates to use ``0`` as default. + + :returns: The parsed address. + """ + if not isinstance(s, str): + raise TypeError("Address.parse requires a string argument") + if s.startswith("["): + # IPv6 + port: str | int + host, _, port = s[1:].rpartition("]") + port = port.lstrip(":") + with _suppress(TypeError, ValueError): + port = int(port) + host = host or default_host or "localhost" + port = port or default_port or 0 + return cls((host, port, 0, 0)) + else: + # IPv4 + host, _, port = s.partition(":") + with _suppress(TypeError, ValueError): + port = int(port) + host = host or default_host or "localhost" + port = port or default_port or 0 + return cls((host, port)) + + @classmethod + def parse_list( + cls, + *s: str, + default_host: str | None = None, + default_port: int | None = None, + ) -> list[Address]: + """ + Parse multiple addresses into a list. + + See :meth:`.parse` for details on the string format. + + Either a whitespace-separated list of strings or multiple strings + can be used. + + >>> Address.parse_list("localhost:7687", "[::1]:7687") + [IPv4Address(('localhost', 7687)), IPv6Address(('::1', 7687, 0, 0))] + >>> Address.parse_list("localhost:7687 [::1]:7687") + [IPv4Address(('localhost', 7687)), IPv6Address(('::1', 7687, 0, 0))] + + :param s: The string(s) to parse. + :param default_host: The default host to use if none is specified. + :data:`None` indicates to use ``"localhost"`` as default. + :param default_port: The default port to use if none is specified. + :data:`None` indicates to use ``0`` as default. + + :returns: The list of parsed addresses. + """ # noqa: E501 can't split the doctest lines + if not all(isinstance(s0, str) for s0 in s): + raise TypeError("Address.parse_list requires a string argument") + return [ + cls.parse(a, default_host, default_port) + for a in " ".join(s).split() + ] + + def __repr__(self): + return f"{self.__class__.__name__}({tuple(self)!r})" + + @property + def _host_name(self) -> t.Any: + return self[0] + + @property + def host(self) -> t.Any: + """ + The host part of the address. + + This is the first part of the address tuple. + + >>> Address(("localhost", 7687)).host + 'localhost' + """ + return self[0] + + @property + def port(self) -> t.Any: + """ + The port part of the address. + + This is the second part of the address tuple. + + >>> Address(("localhost", 7687)).port + 7687 + >>> Address(("localhost", 7687, 0, 0)).port + 7687 + >>> Address(("localhost", "7687")).port + '7687' + >>> Address(("localhost", "http")).port + 'http' + """ + return self[1] + + @property + def _unresolved(self) -> Address: + return self + + @property + def port_number(self) -> int: + """ + The port part of the address as an integer. + + First try to resolve the port as an integer, using + :func:`socket.getservbyname`. If that fails, fall back to parsing the + port as an integer. + + >>> Address(("localhost", 7687)).port_number + 7687 + >>> Address(("localhost", "http")).port_number + 80 + >>> Address(("localhost", "7687")).port_number + 7687 + >>> Address(("localhost", [])).port_number + Traceback (most recent call last): + ... + TypeError: Unknown port value [] + >>> Address(("localhost", "banana-protocol")).port_number + Traceback (most recent call last): + ... + ValueError: Unknown port value 'banana-protocol' + + :returns: The resolved port number. + + :raise ValueError: If the port cannot be resolved. + :raise TypeError: If the port cannot be resolved. + """ + error_cls: type = TypeError + + try: + return getservbyname(self[1]) + except OSError: + # OSError: service/proto not found + error_cls = ValueError + except TypeError: + # TypeError: getservbyname() argument 1 must be str, not X + pass + try: + return int(self[1]) + except ValueError: + error_cls = ValueError + except TypeError: + pass + raise error_cls(f"Unknown port value {self[1]!r}") + + +class IPv4Address(Address): + """ + An IPv4 address (family ``AF_INET``). + + This class is also used for addresses that specify a host name instead of + an IP address. E.g., + + >>> Address(("example.com", 7687)) + IPv4Address(('example.com', 7687)) + + This class should not be instantiated directly. Instead, use + :class:`.Address` or one of its factory methods. + """ + + family = AF_INET + + def __str__(self) -> str: + return "{}:{}".format(*self) + + +class IPv6Address(Address): + """ + An IPv6 address (family ``AF_INET6``). + + This class should not be instantiated directly. Instead, use + :class:`.Address` or one of its factory methods. + """ + + family = AF_INET6 + + def __str__(self) -> str: + return "[{}]:{}".format(*self) + + +# TODO: 6.0 - make this class private +class ResolvedAddress(Address): + _unresolved_host_name: str + + @property + def _host_name(self) -> str: + return self._unresolved_host_name + + @property + def _unresolved(self) -> Address: + return super().__new__(Address, (self._host_name, *self[1:])) + + def __new__(cls, iterable, *, host_name: str) -> ResolvedAddress: + new = super().__new__(cls, iterable) + new = t.cast(ResolvedAddress, new) + new._unresolved_host_name = host_name + return new + + +# TODO: 6.0 - make this class private +class ResolvedIPv4Address(IPv4Address, ResolvedAddress): + pass + + +# TODO: 6.0 - make this class private +class ResolvedIPv6Address(IPv6Address, ResolvedAddress): + pass diff --git a/src/neo4j/_async/driver.py b/src/neo4j/_async/driver.py index a202fc590..6e61f9471 100644 --- a/src/neo4j/_async/driver.py +++ b/src/neo4j/_async/driver.py @@ -29,6 +29,7 @@ T_NotificationMinimumSeverity, ) +from .._addressing import Address from .._api import ( NotificationMinimumSeverity, RoutingControl, @@ -55,7 +56,6 @@ Query, unit_of_work, ) -from ..addressing import Address from ..api import ( AsyncBookmarkManager, Auth, diff --git a/src/neo4j/_async/io/_bolt.py b/src/neo4j/_async/io/_bolt.py index 45ae8fb27..bf3faadff 100644 --- a/src/neo4j/_async/io/_bolt.py +++ b/src/neo4j/_async/io/_bolt.py @@ -23,6 +23,7 @@ from logging import getLogger from time import monotonic +from ..._addressing import ResolvedAddress from ..._async_compat.util import AsyncUtil from ..._auth_management import to_auth_dict from ..._codec.hydration import ( @@ -38,7 +39,6 @@ from ..._io import BoltProtocolVersion from ..._meta import USER_AGENT from ..._sync.config import PoolConfig -from ...addressing import ResolvedAddress from ...api import ServerInfo from ...exceptions import ( ConfigurationError, diff --git a/src/neo4j/_async/io/_bolt_socket.py b/src/neo4j/_async/io/_bolt_socket.py index 95e1705a9..b393ce46a 100644 --- a/src/neo4j/_async/io/_bolt_socket.py +++ b/src/neo4j/_async/io/_bolt_socket.py @@ -23,7 +23,7 @@ import typing as t from contextlib import suppress -from ... import addressing +from ..._addressing import Address from ..._async_compat.network import ( AsyncBoltSocketBase, AsyncNetworkUtil, @@ -44,11 +44,8 @@ import typing_extensions as te + from ..._addressing import ResolvedAddress from ..._deadline import Deadline - from ...addressing import ( - Address, - ResolvedAddress, - ) log = logging.getLogger("neo4j.io") @@ -59,7 +56,7 @@ class HandshakeCtx: ctx: str deadline: Deadline local_port: int - resolved_address: addressing.ResolvedAddress + resolved_address: ResolvedAddress full_response: bytearray = dataclasses.field(default_factory=bytearray) @@ -315,7 +312,7 @@ async def connect( # https://docs.python.org/2/library/errno.html resolved_addresses = AsyncNetworkUtil.resolve_address( - addressing.Address(address), resolver=custom_resolver + Address(address), resolver=custom_resolver ) async for resolved_address in resolved_addresses: deadline_timeout = deadline.to_timeout() diff --git a/src/neo4j/_async/work/result.py b/src/neo4j/_async/work/result.py index 9c911a0be..618d11f55 100644 --- a/src/neo4j/_async/work/result.py +++ b/src/neo4j/_async/work/result.py @@ -66,7 +66,7 @@ if t.TYPE_CHECKING: import pandas # type: ignore[import] - from ...addressing import Address + from ..._addressing import Address from ...graph import Graph diff --git a/src/neo4j/_async_compat/network/_bolt_socket.py b/src/neo4j/_async_compat/network/_bolt_socket.py index d3d1283b5..b9c64dbc5 100644 --- a/src/neo4j/_async_compat/network/_bolt_socket.py +++ b/src/neo4j/_async_compat/network/_bolt_socket.py @@ -53,13 +53,13 @@ if t.TYPE_CHECKING: import typing_extensions as te - from ..._async.io import AsyncBolt - from ..._io import BoltProtocolVersion - from ..._sync.io import Bolt - from ...addressing import ( + from ..._addressing import ( Address, ResolvedAddress, ) + from ..._async.io import AsyncBolt + from ..._io import BoltProtocolVersion + from ..._sync.io import Bolt log = logging.getLogger("neo4j.io") diff --git a/src/neo4j/_async_compat/network/_util.py b/src/neo4j/_async_compat/network/_util.py index 1ac1c38b0..00ec54199 100644 --- a/src/neo4j/_async_compat/network/_util.py +++ b/src/neo4j/_async_compat/network/_util.py @@ -18,7 +18,10 @@ import logging import socket -from ... import addressing +from ..._addressing import ( + Address, + ResolvedAddress, +) from ..util import AsyncUtil @@ -34,7 +37,7 @@ def _resolved_addresses_from_info(info, host_name): continue if addr not in resolved: resolved.append(addr) - yield addressing.ResolvedAddress(addr, host_name=host_name) + yield ResolvedAddress(addr, host_name=host_name) class AsyncNetworkUtil: @@ -89,14 +92,14 @@ async def resolve_address(address, family=0, resolver=None): :param resolver: optional customer resolver function to be called before regular DNS resolution """ - if isinstance(address, addressing.ResolvedAddress): + if isinstance(address, ResolvedAddress): yield address return log.debug("[#0000] _: in: %s", address) if resolver: addresses_resolved = map( - addressing.Address, + Address, await AsyncUtil.callback(resolver, address), ) for address_resolved in addresses_resolved: @@ -171,13 +174,13 @@ def resolve_address(address, family=0, resolver=None): :param resolver: optional customer resolver function to be called before regular DNS resolution """ - if isinstance(address, addressing.ResolvedAddress): + if isinstance(address, ResolvedAddress): yield address return log.debug("[#0000] _: in: %s", address) if resolver: - addresses_resolved = map(addressing.Address, resolver(address)) + addresses_resolved = map(Address, resolver(address)) for address_resolved in addresses_resolved: log.debug( "[#0000] _: custom resolver out: %s", diff --git a/src/neo4j/_routing.py b/src/neo4j/_routing.py index 99a4a2970..86f141d8b 100644 --- a/src/neo4j/_routing.py +++ b/src/neo4j/_routing.py @@ -19,7 +19,7 @@ from logging import getLogger from time import monotonic -from .addressing import Address +from ._addressing import Address log = getLogger("neo4j.pool") diff --git a/src/neo4j/_sync/driver.py b/src/neo4j/_sync/driver.py index 24a61b5c8..54ce54c49 100644 --- a/src/neo4j/_sync/driver.py +++ b/src/neo4j/_sync/driver.py @@ -29,6 +29,7 @@ T_NotificationMinimumSeverity, ) +from .._addressing import Address from .._api import ( NotificationMinimumSeverity, RoutingControl, @@ -55,7 +56,6 @@ Query, unit_of_work, ) -from ..addressing import Address from ..api import ( Auth, BookmarkManager, diff --git a/src/neo4j/_sync/io/_bolt.py b/src/neo4j/_sync/io/_bolt.py index a08324998..bf00170bc 100644 --- a/src/neo4j/_sync/io/_bolt.py +++ b/src/neo4j/_sync/io/_bolt.py @@ -23,6 +23,7 @@ from logging import getLogger from time import monotonic +from ..._addressing import ResolvedAddress from ..._async_compat.util import Util from ..._auth_management import to_auth_dict from ..._codec.hydration import ( @@ -38,7 +39,6 @@ from ..._io import BoltProtocolVersion from ..._meta import USER_AGENT from ..._sync.config import PoolConfig -from ...addressing import ResolvedAddress from ...api import ServerInfo from ...exceptions import ( ConfigurationError, diff --git a/src/neo4j/_sync/io/_bolt_socket.py b/src/neo4j/_sync/io/_bolt_socket.py index 9050a2fae..263b1aa2b 100644 --- a/src/neo4j/_sync/io/_bolt_socket.py +++ b/src/neo4j/_sync/io/_bolt_socket.py @@ -23,7 +23,7 @@ import typing as t from contextlib import suppress -from ... import addressing +from ..._addressing import Address from ..._async_compat.network import ( BoltSocketBase, NetworkUtil, @@ -44,11 +44,8 @@ import typing_extensions as te + from ..._addressing import ResolvedAddress from ..._deadline import Deadline - from ...addressing import ( - Address, - ResolvedAddress, - ) log = logging.getLogger("neo4j.io") @@ -59,7 +56,7 @@ class HandshakeCtx: ctx: str deadline: Deadline local_port: int - resolved_address: addressing.ResolvedAddress + resolved_address: ResolvedAddress full_response: bytearray = dataclasses.field(default_factory=bytearray) @@ -315,7 +312,7 @@ def connect( # https://docs.python.org/2/library/errno.html resolved_addresses = NetworkUtil.resolve_address( - addressing.Address(address), resolver=custom_resolver + Address(address), resolver=custom_resolver ) for resolved_address in resolved_addresses: deadline_timeout = deadline.to_timeout() diff --git a/src/neo4j/_sync/work/result.py b/src/neo4j/_sync/work/result.py index 2c01788d1..3009ab0ed 100644 --- a/src/neo4j/_sync/work/result.py +++ b/src/neo4j/_sync/work/result.py @@ -66,7 +66,7 @@ if t.TYPE_CHECKING: import pandas # type: ignore[import] - from ...addressing import Address + from ..._addressing import Address from ...graph import Graph diff --git a/src/neo4j/_work/summary.py b/src/neo4j/_work/summary.py index 3dc296174..3842a5ec2 100644 --- a/src/neo4j/_work/summary.py +++ b/src/neo4j/_work/summary.py @@ -33,7 +33,7 @@ if t.TYPE_CHECKING: import typing_extensions as te - from ..addressing import Address + from .._addressing import Address from ..api import ServerInfo _T = t.TypeVar("_T") diff --git a/src/neo4j/addressing.py b/src/neo4j/addressing.py index 4cbe2597f..10a5946b0 100644 --- a/src/neo4j/addressing.py +++ b/src/neo4j/addressing.py @@ -16,358 +16,15 @@ from __future__ import annotations -import typing as t -from contextlib import suppress as _suppress -from socket import ( - AddressFamily, - AF_INET, - AF_INET6, - getservbyname, +from ._addressing import ( + Address, + IPv4Address, + IPv6Address, ) -if t.TYPE_CHECKING: - import typing_extensions as te - - -_T = t.TypeVar("_T") - - -if t.TYPE_CHECKING: - - class _WithPeerName(te.Protocol): - def getpeername(self) -> tuple: ... - - __all__ = [ "Address", "IPv4Address", "IPv6Address", - "ResolvedAddress", - "ResolvedIPv4Address", - "ResolvedIPv6Address", ] - - -class _AddressMeta(type(tuple)): # type: ignore[misc] - def __init__(cls, *args, **kwargs): - super().__init__(*args, **kwargs) - cls._ipv4_cls = None - cls._ipv6_cls = None - - def _subclass_by_family(cls, family): - subclasses = [ - sc - for sc in cls.__subclasses__() - if ( - sc.__module__ == cls.__module__ - and getattr(sc, "family", None) == family - ) - ] - if len(subclasses) != 1: - raise ValueError( - f"Class {cls} needs exactly one direct subclass with " - f"attribute `family == {family}` within this module. " - f"Found: {subclasses}" - ) - return subclasses[0] - - @property - def ipv4_cls(cls): - if cls._ipv4_cls is None: - cls._ipv4_cls = cls._subclass_by_family(AF_INET) - return cls._ipv4_cls - - @property - def ipv6_cls(cls): - if cls._ipv6_cls is None: - cls._ipv6_cls = cls._subclass_by_family(AF_INET6) - return cls._ipv6_cls - - -class Address(tuple, metaclass=_AddressMeta): - """ - Base class to represent server addresses within the driver. - - A tuple of two (IPv4) or four (IPv6) elements, representing the address - parts. See also python's :mod:`socket` module for more information. - - >>> Address(("example.com", 7687)) - IPv4Address(('example.com', 7687)) - >>> Address(("127.0.0.1", 7687)) - IPv4Address(('127.0.0.1', 7687)) - >>> Address(("::1", 7687, 0, 0)) - IPv6Address(('::1', 7687, 0, 0)) - - :param iterable: A collection of two or four elements creating an - :class:`.IPv4Address` or :class:`.IPv6Address` instance respectively. - """ - - #: Address family (:data:`socket.AF_INET` or :data:`socket.AF_INET6`). - family: AddressFamily | None = None - - def __new__(cls, iterable: t.Collection) -> Address: - if isinstance(iterable, cls): - return iterable - n_parts = len(iterable) - inst = tuple.__new__(cls, iterable) - if n_parts == 2: - inst.__class__ = cls.ipv4_cls - elif n_parts == 4: - inst.__class__ = cls.ipv6_cls - else: - raise ValueError( - "Addresses must consist of either " - "two parts (IPv4) or four parts (IPv6)" - ) - return inst - - @classmethod - def from_socket(cls, socket: _WithPeerName) -> Address: - """ - Create an address from a socket object. - - Uses the socket's ``getpeername`` method to retrieve the remote - address the socket is connected to. - """ - address = socket.getpeername() - return cls(address) - - @classmethod - def parse( - cls, - s: str, - default_host: str | None = None, - default_port: int | None = None, - ) -> Address: - """ - Parse a string into an address. - - The string must be in the format ``host:port`` (IPv4) or - ``[host]:port`` (IPv6). - If no port is specified, or is empty, ``default_port`` will be used. - If no host is specified, or is empty, ``default_host`` will be used. - - >>> Address.parse("localhost:7687") - IPv4Address(('localhost', 7687)) - >>> Address.parse("[::1]:7687") - IPv6Address(('::1', 7687, 0, 0)) - >>> Address.parse("localhost") - IPv4Address(('localhost', 0)) - >>> Address.parse("localhost", default_port=1234) - IPv4Address(('localhost', 1234)) - - :param s: The string to parse. - :param default_host: The default host to use if none is specified. - :data:`None` indicates to use ``"localhost"`` as default. - :param default_port: The default port to use if none is specified. - :data:`None` indicates to use ``0`` as default. - - :returns: The parsed address. - """ - if not isinstance(s, str): - raise TypeError("Address.parse requires a string argument") - if s.startswith("["): - # IPv6 - port: str | int - host, _, port = s[1:].rpartition("]") - port = port.lstrip(":") - with _suppress(TypeError, ValueError): - port = int(port) - host = host or default_host or "localhost" - port = port or default_port or 0 - return cls((host, port, 0, 0)) - else: - # IPv4 - host, _, port = s.partition(":") - with _suppress(TypeError, ValueError): - port = int(port) - host = host or default_host or "localhost" - port = port or default_port or 0 - return cls((host, port)) - - @classmethod - def parse_list( - cls, - *s: str, - default_host: str | None = None, - default_port: int | None = None, - ) -> list[Address]: - """ - Parse multiple addresses into a list. - - See :meth:`.parse` for details on the string format. - - Either a whitespace-separated list of strings or multiple strings - can be used. - - >>> Address.parse_list("localhost:7687", "[::1]:7687") - [IPv4Address(('localhost', 7687)), IPv6Address(('::1', 7687, 0, 0))] - >>> Address.parse_list("localhost:7687 [::1]:7687") - [IPv4Address(('localhost', 7687)), IPv6Address(('::1', 7687, 0, 0))] - - :param s: The string(s) to parse. - :param default_host: The default host to use if none is specified. - :data:`None` indicates to use ``"localhost"`` as default. - :param default_port: The default port to use if none is specified. - :data:`None` indicates to use ``0`` as default. - - :returns: The list of parsed addresses. - """ # noqa: E501 can't split the doctest lines - if not all(isinstance(s0, str) for s0 in s): - raise TypeError("Address.parse_list requires a string argument") - return [ - cls.parse(a, default_host, default_port) - for a in " ".join(s).split() - ] - - def __repr__(self): - return f"{self.__class__.__name__}({tuple(self)!r})" - - @property - def _host_name(self) -> t.Any: - return self[0] - - @property - def host(self) -> t.Any: - """ - The host part of the address. - - This is the first part of the address tuple. - - >>> Address(("localhost", 7687)).host - 'localhost' - """ - return self[0] - - @property - def port(self) -> t.Any: - """ - The port part of the address. - - This is the second part of the address tuple. - - >>> Address(("localhost", 7687)).port - 7687 - >>> Address(("localhost", 7687, 0, 0)).port - 7687 - >>> Address(("localhost", "7687")).port - '7687' - >>> Address(("localhost", "http")).port - 'http' - """ - return self[1] - - @property - def _unresolved(self) -> Address: - return self - - @property - def port_number(self) -> int: - """ - The port part of the address as an integer. - - First try to resolve the port as an integer, using - :func:`socket.getservbyname`. If that fails, fall back to parsing the - port as an integer. - - >>> Address(("localhost", 7687)).port_number - 7687 - >>> Address(("localhost", "http")).port_number - 80 - >>> Address(("localhost", "7687")).port_number - 7687 - >>> Address(("localhost", [])).port_number - Traceback (most recent call last): - ... - TypeError: Unknown port value [] - >>> Address(("localhost", "banana-protocol")).port_number - Traceback (most recent call last): - ... - ValueError: Unknown port value 'banana-protocol' - - :returns: The resolved port number. - - :raise ValueError: If the port cannot be resolved. - :raise TypeError: If the port cannot be resolved. - """ - error_cls: type = TypeError - - try: - return getservbyname(self[1]) - except OSError: - # OSError: service/proto not found - error_cls = ValueError - except TypeError: - # TypeError: getservbyname() argument 1 must be str, not X - pass - try: - return int(self[1]) - except ValueError: - error_cls = ValueError - except TypeError: - pass - raise error_cls(f"Unknown port value {self[1]!r}") - - -class IPv4Address(Address): - """ - An IPv4 address (family ``AF_INET``). - - This class is also used for addresses that specify a host name instead of - an IP address. E.g., - - >>> Address(("example.com", 7687)) - IPv4Address(('example.com', 7687)) - - This class should not be instantiated directly. Instead, use - :class:`.Address` or one of its factory methods. - """ - - family = AF_INET - - def __str__(self) -> str: - return "{}:{}".format(*self) - - -class IPv6Address(Address): - """ - An IPv6 address (family ``AF_INET6``). - - This class should not be instantiated directly. Instead, use - :class:`.Address` or one of its factory methods. - """ - - family = AF_INET6 - - def __str__(self) -> str: - return "[{}]:{}".format(*self) - - -# TODO: 6.0 - make this class private -class ResolvedAddress(Address): - _unresolved_host_name: str - - @property - def _host_name(self) -> str: - return self._unresolved_host_name - - @property - def _unresolved(self) -> Address: - return super().__new__(Address, (self._host_name, *self[1:])) - - def __new__(cls, iterable, *, host_name: str) -> ResolvedAddress: - new = super().__new__(cls, iterable) - new = t.cast(ResolvedAddress, new) - new._unresolved_host_name = host_name - return new - - -# TODO: 6.0 - make this class private -class ResolvedIPv4Address(IPv4Address, ResolvedAddress): - pass - - -# TODO: 6.0 - make this class private -class ResolvedIPv6Address(IPv6Address, ResolvedAddress): - pass diff --git a/src/neo4j/api.py b/src/neo4j/api.py index d43e69e95..c72b1cf2c 100644 --- a/src/neo4j/api.py +++ b/src/neo4j/api.py @@ -38,7 +38,7 @@ import typing_extensions as te from typing_extensions import Protocol as _Protocol - from .addressing import Address + from ._addressing import Address else: _Protocol = object diff --git a/tests/unit/async_/io/test_neo4j_pool.py b/tests/unit/async_/io/test_neo4j_pool.py index e1549fd06..1517d5d6e 100644 --- a/tests/unit/async_/io/test_neo4j_pool.py +++ b/tests/unit/async_/io/test_neo4j_pool.py @@ -22,6 +22,7 @@ READ_ACCESS, WRITE_ACCESS, ) +from neo4j._addressing import ResolvedAddress from neo4j._async.config import AsyncPoolConfig from neo4j._async.io import ( AcquisitionDatabase, @@ -34,7 +35,6 @@ WorkspaceConfig, ) from neo4j._deadline import Deadline -from neo4j.addressing import ResolvedAddress from neo4j.auth_management import AsyncAuthManagers from neo4j.exceptions import ( Neo4jError, diff --git a/tests/unit/sync/io/test_neo4j_pool.py b/tests/unit/sync/io/test_neo4j_pool.py index 13b9be4e2..7ee1f4a79 100644 --- a/tests/unit/sync/io/test_neo4j_pool.py +++ b/tests/unit/sync/io/test_neo4j_pool.py @@ -22,6 +22,7 @@ READ_ACCESS, WRITE_ACCESS, ) +from neo4j._addressing import ResolvedAddress from neo4j._async_compat.util import Util from neo4j._conf import ( RoutingConfig, @@ -34,7 +35,6 @@ Bolt, Neo4jPool, ) -from neo4j.addressing import ResolvedAddress from neo4j.auth_management import AuthManagers from neo4j.exceptions import ( Neo4jError,