Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve pyright support #1108

Merged
merged 12 commits into from
Apr 9, 2022
2 changes: 1 addition & 1 deletion changes/1108.bugfix.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Improved pyright support by ignoring all errors that go against the design patterns of hikari.
Improved pyright support.
8 changes: 4 additions & 4 deletions hikari/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,12 +517,12 @@ def of(cls, value: Colorish, /) -> Color:
if len(value) != 3:
raise ValueError(f"Color must be an RGB triplet if set to a {type(value).__name__} type")

if any(isinstance(c, float) for c in value):
r, g, b = value
r, g, b = value

if isinstance(r, float) and isinstance(g, float) and isinstance(b, float):
davfsa marked this conversation as resolved.
Show resolved Hide resolved
return cls.from_rgb_float(r, g, b)

if all(isinstance(c, int) for c in value):
r, g, b = value
if isinstance(r, int) and isinstance(g, int) and isinstance(b, int):
return cls.from_rgb(r, g, b)

if isinstance(value, str):
Expand Down
14 changes: 7 additions & 7 deletions hikari/embeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,11 @@ def is_inline(self, value: bool) -> None:
self._inline = value


def _ensure_embed_resource(resource: files.Resourceish, cls: typing.Type[_T]) -> _T:
def _ensure_embed_resource(resource: files.Resourceish) -> files.Resource[files.AsyncReader]:
if isinstance(resource, EmbedResource):
return cls(resource=resource.resource)
return resource.resource

return cls(resource=files.ensure_resource(resource))
return files.ensure_resource(resource)


class Embed:
Expand Down Expand Up @@ -742,7 +742,7 @@ def set_author(
if name is None and url is None and icon is None:
self._author = None
else:
real_icon = _ensure_embed_resource(icon, EmbedResourceWithProxy) if icon is not None else None
real_icon = EmbedResourceWithProxy(resource=_ensure_embed_resource(icon)) if icon is not None else None
self._author = EmbedAuthor(name=name, url=url, icon=real_icon)
return self

Expand Down Expand Up @@ -791,7 +791,7 @@ def set_footer(self, text: typing.Optional[str], *, icon: typing.Optional[files.

self._footer = None
else:
real_icon = _ensure_embed_resource(icon, EmbedResourceWithProxy) if icon is not None else None
real_icon = EmbedResourceWithProxy(resource=_ensure_embed_resource(icon)) if icon is not None else None
self._footer = EmbedFooter(icon=real_icon, text=text)
return self

Expand Down Expand Up @@ -829,7 +829,7 @@ def set_image(self, image: typing.Optional[files.Resourceish] = None, /) -> Embe
This embed. Allows for call chaining.
"""
if image is not None:
self._image = _ensure_embed_resource(image, EmbedImage)
self._image = EmbedImage(resource=_ensure_embed_resource(image))
else:
self._image = None

Expand Down Expand Up @@ -868,7 +868,7 @@ def set_thumbnail(self, image: typing.Optional[files.Resourceish] = None, /) ->
This embed. Allows for call chaining.
"""
if image is not None:
self._thumbnail = _ensure_embed_resource(image, EmbedImage)
self._thumbnail = EmbedImage(resource=_ensure_embed_resource(image))
else:
self._thumbnail = None

Expand Down
4 changes: 2 additions & 2 deletions hikari/impl/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class HTTPTimeoutSettings:
@request_socket_connect.validator
@request_socket_read.validator
@total.validator
def _(self, attrib: attr.Attribute[typing.Optional[float]], value: typing.Optional[float]) -> None:
def _(self, attrib: attr.Attribute[typing.Optional[float]], value: typing.Any) -> None:
# This error won't occur until some time in the future where it will be annoying to
# try and determine the root cause, so validate it NOW.
if value is not None and (not isinstance(value, (float, int)) or value <= 0):
Expand Down Expand Up @@ -317,7 +317,7 @@ class HTTPSettings(config.HTTPSettings):
"""

@max_redirects.validator
def _(self, _: attr.Attribute[typing.Optional[int]], value: typing.Optional[int]) -> None:
def _(self, _: attr.Attribute[typing.Optional[int]], value: typing.Any) -> None:
# This error won't occur until some time in the future where it will be annoying to
# try and determine the root cause, so validate it NOW.
if value is not None and (not isinstance(value, int) or value <= 0):
Expand Down
4 changes: 3 additions & 1 deletion hikari/impl/entity_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,9 @@ def deserialize_audit_log(self, payload: data_binding.JSONObject) -> audit_log_m
new_value = value_converter(new_value) if new_value is not None else None
old_value = value_converter(old_value) if old_value is not None else None

elif not isinstance(key, audit_log_models.AuditLogChangeKey):
elif not isinstance(
key, audit_log_models.AuditLogChangeKey
): # pyright: ignore [reportUnnecessaryIsInstance]
_LOGGER.debug("Unknown audit log change key found %r", key)

changes.append(audit_log_models.AuditLogChange(key=key, new_value=new_value, old_value=old_value))
Expand Down
3 changes: 0 additions & 3 deletions hikari/impl/event_manager_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,9 +556,6 @@ def decorator(
return decorator

def dispatch(self, event: base_events.Event) -> asyncio.Future[typing.Any]:
if not isinstance(event, base_events.Event):
raise TypeError(f"Events must be subclasses of {base_events.Event.__name__}, not {type(event).__name__}")

tasks: typing.List[typing.Coroutine[None, typing.Any, None]] = []

for cls in event.dispatches():
Expand Down
21 changes: 0 additions & 21 deletions hikari/impl/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,6 @@ def proxy_settings(self) -> config_impl.ProxySettings:
return self._rest().proxy_settings


_NONE_OR_UNDEFINED: typing.Final[typing.Tuple[None, undefined.UndefinedType]] = (None, undefined.UNDEFINED)


class RESTApp(traits.ExecutorAware):
"""The base for a HTTP-only Discord application.

Expand Down Expand Up @@ -1246,24 +1243,6 @@ def _build_message_payload( # noqa: C901- Function too complex
if not undefined.any_undefined(embed, embeds):
raise ValueError("You may only specify one of 'embed' or 'embeds', not both")

if attachments is not undefined.UNDEFINED and not isinstance(attachments, typing.Collection):
raise TypeError(
"You passed a non-collection to 'attachments', but this expects a collection. Maybe you meant to "
"use 'attachment' (singular) instead?"
)

if components not in _NONE_OR_UNDEFINED and not isinstance(components, typing.Collection):
raise TypeError(
"You passed a non-collection to 'components', but this expects a collection. Maybe you meant to "
"use 'component' (singular) instead?"
)

if embeds not in _NONE_OR_UNDEFINED and not isinstance(embeds, typing.Collection):
raise TypeError(
"You passed a non-collection to 'embeds', but this expects a collection. Maybe you meant to "
"use 'embed' (singular) instead?"
)
thesadru marked this conversation as resolved.
Show resolved Hide resolved

if undefined.all_undefined(embed, embeds) and isinstance(content, embeds_.Embed):
# Syntactic sugar, common mistake to accidentally send an embed
# as the content, so let's detect this and fix it for the user.
Expand Down
33 changes: 4 additions & 29 deletions hikari/internal/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,28 +134,6 @@ def __setitem__(self, key: KeyT, value: ValueT) -> None:
self._data[key] = value


class _FrozenDict(typing.MutableMapping[KeyT, ValueT]):
__slots__: typing.Sequence[str] = ("_source",)

def __init__(self, source: typing.Dict[KeyT, typing.Tuple[float, ValueT]], /) -> None:
self._source = source

def __getitem__(self, key: KeyT) -> ValueT:
return self._source[key][1]

def __iter__(self) -> typing.Iterator[KeyT]:
return iter(self._source)

def __len__(self) -> int:
return len(self._source)

def __delitem__(self, key: KeyT) -> None:
del self._source[key]

def __setitem__(self, key: KeyT, value: ValueT) -> None:
self._source[key] = (0.0, value)


class LimitedCapacityCacheMap(ExtendedMutableMapping[KeyT, ValueT]):
"""Implementation of a capacity-limited most-recently-inserted mapping.

Expand Down Expand Up @@ -366,10 +344,7 @@ def get_index_or_slice(
if isinstance(index_or_slice, slice):
return tuple(itertools.islice(mapping.values(), index_or_slice.start, index_or_slice.stop, index_or_slice.step))

if isinstance(index_or_slice, int):
try:
return next(itertools.islice(mapping.values(), index_or_slice, None))
except StopIteration:
raise IndexError(index_or_slice) from None

raise TypeError(f"sequence indices must be integers or slices, not {type(index_or_slice).__name__}")
try:
return next(itertools.islice(mapping.values(), index_or_slice, None))
except StopIteration:
raise IndexError(index_or_slice) from None
davfsa marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion hikari/iterators.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def __call__(self, item: ValueT) -> bool:
def __invert__(self) -> typing.Callable[[ValueT], bool]:
return lambda item: not self(item)

def __or__(self, other: All[ValueT]) -> All[ValueT]:
def __or__(self, other: typing.Any) -> All[ValueT]:
if not isinstance(other, All):
raise TypeError(f"unsupported operand type(s) for |: {type(self).__name__!r} and {type(other).__name__!r}")

Expand Down
2 changes: 1 addition & 1 deletion pipelines/mypy.nox.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

@nox.session(reuse_venv=True)
def mypy(session: nox.Session) -> None:
"""Perform static type analysis on Python source code."""
"""Perform static type analysis on Python source code using mypy."""
session.install(
"-r",
"requirements.txt",
Expand Down
21 changes: 21 additions & 0 deletions pipelines/pyright.nox.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@
from pipelines import nox


@nox.session()
def pyright(session: nox.Session) -> None:
"""Perform static type analysis on Python source code using pyright.

At the time of writing this, this pipeline will not run successfully,
as hikari does not have 100% compatibility with pyright just yet. This
exists to make it easier to test and eventually reach that 100% compatibility.
"""
session.install(
"-r",
"requirements.txt",
"-r",
"speedup-requirements.txt",
"-r",
"server-requirements.txt",
"-r",
"dev-requirements.txt",
)
session.run("python", "-m", "pyright", config.MAIN_PACKAGE)


@nox.session()
def verify_types(session: nox.Session) -> None:
"""Verify the "type completeness" of types exported by the library using Pyright."""
Expand Down
5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,11 @@ reportOverlappingOverload = "none" # Type-Vars in last overloads may interfere
reportIncompatibleVariableOverride = "none" # Cannot overwrite abstract properties using attrs
thesadru marked this conversation as resolved.
Show resolved Hide resolved

# Attrs validators will always be unknown
# https:/python-attrs/attrs/issues/795
reportUnknownMemberType = "warning"
reportUntypedFunctionDecorator = "warning"
reportOptionalMemberAccess = "warning"

# for mypy
reportUnnecessaryIsInstance = "none"


[tool.pytest.ini_options]
asyncio_mode = "strict"
xfail_strict = true
Expand Down
13 changes: 0 additions & 13 deletions tests/hikari/impl/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1334,19 +1334,6 @@ def test__build_message_payload_when_both_single_and_plural_args_passed(
):
rest_client._build_message_payload(**{singular_arg: object(), plural_arg: object()})

@pytest.mark.parametrize(
("singular_arg", "plural_arg"),
[("attachment", "attachments"), ("component", "components"), ("embed", "embeds")],
)
def test__build_message_payload_when_non_collection_passed_to_plural(self, rest_client, singular_arg, plural_arg):
expected_error_message = (
f"You passed a non-collection to '{plural_arg}', but this expects a collection. Maybe you meant to use "
f"'{singular_arg}' (singular) instead?"
)

with pytest.raises(TypeError, match=re.escape(expected_error_message)):
rest_client._build_message_payload(**{plural_arg: object()})

def test_interaction_deferred_builder(self, rest_client):
result = rest_client.interaction_deferred_builder(5)

Expand Down
34 changes: 0 additions & 34 deletions tests/hikari/internal/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,35 +80,6 @@ def test___setitem__(self):
assert mock_map == {"hmm": "forearm", "cat": "bag", "ok": "bye", "bye": 4}


class TestFrozenDict:
def test___init__(self):
mock_map = collections._FrozenDict({"foo": (0.432, "bar"), "blam": (0.111, "okok")})
assert mock_map == {"foo": "bar", "blam": "okok"}

def test___getitem__(self):
mock_map = collections._FrozenDict({"blam": (0.432, "bar"), "obar": (0.111, "okok")})
assert mock_map["obar"] == "okok"

def test___iter__(self):
mock_map = collections._FrozenDict({"bye": (0.33, "bye"), "111": (0.2, "222"), "45949": (0.5, "020202")})
assert list(mock_map) == ["bye", "111", "45949"]

def test___len__(self):
mock_map = collections._FrozenDict({"wsw": (0.3, "3"), "fdsa": (0.55, "ewqwe"), "45949": (0.23, "fsasd")})
assert len(mock_map) == 3

def test___delitem__(self):
mock_map = collections._FrozenDict({"rororo": (0.55, "bye bye"), "raw": (0.999, "ywywyw")})
del mock_map["raw"]
assert mock_map == {"rororo": "bye bye"}

def test___setitem__(self):
mock_map = collections._FrozenDict({"rororo": (0.55, "bye 3231"), "2121": (0.999, "4321")})
mock_map["foo bar"] = 42

assert mock_map == {"rororo": "bye 3231", "2121": "4321", "foo bar": 42}


class TestLimitedCapacityCacheMap:
def test___init___with_source(self):
raw_map = {"voo": "doo", "blam": "blast", "foo": "bye"}
Expand Down Expand Up @@ -380,8 +351,3 @@ def test_get_index_or_slice_with_index_outside_range():
def test_get_index_or_slice_with_slice():
test_map = {"o": "b", "b": "o", "a": "m", "arara": "blam", "oof": "no", "rika": "may"}
assert collections.get_index_or_slice(test_map, slice(1, 5, 2)) == ("o", "blam")


def test_get_index_or_slice_with_invalid_type():
with pytest.raises(TypeError):
collections.get_index_or_slice({}, object())
9 changes: 0 additions & 9 deletions tests/hikari/test_audit_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,6 @@ def test_get_item_with_slice(self):
)
assert audit_log[1:5:2] == (entry_1, entry_2)

def test_get_item_with_ivalid_type(self):
with pytest.raises(TypeError):
audit_logs.AuditLog(
entries=[object(), object()],
integrations={},
users={},
webhooks={},
)["OK"]

def test_len(self):
audit_log = audit_logs.AuditLog(
entries={
Expand Down
8 changes: 4 additions & 4 deletions tests/hikari/test_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def test_Color_to_bytes(self):
[
(colors.Color(0xFF051A), colors.Color(0xFF051A)),
(0xFF051A, colors.Color(0xFF051A)),
((1, 0.5, 0), colors.Color(0xFF7F00)),
((1.0, 0.5, 0.0), colors.Color(0xFF7F00)),
([0xFF, 0x5, 0x1A], colors.Color(0xFF051A)),
("#1a2b3c", colors.Color(0x1A2B3C)),
("#123", colors.Color(0x112233)),
Expand Down Expand Up @@ -305,9 +305,9 @@ def test_Color_of_happy_path(self, input, expected_result):
(NotImplemented, r"Could not transform NotImplemented into a Color object"),
((1, 1, 1, 1), r"Color must be an RGB triplet if set to a tuple type"),
((1, "a", 1), r"Could not transform \(1, 'a', 1\) into a Color object"),
((1.1, 1, 1), r"Expected red channel to be in the inclusive range of 0.0 and 1.0"),
((1, 1.1, 1), r"Expected green channel to be in the inclusive range of 0.0 and 1.0"),
((1, 1, 1.1), r"Expected blue channel to be in the inclusive range of 0.0 and 1.0"),
((1.1, 1.0, 1.0), r"Expected red channel to be in the inclusive range of 0.0 and 1.0"),
((1.0, 1.1, 1.0), r"Expected green channel to be in the inclusive range of 0.0 and 1.0"),
((1.0, 1.0, 1.1), r"Expected blue channel to be in the inclusive range of 0.0 and 1.0"),
((), r"Color must be an RGB triplet if set to a tuple type"),
({}, r"Could not transform \{\} into a Color object"),
([], r"Color must be an RGB triplet if set to a list type"),
Expand Down