Added additional Ruff rule sets and fixed all violations.
CI / Formatting (push) Successful in 11s
CI / Linting (push) Successful in 13s
CI / Tests (Python 3.12) (push) Successful in 25s
CI / Tests (Python 3.13) (push) Successful in 25s
CI / Tests (Python 3.14) (push) Successful in 24s
CI / Type Checking (push) Successful in 25s

This commit is contained in:
2026-02-19 14:15:08 -05:00
parent ca4adbcebf
commit 12c0394cf9
10 changed files with 118 additions and 88 deletions
+2 -2
View File
@@ -324,7 +324,7 @@ class Config:
logger.debug(f"Loading configuration from: {self.config_path.absolute()}")
if self.config_path.exists():
try:
with open(self.config_path) as f:
with self.config_path.open() as f:
self._data = yaml.safe_load(f) or {}
except yaml.YAMLError as e:
logger.error(f"Failed to parse config file: {e}")
@@ -385,7 +385,7 @@ class Config:
"""Write current configuration to the YAML file."""
logger.debug(f"Saving configuration to: {self.config_path.absolute()}")
try:
with open(self.config_path, "w") as f:
with self.config_path.open("w") as f:
yaml.safe_dump(
self._ordered_data(), f, default_flow_style=False, sort_keys=False
)
@@ -273,7 +273,9 @@ def _parse_placeholder_date(date_str: str) -> datetime | None:
return None
try:
dt = datetime.strptime(date_part, "%b %d %Y %I:%M:%S %p")
# Intentionally naive: tz abbreviation is resolved separately via
# TIMEZONE_OFFSETS since strptime's %z only handles numeric offsets.
dt = datetime.strptime(date_part, "%b %d %Y %I:%M:%S %p") # noqa: DTZ007
except ValueError:
return None
@@ -369,7 +371,6 @@ async def _evaluate_count(
(counter_name, delta, delta),
)
return str(result)
else:
try:
value = int(modifier_str)
except ValueError as e:
@@ -377,8 +378,7 @@ async def _evaluate_count(
"Invalid $(count): modifier must be an integer (e.g., +5, -1, 0)"
) from e
result = await ctx.storage.fetch_value(
"INSERT OR REPLACE INTO counters (name, value) VALUES (?, ?) "
"RETURNING value",
"INSERT OR REPLACE INTO counters (name, value) VALUES (?, ?) RETURNING value",
(counter_name, value),
)
return str(result)
@@ -429,7 +429,7 @@ async def _evaluate_rand(
raise PlaceholderError(
"Invalid $(rand): arguments must be integers, e.g., $(rand 1 100)"
) from e
return str(random.randint(min(start, stop), max(start, stop)))
return str(random.randint(min(start, stop), max(start, stop))) # noqa: S311
async def _evaluate_countdown(
@@ -455,7 +455,6 @@ async def _evaluate_countdown(
seconds = int(delta.total_seconds())
if seconds > 0:
return _format_duration(seconds)
else:
return "0 seconds"
@@ -186,7 +186,6 @@ def _parse_placeholder(
)
if found_close:
return PlaceholderNode(name, child_nodes), new_pos
else:
# Unclosed placeholder, degrade to literal. Return None so
# the caller emits "$(" as literal and re-scans the rest.
return None
+3 -3
View File
@@ -401,11 +401,11 @@ class RouteDispatcher:
if result is None:
return web.Response(status=204) # No Content.
elif isinstance(result, web.StreamResponse):
if isinstance(result, web.StreamResponse):
return result
elif isinstance(result, dict):
if isinstance(result, dict):
return web.json_response(result)
else: # pragma: no branch — defensive against untyped handlers
# pragma: no branch — defensive against untyped handlers
mod_logger.error( # type: ignore[unreachable]
f"Route handler '{route_info.full_path}' returned "
f"unsupported type: {type(result).__name__}"
+41 -6
View File
@@ -68,16 +68,49 @@ extend-exclude = ["owlbot/_version.py"] # auto-generated by hatch-vcs
[tool.ruff.lint]
select = [
# Core
"F", # Pyflakes
"E", # pycodestyle errors
"W", # pycodestyle warnings
"I", # isort
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"RUF", # Ruff-specific rules
"N", # pep8-naming
"D", # pydocstyle
"I", # isort
"ICN", # flake8-import-conventions
# Correctness & bugs
"B", # flake8-bugbear
"ASYNC", # flake8-async
"DTZ", # flake8-datetimez
"RSE", # flake8-raise
"RET", # flake8-return
"A", # flake8-builtins
"PIE", # flake8-pie
# Modernization & simplification
"UP", # pyupgrade
"SIM", # flake8-simplify
"C4", # flake8-comprehensions
"FLY", # flynt (f-string conversion)
"PTH", # flake8-use-pathlib
# Performance
"PERF", # Perflint
# Security
"S", # flake8-bandit
# Code hygiene
"T10", # flake8-debugger
"T20", # flake8-print
"ERA", # eradicate
"PGH", # pygrep-hooks
"TCH", # flake8-type-checking
# Testing
"PT", # flake8-pytest-style
# Ruff-specific
"RUF", # Ruff-specific rules
]
ignore = [
"D203", # incompatible with D211 (no blank line before class docstring)
@@ -86,6 +119,8 @@ ignore = [
[tool.ruff.lint.per-file-ignores]
"owlbot/__init__.py" = ["E402"] # constants defined before imports intentionally
"owlbot/__main__.py" = ["T201"] # CLI entry point uses print() for user output
"tests/**" = ["S101", "S311"] # assert is standard for pytest; random is fine in tests
[tool.coverage.run]
source = ["owlbot"]
+16 -19
View File
@@ -117,7 +117,7 @@ class TestOnCommandDecorator:
"""Tests the @on_command decorator from owlbot.api.commands."""
@pytest.mark.parametrize(
"name,aliases,auth,mod,cooldown",
("name", "aliases", "auth", "mod", "cooldown"),
[
pytest.param("ping", None, False, False, 0, id="defaults"),
pytest.param("cmd", ["c", "cm"], False, False, 0, id="with-aliases"),
@@ -284,12 +284,11 @@ class TestCommandRegistry:
pass
registry.register("ping", handler, module_name="mod_a")
with pytest.raises(ValueError) as exc_info:
with pytest.raises(
ValueError,
match=r"Command trigger 'ping' conflicts with existing command 'ping'",
):
registry.register("ping", handler, module_name="mod_b")
assert (
str(exc_info.value)
== "Command trigger 'ping' conflicts with existing command 'ping'"
)
def test_register_conflict_alias_with_name(self) -> None:
"""Alias that conflicts with an existing command name raises ValueError."""
@@ -299,12 +298,11 @@ class TestCommandRegistry:
pass
registry.register("ping", handler, module_name="mod_a")
with pytest.raises(ValueError) as exc_info:
with pytest.raises(
ValueError,
match=r"Command trigger 'ping' conflicts with existing command 'ping'",
):
registry.register("other", handler, aliases=["ping"], module_name="mod_b")
assert (
str(exc_info.value)
== "Command trigger 'ping' conflicts with existing command 'ping'"
)
def test_register_conflict_alias_with_alias(self) -> None:
"""Alias that conflicts with an existing alias raises ValueError."""
@@ -314,12 +312,11 @@ class TestCommandRegistry:
pass
registry.register("cmd1", handler, aliases=["c"], module_name="mod_a")
with pytest.raises(ValueError) as exc_info:
with pytest.raises(
ValueError,
match=r"Command trigger 'c' conflicts with existing command 'cmd1'",
):
registry.register("cmd2", handler, aliases=["c"], module_name="mod_b")
assert (
str(exc_info.value)
== "Command trigger 'c' conflicts with existing command 'cmd1'"
)
def test_exists_true(self) -> None:
"""Registered command returns True."""
@@ -447,7 +444,7 @@ class TestCommandRegistry:
assert registry.unregister_by_module("no_such") == 0
@pytest.mark.parametrize(
"message,prefix,expected",
("message", "prefix", "expected"),
[
pytest.param("!ping", "!", ("ping", ""), id="simple-command"),
pytest.param(
@@ -921,7 +918,7 @@ class TestModuleCommands:
assert mod_cmds["mod_a"].module_commands == {}
@pytest.mark.parametrize(
"owner,other_setup,call_module,trigger,expected",
("owner", "other_setup", "call_module", "trigger", "expected"),
[
pytest.param("mod_a", None, "mod_a", "ping", True, id="own-command"),
pytest.param(
@@ -1020,7 +1017,7 @@ class TestCommandContext:
return cmd_ctx, module_ctx, cmd_event, event_ctx
@pytest.mark.parametrize(
"prop,use_is",
("prop", "use_is"),
[
pytest.param("module_name", False, id="module-name"),
pytest.param("storage", True, id="storage"),
+5 -5
View File
@@ -137,7 +137,7 @@ class TestParseTimestamp:
"""Exercises _parse_timestamp() with various input formats."""
@pytest.mark.parametrize(
"ts,expected",
("ts", "expected"),
[
pytest.param(None, None, id="none-input"),
pytest.param("", None, id="empty-string"),
@@ -221,7 +221,7 @@ class TestUser:
assert user.scopes == []
@pytest.mark.parametrize(
"scopes,expected",
("scopes", "expected"),
[
pytest.param(["MODERATOR"], True, id="has-moderator"),
pytest.param(["OTHER"], False, id="other-scope"),
@@ -255,7 +255,7 @@ class TestChatEvent:
assert event.timestamp == datetime(2026, 1, 15, 12, 0, tzinfo=UTC)
@pytest.mark.parametrize(
"raw,expected",
("raw", "expected"),
[
pytest.param("<p>hello</p>", "hello", id="strips-p-tags"),
pytest.param("hello", "hello", id="no-tags"),
@@ -518,7 +518,7 @@ class TestParseEvent:
"""Exercises the top-level parse_event() dispatcher."""
@pytest.mark.parametrize(
"type_str,expected_type,expected_class",
("type_str", "expected_type", "expected_class"),
[
pytest.param("CHAT", EventType.CHAT, ChatEvent, id="chat"),
pytest.param(
@@ -592,7 +592,7 @@ class TestLogEvent:
"""Exercises log_event() output via caplog."""
@pytest.mark.parametrize(
"event_type,event,expected_message",
("event_type", "event", "expected_message"),
[
pytest.param(
EventType.CHAT,
+6 -6
View File
@@ -110,7 +110,7 @@ class TestOnEventDecorator:
"""Tests the @on_event decorator from owlbot.api.events."""
@pytest.mark.parametrize(
"event_types,priority,expected_types,expected_priority",
("event_types", "priority", "expected_types", "expected_priority"),
[
pytest.param(
(EventType.CHAT,),
@@ -173,7 +173,7 @@ class TestPriority:
"""Tests the Priority IntEnum from owlbot.api.events."""
@pytest.mark.parametrize(
"member,value",
("member", "value"),
[
pytest.param(Priority.HIGHEST, 100, id="highest"),
pytest.param(Priority.HIGH, 75, id="high"),
@@ -294,7 +294,7 @@ class TestEventRegistry:
assert registry.get_handler_module(handler) is None
@pytest.mark.parametrize(
"setup_types,expected_return,expected_remaining",
("setup_types", "expected_return", "expected_remaining"),
[
pytest.param(
(EventType.CHAT,),
@@ -795,7 +795,7 @@ class TestCommandDispatchPhase:
"""Tests Phase 2 (command dispatch) in EventDispatcher.dispatch()."""
@pytest.mark.parametrize(
"event_type,event_factory,expected_calls",
("event_type", "event_factory", "expected_calls"),
[
pytest.param(
EventType.CHAT,
@@ -945,7 +945,7 @@ class TestModuleEvents:
assert mod_events.module_events == {}
@pytest.mark.parametrize(
"register_on,unregister_from,expected",
("register_on", "unregister_from", "expected"),
[
pytest.param("mod_a", "mod_a", True, id="own-handler"),
pytest.param("mod_b", "mod_a", False, id="other-modules-handler"),
@@ -1028,7 +1028,7 @@ class TestEventContext:
"""Tests EventContext from owlbot.api.context."""
@pytest.mark.parametrize(
"prop,use_is",
("prop", "use_is"),
[
pytest.param("module_name", False, id="module-name"),
pytest.param("storage", True, id="storage"),
+17 -17
View File
@@ -70,7 +70,7 @@ class TestSimpleSubstitution:
"""Basic happy-path usage of placeholders and plain text."""
@pytest.mark.parametrize(
"template,kwargs,expected",
("template", "kwargs", "expected"),
[
pytest.param(
"Just a plain message.", {}, "Just a plain message.", id="plain-text"
@@ -154,7 +154,7 @@ class TestArgPlaceholders:
"""Positional arguments $(1) through $(9)."""
@pytest.mark.parametrize(
"template,args,expected",
("template", "args", "expected"),
[
pytest.param(
"$(1)$(2)$(3)$(4)$(5)$(6)$(7)$(8)$(9)",
@@ -186,7 +186,7 @@ class TestArgPlaceholders:
assert await process(template, placeholder_storage, args=args) == expected
@pytest.mark.parametrize(
"template,args,expected",
("template", "args", "expected"),
[
pytest.param("$(0)", ["first"], "$(0)", id="index-zero"),
pytest.param("$(10)", ["a"] * 10, "$(10)", id="index-ten"),
@@ -205,7 +205,7 @@ class TestArgPlaceholders:
assert result == expected
@pytest.mark.parametrize(
"template,args,expected",
("template", "args", "expected"),
[
pytest.param(
"$(1 extra)",
@@ -269,7 +269,7 @@ class TestNamedCounters:
assert row["value"] == 5
@pytest.mark.parametrize(
"counter_values,template,expected,db_name,db_value",
("counter_values", "template", "expected", "db_name", "db_value"),
[
pytest.param(
{"deaths": 10},
@@ -310,7 +310,7 @@ class TestNamedCounters:
assert row["value"] == db_value
@pytest.mark.parametrize(
"counter_values,template,expected",
("counter_values", "template", "expected"),
[
pytest.param({}, "$(count deaths +5)", "5", id="plus-five"),
pytest.param(
@@ -425,7 +425,7 @@ class TestCounterNameValidation:
"""Counter names are lowercased and must match ^[a-z0-9_]+$."""
@pytest.mark.parametrize(
"template,expected,db_name,db_value",
("template", "expected", "db_name", "db_value"),
[
pytest.param("$(count Deaths)", "1", "deaths", 1, id="uppercase-name"),
pytest.param("$(count 123)", "1", "123", 1, id="numeric-name"),
@@ -509,7 +509,7 @@ class TestRand:
assert result == f"Alice rolled a {expected_roll}!"
@pytest.mark.parametrize(
"template,expected",
("template", "expected"),
[
pytest.param("$(rand 5 5)", "5", id="equal-bounds"),
pytest.param("$(rand 0 0)", "0", id="zero-bounds"),
@@ -606,7 +606,7 @@ class TestCountdownCountup:
assert await process(template, placeholder_storage) == "0 seconds"
@pytest.mark.parametrize(
"placeholder,bad_date",
("placeholder", "bad_date"),
[
pytest.param("countdown", "not a real date", id="gibberish"),
pytest.param("countdown", "monday", id="weekday-only"),
@@ -640,7 +640,7 @@ class TestCountdownCountup:
assert result == expected
@pytest.mark.parametrize(
"seconds,expected",
("seconds", "expected"),
[
pytest.param(0, "0 seconds", id="zero"),
pytest.param(1, "1 second", id="one-second"),
@@ -775,7 +775,7 @@ class TestNesting:
assert result == "$(test)"
@pytest.mark.parametrize(
"template,args",
("template", "args"),
[
pytest.param("$(rand $(1) $(2))", [], id="no-args"),
pytest.param("$(rand $(1) $(2))", ["5"], id="one-arg"),
@@ -805,7 +805,7 @@ class TestEscaping:
"""Escape sequences and backslash boundary conditions."""
@pytest.mark.parametrize(
"template,kwargs,expected",
("template", "kwargs", "expected"),
[
pytest.param(
r"Use \$(user) to insert your name.",
@@ -856,7 +856,7 @@ class TestCaseInsensitivity:
"""Placeholder names are lowercased before handler lookup."""
@pytest.mark.parametrize(
"template,kwargs,expected",
("template", "kwargs", "expected"),
[
pytest.param("$(USER)", {}, "Alice", id="all-upper-user"),
pytest.param("$(User)", {}, "Alice", id="title-case-user"),
@@ -986,7 +986,7 @@ class TestParserBoundaries:
"""Parser edge cases around incomplete or unusual $( sequences."""
@pytest.mark.parametrize(
"template,expected",
("template", "expected"),
[
pytest.param("$(rand 1", "$(rand 1", id="unclosed-rand"),
pytest.param(
@@ -1017,7 +1017,7 @@ class TestUnknownPlaceholders:
"""Unknown placeholder names pass through unchanged."""
@pytest.mark.parametrize(
"template,expected",
("template", "expected"),
[
pytest.param("$(madeup stuff)", "$(madeup stuff)", id="with-args"),
pytest.param("$(banana)", "$(banana)", id="bare-name"),
@@ -1043,7 +1043,7 @@ class TestEvaluationSafety:
"""Verify that resolved text is never re-parsed as placeholders."""
@pytest.mark.parametrize(
"template,kwargs,expected",
("template", "kwargs", "expected"),
[
pytest.param(
"$(1)", {"args": ["$(user)"]}, "$(user)", id="arg-contains-user"
@@ -1093,7 +1093,7 @@ class TestRuntimeErrors:
"""A PlaceholderError cancels the entire render and returns the message."""
@pytest.mark.parametrize(
"template,expected_error",
("template", "expected_error"),
[
pytest.param(
"$(rand 1)",
+4 -4
View File
@@ -119,7 +119,7 @@ class TestFetchOne:
"""fetch_one() returns a Row or None."""
@pytest.mark.parametrize(
"accessor, expected",
("accessor", "expected"),
[
pytest.param(lambda row: row["name"], "a", id="key-access"),
pytest.param(lambda row: row[1], "a", id="index-access"),
@@ -288,7 +288,7 @@ class TestTransaction:
self, storage_with_table: ModuleStorage
) -> None:
"""Writes inside transaction() are discarded if an exception is raised."""
with pytest.raises(RuntimeError, match="boom"):
with pytest.raises(RuntimeError, match="boom"): # noqa: PT012
async with storage_with_table._checkout(), storage_with_table.transaction():
await storage_with_table.execute(
"INSERT INTO items (name, value) VALUES (?, ?)",
@@ -335,7 +335,7 @@ class TestStorageError:
"""Invalid SQL raises StorageError wrapping the underlying error."""
@pytest.mark.parametrize(
"method, args",
("method", "args"),
[
pytest.param(
"execute",
@@ -372,7 +372,7 @@ class TestStorageError:
await getattr(storage, method)(*args)
@pytest.mark.parametrize(
"method, args",
("method", "args"),
[
pytest.param("execute", ("SELECT 1",), id="execute"),
pytest.param("fetch_one", ("SELECT 1",), id="fetch-one"),