Skip to content

Commit 3bd5cf1

Browse files
authored
Builds: avoid breaking builds when adding a new field to our APIs (#10295)
Closes #10257
1 parent 3b35753 commit 3bd5cf1

File tree

4 files changed

+69
-5
lines changed

4 files changed

+69
-5
lines changed

readthedocs/builds/models.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
)
6262
from readthedocs.builds.version_slug import VersionSlugField
6363
from readthedocs.config import LATEST_CONFIGURATION_VERSION
64-
from readthedocs.core.utils import trigger_build
64+
from readthedocs.core.utils import extract_valid_attributes_for_model, trigger_build
6565
from readthedocs.projects.constants import (
6666
BITBUCKET_COMMIT_URL,
6767
BITBUCKET_URL,
@@ -656,7 +656,17 @@ def __init__(self, *args, **kwargs):
656656
del kwargs[key]
657657
except KeyError:
658658
pass
659-
super().__init__(*args, **kwargs)
659+
valid_attributes, invalid_attributes = extract_valid_attributes_for_model(
660+
model=Version,
661+
attributes=kwargs,
662+
)
663+
if invalid_attributes:
664+
log.warning(
665+
"APIVersion got unexpected attributes.",
666+
invalid_attributes=invalid_attributes,
667+
)
668+
669+
super().__init__(*args, **valid_attributes)
660670

661671
def save(self, *args, **kwargs): # pylint: disable=arguments-differ
662672
return 0

readthedocs/core/utils/__init__.py

+20
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,23 @@ def get_cache_tag(*args):
322322
allowed in slugs to avoid collisions.
323323
"""
324324
return ':'.join(args)
325+
326+
327+
def extract_valid_attributes_for_model(model, attributes):
328+
"""
329+
Extract the valid attributes for a model from a dictionary of attributes.
330+
331+
:param model: Model class to extract the attributes for.
332+
:param attributes: Dictionary of attributes to extract.
333+
:returns: Tuple with the valid attributes and the invalid attributes if any.
334+
"""
335+
attributes = attributes.copy()
336+
valid_field_names = {field.name for field in model._meta.get_fields()}
337+
valid_attributes = {}
338+
# We can't change a dictionary while interating over its keys,
339+
# so we make a copy of its keys.
340+
keys = list(attributes.keys())
341+
for key in keys:
342+
if key in valid_field_names:
343+
valid_attributes[key] = attributes.pop(key)
344+
return valid_attributes, attributes

readthedocs/projects/models.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from readthedocs.constants import pattern_opts
3131
from readthedocs.core.history import ExtraHistoricalRecords
3232
from readthedocs.core.resolver import resolve, resolve_domain
33-
from readthedocs.core.utils import slugify
33+
from readthedocs.core.utils import extract_valid_attributes_for_model, slugify
3434
from readthedocs.core.utils.url import unsafe_join_url_path
3535
from readthedocs.domains.querysets import DomainQueryset
3636
from readthedocs.projects import constants
@@ -1382,7 +1382,18 @@ def __init__(self, *args, **kwargs):
13821382
del kwargs[key]
13831383
except KeyError:
13841384
pass
1385-
super().__init__(*args, **kwargs)
1385+
1386+
valid_attributes, invalid_attributes = extract_valid_attributes_for_model(
1387+
model=Project,
1388+
attributes=kwargs,
1389+
)
1390+
if invalid_attributes:
1391+
log.warning(
1392+
"APIProject got unexpected attributes.",
1393+
invalid_attributes=invalid_attributes,
1394+
)
1395+
1396+
super().__init__(*args, **valid_attributes)
13861397

13871398
# Overwrite the database property with the value from the API
13881399
self.ad_free = ad_free

readthedocs/rtd_tests/tests/test_api.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
EXTERNAL_VERSION_STATE_CLOSED,
4747
LATEST,
4848
)
49-
from readthedocs.builds.models import Build, BuildCommandResult, Version
49+
from readthedocs.builds.models import APIVersion, Build, BuildCommandResult, Version
5050
from readthedocs.integrations.models import Integration
5151
from readthedocs.oauth.models import (
5252
RemoteOrganization,
@@ -854,6 +854,29 @@ def test_init_api_project(self):
854854
{'RELEASE': 'prod'},
855855
)
856856

857+
def test_invalid_attributes_api_project(self):
858+
invalid_attribute = "invalid_attribute"
859+
project_data = {
860+
"name": "Test Project",
861+
"slug": "test-project",
862+
"show_advertising": True,
863+
invalid_attribute: "nope",
864+
}
865+
api_project = APIProject(**project_data)
866+
self.assertFalse(hasattr(api_project, invalid_attribute))
867+
868+
def test_invalid_attributes_api_version(self):
869+
invalid_attribute = "invalid_attribute"
870+
version_data = {
871+
"type": "branch",
872+
"identifier": "main",
873+
"verbose_name": "main",
874+
"slug": "v2",
875+
invalid_attribute: "nope",
876+
}
877+
api_version = APIVersion(**version_data)
878+
self.assertFalse(hasattr(api_version, invalid_attribute))
879+
857880
@override_settings(
858881
RTD_DEFAULT_FEATURES={
859882
TYPE_CONCURRENT_BUILDS: 4,

0 commit comments

Comments
 (0)