Skip to content

Commit 0198f35

Browse files
authored
Merge pull request #199 from contentstack/enh/dx-7324
Added Branches support in entry variants
2 parents 2d11d26 + fbee506 commit 0198f35

7 files changed

Lines changed: 291 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# CHANGELOG
22

3+
## _v2.7.0_
4+
5+
### **Date: 20-July-2026**
6+
7+
- Added optional branch support for entry variants on `Entry.variants()` and `ContentType.variants()`.
8+
39
## _v2.6.1_
410

511
### **Date: 13-July-2026**

contentstack/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def get_contentstack_endpoint(region='us', service='', omit_https=False):
4242
__title__ = 'contentstack-delivery-python'
4343
__author__ = 'contentstack'
4444
__status__ = 'debug'
45-
__version__ = 'v2.6.1'
45+
__version__ = 'v2.7.0'
4646
__endpoint__ = 'cdn.contentstack.io'
4747
__email__ = 'support@contentstack.com'
4848
__developer_email__ = 'mobile@contentstack.com'

contentstack/contenttype.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,20 @@ def find(self, params=None):
119119
result = self.http_instance.get(url)
120120
return result
121121

122-
def variants(self, variant_uid: str | list[str], params: dict = None):
122+
def variants(self, variant_uid: str | list[str], branch: str = None, params: dict = None):
123123
"""
124124
Fetches the variants of the content type
125-
:param variant_uid: {str} -- variant_uid
126-
:return: Entry, so you can chain this call.
125+
:param variant_uid: {str | list[str]} -- variant UID or list of variant UIDs
126+
:param branch: {str} -- optional branch name to scope the variant request
127+
:param params: {dict} -- optional query parameters
128+
:return: Variants, so you can chain this call.
127129
"""
128130
return Variants(
129131
http_instance=self.http_instance,
130132
content_type_uid=self.__content_type_uid,
131133
entry_uid=None,
132134
variant_uid=variant_uid,
135+
branch=branch,
133136
params=params,
134137
logger=None
135138
)

contentstack/entry.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,17 +267,20 @@ def _merged_response(self):
267267
return merged_response # Now correctly returns a dictionary
268268
raise ValueError(ErrorMessages.MISSING_LIVE_PREVIEW_KEYS)
269269

270-
def variants(self, variant_uid: str | list[str], params: dict = None):
270+
def variants(self, variant_uid: str | list[str], branch: str = None, params: dict = None):
271271
"""
272272
Fetches the variants of the entry
273-
:param variant_uid: {str} -- variant_uid
274-
:return: Entry, so you can chain this call.
273+
:param variant_uid: {str | list[str]} -- variant UID or list of variant UIDs
274+
:param branch: {str} -- optional branch name to scope the variant request
275+
:param params: {dict} -- optional query parameters
276+
:return: Variants, so you can chain this call.
275277
"""
276278
return Variants(
277279
http_instance=self.http_instance,
278280
content_type_uid=self.content_type_id,
279281
entry_uid=self.entry_uid,
280282
variant_uid=variant_uid,
283+
branch=branch,
281284
params=params,
282285
logger=self.logger
283286
)

contentstack/variants.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def __init__(self,
2020
content_type_uid=None,
2121
entry_uid=None,
2222
variant_uid=None,
23+
branch=None,
2324
params=None,
2425
logger=None):
2526

@@ -30,29 +31,47 @@ def __init__(self,
3031
self.content_type_id = content_type_uid
3132
self.entry_uid = entry_uid
3233
self.variant_uid = variant_uid
34+
self.branch = branch
3335
self.logger = logger or logging.getLogger(__name__)
3436
self.entry_param = params or {}
3537

38+
def _prepare_variant_headers(self):
39+
headers = self.http_instance.headers.copy()
40+
if isinstance(self.variant_uid, str):
41+
headers['x-cs-variant-uid'] = self.variant_uid
42+
elif isinstance(self.variant_uid, list):
43+
headers['x-cs-variant-uid'] = ','.join(self.variant_uid)
44+
if self.branch is not None:
45+
headers['branch'] = self.branch
46+
return headers
47+
48+
def _apply_variant_headers(self, headers):
49+
self._original_branch = self.http_instance.headers.get('branch')
50+
self.http_instance.headers.update(headers)
51+
52+
def _cleanup_variant_headers(self):
53+
self.http_instance.headers.pop('x-cs-variant-uid', None)
54+
if self.branch is not None:
55+
if self._original_branch is not None:
56+
self.http_instance.headers['branch'] = self._original_branch
57+
else:
58+
self.http_instance.headers.pop('branch', None)
59+
3660
def find(self, params=None):
3761
"""
3862
find the variants of the entry of a particular content type
3963
:param self.variant_uid: {str} -- self.variant_uid
4064
:return: Entry, so you can chain this call.
4165
"""
42-
headers = self.http_instance.headers.copy() # Create a local copy of headers
43-
if isinstance(self.variant_uid, str):
44-
headers['x-cs-variant-uid'] = self.variant_uid
45-
elif isinstance(self.variant_uid, list):
46-
headers['x-cs-variant-uid'] = ','.join(self.variant_uid)
47-
66+
headers = self._prepare_variant_headers()
4867
if params is not None:
4968
self.entry_param.update(params)
5069
encoded_params = parse.urlencode(self.entry_param)
5170
endpoint = self.http_instance.endpoint
5271
url = f'{endpoint}/content_types/{self.content_type_id}/entries?{encoded_params}'
53-
self.http_instance.headers.update(headers)
72+
self._apply_variant_headers(headers)
5473
result = self.http_instance.get(url)
55-
self.http_instance.headers.pop('x-cs-variant-uid', None)
74+
self._cleanup_variant_headers()
5675
return result
5776

5877
def fetch(self, params=None):
@@ -77,18 +96,13 @@ def fetch(self, params=None):
7796
if self.entry_uid is None:
7897
raise ValueError(ErrorMessages.ENTRY_UID_REQUIRED)
7998
else:
80-
headers = self.http_instance.headers.copy() # Create a local copy of headers
81-
if isinstance(self.variant_uid, str):
82-
headers['x-cs-variant-uid'] = self.variant_uid
83-
elif isinstance(self.variant_uid, list):
84-
headers['x-cs-variant-uid'] = ','.join(self.variant_uid)
85-
99+
headers = self._prepare_variant_headers()
86100
if params is not None:
87101
self.entry_param.update(params)
88102
encoded_params = parse.urlencode(self.entry_param)
89103
endpoint = self.http_instance.endpoint
90104
url = f'{endpoint}/content_types/{self.content_type_id}/entries/{self.entry_uid}?{encoded_params}'
91-
self.http_instance.headers.update(headers)
105+
self._apply_variant_headers(headers)
92106
result = self.http_instance.get(url)
93-
self.http_instance.headers.pop('x-cs-variant-uid', None)
107+
self._cleanup_variant_headers()
94108
return result

tests/test_entry.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
HOST = config.HOST
1010
FAQ_UID = config.FAQ_UID # Add this in your config.py
1111
VARIANT_UID = config.VARIANT_UID
12+
BRANCH = getattr(config, 'BRANCH', None)
1213

1314
class TestEntry(unittest.TestCase):
1415

@@ -318,6 +319,48 @@ def test_41_entry_variants_multiple_uids(self):
318319
result = entry.fetch()
319320
self.assertIn('variants', result['entry']['publish_details'])
320321

322+
@unittest.skipIf(BRANCH is None, "config.BRANCH is required for branch variant tests")
323+
def test_41a_entry_variants_with_branch(self):
324+
"""Test single entry variants with branch"""
325+
content_type = self.stack.content_type('faq')
326+
result = content_type.entry(FAQ_UID).variants(VARIANT_UID, BRANCH).fetch()
327+
self.assertIn('variants', result['entry']['publish_details'])
328+
329+
@unittest.skipIf(BRANCH is None, "config.BRANCH is required for branch variant tests")
330+
def test_41b_entry_variants_multiple_uids_with_branch(self):
331+
"""Test single entry variants with multiple UIDs and branch"""
332+
content_type = self.stack.content_type('faq')
333+
result = content_type.entry(FAQ_UID).variants([VARIANT_UID], BRANCH).fetch()
334+
self.assertIn('variants', result['entry']['publish_details'])
335+
336+
@unittest.skipIf(BRANCH is None, "config.BRANCH is required for branch variant tests")
337+
def test_41c_content_type_variants_find_with_branch(self):
338+
"""Test entries query variants with branch"""
339+
content_type = self.stack.content_type('faq')
340+
result = content_type.variants(VARIANT_UID, BRANCH).find()
341+
self.assertIn('variants', result['entries'][0]['publish_details'])
342+
343+
@unittest.skipIf(BRANCH is None, "config.BRANCH is required for branch variant tests")
344+
def test_41d_content_type_variants_multiple_uids_find_with_branch(self):
345+
"""Test entries query variants with multiple UIDs and branch"""
346+
content_type = self.stack.content_type('faq')
347+
result = content_type.variants([VARIANT_UID], BRANCH).find()
348+
self.assertIn('variants', result['entries'][0]['publish_details'])
349+
350+
@unittest.skipIf(BRANCH is None, "config.BRANCH is required for branch variant tests")
351+
def test_41e_content_type_variants_branch_as_keyword_arg(self):
352+
"""Test content_type.variants() with branch passed as keyword argument"""
353+
content_type = self.stack.content_type('faq')
354+
result = content_type.variants(VARIANT_UID, branch=BRANCH).find()
355+
self.assertIn('variants', result['entries'][0]['publish_details'])
356+
357+
@unittest.skipIf(BRANCH is None, "config.BRANCH is required for branch variant tests")
358+
def test_41f_content_type_variants_branch_with_params(self):
359+
"""Test content_type.variants() with branch and extra query params"""
360+
content_type = self.stack.content_type('faq')
361+
result = content_type.variants(VARIANT_UID, branch=BRANCH, params={'locale': 'en-us'}).find()
362+
self.assertIn('variants', result['entries'][0]['publish_details'])
363+
321364
def test_42_entry_environment_removal(self):
322365
"""Test entry remove_environment method"""
323366
entry = (self.stack.content_type('faq')

0 commit comments

Comments
 (0)