From f288ac4a1b3a83c5e4da2b80f452ccb9b93c1f0e Mon Sep 17 00:00:00 2001 From: Lucas Zampieri Date: Fri, 5 Sep 2025 14:33:18 +0100 Subject: [PATCH 1/3] feat: add display name field mapping for JIRA custom fields Enable access to custom fields using readable display names in addition to field IDs. Users can access fields like issue.fields.epic_link alongside issue.fields.customfield_10001. - Add display name fields during JSON parsing in dict2resource() - Convert display names to valid Python identifiers (e.g. 'Epic Link' -> 'epic_link') - Only create mappings for fields present in API response - Maintain full backwards compatibility with existing field ID access Signed-off-by: Lucas Zampieri --- jira/client.py | 3 +++ jira/resilientsession.py | 1 + jira/resources.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/jira/client.py b/jira/client.py index b23aff2de..2328774c5 100644 --- a/jira/client.py +++ b/jira/client.py @@ -674,6 +674,9 @@ def __init__( self._fields_cache_value: dict[str, str] = {} # access via self._fields_cache + # Store fields cache reference on session for display name field injection + self._session.fields_cache = self._fields_cache + @property def _fields_cache(self) -> dict[str, str]: """Cached dictionary of {Field Name: Field ID}. Lazy loaded.""" diff --git a/jira/resilientsession.py b/jira/resilientsession.py index 39ce7643d..c551795c4 100644 --- a/jira/resilientsession.py +++ b/jira/resilientsession.py @@ -160,6 +160,7 @@ def __init__(self, timeout=None, max_retries: int = 3, max_retry_delay: int = 60 self.timeout = timeout self.max_retries = max_retries self.max_retry_delay = max_retry_delay + self.fields_cache: dict[str, str] = {} super().__init__() # Indicate our preference for JSON to avoid https://bitbucket.org/bspeakmon/jira-python/issue/46 and https://jira.atlassian.com/browse/JRA-38551 diff --git a/jira/resources.py b/jira/resources.py index 7127e2402..2209c9649 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -1681,8 +1681,44 @@ def dict2resource( setattr(top, i, seq_list) else: setattr(top, i, j) + + if session and hasattr(session, 'fields_cache') and session.fields_cache: + _add_display_name_fields(top, session) + return top +def convert_display_name_to_python_name(display_name: str) -> str: + """Convert JIRA field display name to Python attribute name. + + Args: + display_name: JIRA field display name (e.g., "Epic Link", "Story Points") + + Returns: + Python-compatible attribute name (e.g., "epic_link", "story_points") + """ + python_name = re.sub(r'[^a-zA-Z0-9_]', '_', display_name.lower()) + python_name = re.sub(r'_+', '_', python_name).strip('_') + if python_name and python_name[0].isdigit(): + python_name = 'field_' + python_name + return python_name + +def _add_display_name_fields(obj: PropertyHolder, session) -> None: + """Create readable field name aliases for JIRA custom fields. + + Adds attributes like 'epic_link' alongside 'customfield_10001' + """ + custom_fields = [attr for attr in dir(obj) if attr.startswith('customfield_')] + if not custom_fields: + return + + for display_name, field_id in session.fields_cache.items(): + if field_id in set(custom_fields): + python_name = convert_display_name_to_python_name(display_name) + + if not hasattr(obj, python_name): + field_value = getattr(obj, field_id) + setattr(obj, python_name, field_value) + resource_class_map: dict[str, type[Resource]] = { # Jira-specific resources From 5f469748283105399304d919bbe39f5aa5f79903 Mon Sep 17 00:00:00 2001 From: Lucas Zampieri Date: Mon, 8 Sep 2025 10:43:19 +0100 Subject: [PATCH 2/3] test: add comprehensive tests for display name field mapping Add test coverage for display name field mapping functionality: - Unit tests for field name conversion logic - Integration tests with real JIRA data - Mock tests for controlled edge case validation - Issue-specific behavior testing - Backwards compatibility verification - Field equivalence validation Tests cover: - Basic field name conversion (Epic Link -> epic_link) - Special character handling and numeric prefixes - Name collision prevention and duplicate field detection - Empty/None value handling and missing field cache scenarios - Performance impact validation Signed-off-by: Lucas Zampieri --- tests/resources/test_issue_display_names.py | 161 +++++++++++++++++ tests/test_display_name_fields.py | 188 ++++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 tests/resources/test_issue_display_names.py create mode 100644 tests/test_display_name_fields.py diff --git a/tests/resources/test_issue_display_names.py b/tests/resources/test_issue_display_names.py new file mode 100644 index 000000000..07d48aef2 --- /dev/null +++ b/tests/resources/test_issue_display_names.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +from jira.resources import convert_display_name_to_python_name +from tests.conftest import JiraTestCase + + +class IssueDisplayNameFieldsTest(JiraTestCase): + def setUp(self) -> None: + super().setUp() + self.issue_1 = self.test_manager.project_b_issue1 + self.issue_1_obj = self.test_manager.project_b_issue1_obj + + def test_issue_has_display_name_fields(self): + issue = self.issue_1_obj + all_attrs = [attr for attr in dir(issue.fields) if not attr.startswith('__')] + custom_field_ids = [attr for attr in all_attrs if attr.startswith('customfield_')] + + self.assertGreater(len(custom_field_ids), 0) + + standard_fields = ['summary', 'status', 'priority', 'created'] + for field in standard_fields: + self.assertIn(field, all_attrs) + + expected_minimum = len(custom_field_ids) + len(standard_fields) + self.assertGreater(len(all_attrs), expected_minimum) + + def test_issue_field_access_patterns(self): + issue = self.issue_1_obj + + self.assertIsNotNone(issue.fields.summary) + self.assertIsNotNone(issue.fields.status) + + custom_fields = [attr for attr in dir(issue.fields) if attr.startswith('customfield_')] + if custom_fields: + getattr(issue.fields, custom_fields[0]) + + all_fields = dir(issue.fields) + self.assertIsInstance(all_fields, list) + self.assertGreater(len(all_fields), 10) + + def test_issue_field_equivalence_real_data(self): + issue = self.issue_1_obj + + if not hasattr(self.jira, '_fields_cache') or not self.jira._fields_cache: + self.skipTest("JIRA instance doesn't have fields cache populated") + + fields_cache = self.jira._fields_cache + tested_pairs = 0 + + for display_name, field_id in fields_cache.items(): + if tested_pairs >= 3: + break + + if hasattr(issue.fields, field_id): + python_name = convert_display_name_to_python_name(display_name) + + if hasattr(issue.fields, python_name): + original_value = getattr(issue.fields, field_id) + display_value = getattr(issue.fields, python_name) + self.assertEqual(original_value, display_value) + tested_pairs += 1 + + if tested_pairs == 0: + self.skipTest("No suitable field pairs found for equivalence testing") + + def test_issue_custom_field_values_preserved(self): + issue = self.issue_1_obj + + custom_fields_with_values = [] + for attr in dir(issue.fields): + if attr.startswith('customfield_'): + value = getattr(issue.fields, attr, None) + if value is not None: + custom_fields_with_values.append((attr, value)) + + self.assertGreater(len(custom_fields_with_values), 0) + + fields_cache = getattr(self.jira, '_fields_cache', {}) + + for field_id, original_value in custom_fields_with_values[:3]: + display_name = None + for name, fid in fields_cache.items(): + if fid == field_id: + display_name = name + break + + if display_name: + python_name = convert_display_name_to_python_name(display_name) + + if hasattr(issue.fields, python_name): + display_value = getattr(issue.fields, python_name) + self.assertIs( + original_value, display_value, + f"Values should be the same object: {field_id} vs {python_name}" + ) + + def test_issue_fields_dir_includes_display_names(self): + issue = self.issue_1_obj + all_attrs = dir(issue.fields) + + standard_fields = ['summary', 'status', 'priority', 'issuetype'] + for field in standard_fields: + self.assertIn(field, all_attrs) + + custom_field_ids = [attr for attr in all_attrs if attr.startswith('customfield_')] + self.assertGreater(len(custom_field_ids), 0) + + standard_and_custom = set(standard_fields + custom_field_ids + + ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', + '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', + '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', + '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', + '__str__', '__subclasshook__', '__weakref__', '_issue_session', + 'aggregateprogress', 'aggregatetimeestimate', 'aggregatetimeoriginalestimate', + 'aggregatetimespent', 'archivedby', 'archiveddate', 'assignee', 'attachment', + 'comment', 'components', 'created', 'creator', 'description', 'duedate', + 'environment', 'fixVersions', 'issuelinks', 'labels', 'lastViewed', + 'progress', 'project', 'reporter', 'resolution', 'resolutiondate', + 'security', 'subtasks', 'timeestimate', 'timeoriginalestimate', + 'timespent', 'timetracking', 'updated', 'versions', 'votes', + 'watches', 'worklog', 'workratio']) + + display_name_fields = [attr for attr in all_attrs if attr not in standard_and_custom] + self.assertGreater(len(display_name_fields), 0) + + def test_issue_creation_with_display_names(self): + fresh_issue = self.jira.issue(self.issue_1) + + all_attrs = dir(fresh_issue.fields) + custom_fields = [attr for attr in all_attrs if attr.startswith('customfield_')] + + expected_display_names = len(custom_fields) > 0 + + if expected_display_names: + standard_and_meta_fields = { + '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', + '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', + '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', + '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', + '__str__', '__subclasshook__', '__weakref__', '_issue_session', + 'aggregateprogress', 'aggregatetimeestimate', 'aggregatetimeoriginalestimate', + 'aggregatetimespent', 'archivedby', 'archiveddate', 'assignee', 'attachment', + 'comment', 'components', 'created', 'creator', 'description', 'duedate', + 'environment', 'fixVersions', 'issuelinks', 'issuetype', 'labels', 'lastViewed', + 'priority', 'progress', 'project', 'reporter', 'resolution', 'resolutiondate', + 'security', 'status', 'subtasks', 'summary', 'timeestimate', + 'timeoriginalestimate', 'timespent', 'timetracking', 'updated', + 'versions', 'votes', 'watches', 'worklog', 'workratio' + } + + potential_display_names = [ + attr for attr in all_attrs + if attr not in standard_and_meta_fields and not attr.startswith('customfield_') + ] + + self.assertGreater(len(potential_display_names), 0) + + +if __name__ == '__main__': + import unittest + unittest.main() diff --git a/tests/test_display_name_fields.py b/tests/test_display_name_fields.py new file mode 100644 index 000000000..65adeb919 --- /dev/null +++ b/tests/test_display_name_fields.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import unittest +from unittest.mock import MagicMock + +from jira.resources import ( + PropertyHolder, + _add_display_name_fields, + convert_display_name_to_python_name, +) +from tests.conftest import JiraTestCase + + +class DisplayNameFieldConversionTest(unittest.TestCase): + def test_basic_field_name_conversion(self): + self.assertEqual(convert_display_name_to_python_name("Epic Link"), "epic_link") + self.assertEqual(convert_display_name_to_python_name("Story Points"), "story_points") + self.assertEqual(convert_display_name_to_python_name("Internal Target Milestone"), "internal_target_milestone") + + def test_special_character_handling(self): + self.assertEqual(convert_display_name_to_python_name("Epic-Link"), "epic_link") + self.assertEqual(convert_display_name_to_python_name("Epic Link---Test"), "epic_link_test") + self.assertEqual(convert_display_name_to_python_name("Field!!Name@@Here"), "field_name_here") + self.assertEqual(convert_display_name_to_python_name("-Epic Link-"), "epic_link") + self.assertEqual(convert_display_name_to_python_name("__Field Name__"), "field_name") + + def test_numeric_field_names(self): + self.assertEqual(convert_display_name_to_python_name("10 Point Scale"), "field_10_point_scale") + self.assertEqual(convert_display_name_to_python_name("3rd Party Integration"), "field_3rd_party_integration") + self.assertEqual(convert_display_name_to_python_name("2023 Budget"), "field_2023_budget") + + def test_edge_cases(self): + self.assertEqual(convert_display_name_to_python_name("A"), "a") + self.assertEqual(convert_display_name_to_python_name("1"), "field_1") + self.assertEqual(convert_display_name_to_python_name("epic_link"), "epic_link") + self.assertEqual(convert_display_name_to_python_name("STORY_POINTS"), "story_points") + self.assertEqual(convert_display_name_to_python_name("CamelCaseField"), "camelcasefield") + + +class DisplayNameFieldIntegrationTest(JiraTestCase): + def setUp(self): + super().setUp() + self.issue_1 = self.test_manager.project_b_issue1 + self.issue_1_obj = self.test_manager.project_b_issue1_obj + + def test_display_name_field_creation(self): + issue = self.issue_1_obj + all_fields = dir(issue.fields) + custom_field_ids = [f for f in all_fields if f.startswith('customfield_')] + + self.assertGreater(len(custom_field_ids), 0) + total_fields = len([f for f in all_fields if not f.startswith('__')]) + self.assertGreater(total_fields, len(custom_field_ids) + 20) + + def test_field_equivalence(self): + issue = self.issue_1_obj + + if hasattr(self.jira, '_fields_cache') and self.jira._fields_cache: + fields_cache = self.jira._fields_cache + tested_equivalence = False + + for display_name, field_id in list(fields_cache.items())[:5]: + if hasattr(issue.fields, field_id): + python_name = convert_display_name_to_python_name(display_name) + + if hasattr(issue.fields, python_name): + original_value = getattr(issue.fields, field_id) + display_value = getattr(issue.fields, python_name) + self.assertEqual(original_value, display_value) + tested_equivalence = True + break + + if not tested_equivalence: + self.skipTest("No suitable fields found for equivalence testing") + + def test_backwards_compatibility(self): + issue = self.issue_1_obj + + standard_fields = ['summary', 'status', 'priority', 'created', 'updated'] + for field_name in standard_fields: + self.assertTrue(hasattr(issue.fields, field_name)) + value = getattr(issue.fields, field_name) + self.assertIsNotNone(value) + + custom_fields = [attr for attr in dir(issue.fields) if attr.startswith('customfield_')] + self.assertGreater(len(custom_fields), 0) + + for field_id in custom_fields[:3]: + getattr(issue.fields, field_id) + + +class DisplayNameFieldMockTest(unittest.TestCase): + def _create_mock_property_holder(self, field_data: dict) -> PropertyHolder: + obj = PropertyHolder() + for field_name, field_value in field_data.items(): + setattr(obj, field_name, field_value) + return obj + + def _create_mock_session(self, fields_cache: dict) -> MagicMock: + session = MagicMock() + session.fields_cache = fields_cache + return session + + def test_display_name_creation_with_mock_data(self): + mock_fields = { + 'customfield_10001': 'EPIC-123', + 'customfield_10002': 42, + 'customfield_10003': ['label1', 'label2'], + 'summary': 'Test Issue' + } + + mock_cache = { + 'Epic Link': 'customfield_10001', + 'Story Points': 'customfield_10002', + 'Labels': 'customfield_10003' + } + + obj = self._create_mock_property_holder(mock_fields) + session = self._create_mock_session(mock_cache) + + _add_display_name_fields(obj, session) + + self.assertTrue(hasattr(obj, 'epic_link')) + self.assertTrue(hasattr(obj, 'story_points')) + self.assertTrue(hasattr(obj, 'labels')) + + self.assertEqual(obj.epic_link, 'EPIC-123') + self.assertEqual(obj.story_points, 42) + self.assertEqual(obj.labels, ['label1', 'label2']) + + self.assertEqual(obj.customfield_10001, 'EPIC-123') + self.assertEqual(obj.customfield_10002, 42) + self.assertEqual(obj.customfield_10003, ['label1', 'label2']) + + def test_no_custom_fields(self): + obj = self._create_mock_property_holder({ + 'summary': 'Test Issue', + 'status': 'Open', + 'priority': 'High' + }) + session = self._create_mock_session({'Epic Link': 'customfield_10001'}) + + initial_attrs = set(dir(obj)) + _add_display_name_fields(obj, session) + final_attrs = set(dir(obj)) + + self.assertEqual(initial_attrs, final_attrs) + + def test_name_collision_prevention(self): + obj = self._create_mock_property_holder({ + 'customfield_10001': 'epic-value', + 'summary': 'Original Summary' + }) + + session = self._create_mock_session({ + 'Summary': 'customfield_10001' + }) + + _add_display_name_fields(obj, session) + + self.assertEqual(obj.summary, 'Original Summary') + self.assertNotEqual(obj.summary, 'epic-value') + + def test_none_and_empty_values(self): + obj = self._create_mock_property_holder({ + 'customfield_10001': None, + 'customfield_10002': '', + 'customfield_10003': [] + }) + session = self._create_mock_session({ + 'Epic Link': 'customfield_10001', + 'Summary': 'customfield_10002', + 'Labels': 'customfield_10003' + }) + + _add_display_name_fields(obj, session) + + self.assertTrue(hasattr(obj, 'epic_link')) + self.assertTrue(hasattr(obj, 'summary')) + self.assertTrue(hasattr(obj, 'labels')) + + self.assertIsNone(obj.epic_link) + self.assertEqual(obj.summary, '') + self.assertEqual(obj.labels, []) + + +if __name__ == '__main__': + unittest.main() From 6a6c0eba6bbac7aa9bfcf07d1c73bf4cc1036b9c Mon Sep 17 00:00:00 2001 From: Lucas Zampieri Date: Mon, 15 Sep 2025 10:09:33 +0100 Subject: [PATCH 3/3] docs: add display name field mapping examples and replace deprecated epic_link - Add custom field display name mapping section to docs/examples.rst - Replace deprecated epic_link examples with story_points - Update test cases to use current non-deprecated field examples Signed-off-by: Lucas Zampieri --- docs/examples.rst | 22 +++++++++++++++++++++ jira/resources.py | 6 +++--- tests/test_display_name_fields.py | 32 +++++++++++++++---------------- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index d4e6e1cfa..6de22e5a5 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -291,6 +291,28 @@ content needs to be formatted using the Atlassian Document Format (ADF):: Fields ------ +Custom Field Display Name Mapping +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Access custom fields using display names instead of ``customfield_XXXX`` IDs:: + + # Both return the same value + issue.fields.customfield_10001 # traditional + issue.fields.story_points # display name + + # Example usage + if hasattr(issue.fields, 'story_points'): + print(f"Points: {issue.fields.story_points}") + +Field names are converted to Python identifiers:: + + # "Story Points" -> story_points + # "Epic Link" -> epic_link + # "3rd Party" -> field_3rd_party + +.. note:: + Display name fields won't overwrite existing attributes. + Example for accessing the worklogs:: issue.fields.worklog.worklogs # list of Worklog objects diff --git a/jira/resources.py b/jira/resources.py index 2209c9649..c6ac2c917 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -1691,10 +1691,10 @@ def convert_display_name_to_python_name(display_name: str) -> str: """Convert JIRA field display name to Python attribute name. Args: - display_name: JIRA field display name (e.g., "Epic Link", "Story Points") + display_name: JIRA field display name (e.g., "Story Points", "Sprint") Returns: - Python-compatible attribute name (e.g., "epic_link", "story_points") + Python-compatible attribute name (e.g., "story_points", "sprint") """ python_name = re.sub(r'[^a-zA-Z0-9_]', '_', display_name.lower()) python_name = re.sub(r'_+', '_', python_name).strip('_') @@ -1705,7 +1705,7 @@ def convert_display_name_to_python_name(display_name: str) -> str: def _add_display_name_fields(obj: PropertyHolder, session) -> None: """Create readable field name aliases for JIRA custom fields. - Adds attributes like 'epic_link' alongside 'customfield_10001' + Adds attributes like 'story_points' alongside 'customfield_10001' """ custom_fields = [attr for attr in dir(obj) if attr.startswith('customfield_')] if not custom_fields: diff --git a/tests/test_display_name_fields.py b/tests/test_display_name_fields.py index 65adeb919..2f8bfd79d 100644 --- a/tests/test_display_name_fields.py +++ b/tests/test_display_name_fields.py @@ -13,15 +13,15 @@ class DisplayNameFieldConversionTest(unittest.TestCase): def test_basic_field_name_conversion(self): - self.assertEqual(convert_display_name_to_python_name("Epic Link"), "epic_link") self.assertEqual(convert_display_name_to_python_name("Story Points"), "story_points") self.assertEqual(convert_display_name_to_python_name("Internal Target Milestone"), "internal_target_milestone") + self.assertEqual(convert_display_name_to_python_name("Epic Link"), "epic_link") def test_special_character_handling(self): - self.assertEqual(convert_display_name_to_python_name("Epic-Link"), "epic_link") - self.assertEqual(convert_display_name_to_python_name("Epic Link---Test"), "epic_link_test") + self.assertEqual(convert_display_name_to_python_name("Story-Points"), "story_points") + self.assertEqual(convert_display_name_to_python_name("Business Value---Score"), "business_value_score") self.assertEqual(convert_display_name_to_python_name("Field!!Name@@Here"), "field_name_here") - self.assertEqual(convert_display_name_to_python_name("-Epic Link-"), "epic_link") + self.assertEqual(convert_display_name_to_python_name("-Story Points-"), "story_points") self.assertEqual(convert_display_name_to_python_name("__Field Name__"), "field_name") def test_numeric_field_names(self): @@ -32,7 +32,7 @@ def test_numeric_field_names(self): def test_edge_cases(self): self.assertEqual(convert_display_name_to_python_name("A"), "a") self.assertEqual(convert_display_name_to_python_name("1"), "field_1") - self.assertEqual(convert_display_name_to_python_name("epic_link"), "epic_link") + self.assertEqual(convert_display_name_to_python_name("story_points"), "story_points") self.assertEqual(convert_display_name_to_python_name("STORY_POINTS"), "story_points") self.assertEqual(convert_display_name_to_python_name("CamelCaseField"), "camelcasefield") @@ -103,15 +103,15 @@ def _create_mock_session(self, fields_cache: dict) -> MagicMock: def test_display_name_creation_with_mock_data(self): mock_fields = { - 'customfield_10001': 'EPIC-123', + 'customfield_10001': 5, 'customfield_10002': 42, 'customfield_10003': ['label1', 'label2'], 'summary': 'Test Issue' } mock_cache = { - 'Epic Link': 'customfield_10001', - 'Story Points': 'customfield_10002', + 'Story Points': 'customfield_10001', + 'Sprint': 'customfield_10002', 'Labels': 'customfield_10003' } @@ -120,15 +120,15 @@ def test_display_name_creation_with_mock_data(self): _add_display_name_fields(obj, session) - self.assertTrue(hasattr(obj, 'epic_link')) self.assertTrue(hasattr(obj, 'story_points')) + self.assertTrue(hasattr(obj, 'sprint')) self.assertTrue(hasattr(obj, 'labels')) - self.assertEqual(obj.epic_link, 'EPIC-123') - self.assertEqual(obj.story_points, 42) + self.assertEqual(obj.story_points, 5) + self.assertEqual(obj.sprint, 42) self.assertEqual(obj.labels, ['label1', 'label2']) - self.assertEqual(obj.customfield_10001, 'EPIC-123') + self.assertEqual(obj.customfield_10001, 5) self.assertEqual(obj.customfield_10002, 42) self.assertEqual(obj.customfield_10003, ['label1', 'label2']) @@ -138,7 +138,7 @@ def test_no_custom_fields(self): 'status': 'Open', 'priority': 'High' }) - session = self._create_mock_session({'Epic Link': 'customfield_10001'}) + session = self._create_mock_session({'Story Points': 'customfield_10001'}) initial_attrs = set(dir(obj)) _add_display_name_fields(obj, session) @@ -168,18 +168,18 @@ def test_none_and_empty_values(self): 'customfield_10003': [] }) session = self._create_mock_session({ - 'Epic Link': 'customfield_10001', + 'Story Points': 'customfield_10001', 'Summary': 'customfield_10002', 'Labels': 'customfield_10003' }) _add_display_name_fields(obj, session) - self.assertTrue(hasattr(obj, 'epic_link')) + self.assertTrue(hasattr(obj, 'story_points')) self.assertTrue(hasattr(obj, 'summary')) self.assertTrue(hasattr(obj, 'labels')) - self.assertIsNone(obj.epic_link) + self.assertIsNone(obj.story_points) self.assertEqual(obj.summary, '') self.assertEqual(obj.labels, [])