diff --git a/src/twisted/internet/endpoints.py b/src/twisted/internet/endpoints.py index 3f76e53489f..ef18d6edad1 100644 --- a/src/twisted/internet/endpoints.py +++ b/src/twisted/internet/endpoints.py @@ -796,6 +796,27 @@ def __init__(self, reactor, host, port, timeout=30, bindAddress=None, self._attemptDelay = attemptDelay + def __repr__(self): + """ + Produce a string representation of the L{HostnameEndpoint}. + + @return: A L{str} + """ + if self._badHostname: + # Use the backslash-encoded version of the string passed to the + # constructor, which is already a native string. + host = self._hostStr + elif isIPv6Address(self._hostStr): + host = '[{}]'.format(self._hostStr) + else: + # Convert the bytes representation to a native string to ensure + # that we display the punycoded version of the hostname, which is + # more useful than any IDN version as it can be easily copy-pasted + # into debugging tools. + host = nativeString(self._hostBytes) + return "".join([""]) + + def _getNameResolverAndMaybeWarn(self, reactor): """ Retrieve a C{nameResolver} callable and warn the caller's diff --git a/src/twisted/internet/test/test_endpoints.py b/src/twisted/internet/test/test_endpoints.py index bda809e089b..f9e3f819850 100644 --- a/src/twisted/internet/test/test_endpoints.py +++ b/src/twisted/internet/test/test_endpoints.py @@ -2476,6 +2476,58 @@ def test_deferBadEncodingToConnect(self): self.assertIn("\\u2ff0-garbage-\\u2ff0", str(err)) +class HostnameEndpointReprTests(unittest.SynchronousTestCase): + def test_allASCII(self): + """ + The string representation of L{HostnameEndpoint} includes the host and + port passed to the constructor. + """ + endpoint = endpoints.HostnameEndpoint( + deterministicResolvingReactor(Clock(), []), + 'example.com', 80, + ) + + self.assertEqual("", repr(endpoint)) + + def test_idnaHostname(self): + """ + When IDN is passed to the L{HostnameEndpoint} constructor the string + representation includes the punycode version of the host. + """ + endpoint = endpoints.HostnameEndpoint( + deterministicResolvingReactor(Clock(), []), + u'b\xfccher.ch', 443, + ) + + self.assertEqual("", repr(endpoint)) + + def test_hostIPv6Address(self): + """ + When the host passed to L{HostnameEndpoint} is an IPv6 address it is + wrapped in brackets in the string representation, like in a URI. This + prevents the colon separating the host from the port from being + ambiguous. + """ + endpoint = endpoints.HostnameEndpoint( + deterministicResolvingReactor(Clock(), []), + b'::1', 22, + ) + + self.assertEqual("", repr(endpoint)) + + def test_badEncoding(self): + """ + When a bad hostname is passed to L{HostnameEndpoint}, the string + representation displays invalid characters in backslash-escaped form. + """ + endpoint = endpoints.HostnameEndpoint( + deterministicResolvingReactor(Clock(), []), + b'\xff-garbage-\xff', 80 + ) + + self.assertEqual('', repr(endpoint)) + + class HostnameEndpointsGAIFailureTests(unittest.TestCase): """ diff --git a/src/twisted/newsfragments/9341.feature b/src/twisted/newsfragments/9341.feature new file mode 100644 index 00000000000..21726170d8d --- /dev/null +++ b/src/twisted/newsfragments/9341.feature @@ -0,0 +1 @@ +twisted.internet.endpoints.HostnameEndpoint now has a __repr__ method which includes the host and port to which the endpoint connects.