Skip to content

Commit 217d736

Browse files
authored
Mark all pydantic models as frozen (#796)
* Update pydantic factories to allow us to use `model_construct` This is useful for when we want to build a model with invalid data, then dump it to JSON to use with the test client * Update tests to account for frozen models * When instantiating factory with no args, use fixture directly * Use provided `BugzillaWebhookFactory` fixture instead of alias
1 parent a621467 commit 217d736

File tree

13 files changed

+432
-427
lines changed

13 files changed

+432
-427
lines changed

jbi/bugzilla/service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,11 @@ def get_description(self, bug_id: int):
9393
def refresh_bug_data(self, bug: BugzillaBug):
9494
"""Re-fetch a bug to ensure we have the most up-to-date data"""
9595

96-
updated_bug = self.client.get_bug(bug.id)
96+
refreshed_bug_data = self.client.get_bug(bug.id)
9797
# When bugs come in as webhook payloads, they have a "comment"
9898
# attribute, but this field isn't available when we get a bug by ID.
9999
# So, we make sure to add the comment back if it was present on the bug.
100-
updated_bug.comment = bug.comment
100+
updated_bug = refreshed_bug_data.model_copy(update={"comment": bug.comment})
101101
return updated_bug
102102

103103
def list_webhooks(self):

jbi/log.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def filter(self, record) -> bool:
7878
}
7979

8080

81-
class RequestSummary(BaseModel):
81+
class RequestSummary(BaseModel, frozen=True):
8282
"""Request summary as specified by MozLog format"""
8383

8484
agent: str

jbi/models.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
JIRA_HOSTNAMES = ("jira", "atlassian")
3030

3131

32-
class ActionSteps(BaseModel):
32+
class ActionSteps(BaseModel, frozen=True):
3333
"""Step functions to run for each type of Bugzilla webhook payload"""
3434

3535
new: list[str] = [
@@ -63,7 +63,7 @@ def validate_steps(cls, function_names: list[str]):
6363
return function_names
6464

6565

66-
class JiraComponents(BaseModel):
66+
class JiraComponents(BaseModel, frozen=True):
6767
"""Controls how Jira components are set on issues in the `maybe_update_components` step."""
6868

6969
use_bug_component: bool = True
@@ -72,7 +72,7 @@ class JiraComponents(BaseModel):
7272
set_custom_components: list[str] = []
7373

7474

75-
class ActionParams(BaseModel):
75+
class ActionParams(BaseModel, frozen=True):
7676
"""Params passed to Action step functions"""
7777

7878
jira_project_key: str
@@ -84,7 +84,7 @@ class ActionParams(BaseModel):
8484
issue_type_map: dict[str, str] = {"task": "Task", "defect": "Bug"}
8585

8686

87-
class Action(BaseModel):
87+
class Action(BaseModel, frozen=True):
8888
"""
8989
Action is the inner model for each action in the configuration file"""
9090

@@ -164,25 +164,23 @@ def validate_actions(cls, actions: list[Action]):
164164
model_config = ConfigDict(ignored_types=(functools.cached_property,))
165165

166166

167-
class BugzillaWebhookUser(BaseModel):
167+
class BugzillaWebhookUser(BaseModel, frozen=True):
168168
"""Bugzilla User Object"""
169169

170170
id: int
171171
login: str
172172
real_name: str
173173

174174

175-
class BugzillaWebhookEventChange(BaseModel):
175+
class BugzillaWebhookEventChange(BaseModel, frozen=True, coerce_numbers_to_str=True):
176176
"""Bugzilla Change Object"""
177177

178-
model_config = ConfigDict(coerce_numbers_to_str=True)
179-
180178
field: str
181179
removed: str
182180
added: str
183181

184182

185-
class BugzillaWebhookEvent(BaseModel):
183+
class BugzillaWebhookEvent(BaseModel, frozen=True):
186184
"""Bugzilla Event Object"""
187185

188186
action: str
@@ -198,7 +196,7 @@ def changed_fields(self) -> list[str]:
198196
return [c.field for c in self.changes] if self.changes else []
199197

200198

201-
class BugzillaWebhookComment(BaseModel):
199+
class BugzillaWebhookComment(BaseModel, frozen=True):
202200
"""Bugzilla Comment Object"""
203201

204202
body: Optional[str] = None
@@ -208,7 +206,7 @@ class BugzillaWebhookComment(BaseModel):
208206
creation_time: Optional[datetime.datetime] = None
209207

210208

211-
class BugzillaBug(BaseModel):
209+
class BugzillaBug(BaseModel, frozen=True):
212210
"""Bugzilla Bug Object"""
213211

214212
id: int
@@ -293,7 +291,7 @@ def lookup_action(self, actions: Actions) -> Action:
293291
raise ActionNotFoundError(", ".join(actions.by_tag.keys()))
294292

295293

296-
class BugzillaWebhookRequest(BaseModel):
294+
class BugzillaWebhookRequest(BaseModel, frozen=True):
297295
"""Bugzilla Webhook Request Object"""
298296

299297
webhook_id: int
@@ -302,7 +300,7 @@ class BugzillaWebhookRequest(BaseModel):
302300
bug: BugzillaBug
303301

304302

305-
class BugzillaComment(BaseModel):
303+
class BugzillaComment(BaseModel, frozen=True):
306304
"""Bugzilla Comment"""
307305

308306
id: int
@@ -314,14 +312,14 @@ class BugzillaComment(BaseModel):
314312
BugzillaComments = TypeAdapter(list[BugzillaComment])
315313

316314

317-
class BugzillaApiResponse(BaseModel):
315+
class BugzillaApiResponse(BaseModel, frozen=True):
318316
"""Bugzilla Response Object"""
319317

320318
faults: Optional[list] = None
321319
bugs: Optional[list[BugzillaBug]] = None
322320

323321

324-
class BugzillaWebhook(BaseModel):
322+
class BugzillaWebhook(BaseModel, frozen=True):
325323
"""Bugzilla Webhook"""
326324

327325
id: int
@@ -343,13 +341,13 @@ def slug(self):
343341
return f"{self.id}-{name}-{product}"
344342

345343

346-
class BugzillaWebhooksResponse(BaseModel):
344+
class BugzillaWebhooksResponse(BaseModel, frozen=True):
347345
"""Bugzilla Webhooks List Response Object"""
348346

349347
webhooks: Optional[list[BugzillaWebhook]] = None
350348

351349

352-
class Context(BaseModel):
350+
class Context(BaseModel, frozen=True):
353351
"""Generic log context throughout JBI"""
354352

355353
def update(self, **kwargs):

tests/conftest.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import tests.fixtures.factories as factories
1313
from jbi import Operation, bugzilla, jira
1414
from jbi.app import app
15-
from jbi.configuration import get_actions
1615
from jbi.environment import Settings
1716
from jbi.models import ActionContext
1817

@@ -56,7 +55,7 @@ def mocked_statsd():
5655

5756
register(factories.ActionContextFactory)
5857
register(factories.ActionFactory)
59-
register(factories.ActionsFactory)
58+
register(factories.ActionsFactory, "_actions")
6059
register(factories.ActionParamsFactory)
6160
register(factories.BugFactory)
6261
register(factories.BugzillaWebhookFactory)
@@ -71,7 +70,6 @@ def mocked_statsd():
7170
register(
7271
factories.ActionContextFactory, "context_create_example", operation=Operation.CREATE
7372
)
74-
register(factories.WebhookFactory, "webhook_create_example")
7573

7674

7775
@pytest.fixture
@@ -86,9 +84,9 @@ def settings():
8684
return Settings()
8785

8886

89-
@pytest.fixture(autouse=True)
90-
def actions():
91-
return get_actions()
87+
@pytest.fixture()
88+
def actions(actions_factory):
89+
return actions_factory()
9290

9391

9492
@pytest.fixture(autouse=True)

tests/fixtures/factories.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,24 @@
33
from jbi import Operation, models
44

55

6-
class ActionParamsFactory(factory.Factory):
6+
class PydanticFactory(factory.Factory):
7+
"""
8+
- factory_instance(**kwargs) -> Model(**kwargs)
9+
- factory_instance.create(**kwargs) -> Model(**kwargs)
10+
- factory_instance.build(**kwargs) -> Model.model_construct(**kwargs)
11+
12+
https://docs.pydantic.dev/latest/api/base_model/#pydantic.main.BaseModel.model_construct
13+
"""
14+
15+
class Meta:
16+
abstract = True
17+
18+
@classmethod
19+
def _build(cls, model_class, *args, **kwargs):
20+
return model_class.model_construct(**kwargs)
21+
22+
23+
class ActionParamsFactory(PydanticFactory):
724
class Meta:
825
model = models.ActionParams
926

@@ -15,7 +32,7 @@ class Meta:
1532
issue_type_map = {"task": "Task", "defect": "Bug"}
1633

1734

18-
class ActionFactory(factory.Factory):
35+
class ActionFactory(PydanticFactory):
1936
class Meta:
2037
model = models.Action
2138

@@ -25,14 +42,14 @@ class Meta:
2542
parameters = factory.SubFactory(ActionParamsFactory)
2643

2744

28-
class ActionsFactory(factory.Factory):
45+
class ActionsFactory(PydanticFactory):
2946
class Meta:
3047
model = models.Actions
3148

3249
root = factory.List([factory.SubFactory(ActionFactory)])
3350

3451

35-
class BugzillaWebhookCommentFactory(factory.Factory):
52+
class BugzillaWebhookCommentFactory(PydanticFactory):
3653
class Meta:
3754
model = models.BugzillaWebhookComment
3855

@@ -43,7 +60,7 @@ class Meta:
4360
creation_time = None
4461

4562

46-
class BugFactory(factory.Factory):
63+
class BugFactory(PydanticFactory):
4764
class Meta:
4865
model = models.BugzillaBug
4966

@@ -71,7 +88,7 @@ class Params:
7188
whiteboard = "[devtest]"
7289

7390

74-
class WebhookUserFactory(factory.Factory):
91+
class WebhookUserFactory(PydanticFactory):
7592
class Meta:
7693
model = models.BugzillaWebhookUser
7794

@@ -80,7 +97,7 @@ class Meta:
8097
real_name = "Nobody [ :nobody ]"
8198

8299

83-
class WebhookEventChangeFactory(factory.Factory):
100+
class WebhookEventChangeFactory(PydanticFactory):
84101
class Meta:
85102
model = models.BugzillaWebhookEventChange
86103

@@ -89,7 +106,7 @@ class Meta:
89106
added = "new value"
90107

91108

92-
class WebhookEventFactory(factory.Factory):
109+
class WebhookEventFactory(PydanticFactory):
93110
class Meta:
94111
model = models.BugzillaWebhookEvent
95112

@@ -101,7 +118,7 @@ class Meta:
101118
user = factory.SubFactory(WebhookUserFactory)
102119

103120

104-
class WebhookFactory(factory.Factory):
121+
class WebhookFactory(PydanticFactory):
105122
class Meta:
106123
model = models.BugzillaWebhookRequest
107124

@@ -111,7 +128,7 @@ class Meta:
111128
webhook_name = "local-test"
112129

113130

114-
class CommentFactory(factory.Factory):
131+
class CommentFactory(PydanticFactory):
115132
class Meta:
116133
model = models.BugzillaComment
117134

@@ -123,7 +140,7 @@ class Meta:
123140
creator = "[email protected]"
124141

125142

126-
class JiraContextFactory(factory.Factory):
143+
class JiraContextFactory(PydanticFactory):
127144
class Meta:
128145
model = models.JiraContext
129146

@@ -132,7 +149,7 @@ class Meta:
132149
labels = []
133150

134151

135-
class ActionContextFactory(factory.Factory):
152+
class ActionContextFactory(PydanticFactory):
136153
class Meta:
137154
model = models.ActionContext
138155

@@ -143,7 +160,7 @@ class Meta:
143160
jira = factory.SubFactory(JiraContextFactory)
144161

145162

146-
class BugzillaWebhookFactory(factory.Factory):
163+
class BugzillaWebhookFactory(PydanticFactory):
147164
class Meta:
148165
model = models.BugzillaWebhook
149166

@@ -156,19 +173,3 @@ class Meta:
156173
name = "Test Webhooks"
157174
product = "Firefox"
158175
url = "http://server.example.com/bugzilla_webhook"
159-
160-
161-
__all__ = [
162-
"ActionContextFactory",
163-
"ActionFactory",
164-
"ActionParamsFactory",
165-
"ActionsFactory",
166-
"BugFactory",
167-
"BugzillaWebhookFactory",
168-
"CommentFactory",
169-
"JiraContextFactory",
170-
"WebhookEventChangeFactory",
171-
"WebhookEventFactory",
172-
"WebhookFactory",
173-
"WebhookUserFactory",
174-
]

tests/unit/jira/test_client.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,7 @@ def test_paginated_projects_no_keys(settings, jira_client, mocked_responses):
7979
assert resp == mocked_response_data
8080

8181

82-
def test_paginated_projects_with_keys(
83-
settings, jira_client, mocked_responses, action_factory
84-
):
85-
action_factory()
82+
def test_paginated_projects_with_keys(settings, jira_client, mocked_responses):
8683
url = f"{settings.jira_base_url}rest/api/2/project/search"
8784
mocked_response_data = {"some": "data"}
8885
mocked_responses.add(

0 commit comments

Comments
 (0)