From bafa759d064555ff00d70eb9d429e2ab163d3370 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 7 Dec 2022 11:03:19 +0100 Subject: [PATCH 1/4] API docs: elaborate address resolver docs --- docs/source/api.rst | 69 ++++-- docs/source/async_api.rst | 40 ++-- src/neo4j/_async/driver.py | 2 +- src/neo4j/_async/io/_pool.py | 2 +- src/neo4j/_async/work/result.py | 8 +- src/neo4j/_async/work/transaction.py | 8 +- .../_async_compat/network/_bolt_socket.py | 8 +- src/neo4j/_async_compat/network/_util.py | 4 +- src/neo4j/_data.py | 2 +- src/neo4j/_sync/driver.py | 2 +- src/neo4j/_sync/io/_pool.py | 2 +- src/neo4j/_sync/work/result.py | 8 +- src/neo4j/_sync/work/transaction.py | 8 +- src/neo4j/addressing.py | 210 ++++++++++++++---- src/neo4j/debug.py | 4 +- src/neo4j/time/__init__.py | 4 +- tests/unit/async_/test_addressing.py | 4 +- tests/unit/sync/test_addressing.py | 4 +- tox.ini | 1 + 19 files changed, 280 insertions(+), 110 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 509bbfbac..987656a8f 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -265,8 +265,8 @@ The maximum total number of connections allowed, per host (i.e. cluster nodes), ``resolver`` ------------ -A custom resolver function to resolve host and port values ahead of DNS resolution. -This function is called with a 2-tuple of (host, port) and should return an iterable of 2-tuples (host, port). +A custom resolver function to resolve any addresses the driver receives ahead of DNS resolution. +This function is called with an :class:`.Address` and should return an iterable of :class:`.Address` objects or values that can be used to construct :class:`.Address` objects. If no custom resolver function is supplied, the internal resolver moves straight to regular DNS resolution. @@ -274,24 +274,36 @@ For example: .. code-block:: python - from neo4j import GraphDatabase + import neo4j - def custom_resolver(socket_address): - if socket_address == ("example.com", 9999): - yield "::1", 7687 - yield "127.0.0.1", 7687 - else: - from socket import gaierror - raise gaierror("Unexpected socket address %r" % socket_address) + def custom_resolver(socket_address): + # assert isinstance(socket_address, neo4j.Address) + if socket_address != ("example.com", 9999): + raise OSError(f"Unexpected socket address {socket_address!r}") + # You can return any neo4j.Address object + yield neo4j.Address(("localhost", 7687)) # IPv4 + yield neo4j.Address(("::1", 7687, 0, 0)) # IPv6 + yield neo4j.Address.parse("localhost:7687") + yield neo4j.Address.parse("[::1]:7687") - driver = GraphDatabase.driver("neo4j://example.com:9999", - auth=("neo4j", "password"), - resolver=custom_resolver) + # or any tuple that can be passed to neo4j.Address(...). + # Initially, this will be interpreted as IPv4, but DNS resolution + # will turn it into IPv6 if appropriate. + yield "::1", 7687 + # This will be interpreted as IPv6 directly, but DNS resolution will + # happen still. + yield "::1", 7687, 0, 0 + yield "127.0.0.1", 7687 -:Default: :const:`None` + driver = neo4j.GraphDatabase.driver("neo4j://example.com:9999", + auth=("neo4j", "password"), + resolver=custom_resolver) + + +:Default: :data:`None` .. _trust-ref: @@ -337,8 +349,8 @@ If given, ``encrypted`` and ``trusted_certificates`` have no effect. Its usage is strongly discouraged and comes without any guarantees. -:Type: :class:`ssl.SSLContext` or :const:`None` -:Default: :const:`None` +:Type: :class:`ssl.SSLContext` or :data:`None` +:Default: :data:`None` .. versionadded:: 5.0 @@ -656,7 +668,7 @@ context of the impersonated user. For this, the user for which the :Type: ``str``, None -:Default: :const:`None` +:Default: :data:`None` .. _default-access-mode-ref: @@ -721,8 +733,8 @@ See :class:`.BookmarkManager` for more information. For simple use-cases, it often suffices that work within a single session is automatically causally consistent. -:Type: :const:`None` or :class:`.BookmarkManager` -:Default: :const:`None` +:Type: :data:`None` or :class:`.BookmarkManager` +:Default: :data:`None` .. versionadded:: 5.0 @@ -1069,7 +1081,7 @@ The core types with their general mappings are listed below: +------------------------+---------------------------------------------------------------------------------------------------------------------------+ | Cypher Type | Python Type | +========================+===========================================================================================================================+ -| Null | :const:`None` | +| Null | :data:`None` | +------------------------+---------------------------------------------------------------------------------------------------------------------------+ | Boolean | :class:`bool` | +------------------------+---------------------------------------------------------------------------------------------------------------------------+ @@ -1295,6 +1307,23 @@ BookmarkManager :members: +************************* +Constants, Enums, Helpers +************************* + +.. autoclass:: neo4j.Address + :show-inheritance: + :members: + + +.. autoclass:: neo4j.IPv4Address() + :show-inheritance: + + +.. autoclass:: neo4j.IPv6Address() + :show-inheritance: + + .. _errors-ref: ****** diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 20c5746f3..06be5c0dc 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -152,8 +152,8 @@ driver accepts an async custom resolver function: ``resolver`` ------------ -A custom resolver function to resolve host and port values ahead of DNS resolution. -This function is called with a 2-tuple of (host, port) and should return an iterable of 2-tuples (host, port). +A custom resolver function to resolve any addresses the driver receives ahead of DNS resolution. +This function is called with an :class:`.Address` and should return an iterable of :class:`.Address` objects or values that can be used to construct :class:`.Address` objects. If no custom resolver function is supplied, the internal resolver moves straight to regular DNS resolution. @@ -163,16 +163,28 @@ For example: .. code-block:: python - from neo4j import AsyncGraphDatabase + import neo4j - async def custom_resolver(socket_address): - if socket_address == ("example.com", 9999): - yield "::1", 7687 - yield "127.0.0.1", 7687 - else: - from socket import gaierror - raise gaierror("Unexpected socket address %r" % socket_address) + def custom_resolver(socket_address): + # assert isinstance(socket_address, neo4j.Address) + if socket_address != ("example.com", 9999): + raise OSError(f"Unexpected socket address {socket_address!r}") + + # You can return any neo4j.Address object + yield neo4j.Address(("localhost", 7687)) # IPv4 + yield neo4j.Address(("::1", 7687, 0, 0)) # IPv6 + yield neo4j.Address.parse("localhost:7687") + yield neo4j.Address.parse("[::1]:7687") + + # or any tuple that can be passed to neo4j.Address(...). + # Initially, this will be interpreted as IPv4, but DNS resolution + # will turn it into IPv6 if appropriate. + yield "::1", 7687 + # This will be interpreted as IPv6 directly, but DNS resolution will + # happen still. + yield "::1", 7687, 0, 0 + yield "127.0.0.1", 7687 # alternatively @@ -180,12 +192,12 @@ For example: ... - driver = AsyncGraphDatabase.driver("neo4j://example.com:9999", + driver = neo4j.GraphDatabase.driver("neo4j://example.com:9999", auth=("neo4j", "password"), resolver=custom_resolver) -:Default: :const:`None` +:Default: :data:`None` @@ -406,8 +418,8 @@ See :class:`BookmarkManager` for more information. group a series of queries together that will be causally chained automatically. -:Type: :const:`None`, :class:`BookmarkManager`, or :class:`AsyncBookmarkManager` -:Default: :const:`None` +:Type: :data:`None`, :class:`BookmarkManager`, or :class:`AsyncBookmarkManager` +:Default: :data:`None` **This is experimental.** (See :ref:`filter-warnings-ref`) It might be changed or removed any time even without prior notice. diff --git a/src/neo4j/_async/driver.py b/src/neo4j/_async/driver.py index 06f116a24..aa20efd18 100644 --- a/src/neo4j/_async/driver.py +++ b/src/neo4j/_async/driver.py @@ -274,7 +274,7 @@ def bookmark_manager( Function which will be called whenever the set of bookmarks handled by the bookmark manager gets updated with the new internal bookmark set. It will receive the new set of bookmarks - as a :class:`.Bookmarks` object and return :const:`None`. + as a :class:`.Bookmarks` object and return :data:`None`. :returns: A default implementation of :class:`AsyncBookmarkManager`. diff --git a/src/neo4j/_async/io/_pool.py b/src/neo4j/_async/io/_pool.py index deaa27c37..8a0e9ebcf 100644 --- a/src/neo4j/_async/io/_pool.py +++ b/src/neo4j/_async/io/_pool.py @@ -679,7 +679,7 @@ async def update_connection_pool(self, *, database): routing_table = await self.get_or_create_routing_table(database) servers = routing_table.servers() for address in list(self.connections): - if address.unresolved not in servers: + if address._unresolved not in servers: await super(AsyncNeo4jPool, self).deactivate(address) async def ensure_routing_table_is_fresh( diff --git a/src/neo4j/_async/work/result.py b/src/neo4j/_async/work/result.py index ffcf18679..db6b6acbf 100644 --- a/src/neo4j/_async/work/result.py +++ b/src/neo4j/_async/work/result.py @@ -418,7 +418,7 @@ async def single(self, strict: bool = False) -> t.Optional[Record]: there is not exactly one record left. If ``strict`` is :const:`False`, fewer than one record will make this - method return :const:`None`, more than one record will make this method + method return :data:`None`, more than one record will make this method emit a warning and return the first record. :param strict: @@ -427,7 +427,7 @@ async def single(self, strict: bool = False) -> t.Optional[Record]: warning if there are more than 1 record. :const:`False` by default. - :returns: the next :class:`neo4j.Record` or :const:`None` if none remain + :returns: the next :class:`neo4j.Record` or :data:`None` if none remain :warns: if more than one record is available and ``strict`` is :const:`False` @@ -496,7 +496,7 @@ async def peek(self) -> t.Optional[Record]: This leaves the record in the buffer for further processing. - :returns: the next :class:`neo4j.Record` or :const:`None` if none + :returns: the next :class:`neo4j.Record` or :data:`None` if none remain. :raises ResultConsumedError: if the transaction from which this result @@ -683,7 +683,7 @@ async def to_df( :param parse_dates: If :const:`True`, columns that exclusively contain :class:`time.DateTime` objects, :class:`time.Date` objects, or - :const:`None`, will be converted to :class:`pandas.Timestamp`. + :data:`None`, will be converted to :class:`pandas.Timestamp`. :raises ImportError: if `pandas` library is not available. :raises ResultConsumedError: if the transaction from which this result diff --git a/src/neo4j/_async/work/transaction.py b/src/neo4j/_async/work/transaction.py index 40b546323..7baf8387f 100644 --- a/src/neo4j/_async/work/transaction.py +++ b/src/neo4j/_async/work/transaction.py @@ -111,10 +111,10 @@ async def run( arguments, or as a mixture of both. For example, the `run` queries below are all equivalent:: - >>> query = "CREATE (a:Person { name: $name, age: $age })" - >>> result = await tx.run(query, {"name": "Alice", "age": 33}) - >>> result = await tx.run(query, {"name": "Alice"}, age=33) - >>> result = await tx.run(query, name="Alice", age=33) + query = "CREATE (a:Person { name: $name, age: $age })" + result = await tx.run(query, {"name": "Alice", "age": 33}) + result = await tx.run(query, {"name": "Alice"}, age=33) + result = await tx.run(query, name="Alice", age=33) Parameter values can be of any type supported by the Neo4j type system. In Python, this includes :class:`bool`, :class:`int`, diff --git a/src/neo4j/_async_compat/network/_bolt_socket.py b/src/neo4j/_async_compat/network/_bolt_socket.py index cdbe28524..da0aae661 100644 --- a/src/neo4j/_async_compat/network/_bolt_socket.py +++ b/src/neo4j/_async_compat/network/_bolt_socket.py @@ -207,7 +207,7 @@ async def _connect_secure(cls, resolved_address, timeout, keep_alive, ssl): ssl_kwargs = {} if ssl is not None: - hostname = resolved_address.host_name or None + hostname = resolved_address._host_name or None ssl_kwargs.update( ssl=ssl, server_hostname=hostname if HAS_SNI else None ) @@ -232,7 +232,7 @@ async def _connect_secure(cls, resolved_address, timeout, keep_alive, ssl): raise BoltProtocolError( "When using an encrypted socket, the server should " "always provide a certificate", - address=(resolved_address.host_name, local_port) + address=(resolved_address._host_name, local_port) ) return cls(reader, protocol, writer) @@ -257,7 +257,7 @@ async def _connect_secure(cls, resolved_address, timeout, keep_alive, ssl): await cls.close_socket(s) raise BoltSecurityError( message="Failed to establish encrypted connection.", - address=(resolved_address.host_name, local_port) + address=(resolved_address._host_name, local_port) ) from error except Exception as error: log.debug("[#0000] S: %s %s", type(error).__name__, @@ -650,7 +650,7 @@ def connect(cls, address, *, timeout, custom_resolver, ssl_context, s = None try: s = BoltSocket._connect(resolved_address, timeout, keep_alive) - s = BoltSocket._secure(s, resolved_address.host_name, + s = BoltSocket._secure(s, resolved_address._host_name, ssl_context) return BoltSocket._handshake(s, resolved_address) except (BoltError, DriverError, OSError) as error: diff --git a/src/neo4j/_async_compat/network/_util.py b/src/neo4j/_async_compat/network/_util.py index 708d3eb9b..c292cd221 100644 --- a/src/neo4j/_async_compat/network/_util.py +++ b/src/neo4j/_async_compat/network/_util.py @@ -47,7 +47,7 @@ async def _dns_resolver(address, family=0): ) except OSError: raise ValueError("Cannot resolve address {}".format(address)) - return list(_resolved_addresses_from_info(info, address.host_name)) + return list(_resolved_addresses_from_info(info, address._host_name)) @staticmethod async def resolve_address(address, family=0, resolver=None): @@ -117,7 +117,7 @@ def _dns_resolver(address, family=0): ) except OSError: raise ValueError("Cannot resolve address {}".format(address)) - return _resolved_addresses_from_info(info, address.host_name) + return _resolved_addresses_from_info(info, address._host_name) @staticmethod def resolve_address(address, family=0, resolver=None): diff --git a/src/neo4j/_data.py b/src/neo4j/_data.py index 9a680f77d..0046fd122 100644 --- a/src/neo4j/_data.py +++ b/src/neo4j/_data.py @@ -246,7 +246,7 @@ def data(self, *keys: _K) -> t.Dict[str, t.Any]: """ Return the keys and values of this record as a dictionary, optionally including only certain values by index or key. Keys provided in the items that are not in the record will be - inserted with a value of :const:`None`; indexes provided + inserted with a value of :data:`None`; indexes provided that are out of bounds will trigger an :exc:`IndexError`. :param keys: indexes or keys of the items to include; if none diff --git a/src/neo4j/_sync/driver.py b/src/neo4j/_sync/driver.py index 191922f95..86b4592f3 100644 --- a/src/neo4j/_sync/driver.py +++ b/src/neo4j/_sync/driver.py @@ -271,7 +271,7 @@ def bookmark_manager( Function which will be called whenever the set of bookmarks handled by the bookmark manager gets updated with the new internal bookmark set. It will receive the new set of bookmarks - as a :class:`.Bookmarks` object and return :const:`None`. + as a :class:`.Bookmarks` object and return :data:`None`. :returns: A default implementation of :class:`BookmarkManager`. diff --git a/src/neo4j/_sync/io/_pool.py b/src/neo4j/_sync/io/_pool.py index bec22e370..1a3819822 100644 --- a/src/neo4j/_sync/io/_pool.py +++ b/src/neo4j/_sync/io/_pool.py @@ -679,7 +679,7 @@ def update_connection_pool(self, *, database): routing_table = self.get_or_create_routing_table(database) servers = routing_table.servers() for address in list(self.connections): - if address.unresolved not in servers: + if address._unresolved not in servers: super(Neo4jPool, self).deactivate(address) def ensure_routing_table_is_fresh( diff --git a/src/neo4j/_sync/work/result.py b/src/neo4j/_sync/work/result.py index 0f467d6b2..71a4b6720 100644 --- a/src/neo4j/_sync/work/result.py +++ b/src/neo4j/_sync/work/result.py @@ -418,7 +418,7 @@ def single(self, strict: bool = False) -> t.Optional[Record]: there is not exactly one record left. If ``strict`` is :const:`False`, fewer than one record will make this - method return :const:`None`, more than one record will make this method + method return :data:`None`, more than one record will make this method emit a warning and return the first record. :param strict: @@ -427,7 +427,7 @@ def single(self, strict: bool = False) -> t.Optional[Record]: warning if there are more than 1 record. :const:`False` by default. - :returns: the next :class:`neo4j.Record` or :const:`None` if none remain + :returns: the next :class:`neo4j.Record` or :data:`None` if none remain :warns: if more than one record is available and ``strict`` is :const:`False` @@ -496,7 +496,7 @@ def peek(self) -> t.Optional[Record]: This leaves the record in the buffer for further processing. - :returns: the next :class:`neo4j.Record` or :const:`None` if none + :returns: the next :class:`neo4j.Record` or :data:`None` if none remain. :raises ResultConsumedError: if the transaction from which this result @@ -683,7 +683,7 @@ def to_df( :param parse_dates: If :const:`True`, columns that exclusively contain :class:`time.DateTime` objects, :class:`time.Date` objects, or - :const:`None`, will be converted to :class:`pandas.Timestamp`. + :data:`None`, will be converted to :class:`pandas.Timestamp`. :raises ImportError: if `pandas` library is not available. :raises ResultConsumedError: if the transaction from which this result diff --git a/src/neo4j/_sync/work/transaction.py b/src/neo4j/_sync/work/transaction.py index fdccbf3d9..7ed47e6d9 100644 --- a/src/neo4j/_sync/work/transaction.py +++ b/src/neo4j/_sync/work/transaction.py @@ -111,10 +111,10 @@ def run( arguments, or as a mixture of both. For example, the `run` queries below are all equivalent:: - >>> query = "CREATE (a:Person { name: $name, age: $age })" - >>> result = tx.run(query, {"name": "Alice", "age": 33}) - >>> result = tx.run(query, {"name": "Alice"}, age=33) - >>> result = tx.run(query, name="Alice", age=33) + query = "CREATE (a:Person { name: $name, age: $age })" + result = tx.run(query, {"name": "Alice", "age": 33}) + result = tx.run(query, {"name": "Alice"}, age=33) + result = tx.run(query, name="Alice", age=33) Parameter values can be of any type supported by the Neo4j type system. In Python, this includes :class:`bool`, :class:`int`, diff --git a/src/neo4j/addressing.py b/src/neo4j/addressing.py index 6e26fc6d7..e459e53c8 100644 --- a/src/neo4j/addressing.py +++ b/src/neo4j/addressing.py @@ -79,22 +79,83 @@ def ipv6_cls(self): 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: t.Optional[AddressFamily] = 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: t.Type[_TAddress], + cls, socket: _WithPeerName - ) -> _TAddress: + ) -> 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: t.Type[_TAddress], + cls, s: str, default_host: t.Optional[str] = None, default_port: t.Optional[int] = None - ) -> _TAddress: + ) -> Address: + """Parse a string into an address. + + The string must be in the format ``host:port`` (IPv4) or + ``[host]:port`` (IPv6). + If not port is specified, or is emtpy, ``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` will be substituted with ``"localhost"``. + :param default_port: The default port to use if none is specified. + :data:`None` will be substituted with ``0``. + + :return: The parsed address. + """ if not isinstance(s, str): raise TypeError("Address.parse requires a string argument") if s.startswith("["): @@ -120,72 +181,134 @@ def parse( @classmethod def parse_list( - cls: t.Type[_TAddress], + cls, *s: str, default_host: t.Optional[str] = None, default_port: t.Optional[int] = None - ) -> t.List[_TAddress]: - """ Parse a string containing one or more socket addresses, each - separated by whitespace. + ) -> t.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` will be substituted with ``"localhost"``. + :param default_port: The default port to use if none is specified. + :data:`None` will be substituted with ``0``. + + :return: The list of parsed addresses. """ 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 __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 - - #: Address family (AF_INET or AF_INET6) - family: t.Optional[AddressFamily] = None - def __repr__(self): return "{}({!r})".format(self.__class__.__name__, tuple(self)) @property - def host_name(self) -> str: + def _host_name(self) -> t.Any: return self[0] @property - def host(self) -> str: + 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) -> int: + 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: + 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 + :meth:`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: t.Type = TypeError + try: return getservbyname(self[1]) - except (OSError, TypeError): + except OSError: # OSError: service/proto not found + error_cls = ValueError + except TypeError: # TypeError: getservbyname() argument 1 must be str, not X - try: - return int(self[1]) - except (TypeError, ValueError) as e: - raise type(e)("Unknown port value %r" % self[1]) + pass + try: + return int(self[1]) + except ValueError: + error_cls = ValueError + except TypeError: + pass + raise error_cls("Unknown port value %r" % self[1]) + +class IPv4Address(Address): + """An IPv4 address (family ``AF_INET``). -_TAddress = t.TypeVar("_TAddress", bound=Address) + 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)) -class IPv4Address(Address): + This class should not be instantiated directly. Instead, use + :class:`.Address` or one of its factory methods. + """ family = AF_INET @@ -194,6 +317,11 @@ def __str__(self) -> str: class IPv6Address(Address): + """An IPv6 address (family ``AF_INETl``). + + This class should not be instantiated directly. Instead, use + :class:`.Address` or one of its factory methods. + """ family = AF_INET6 @@ -203,20 +331,20 @@ def __str__(self) -> str: class ResolvedAddress(Address): - _host_name: str + _unresolved_host_name: str @property - def host_name(self) -> str: - return self._host_name + def _host_name(self) -> str: + return self._unresolved_host_name @property - def unresolved(self) -> Address: + 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._host_name = host_name + new._unresolved_host_name = host_name return new diff --git a/src/neo4j/debug.py b/src/neo4j/debug.py index 5af57a395..579f45e74 100644 --- a/src/neo4j/debug.py +++ b/src/neo4j/debug.py @@ -155,9 +155,9 @@ def watch( """Enable logging for all loggers. :param level: Minimum log level to show. - If :const:`None`, the ``default_level`` is used. + If :data:`None`, the ``default_level`` is used. :param out: Output stream for all loggers. - If :const:`None`, the ``default_out`` is used. + If :data:`None`, the ``default_out`` is used. :type out: stream or file-like object """ if level is None: diff --git a/src/neo4j/time/__init__.py b/src/neo4j/time/__init__.py index 7ad53a40c..0ddc84289 100644 --- a/src/neo4j/time/__init__.py +++ b/src/neo4j/time/__init__.py @@ -1988,7 +1988,7 @@ class DateTime(date_time_base_class, metaclass=DateTimeType): Regular construction of a :class:`.DateTime` object requires at least the `year`, `month` and `day` arguments to be supplied. The optional `hour`, `minute` and `second` arguments default to zero and - `tzinfo` defaults to :const:`None`. + `tzinfo` defaults to :data:`None`. `year`, `month`, and `day` are passed to the constructor of :class:`.Date`. `hour`, `minute`, `second`, `nanosecond`, and `tzinfo` are passed to the @@ -1997,7 +1997,7 @@ class DateTime(date_time_base_class, metaclass=DateTimeType): >>> dt = DateTime(2018, 4, 30, 12, 34, 56, 789123456); dt neo4j.time.DateTime(2018, 4, 30, 12, 34, 56, 789123456) >>> dt.second - 56.789123456 + 56 """ __date: Date diff --git a/tests/unit/async_/test_addressing.py b/tests/unit/async_/test_addressing.py index 75a036f9c..ca5eb0f01 100644 --- a/tests/unit/async_/test_addressing.py +++ b/tests/unit/async_/test_addressing.py @@ -107,7 +107,7 @@ def custom_resolver(_): custom_resolved = [("127.0.0.1", 7687), ("localhost", 4321)] address = Address(("foobar", 1234)) - unresolved = address.unresolved + unresolved = address._unresolved assert address.__class__ == unresolved.__class__ assert address == unresolved resolved = AsyncNetworkUtil.resolve_address( @@ -115,7 +115,7 @@ def custom_resolver(_): ) resolved_list = await AsyncUtil.list(resolved) custom_resolved_addresses = sorted(Address(a) for a in custom_resolved) - unresolved_list = sorted(a.unresolved for a in resolved_list) + unresolved_list = sorted(a._unresolved for a in resolved_list) assert custom_resolved_addresses == unresolved_list assert (list(map(lambda a: a.__class__, custom_resolved_addresses)) == list(map(lambda a: a.__class__, unresolved_list))) diff --git a/tests/unit/sync/test_addressing.py b/tests/unit/sync/test_addressing.py index 444b289d4..190ac4169 100644 --- a/tests/unit/sync/test_addressing.py +++ b/tests/unit/sync/test_addressing.py @@ -107,7 +107,7 @@ def custom_resolver(_): custom_resolved = [("127.0.0.1", 7687), ("localhost", 4321)] address = Address(("foobar", 1234)) - unresolved = address.unresolved + unresolved = address._unresolved assert address.__class__ == unresolved.__class__ assert address == unresolved resolved = NetworkUtil.resolve_address( @@ -115,7 +115,7 @@ def custom_resolver(_): ) resolved_list = Util.list(resolved) custom_resolved_addresses = sorted(Address(a) for a in custom_resolved) - unresolved_list = sorted(a.unresolved for a in resolved_list) + unresolved_list = sorted(a._unresolved for a in resolved_list) assert custom_resolved_addresses == unresolved_list assert (list(map(lambda a: a.__class__, custom_resolved_addresses)) == list(map(lambda a: a.__class__, unresolved_list))) diff --git a/tox.ini b/tox.ini index ee21d3ce4..82fb5767d 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ usedevelop = true commands = coverage erase unit: coverage run -m pytest -W error -v {posargs} tests/unit + unit: coverage run -m pytest -v --doctest-modules {posargs} src integration: coverage run -m pytest -W error -v {posargs} tests/integration performance: python -m pytest --benchmark-autosave -v {posargs} tests/performance unit,integration: coverage report From 4526578b6249ae76ed908dc4d3a32051017e28ae Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Wed, 7 Dec 2022 11:33:17 +0100 Subject: [PATCH 2/4] API docs: fix CSS: vertical alignment of dl/dt/dd --- docs/source/_static/nature_custom.css_t | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/_static/nature_custom.css_t b/docs/source/_static/nature_custom.css_t index eedcdc740..f1f54a2ef 100644 --- a/docs/source/_static/nature_custom.css_t +++ b/docs/source/_static/nature_custom.css_t @@ -311,3 +311,7 @@ li > p:first-child { li > p:last-child { margin-top: 10px; } + +dl { + align-items: baseline; +} From a08a54d0ebca42676d23b395b8e5542c751b8b6e Mon Sep 17 00:00:00 2001 From: Stefano Ottolenghi Date: Thu, 8 Dec 2022 10:00:15 +0100 Subject: [PATCH 3/4] Editorial changes Signed-off-by: Rouven Bauer --- docs/source/api.rst | 2 +- docs/source/async_api.rst | 4 ++-- src/neo4j/addressing.py | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 987656a8f..e522425fe 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -293,7 +293,7 @@ For example: # will turn it into IPv6 if appropriate. yield "::1", 7687 # This will be interpreted as IPv6 directly, but DNS resolution will - # happen still. + # still happen. yield "::1", 7687, 0, 0 yield "127.0.0.1", 7687 diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 06be5c0dc..dc18d23b8 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -166,7 +166,7 @@ For example: import neo4j - def custom_resolver(socket_address): + async def custom_resolver(socket_address): # assert isinstance(socket_address, neo4j.Address) if socket_address != ("example.com", 9999): raise OSError(f"Unexpected socket address {socket_address!r}") @@ -182,7 +182,7 @@ For example: # will turn it into IPv6 if appropriate. yield "::1", 7687 # This will be interpreted as IPv6 directly, but DNS resolution will - # happen still. + # still happen. yield "::1", 7687, 0, 0 yield "127.0.0.1", 7687 diff --git a/src/neo4j/addressing.py b/src/neo4j/addressing.py index e459e53c8..74d0528b0 100644 --- a/src/neo4j/addressing.py +++ b/src/neo4j/addressing.py @@ -136,7 +136,7 @@ def parse( The string must be in the format ``host:port`` (IPv4) or ``[host]:port`` (IPv6). - If not port is specified, or is emtpy, ``default_port`` will be used. + 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") @@ -150,9 +150,9 @@ def parse( :param s: The string to parse. :param default_host: The default host to use if none is specified. - :data:`None` will be substituted with ``"localhost"``. + :data:`None` idicates to use ``"localhost"`` as default. :param default_port: The default port to use if none is specified. - :data:`None` will be substituted with ``0``. + :data:`None` idicates to use ``0`` as default. :return: The parsed address. """ @@ -200,9 +200,9 @@ def parse_list( :param s: The string(s) to parse. :param default_host: The default host to use if none is specified. - :data:`None` will be substituted with ``"localhost"``. + :data:`None` indicates to use ``"localhost"`` as default. :param default_port: The default port to use if none is specified. - :data:`None` will be substituted with ``0``. + :data:`None` indicates to use ``0`` as default. :return: The list of parsed addresses. """ From d958e595de5cda554e99783e653966893d3aee3a Mon Sep 17 00:00:00 2001 From: Stefano Ottolenghi Date: Thu, 8 Dec 2022 12:07:43 +0100 Subject: [PATCH 4/4] Fix typo Signed-off-by: Rouven Bauer --- src/neo4j/addressing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/neo4j/addressing.py b/src/neo4j/addressing.py index 74d0528b0..364365ff3 100644 --- a/src/neo4j/addressing.py +++ b/src/neo4j/addressing.py @@ -150,9 +150,9 @@ def parse( :param s: The string to parse. :param default_host: The default host to use if none is specified. - :data:`None` idicates to use ``"localhost"`` as default. + :data:`None` indicates to use ``"localhost"`` as default. :param default_port: The default port to use if none is specified. - :data:`None` idicates to use ``0`` as default. + :data:`None` indicates to use ``0`` as default. :return: The parsed address. """