Skip to content

Commit 9c82460

Browse files
authored
Prepare for patch release v3.0.3 (#1035)
1 parent b8bbb84 commit 9c82460

File tree

6 files changed

+85
-27
lines changed

6 files changed

+85
-27
lines changed

docs/_static/switcher.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,53 @@
11
[
22
{
33
"name": "dev",
4-
"version": "3.1.0.dev0",
4+
"version": "3.1.*.dev0",
55
"url": "https://pydicom.github.io/pynetdicom/dev/"
66
},
77
{
88
"name": "3.0 (stable)",
9-
"version": "3.0.2",
9+
"version": "3.0.*",
1010
"url": "https://pydicom.github.io/pynetdicom/stable/",
1111
"preferred": true
1212
},
1313
{
1414
"name": "2.1",
15-
"version": "2.1.1",
15+
"version": "2.1.*",
1616
"url": "https://pydicom.github.io/pynetdicom/2.1/"
1717
},
1818
{
1919
"name": "2.0",
20-
"version": "2.0.3",
20+
"version": "2.0.*",
2121
"url": "https://pydicom.github.io/pynetdicom/2.0/"
2222
},
2323
{
2424
"name": "1.5",
25-
"version": "1.5.7",
25+
"version": "1.5.*",
2626
"url": "https://pydicom.github.io/pynetdicom/1.5/"
2727
},
2828
{
2929
"name": "1.4",
30-
"version": "1.4.1",
30+
"version": "1.4.*",
3131
"url": "https://pydicom.github.io/pynetdicom/1.4/"
3232
},
3333
{
3434
"name": "1.3",
35-
"version": "1.3.1",
35+
"version": "1.3.*",
3636
"url": "https://pydicom.github.io/pynetdicom/1.3/"
3737
},
3838
{
3939
"name": "1.2",
40-
"version": "1.2.0",
40+
"version": "1.2.*",
4141
"url": "https://pydicom.github.io/pynetdicom/1.2/"
4242
},
4343
{
4444
"name": "1.1",
45-
"version": "1.1.0",
45+
"version": "1.1.*",
4646
"url": "https://pydicom.github.io/pynetdicom/1.1/"
4747
},
4848
{
4949
"name": "1.0",
50-
"version": "1.0.0",
50+
"version": "1.0.*",
5151
"url": "https://pydicom.github.io/pynetdicom/1.0/"
5252
}
5353
]

docs/changelog/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Releases
77
.. toctree::
88
:maxdepth: 1
99

10+
v3.0.3
1011
v3.0.2
1112
v3.0.1
1213
v3.0.0

docs/changelog/v3.0.3.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.. _v3.0.3:
2+
3+
3.0.3
4+
=====
5+
6+
Fixes
7+
-----
8+
9+
* Fixed being unable to resolve IPv4 address when using the hostname (:issue:`1033`, :pr:`1034`)

pynetdicom/tests/test_transport.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import sys
1212
import threading
1313
import time
14+
from unittest import mock
1415

1516
import pytest
1617

@@ -56,6 +57,14 @@
5657

5758
# debug_logger()
5859

60+
_original_getaddrinfo = socket.getaddrinfo
61+
62+
63+
def getaddrinfo_no_ipv4(*args, **kwargs):
64+
"""Calls socket.getaddrinfo and leaves out the IPv4 results."""
65+
entries = _original_getaddrinfo(*args, **kwargs)
66+
return [addr for addr in entries if addr[0] != socket.AF_INET]
67+
5968

6069
class TestAddressInformation:
6170
"""Tests for AssociationInformation."""
@@ -93,6 +102,24 @@ def test_ipv6_init_maximal(self):
93102
assert addr.scope_id == 11
94103
assert addr.as_tuple == ("::1", 0, 10, 11)
95104

105+
def test_no_ipv4(self):
106+
# Check to ensure we have IPv6 entries
107+
localhost_entries = getaddrinfo_no_ipv4("localhost", 0)
108+
109+
with mock.patch("socket.getaddrinfo", new=getaddrinfo_no_ipv4):
110+
if localhost_entries:
111+
addr = AddressInformation("localhost", 0)
112+
assert addr.address == "::1"
113+
assert addr.port == 0
114+
115+
addr = AddressInformation("", 0)
116+
assert addr.address in ("::0", "::")
117+
assert addr.port == 0
118+
119+
# IPv6 does not use broadcast.
120+
with pytest.raises(socket.gaierror, match="Address resolution failed"):
121+
AddressInformation("<broadcast>", 0)
122+
96123
def test_from_tuple(self):
97124
addr = AddressInformation.from_tuple(("localhost", get_port()))
98125
assert isinstance(addr, AddressInformation)
@@ -106,7 +133,7 @@ def test_from_tuple(self):
106133

107134
addr = AddressInformation.from_tuple(("::0", get_port("remote")))
108135
assert isinstance(addr, AddressInformation)
109-
assert addr.address == "::0"
136+
assert addr.address in ("::0", "::")
110137
assert addr.port == get_port("remote")
111138

112139
def test_from_add_port(self):
@@ -117,14 +144,14 @@ def test_from_add_port(self):
117144

118145
addr = AddressInformation.from_addr_port("::0", get_port("remote"))
119146
assert isinstance(addr, AddressInformation)
120-
assert addr.address == "::0"
147+
assert addr.address in ("::0", "::")
121148
assert addr.port == get_port("remote")
122149
assert addr.flowinfo == 0
123150
assert addr.scope_id == 0
124151

125152
addr = AddressInformation.from_addr_port(("::0", 12, 13), get_port("remote"))
126153
assert isinstance(addr, AddressInformation)
127-
assert addr.address == "::0"
154+
assert addr.address in ("::0", "::")
128155
assert addr.port == get_port("remote")
129156
assert addr.flowinfo == 12
130157
assert addr.scope_id == 13
@@ -143,12 +170,16 @@ def test_address(self):
143170
assert addr.address == "192.168.0.1"
144171
assert addr.address_family == socket.AF_INET
145172
addr.address = "::0"
146-
assert addr.address == "::0"
173+
assert addr.address in ("::0", "::")
147174
assert addr.address_family == socket.AF_INET6
148175
addr.address = "192.168.0.1"
149176
assert addr.address == "192.168.0.1"
150177
assert addr.address_family == socket.AF_INET
151178

179+
def test_resolve_hostname(self):
180+
with pytest.raises(socket.gaierror):
181+
AddressInformation("remotehost", 0)
182+
152183

153184
class TestTConnect:
154185
"""Tests for T_CONNECT."""
@@ -164,9 +195,9 @@ def test_bad_addr_raises(self):
164195
def test_address_request(self):
165196
"""Test init with an A-ASSOCIATE primitive"""
166197
request = A_ASSOCIATE()
167-
request.called_presentation_address = AddressInformation("123.4", 12)
198+
request.called_presentation_address = AddressInformation("1.2.3.4", 12)
168199
conn = T_CONNECT(request)
169-
assert conn.address == ("123.4", 12)
200+
assert conn.address == ("1.2.3.4", 12)
170201
assert conn.request is request
171202

172203
msg = r"A connection attempt has not yet been made"
@@ -176,7 +207,7 @@ def test_address_request(self):
176207
def test_result_setter(self):
177208
"""Test setting the result value."""
178209
request = A_ASSOCIATE()
179-
request.called_presentation_address = AddressInformation("123.4", 12)
210+
request.called_presentation_address = AddressInformation("1.2.3.4", 12)
180211
conn = T_CONNECT(request)
181212

182213
msg = r"Invalid connection result 'foo'"
@@ -191,7 +222,7 @@ def test_result_setter(self):
191222

192223
def test_address_inf(self):
193224
request = A_ASSOCIATE()
194-
request.called_presentation_address = AddressInformation("123.4", 12)
225+
request.called_presentation_address = AddressInformation("1.2.3.4", 12)
195226
conn = T_CONNECT(request)
196227
assert conn.address_info is request.called_presentation_address
197228

pynetdicom/transport.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ def address(self) -> str:
8787
Parameters
8888
----------
8989
value : str
90-
The IPv4 or IPv6 address. The following conversion will be made:
90+
The hostname, or IPv4 or IPv6 address. The following conversion will be made:
9191
92-
* ``""`` (INADDR_ANY) -> ``"0.0.0.0"``
92+
* ``""`` (INADDR_ANY) -> ``"0.0.0.0"`` or ``"::"``
9393
* ``"<broadcast>"`` (INADDR_BROADCAST) -> ``"255.255.255.255"``
94-
* ``"localhost"`` -> ``"127.0.0.1"``
94+
* ``"localhost"`` -> ``"127.0.0.1"`` or ``"::1"``
9595
9696
Returns
9797
-------
@@ -102,11 +102,28 @@ def address(self) -> str:
102102

103103
@address.setter
104104
def address(self, value: str) -> None:
105-
value = "0.0.0.0" if value == "" else value
106-
value = "127.0.0.1" if value == "localhost" else value
107-
value = "255.255.255.255" if value == "<broadcast>" else value
108-
109-
self._addr = value
105+
if value:
106+
# getaddrinfo does not handle <broadcast>. IPv6 does not use broadcast.
107+
value = "255.255.255.255" if value == "<broadcast>" else value
108+
flags = 0
109+
else:
110+
# getaddrinfo interprets "" as "0.0.0.0" or "::". Set the AI_PASSIVE flag
111+
# to return an address that can be used with BIND.
112+
flags = socket.AI_PASSIVE
113+
114+
# getaddrinfo translates a hostname into IPv4 and/or IPv6-addresses.
115+
entries = socket.getaddrinfo(value if value else None, 0, flags=flags)
116+
117+
# Use the first IPv4 address, or the first IPv6 address if there are no
118+
# IPv4 addresses available.
119+
ipv4_entries = [addr for addr in entries if addr[0] == socket.AF_INET]
120+
ipv6_entries = [addr for addr in entries if addr[0] == socket.AF_INET6]
121+
if ipv4_entries:
122+
self._addr = cast(str, ipv4_entries[0][4][0])
123+
elif ipv6_entries:
124+
self._addr = cast(str, ipv6_entries[0][4][0])
125+
else:
126+
raise socket.gaierror("Address resolution failed")
110127

111128
@property
112129
def address_family(self) -> socket.AddressFamily:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ maintainers = [
3030
]
3131
name = "pynetdicom"
3232
readme = "README.rst"
33-
version = "3.0.2"
33+
version = "3.0.3"
3434
requires-python = ">=3.10"
3535
dependencies = ["pydicom >=3, <4"]
3636

0 commit comments

Comments
 (0)