Skip to content

Commit d4fe3c3

Browse files
authored
Expect only one username-password pair in ConnectionParameters.from_pydantic_multihost_hosts() (#72)
1 parent 927c6ba commit d4fe3c3

File tree

4 files changed

+134
-51
lines changed

4 files changed

+134
-51
lines changed

pyproject.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ repository = "https://github.com/vrslev/stompman"
2222
[tool.uv]
2323
dev-dependencies = [
2424
"anyio~=4.4.0",
25-
"mypy~=1.11.1",
25+
"mypy~=1.11.2",
2626
"pytest-cov~=5.0.0",
2727
"pytest~=8.3.2",
28-
"ruff~=0.5.7",
29-
"uvloop~=0.19.0",
30-
"hypothesis~=6.111.0",
28+
"ruff~=0.6.2",
29+
"uvloop~=0.20.0",
30+
"hypothesis~=6.111.2",
3131
"polyfactory~=2.16.2",
32-
"faker~=26.3.0",
32+
"faker~=28.0.0",
3333
]
3434

3535
[build-system]
@@ -67,7 +67,7 @@ ignore = [
6767
"PLC2801",
6868
"PLR0913",
6969
]
70-
extend-per-file-ignores = { "tests/*" = ["S101", "SLF001", "ARG"] }
70+
extend-per-file-ignores = { "tests/*" = ["S101", "SLF001", "ARG", "PLR6301"] }
7171

7272
[tool.pytest.ini_options]
7373
addopts = "--cov -s -vv"

stompman/config.py

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,40 +37,56 @@ def unescaped_passcode(self) -> str:
3737

3838
@classmethod
3939
def from_pydantic_multihost_hosts(cls, hosts: list[MultiHostHostLike]) -> list[Self]:
40-
"""Create connection parameters from a list of `MultiHostUrl` objects.
40+
"""Create connection parameters from `pydantic_code.MultiHostUrl.hosts()`.
4141
4242
.. code-block:: python
43-
import stompman.
44-
45-
ArtemisDsn = typing.Annotated[
46-
pydantic_core.MultiHostUrl,
47-
pydantic.UrlConstraints(
48-
host_required=True,
49-
allowed_schemes=["tcp"],
50-
),
51-
]
52-
53-
async with stompman.Client(
54-
servers=stompman.ConnectionParameters.from_pydantic_multihost_hosts(
55-
ArtemisDsn("tcp://lev:pass@host1:61616,lev:pass@host1:61617,lev:pass@host2:61616").hosts()
56-
),
57-
):
58-
...
43+
import stompman
44+
45+
ArtemisDsn = typing.Annotated[
46+
pydantic_core.MultiHostUrl,
47+
pydantic.UrlConstraints(
48+
host_required=True,
49+
allowed_schemes=["tcp"],
50+
),
51+
]
52+
53+
async with stompman.Client(
54+
servers=stompman.ConnectionParameters.from_pydantic_multihost_hosts(
55+
ArtemisDsn("tcp://lev:pass@host1:61616,host2:61617,host3:61618").hosts()
56+
),
57+
):
58+
...
5959
"""
60-
servers: list[Self] = []
60+
all_hosts: list[tuple[str, int]] = []
61+
all_credentials: list[tuple[str, str]] = []
62+
6163
for host in hosts:
6264
if host["host"] is None:
6365
msg = "host must be set"
6466
raise ValueError(msg)
6567
if host["port"] is None:
6668
msg = "port must be set"
6769
raise ValueError(msg)
68-
if host["username"] is None:
69-
msg = "username must be set"
70-
raise ValueError(msg)
71-
if host["password"] is None:
72-
msg = "password must be set"
73-
raise ValueError(msg)
70+
all_hosts.append((host["host"], host["port"]))
71+
72+
username, password = host["username"], host["password"]
73+
if username is None:
74+
if password is not None:
75+
msg = "password is set, username must be set"
76+
raise ValueError(msg)
77+
elif password is None:
78+
if username is not None:
79+
msg = "username is set, password must be set"
80+
raise ValueError(msg)
81+
else:
82+
all_credentials.append((username, password))
83+
84+
if not all_credentials:
85+
msg = "username and password must be set"
86+
raise ValueError(msg)
87+
if len(all_credentials) != 1:
88+
msg = "only one username-password pair must be set"
89+
raise ValueError(msg)
7490

75-
servers.append(cls(host=host["host"], port=host["port"], login=host["username"], passcode=host["password"]))
76-
return servers
91+
login, passcode = all_credentials[0]
92+
return [cls(host=host, port=port, login=login, passcode=passcode) for (host, port) in all_hosts]

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def anyio_backend(request: pytest.FixtureRequest) -> object:
2424

2525

2626
@pytest.fixture
27-
def mock_sleep(monkeypatch: pytest.MonkeyPatch) -> None: # noqa: PT004
27+
def mock_sleep(monkeypatch: pytest.MonkeyPatch) -> None:
2828
original_sleep = asyncio.sleep
2929
monkeypatch.setattr("asyncio.sleep", lambda _: original_sleep(0))
3030

tests/test_config.py

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,92 @@
1-
from typing import Any
1+
from typing import TypedDict
22

33
import faker
44
import pytest
5+
from polyfactory.factories.typed_dict_factory import TypedDictFactory
56

67
import stompman
8+
from stompman.config import MultiHostHostLike
79

810

9-
def test_connection_parameters_from_pydantic_multihost_hosts(faker: faker.Faker) -> None:
10-
full_host: dict[str, Any] = {
11-
"username": faker.pystr(),
12-
"password": faker.pystr(),
13-
"host": faker.pystr(),
14-
"port": faker.pyint(),
15-
}
16-
assert stompman.ConnectionParameters.from_pydantic_multihost_hosts(
17-
[{**full_host, "port": index} for index in range(5)] # type: ignore[typeddict-item]
18-
) == [
19-
stompman.ConnectionParameters(full_host["host"], index, full_host["username"], full_host["password"])
20-
for index in range(5)
21-
]
22-
23-
for key in ("username", "password", "host", "port"):
24-
with pytest.raises(ValueError, match=f"{key} must be set"):
25-
assert stompman.ConnectionParameters.from_pydantic_multihost_hosts([{**full_host, key: None}, full_host]) # type: ignore[typeddict-item, list-item]
11+
class StrictMultiHostHostLike(TypedDict):
12+
username: str
13+
password: str
14+
host: str
15+
port: int
16+
17+
18+
class StrictMultiHostHostLikeFactory(TypedDictFactory[StrictMultiHostHostLike]): ...
19+
20+
21+
class MultiHostHostLikeFactory(TypedDictFactory[MultiHostHostLike]): ...
22+
23+
24+
class TestConnectionParametersFromPydanticMultiHostHosts:
25+
def test_ok(self, faker: faker.Faker) -> None:
26+
hosts: list[MultiHostHostLike] = [
27+
{"host": "host1", "port": 1, "username": None, "password": None},
28+
{"host": "host2", "port": 2, "username": None, "password": None},
29+
{"host": "host3", "port": 3, "username": None, "password": None},
30+
{"host": "host4", "port": 4, "username": None, "password": None},
31+
]
32+
host_with_credentials = faker.pyint(min_value=0, max_value=3)
33+
hosts[host_with_credentials]["username"] = "lev"
34+
hosts[host_with_credentials]["password"] = "pass" # noqa: S105
35+
36+
result = stompman.ConnectionParameters.from_pydantic_multihost_hosts(hosts)
37+
38+
assert result == [
39+
stompman.ConnectionParameters("host1", 1, "lev", "pass"),
40+
stompman.ConnectionParameters("host2", 2, "lev", "pass"),
41+
stompman.ConnectionParameters("host3", 3, "lev", "pass"),
42+
stompman.ConnectionParameters("host4", 4, "lev", "pass"),
43+
]
44+
45+
def test_no_host_or_port_or_both(self, faker: faker.Faker) -> None:
46+
cases: list[MultiHostHostLike] = [
47+
{"host": None, "port": faker.pyint(), "username": faker.pystr(), "password": faker.pystr()},
48+
{"host": faker.pystr(), "port": None, "username": faker.pystr(), "password": faker.pystr()},
49+
{"host": None, "port": None, "username": faker.pystr(), "password": faker.pystr()},
50+
]
51+
52+
for host in cases:
53+
with pytest.raises(ValueError, match="must be set"):
54+
stompman.ConnectionParameters.from_pydantic_multihost_hosts([host])
55+
56+
def test_no_username(self, faker: faker.Faker) -> None:
57+
hosts: list[MultiHostHostLike] = [
58+
{"host": faker.pystr(), "port": faker.pyint(), "username": None, "password": faker.pystr()},
59+
{"host": faker.pystr(), "port": faker.pyint(), "username": faker.pystr(), "password": faker.pystr()},
60+
]
61+
62+
with pytest.raises(ValueError, match="username must be set"):
63+
stompman.ConnectionParameters.from_pydantic_multihost_hosts(hosts)
64+
65+
def test_no_password(self, faker: faker.Faker) -> None:
66+
hosts: list[MultiHostHostLike] = [
67+
{"host": faker.pystr(), "port": faker.pyint(), "username": faker.pystr(), "password": None},
68+
{"host": faker.pystr(), "port": faker.pyint(), "username": faker.pystr(), "password": faker.pystr()},
69+
]
70+
71+
with pytest.raises(ValueError, match="password must be set"):
72+
stompman.ConnectionParameters.from_pydantic_multihost_hosts(hosts)
73+
74+
def test_no_credentials(self, faker: faker.Faker) -> None:
75+
cases: list[MultiHostHostLike] = [
76+
{"host": faker.pystr(), "port": faker.pyint(), "username": None, "password": None},
77+
{"host": faker.pystr(), "port": faker.pyint(), "username": None, "password": None},
78+
]
79+
80+
for host in cases:
81+
with pytest.raises(ValueError, match="username and password must be set"):
82+
stompman.ConnectionParameters.from_pydantic_multihost_hosts([host])
83+
84+
def test_multiple_credentials(self, faker: faker.Faker) -> None:
85+
hosts: list[MultiHostHostLike] = [
86+
{"host": faker.pystr(), "port": faker.pyint(), "username": None, "password": None},
87+
{"host": faker.pystr(), "port": faker.pyint(), "username": faker.pystr(), "password": faker.pystr()},
88+
{"host": faker.pystr(), "port": faker.pyint(), "username": faker.pystr(), "password": faker.pystr()},
89+
]
90+
91+
with pytest.raises(ValueError, match="only one username-password pair must be set"):
92+
stompman.ConnectionParameters.from_pydantic_multihost_hosts(hosts)

0 commit comments

Comments
 (0)