Skip to content

Commit 635a014

Browse files
fix: smell too many instance attributes
1 parent 8ca62c7 commit 635a014

16 files changed

Lines changed: 464 additions & 282 deletions

File tree

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,17 @@ TEST-**.xml
125125
# Mac OS .DS_Store, which is a file that stores custom attributes of its containing folder
126126
.DS_Store
127127
*.env*
128+
129+
# # Métricas
130+
/metrics-before-radon
131+
/metrics-before-pylint
132+
/metrics-after-pylint
133+
/metrics-before-codecarbon
134+
/metrics-before-pytest
135+
136+
extract_metrics_before_radon.py
137+
extract_metrics_before_pylint.py
138+
extract_metrics_after_pylint.py
139+
extract_score_before_pylint.py
140+
extract_metrics_before_codecarbon.py
141+
extract_metrics_before_pytest.py

bot/exts/filtering/_filter_context.py

Lines changed: 109 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,60 +25,130 @@ class Event(Enum):
2525

2626

2727
@dataclass
28-
class FilterContext:
29-
"""A dataclass containing the information that should be filtered, and output information of the filtering."""
28+
class FilterInput: # pylint: disable=too-many-instance-attributes
29+
"""Input data for filtering: event details and message content."""
3030

31-
# Input context
32-
event: Event # The type of event
33-
author: User | Member | None # Who triggered the event
34-
channel: TextChannel | VoiceChannel | StageChannel | Thread | DMChannel | None # The channel involved
35-
content: str | Iterable # What actually needs filtering. The Iterable type depends on the filter list.
36-
message: Message | None # The message involved
37-
embeds: list[Embed] = field(default_factory=list) # Any embeds involved
38-
attachments: list[discord.Attachment | FileAttachment] = field(default_factory=list) # Any attachments sent.
31+
event: Event
32+
author: User | Member | None
33+
channel: TextChannel | VoiceChannel | StageChannel | Thread | DMChannel | None
34+
content: str | Iterable
35+
message: Message | None
36+
embeds: list[Embed] = field(default_factory=list)
37+
attachments: list[discord.Attachment | FileAttachment] = field(default_factory=list)
3938
before_message: Message | None = None
4039
message_cache: MessageCache | None = None
41-
# Output context
42-
dm_content: str = "" # The content to DM the invoker
43-
dm_embed: str = "" # The embed description to DM the invoker
44-
send_alert: bool = False # Whether to send an alert for the moderators
45-
alert_content: str = "" # The content of the alert
46-
alert_embeds: list[Embed] = field(default_factory=list) # Any embeds to add to the alert
47-
action_descriptions: list[str] = field(default_factory=list) # What actions were taken
48-
matches: list[str] = field(default_factory=list) # What exactly was found
49-
notification_domain: str = "" # A domain to send the user for context
50-
filter_info: dict[Filter, str] = field(default_factory=dict) # Additional info from a filter.
51-
messages_deletion: bool = False # Whether the messages were deleted. Can't upload deletion log otherwise.
52-
blocked_exts: set[str] = field(default_factory=set) # Any extensions blocked (used for snekbox)
40+
41+
42+
@dataclass
43+
class FilterOutput: # pylint: disable=too-many-instance-attributes
44+
"""Output data produced by filtering: alerts, actions, and results."""
45+
46+
dm_content: str = ""
47+
dm_embed: str = ""
48+
send_alert: bool = False
49+
alert_content: str = ""
50+
alert_embeds: list[Embed] = field(default_factory=list)
51+
action_descriptions: list[str] = field(default_factory=list)
52+
matches: list[str] = field(default_factory=list)
53+
notification_domain: str = ""
54+
filter_info: dict[Filter, str] = field(default_factory=dict)
55+
messages_deletion: bool = False
56+
blocked_exts: set[str] = field(default_factory=set)
5357
potential_phish: dict[FilterList, set[str]] = field(default_factory=dict)
54-
# Additional actions to perform
58+
59+
60+
_FILTER_CONTEXT_DIRECT_FIELDS = frozenset({
61+
'input', 'output', 'additional_actions', 'related_messages',
62+
'related_channels', 'uploaded_attachments', 'upload_deletion_logs',
63+
})
64+
65+
66+
@dataclass
67+
class FilterContext:
68+
"""A dataclass containing the information that should be filtered, and output information of the filtering."""
69+
70+
input: FilterInput
71+
output: FilterOutput
5572
additional_actions: list[Callable[[FilterContext], Coroutine]] = field(default_factory=list)
56-
related_messages: set[Message] = field(default_factory=set) # Deletion will include these.
73+
related_messages: set[Message] = field(default_factory=set)
5774
related_channels: set[TextChannel | Thread | DMChannel] = field(default_factory=set)
58-
uploaded_attachments: dict[int, list[str]] = field(default_factory=dict) # Message ID to attachment URLs.
59-
upload_deletion_logs: bool = True # Whether it's allowed to upload deletion logs.
75+
uploaded_attachments: dict[int, list[str]] = field(default_factory=dict)
76+
upload_deletion_logs: bool = True
77+
78+
@property
79+
def in_guild(self) -> bool:
80+
"""Whether the context is from a guild channel (not a DM)."""
81+
return self.input.channel is None or self.input.channel.guild is not None
82+
83+
def __getattr__(self, name):
84+
try:
85+
input_obj = object.__getattribute__(self, 'input')
86+
if hasattr(input_obj, name):
87+
return getattr(input_obj, name)
88+
except AttributeError:
89+
pass
90+
try:
91+
output_obj = object.__getattribute__(self, 'output')
92+
if hasattr(output_obj, name):
93+
return getattr(output_obj, name)
94+
except AttributeError:
95+
pass
96+
raise AttributeError(f"'FilterContext' has no attribute '{name}'")
6097

61-
def __post_init__(self):
62-
# If it's in the context of a DM channel, self.channel won't be None, but self.channel.guild will.
63-
self.in_guild = self.channel is None or self.channel.guild is not None
98+
def __setattr__(self, name, value):
99+
if name in _FILTER_CONTEXT_DIRECT_FIELDS:
100+
object.__setattr__(self, name, value)
101+
return
102+
try:
103+
input_obj = object.__getattribute__(self, 'input')
104+
if hasattr(input_obj, name):
105+
setattr(input_obj, name, value)
106+
return
107+
except AttributeError:
108+
pass
109+
try:
110+
output_obj = object.__getattribute__(self, 'output')
111+
if hasattr(output_obj, name):
112+
setattr(output_obj, name, value)
113+
return
114+
except AttributeError:
115+
pass
116+
object.__setattr__(self, name, value)
64117

65118
@classmethod
66119
def from_message(
67120
cls, event: Event, message: Message, before: Message | None = None, cache: MessageCache | None = None
68121
) -> FilterContext:
69122
"""Create a filtering context from the attributes of a message."""
70123
return cls(
71-
event,
72-
message.author,
73-
message.channel,
74-
message.content,
75-
message,
76-
message.embeds,
77-
message.attachments,
78-
before,
79-
cache
124+
FilterInput(
125+
event,
126+
message.author,
127+
message.channel,
128+
message.content,
129+
message,
130+
message.embeds,
131+
message.attachments,
132+
before,
133+
cache
134+
),
135+
FilterOutput()
80136
)
81137

82138
def replace(self, **changes) -> FilterContext:
83139
"""Return a new context object assigning new values to the specified fields."""
84-
return replace(self, **changes)
140+
input_fields = FilterInput.__dataclass_fields__
141+
output_fields = FilterOutput.__dataclass_fields__
142+
input_changes = {}
143+
output_changes = {}
144+
context_changes = {}
145+
for k, v in changes.items():
146+
if k in input_fields:
147+
input_changes[k] = v
148+
elif k in output_fields:
149+
output_changes[k] = v
150+
else:
151+
context_changes[k] = v
152+
new_input = replace(self.input, **input_changes) if input_changes else self.input
153+
new_output = replace(self.output, **output_changes) if output_changes else self.output
154+
return FilterContext(new_input, new_output, **context_changes)

bot/exts/filtering/_filter_lists/antispam.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pydis_core.utils import scheduling
1414
from pydis_core.utils.logging import get_logger
1515

16-
from bot.exts.filtering._filter_context import FilterContext
16+
from bot.exts.filtering._filter_context import FilterContext, FilterInput, FilterOutput
1717
from bot.exts.filtering._filter_lists.filter_list import ListType, SubscribingAtomicList, UniquesListBase
1818
from bot.exts.filtering._filters.antispam import antispam_filter_types
1919
from bot.exts.filtering._filters.filter import Filter, UniqueFilter
@@ -158,7 +158,7 @@ async def send_alert(self, antispam_list: AntispamList) -> None:
158158
return
159159

160160
ctx, *other_contexts = self.contexts
161-
new_ctx = FilterContext(ctx.event, ctx.author, ctx.channel, ctx.content, ctx.message)
161+
new_ctx = FilterContext(FilterInput(ctx.event, ctx.author, ctx.channel, ctx.content, ctx.message), FilterOutput())
162162
all_descriptions_counts = Counter(reduce(
163163
add, (other_ctx.action_descriptions for other_ctx in other_contexts), ctx.action_descriptions
164164
))

bot/exts/filtering/_filters/filter.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77
from bot.exts.filtering._filter_context import Event, FilterContext
88
from bot.exts.filtering._settings import Defaults, create_settings
99
from bot.exts.filtering._utils import FieldRequiring
10+
from dataclasses import dataclass
11+
12+
import arrow
13+
14+
15+
@dataclass
16+
class FilterTimestamps:
17+
"""Timestamps for when a filter was created and last updated."""
18+
created_at: arrow.Arrow
19+
updated_at: arrow.Arrow
1020

1121

1222
class Filter(FieldRequiring):
@@ -23,12 +33,14 @@ class Filter(FieldRequiring):
2333
# If a subclass uses extra fields, it should assign the pydantic model type to this variable.
2434
extra_fields_type = None
2535

26-
def __init__(self, filter_data: dict, defaults: Defaults | None = None):
36+
def __init__(self, filter_data: dict, defaults: Defaults | None=None):
2737
self.id = filter_data["id"]
2838
self.content = filter_data["content"]
2939
self.description = filter_data["description"]
30-
self.created_at = arrow.get(filter_data["created_at"])
31-
self.updated_at = arrow.get(filter_data["updated_at"])
40+
self.timestamps = FilterTimestamps(
41+
created_at=arrow.get(filter_data["created_at"]),
42+
updated_at=arrow.get(filter_data["updated_at"])
43+
)
3244
self.actions, self.validations = create_settings(filter_data["settings"], defaults=defaults)
3345
if self.extra_fields_type:
3446
self.extra_fields = self.extra_fields_type.model_validate(filter_data["additional_settings"])
@@ -75,6 +87,15 @@ async def process_input(cls, content: str, description: str) -> tuple[str, str]:
7587
A BadArgument should be raised if the content can't be used.
7688
"""
7789
return content, description
90+
91+
92+
@property
93+
def created_at(self) -> arrow.Arrow:
94+
return self.timestamps.created_at
95+
96+
@property
97+
def updated_at(self) -> arrow.Arrow:
98+
return self.timestamps.updated_at
7899

79100
def __str__(self) -> str:
80101
"""A string representation of the filter."""

0 commit comments

Comments
 (0)