Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

748: handle obj.get(k, default) call pattern for backward compatibility #755

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions pyxform/survey_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
import re
import warnings
from collections.abc import Callable, Generator, Iterable, Mapping
from itertools import chain
from typing import TYPE_CHECKING, Optional
Expand Down Expand Up @@ -35,6 +36,7 @@
)
SURVEY_ELEMENT_EXTRA_FIELDS = ("_survey_element_xpath",)
SURVEY_ELEMENT_SLOTS = (*SURVEY_ELEMENT_FIELDS, *SURVEY_ELEMENT_EXTRA_FIELDS)
_GET_SENTINEL = object()


class SurveyElement(Mapping):
Expand All @@ -53,6 +55,22 @@ def __hash__(self):
def __getitem__(self, key):
return self.__getattribute__(key)

def get(self, key, default=_GET_SENTINEL):
try:
return self.__getattribute__(key)
except AttributeError:
# Sentinel used rather than None since caller may write `default=None`.
if default is _GET_SENTINEL:
raise
warnings.warn(
"The `obj.get(key, default)` usage pattern will be removed in a future "
"version of pyxform. Please check the object type to ensure the "
"attribute will exist, or use `hasattr(obj, key, default)` instead.",
DeprecationWarning,
stacklevel=2, # level 1 = here, 2 = caller.
)
return default

@staticmethod
def get_slot_names() -> tuple[str, ...]:
"""Each subclass must provide a list of slots from itself and all parents."""
Expand Down
67 changes: 67 additions & 0 deletions tests/test_survey_element.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import warnings
from unittest import TestCase

from pyxform.survey_element import SurveyElement


class TestSurveyElementMappingBehaviour(TestCase):
def tearDown(self):
# Undo the warnings filter set in the below test.
warnings.resetwarnings()

def test_get_call_patterns_equivalent_to_base_dict(self):
"""Should find that, except for deprecated usage, SurveyElement is dict-like."""
# To demonstrate how dict normally works using same test cases.
_dict = {"name": "test", "label": None}
# getattr
self.assertEqual("default", getattr(_dict, "foo", "default"))
# defined key, no default
self.assertEqual("test", _dict.get("name"))
# defined key, with default
self.assertEqual("test", _dict.get("name", "default"))
# defined key, with None value
self.assertEqual(None, _dict.get("label"))
# defined key, with None value, with default
self.assertEqual(None, _dict.get("label", "default"))
# undefined key, with default
self.assertEqual("default", _dict.get("foo", "default"))
# undefined key, with default None
self.assertEqual(None, _dict.get("foo", None))
# other access patterns for undefined key
self.assertEqual(None, _dict.get("foo"))
with self.assertRaises(AttributeError):
_ = _dict.foo
with self.assertRaises(KeyError):
_ = _dict["foo"]

elem = SurveyElement(name="test")
# getattr
self.assertEqual("default", getattr(elem, "foo", "default"))
# defined key, no default
self.assertEqual("test", elem.get("name"))
# defined key, with default
self.assertEqual("test", elem.get("name", "default"))
# defined key, with None value
self.assertEqual(None, elem.get("label"))
# defined key, with None value, with default
self.assertEqual(None, elem.get("label", "default"))
# undefined key, with default
with self.assertWarns(DeprecationWarning) as warned:
self.assertEqual("default", elem.get("foo", "default"))
# Warning points to caller, rather than survey_element or collections.abc.
self.assertEqual(__file__, warned.filename)
# undefined key, with default None
with self.assertWarns(DeprecationWarning) as warned:
self.assertEqual(None, elem.get("foo", None))
# Callers can disable warnings at module-level.
warnings.simplefilter("ignore", DeprecationWarning)
with warnings.catch_warnings(record=True) as warned:
elem.get("foo", "default")
self.assertEqual(0, len(warned))
# other access patterns for undefined key
with self.assertRaises(AttributeError):
_ = elem.get("foo")
with self.assertRaises(AttributeError):
_ = elem.foo
with self.assertRaises(AttributeError):
_ = elem["foo"]