Skip to content

Commit

Permalink
REF: Replace custom nested attribute functions with pydash usage
Browse files Browse the repository at this point in the history
  • Loading branch information
cortadocodes committed Dec 16, 2024
1 parent d2869e0 commit a9d7888
Show file tree
Hide file tree
Showing 4 changed files with 20 additions and 49 deletions.
4 changes: 2 additions & 2 deletions octue/cloud/pub_sub/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from octue.cloud.events.handler import AbstractEventHandler
from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA
from octue.utils.decoders import OctueJSONDecoder
from octue.utils.objects import getattr_or_subscribe
from octue.utils.objects import get_nested_attribute
from octue.utils.threads import RepeatingTimer


Expand All @@ -28,7 +28,7 @@ def extract_event_and_attributes_from_pub_sub_message(message):
:return (any, dict): the extracted event and its attributes
"""
# Cast attributes to a dictionary to avoid defaultdict-like behaviour from Pub/Sub message attributes container.
attributes = dict(getattr_or_subscribe(message, "attributes"))
attributes = dict(get_nested_attribute(message, "attributes"))

# Deserialise the `parent_question_uuid`, `forward_logs`, and `retry_count`, fields if they're present
# (don't assume they are before validation).
Expand Down
6 changes: 4 additions & 2 deletions octue/mixins/filterable.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import collections.abc
import numbers

import pydash

from octue import exceptions
from octue.utils.objects import get_nested_attribute, has_nested_attribute
from octue.utils.objects import get_nested_attribute


def generate_complementary_filters(name, func):
Expand Down Expand Up @@ -208,7 +210,7 @@ def _try_equals_filter_shortcut(self, filter_name, filter_value, error):
"""
possible_attribute_name = ".".join(filter_name.split("__"))

if has_nested_attribute(self, possible_attribute_name):
if pydash.has(self, possible_attribute_name):
attribute = get_nested_attribute(self, possible_attribute_name)
filter_ = self._get_filter(attribute, "equals")
return filter_(attribute, filter_value)
Expand Down
45 changes: 11 additions & 34 deletions octue/utils/objects.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,25 @@
import functools
import itertools

import pydash

def get_nested_attribute(instance, nested_attribute_name):

def get_nested_attribute(instance, name):
"""Get the value of a nested attribute from a class instance or dictionary, with each level of nesting being
another dictionary or class instance.
:param dict|object instance:
:param str nested_attribute_names: dot-separated nested attribute name e.g. "a.b.c", "a.b", or "a"
:param str name: a dot-separated nested attribute name e.g. "a.b.c", "a.b", or "a"
:return any:
"""
nested_attribute_names = nested_attribute_name.split(".")
return functools.reduce(getattr_or_subscribe, nested_attribute_names, instance)


def has_nested_attribute(instance, nested_attribute_name):
"""Check if a class instance or dictionary has a nested attribute with the given name (each level of nesting being
another dictionary or class instance).
:param dict|object instance:
:param str nested_attribute_names: dot-separated nested attribute name e.g. "a.b.c", "a.b", or "a"
:return bool:
"""
try:
get_nested_attribute(instance, nested_attribute_name)
except AttributeError:
return False
return True

# This is a random number used instead of `None` to signal that an attribute is missing and does not just have a
# `None` value.
missing_indicator = "7027907024295393"
attribute = pydash.get(instance, name, default=missing_indicator)

def getattr_or_subscribe(instance, name):
"""Get an attribute from a class instance or a value from a dictionary.
if attribute == missing_indicator:
raise AttributeError(f"{instance!r} does not have an attribute or key named {name!r}.")

:param dict|object instance:
:param str name: name of attribute or dictionary key
:return any:
"""
try:
return getattr(instance, name)
except AttributeError:
try:
return instance[name]
except (TypeError, KeyError):
raise AttributeError(f"{instance!r} does not have an attribute or key named {name!r}.")
return attribute


def dictionary_product(keep_none_values=False, **kwargs):
Expand Down
14 changes: 3 additions & 11 deletions tests/utils/test_objects.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
from unittest import TestCase
from unittest.mock import Mock

from octue.utils.objects import dictionary_product, get_nested_attribute, getattr_or_subscribe
from octue.utils.objects import dictionary_product, get_nested_attribute


class TestObjects(TestCase):
def test_getattr_or_subscribe_with_dictionary(self):
"""Test that the getattr_or_subscribe function can get values from a dictionary."""
self.assertEqual(getattr_or_subscribe(instance={"hello": "world"}, name="hello"), "world")

def test_getattr_or_subscribe_with_object(self):
"""Test that the getattr_or_subscribe function can get attribute values from a class instance."""
self.assertEqual(getattr_or_subscribe(instance=Mock(hello="world"), name="hello"), "world")

def test_get_nested_attribute(self):
"""Test that nested attributes can be accessed."""
inner_mock = Mock(b=3)
outer_mock = Mock(a=inner_mock)
self.assertEqual(get_nested_attribute(instance=outer_mock, nested_attribute_name="a.b"), 3)
self.assertEqual(get_nested_attribute(instance=outer_mock, name="a.b"), 3)

def test_get_nested_dictionary_attribute(self):
"""Test that nested attributes ending in a dictionary key can be accessed."""
inner_mock = Mock(b={"hello": "world"})
outer_mock = Mock(a=inner_mock)
self.assertEqual(get_nested_attribute(instance=outer_mock, nested_attribute_name="a.b.hello"), "world")
self.assertEqual(get_nested_attribute(instance=outer_mock, name="a.b.hello"), "world")


class TestDictionaryProduct(TestCase):
Expand Down

0 comments on commit a9d7888

Please sign in to comment.