From 2d015dd17ff2c775ad8f23d19c5a67da514314b2 Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Tue, 15 Jan 2019 20:13:28 -0600 Subject: [PATCH] Fixed handling of characters that have no uppercase (#3403) --- holoviews/core/tree.py | 8 +++++--- holoviews/core/util.py | 13 +++++++++++++ holoviews/tests/core/testlayouts.py | 17 +++++++++++++++-- holoviews/tests/core/testutils.py | 18 +++++++++++++++++- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/holoviews/core/tree.py b/holoviews/core/tree.py index 27568567c3..5464e74c0e 100644 --- a/holoviews/core/tree.py +++ b/holoviews/core/tree.py @@ -218,12 +218,13 @@ def __delitem__(self, identifier): def __setattr__(self, identifier, val): # Getattr is skipped for root and first set of children shallow = (self.parent is None or self.parent.parent is None) - if identifier[0].isupper() and self.fixed and shallow: + + if util.tree_attribute(identifier) and self.fixed and shallow: raise AttributeError(self._fixed_error % identifier) super(AttrTree, self).__setattr__(identifier, val) - if identifier[0].isupper(): + if util.tree_attribute(identifier): if not identifier in self.children: self.children.append(identifier) self._propagate((identifier,), val) @@ -254,7 +255,8 @@ def __getattr__(self, identifier): if sanitized in self.children: return self.__dict__[sanitized] - if not sanitized.startswith('_') and identifier[0].isupper(): + + if not sanitized.startswith('_') and util.tree_attribute(identifier): self.children.append(sanitized) dir_mode = self.__dict__['_dir_mode'] child_tree = self.__class__(identifier=sanitized, diff --git a/holoviews/core/util.py b/holoviews/core/util.py index c61eb5b7a4..cba7e7fd63 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -338,6 +338,19 @@ def deephash(obj): return None +def tree_attribute(identifier): + """ + Predicate that returns True for custom attributes added to AttrTrees + that are not methods, properties or internal attributes. + + These custom attributes start with a capitalized character when + applicable (not applicable to underscore or certain unicode characters) + """ + if identifier[0].upper().isupper() is False and identifier[0] != '_': + return True + else: + return identifier[0].isupper() + def argspec(callable_obj): """ Returns an ArgSpec object for functions, staticmethods, instance diff --git a/holoviews/tests/core/testlayouts.py b/holoviews/tests/core/testlayouts.py index ac57222715..3a26071035 100644 --- a/holoviews/tests/core/testlayouts.py +++ b/holoviews/tests/core/testlayouts.py @@ -1,7 +1,13 @@ +# -*- coding: utf-8 -*- +""" +Tests of Layout and related classes +""" + +import sys from holoviews import AdjointLayout, NdLayout, GridSpace, Layout, Element, HoloMap, Overlay -from holoviews.element import HLine +from holoviews.element import HLine, Curve from holoviews.element.comparison import ComparisonTestCase - +from unittest import SkipTest class CompositeTest(ComparisonTestCase): "For testing of basic composite element types" @@ -19,6 +25,13 @@ def setUp(self): def test_add_operator(self): self.assertEqual(type(self.view1 + self.view2), Layout) + def test_add_unicode_py3(self): + "Test to avoid regression of #3403 where unicode characters don't capitalize" + if sys.version_info.major == 2: raise SkipTest + layout = Curve([-1,-2,-3]) + Curve([1,2,3]) .relabel('𝜗_1 vs th_2') + elements = list(layout) + self.assertEqual(len(elements), 2) + class AdjointLayoutTest(CompositeTest): diff --git a/holoviews/tests/core/testutils.py b/holoviews/tests/core/testutils.py index 097a1dceb3..b1e2b37995 100644 --- a/holoviews/tests/core/testutils.py +++ b/holoviews/tests/core/testutils.py @@ -19,7 +19,7 @@ sanitize_identifier_fn, find_range, max_range, wrap_tuple_streams, deephash, merge_dimensions, get_path, make_path_unique, compute_density, date_range, dt_to_int, compute_edges, isfinite, cross_index, closest_match, - dimension_range + dimension_range, tree_attribute ) from holoviews import Dimension, Element from holoviews.streams import PointerXY @@ -191,6 +191,22 @@ def test_prefix_test3_py3(self): self.assertEqual(prefixed, True) +class TestTreeAttribute(ComparisonTestCase): + + def test_simple_lowercase_string(self): + self.assertEqual(tree_attribute('lowercase'), False) + + def test_simple_uppercase_string(self): + self.assertEqual(tree_attribute('UPPERCASE'), True) + + def test_unicode_string(self): + if py_version != 2: raise SkipTest + self.assertEqual(tree_attribute('𝜗unicode'), True) + + def test_underscore_string(self): + self.assertEqual(tree_attribute('_underscore'), False) + + class TestSanitizationPy2(ComparisonTestCase): """ Tests of sanitize_identifier (Python 2)