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.