Skip to content

Commit fdbc4ba

Browse files
authored
Merge branch 'main' into develop
2 parents 4f5f662 + 551b050 commit fdbc4ba

8 files changed

Lines changed: 72 additions & 10 deletions

File tree

.github/workflows/commitlint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
"rules": {
2828
"type-enum": [2, "always", [
2929
"feat", "fix", "refactor", "build", "deps", "bug",
30-
"chore", "docs", "test", "style", "ci", "perf"
30+
"chore", "docs", "test", "tests", "style", "ci", "perf"
3131
]],
3232
"scope-enum": [1, "always", [
3333
"core", "helpers", "deps", "release", "ci",

.github/workflows/labeler.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: Auto Label PR
22

33
on:
4-
pull_request:
4+
pull_request_target:
55
types: [opened, synchronize]
66

77
permissions:
@@ -13,6 +13,7 @@ jobs:
1313
name: Auto Label
1414
runs-on: ubuntu-latest
1515
steps:
16+
- uses: actions/checkout@v6
1617
- uses: actions/labeler@v6
1718
with:
1819
repo-token: ${{ secrets.GITHUB_TOKEN }}

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ classifiers = [
2323
]
2424
dependencies = [
2525
"httpx>=0.28",
26+
"typing-extensions>=4.15.0",
2627
]
2728

2829
[project.scripts]

src/swgoh_comlink/_base.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from typing import Any
1616
from urllib.parse import urlparse, urlunparse
1717

18+
from typing_extensions import Self
19+
1820
from .exceptions import SwgohComlinkValueError
1921
from .helpers import Constants
2022

@@ -92,11 +94,11 @@ def __repr__(self) -> str:
9294
f"secret_key={self._mask(self.secret_key)!r})"
9395
)
9496

95-
def __new__(cls, *args: Any, **kwargs: Any) -> SwgohComlinkBase:
97+
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
9698
"""Prevent instances of this base class from being created directly."""
9799
if cls is SwgohComlinkBase:
98100
raise TypeError(f"Only subclasses of '{cls.__name__}' may be instantiated.")
99-
return object.__new__(cls)
101+
return super().__new__(cls)
100102

101103
def __init__(
102104
self,

tests/integration/test_async_client.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import pytest
44

55
from swgoh_comlink import SwgohComlinkAsync
6-
from swgoh_comlink.helpers import DataItems
76

87
from .conftest import COMLINK_URL, TEST_ALLYCODE
98

@@ -71,8 +70,8 @@ async def test_get_guilds_by_name(async_comlink):
7170

7271

7372
async def test_get_game_data_filtered(async_comlink):
74-
"""POST /data with DataItems filter returns game data subset."""
75-
result = await async_comlink.get_game_data(items=DataItems.SEGMENT1)
73+
"""POST /data with request_segment=1 returns a non-empty game data subset."""
74+
result = await async_comlink.get_game_data(request_segment=1)
7675
assert isinstance(result, dict)
7776
assert len(result) > 0
7877

tests/integration/test_sync_client.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import pytest
44

55
from swgoh_comlink import SwgohComlink
6-
from swgoh_comlink.helpers import DataItems
76

87
from .conftest import COMLINK_URL, TEST_ALLYCODE
98

@@ -71,8 +70,8 @@ def test_get_guilds_by_name(comlink):
7170

7271

7372
def test_get_game_data_filtered(comlink):
74-
"""POST /data with DataItems filter returns game data subset."""
75-
result = comlink.get_game_data(items=DataItems.SEGMENT1)
73+
"""POST /data with request_segment=1 returns a non-empty game data subset."""
74+
result = comlink.get_game_data(request_segment=1)
7675
assert isinstance(result, dict)
7776
assert len(result) > 0
7877

tests/test_base_client.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from typing import get_type_hints
2+
3+
import pytest
4+
from typing_extensions import Self
5+
6+
from swgoh_comlink import SwgohComlink
7+
from swgoh_comlink._base import SwgohComlinkBase
8+
9+
10+
def test_base_class_new_typing():
11+
"""Test that SwgohComlinkBase __new__ returns correct type."""
12+
type_hints = get_type_hints(obj=SwgohComlinkBase.__new__)
13+
assert type_hints.get("return") == Self
14+
15+
16+
def test_base_class_instantiation():
17+
"""Test that SwgohComlinkBase prevents direct instantiation."""
18+
with pytest.raises(TypeError, match="Only subclasses of 'SwgohComlinkBase' may be instantiated"):
19+
SwgohComlinkBase()
20+
21+
class ChildSwgohClient(SwgohComlinkBase):
22+
pass
23+
24+
child_instance = ChildSwgohClient()
25+
assert isinstance(child_instance, ChildSwgohClient)
26+
27+
client = SwgohComlink()
28+
assert isinstance(client, SwgohComlink)

tests/unit/test_base.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,21 @@
55
import hashlib
66
import logging
77
from json import dumps
8+
from typing import TYPE_CHECKING
89

910
import pytest
1011

1112
from swgoh_comlink import SwgohComlink
1213
from swgoh_comlink._base import _SENSITIVE_KEYS, SwgohComlinkBase, param_alias, sanitize_url
1314
from swgoh_comlink.exceptions import SwgohComlinkValueError
1415

16+
if TYPE_CHECKING:
17+
# `typing.assert_type` is 3.11+; `typing_extensions` works on every
18+
# supported Python version and is already in the dev dependency closure.
19+
from typing_extensions import assert_type
20+
21+
from swgoh_comlink import SwgohComlinkAsync
22+
1523
# ── sanitize_url ─────────────────────────────────────────────────────────
1624

1725

@@ -79,6 +87,30 @@ def test_cannot_instantiate_base_directly(self):
7987
SwgohComlinkBase()
8088

8189

90+
# ── Constructor type inference (PR #98 regression guard) ─────────────────
91+
#
92+
# `SwgohComlinkBase.__new__` must return `Self` so that subclass constructor
93+
# calls infer as the concrete subclass, not the base. Without this, type
94+
# checkers reject subclass-only attribute access such as
95+
# `SwgohComlink(...).get_player(...)`.
96+
#
97+
# The function below is intentionally never invoked at runtime — it lives
98+
# under `if TYPE_CHECKING:` so the static type checker (mypy / ty) validates
99+
# the `assert_type` calls without spinning up real httpx clients.
100+
101+
if TYPE_CHECKING:
102+
103+
def _typecheck_constructor_returns_concrete_subclass() -> None:
104+
sync_client = SwgohComlink()
105+
assert_type(sync_client, SwgohComlink)
106+
# Subclass-only attribute access — must resolve without error.
107+
_ = sync_client.get_player
108+
109+
async_client = SwgohComlinkAsync()
110+
assert_type(async_client, SwgohComlinkAsync)
111+
_ = async_client.get_player
112+
113+
82114
# ── Constructor ──────────────────────────────────────────────────────────
83115

84116

0 commit comments

Comments
 (0)