From bef21515d98bb24ebc6071d466babcf2a75ffe2b Mon Sep 17 00:00:00 2001 From: Tom Keffer Date: Sun, 18 Oct 2020 05:52:57 -0700 Subject: [PATCH] Squashed commit of the following: commit bdae391f4cc0b16eb2914b3b077e8c32027e40e9 Author: Tom Keffer Date: Sun Oct 18 05:44:29 2020 -0700 Ignore attributes 'mro', 'im_func', 'func_code', '__func__', '__code__', '__init__', '__self__' commit 81d901f2227682dac19ab739c6c0b6c6c7d3221f Author: Tom Keffer Date: Thu Sep 24 17:47:15 2020 -0700 Pass on the option_dict. Get rid of no longer used aggregation_interval. Generalize __call__() commit 9020b6a14da717aae5f791bdb6d9a6f5a132695e Author: Tom Keffer Date: Tue Sep 22 03:52:24 2020 -0700 __call__ returns new instance of AggTypeBinder, rather than modify self commit 8f4bfb9ac3d1b0cb7b07650fa87661351095e21e Author: Tom Keffer Date: Fri Sep 18 15:25:42 2020 -0700 Introduced class AggTypeBinder. In anticipation of allowing tags that return JSON structures. --- bin/weewx/cheetahgenerator.py | 45 ++++++++--- bin/weewx/tags.py | 135 ++++++++++++++++++++------------ bin/weewx/tests/test_cheetah.py | 11 +++ 3 files changed, 132 insertions(+), 59 deletions(-) diff --git a/bin/weewx/cheetahgenerator.py b/bin/weewx/cheetahgenerator.py index 3129ecd5..a906af2f 100644 --- a/bin/weewx/cheetahgenerator.py +++ b/bin/weewx/cheetahgenerator.py @@ -617,17 +617,42 @@ class AssureUnicode(Cheetah.Filters.Filter): """Assures that whatever a search list extension might return, it will be converted into Unicode. """ - def filter(self, val, **dummy_kw): + def filter(self, val, **kwargs): + """Convert the expression 'val' to unicode.""" + # There is a 2x4 matrix of possibilities: + # input PY2 PY3 + # _____ ________ _______ + # bytes decode() decode() + # str decode() -done- + # unicode -done- N/A + # object str().decode() str() if val is None: - filtered = u'' + return u'' + + # Is it already unicode? This takes care of cells 4 and 5. + if isinstance(val, six.text_type): + filtered = val + # This conditional covers cells 1,2, and 3. That is, val is a byte string + elif isinstance(val, six.binary_type): + filtered = val.decode('utf-8') + # That leaves cells 7 and 8, that is val is an object, such as a ValueHelper else: + # Must be an object. Convert to string try: - # Assume val is some kind of string. Try coercing it directly into unicode - filtered = six.ensure_text(val, encoding='utf-8') - except TypeError: - # It's not Unicode nor a byte-string. Must be a primitive or a ValueHelper. - # Under Python 2, six.text_type is equivalent to calling unicode(val). - # Under Python 3, it is equivalent to calling str(val). - # So, the results always end up as Unicode. - filtered = six.text_type(val) + # For late tag bindings, the following forces the invocation of __str__(), + # which can force an XTypes query. For a tag such as $day.foobar.min, where + # 'foobar' is an unknown type, this will cause an attribute error. Be prepared + # to catch it. + filtered = str(val) + except AttributeError as e: + # Offer a debug message. + log.debug("Unrecognized: %s", kwargs.get('rawExpr', e)) + # Return the raw expression, if available. Otherwise, the exception message + # concatenated with a question mark. + filtered = kwargs.get('rawExpr', str(e) + '?') + if six.PY2: + # For Python 2, we have to take an additional step of converting from + # type 'str', which is a byte string, to a unicode string + filtered = filtered.decode('utf-8') + return filtered diff --git a/bin/weewx/tags.py b/bin/weewx/tags.py index 3ae72dc2..a8f704c3 100644 --- a/bin/weewx/tags.py +++ b/bin/weewx/tags.py @@ -14,6 +14,9 @@ from weeutil.weeutil import to_int from weewx.units import ValueTuple +# Attributes we are to ignore. Cheetah calls these housekeeping functions. +IGNORE_ATTR = ['mro', 'im_func', 'func_code', '__func__', '__code__', '__init__', '__self__'] + # =============================================================================== # Class TimeBinder # =============================================================================== @@ -30,7 +33,7 @@ class TimeBinder(object): formatter=weewx.units.Formatter(), converter=weewx.units.Converter(), **option_dict): """Initialize an instance of DatabaseBinder. - + db_lookup: A function with call signature db_lookup(data_binding), which returns a database manager and where data_binding is an optional binding name. If not given, then a default binding will be used. @@ -257,9 +260,8 @@ class TimespanBinder(object): returns: An instance of class ObservationBinder.""" - # This is to get around bugs in the Python version of Cheetah's namemapper: - if obs_type in ['__call__', 'has_key']: - raise AttributeError + if obs_type in IGNORE_ATTR: + raise AttributeError(obs_type) # Return an ObservationBinder: if an attribute is # requested from it, an aggregation value will be returned. @@ -273,11 +275,11 @@ class TimespanBinder(object): # =============================================================================== class ObservationBinder(object): - """This is the final class in the chain of helper classes. It binds the + """This is the next class in the chain of helper classes. It binds the database, a time period, and an observation type all together. - When an aggregation type (eg, 'max') is given as an attribute to it, it runs the - query against the database, assembles the result, and returns it as a ValueHelper. + When an aggregation type (eg, 'max') is given as an attribute to it, it binds it to + an instance of AggTypeBinder and returns it. """ def __init__(self, obs_type, timespan, db_lookup, data_binding, context, @@ -316,41 +318,28 @@ class ObservationBinder(object): self.converter = converter self.option_dict = option_dict - def max_ge(self, val): - return self._do_query('max_ge', val=val) + def __getattr__(self, aggregation_type): + """Use the specified aggregation type - def max_le(self, val): - return self._do_query('max_le', val=val) - - def min_ge(self, val): - return self._do_query('min_ge', val=val) - - def min_le(self, val): - return self._do_query('min_le', val=val) - - def sum_ge(self, val): - return self._do_query('sum_ge', val=val) - - def sum_le(self, val): - return self._do_query('sum_le', val=val) - - def __getattr__(self, aggregate_type): - """Return statistical summary using a given aggregate type. - - aggregate_type: The type of aggregation over which the summary is to be done. This is + aggregation_type: The type of aggregation over which the summary is to be done. This is normally something like 'sum', 'min', 'mintime', 'count', etc. However, there are two special aggregation types that can be used to determine the existence of data: 'exists': Return True if the observation type exists in the database. 'has_data': Return True if the type exists and there is a non-zero number of entries over the aggregation period. - returns: A ValueHelper containing the aggregation data. + returns: An instance of AggTypeBinder, which is bound to the aggregation type. """ - - # This is to get around bugs in the Python version of Cheetah's namemapper: - if aggregate_type in ['__call__', 'has_key']: - raise AttributeError - return self._do_query(aggregate_type) + if aggregation_type in IGNORE_ATTR: + raise AttributeError(aggregation_type) + return AggTypeBinder(aggregation_type=aggregation_type, + obs_type=self.obs_type, + timespan=self.timespan, + db_lookup=self.db_lookup, + data_binding=self.data_binding, + context=self.context, + formatter=self.formatter, converter=self.converter, + **self.option_dict) @property def exists(self): @@ -360,19 +349,68 @@ class ObservationBinder(object): def has_data(self): return self.db_lookup(self.data_binding).has_data(self.obs_type, self.timespan) - def _do_query(self, aggregate_type, val=None): + +# =============================================================================== +# Class AggTypeBinder +# =============================================================================== + +class AggTypeBinder(object): + """This is the final class in the chain of helper classes. It binds everything needed + for a query.""" + + def __init__(self, aggregation_type, obs_type, timespan, db_lookup, data_binding, context, + formatter=weewx.units.Formatter(), converter=weewx.units.Converter(), + **option_dict): + self.aggregation_type = aggregation_type + self.obs_type = obs_type + self.timespan = timespan + self.db_lookup = db_lookup + self.data_binding = data_binding + self.context = context + self.formatter = formatter + self.converter = converter + self.option_dict = option_dict + + def __call__(self, *args, **kwargs): + """Offer a call option for expressions such as $month.outTemp.max_ge((90.0, 'degree_F')). + + In this example, self.aggregation_type would be 'max_ge', and val would be the tuple + (90.0, 'degree_F'). + """ + if len(args): + self.option_dict['val'] = args[0] + self.option_dict.update(kwargs) + return self + + def __str__(self): + """Need a string representation. Force the query, return as string.""" + vh = self._do_query() + return str(vh) + + def _do_query(self): """Run a query against the databases, using the given aggregation type.""" db_manager = self.db_lookup(self.data_binding) try: # If we cannot perform the aggregation, we will get an UnknownType or # UnknownAggregation error. Be prepared to catch it. - result = weewx.xtypes.get_aggregate(self.obs_type, self.timespan, aggregate_type, - db_manager, val=val, **self.option_dict) + result = weewx.xtypes.get_aggregate(self.obs_type, self.timespan, + self.aggregation_type, + db_manager, **self.option_dict) except (weewx.UnknownType, weewx.UnknownAggregation): - # Signal Cheetah that we don't know how to do this by raiing an AttributeError. + # Signal Cheetah that we don't know how to do this by raising an AttributeError. raise AttributeError(self.obs_type) return weewx.units.ValueHelper(result, self.context, self.formatter, self.converter) + def __getattr__(self, attr): + # The following is an optimization, so we avoid doing an SQL query for these kinds of + # housekeeping attribute queries done by Cheetah's NameMapper + if attr in IGNORE_ATTR: + raise AttributeError(attr) + # Do the query, getting a ValueHelper back + vh = self._do_query() + # Now seek the desired attribute of the ValueHelper and return + return getattr(vh, attr) + # =============================================================================== # Class RecordBinder @@ -410,7 +448,7 @@ class RecordBinder(object): class CurrentObj(object): """Helper class for the "Current" record. Hits the database lazily. - + This class allows tags such as: $current.barometer """ @@ -427,9 +465,9 @@ class CurrentObj(object): def __getattr__(self, obs_type): """Return the given observation type.""" - # This is to get around bugs in the Python version of Cheetah's namemapper: - if obs_type in ['__call__', 'has_key']: - raise AttributeError + + if obs_type in IGNORE_ATTR: + raise AttributeError(obs_type) # TODO: Refactor the following to be a separate function. @@ -474,8 +512,8 @@ class CurrentObj(object): # =============================================================================== class TrendObj(object): - """Helper class that calculates trends. - + """Helper class that calculates trends. + This class allows tags such as: $trend.barometer """ @@ -483,9 +521,9 @@ class TrendObj(object): def __init__(self, time_delta, time_grace, db_lookup, data_binding, nowtime, formatter, converter, **option_dict): # @UnusedVariable """Initialize a Trend object - + time_delta: The time difference over which the trend is to be calculated - + time_grace: A time within this amount is accepted. """ self.time_delta_val = time_delta @@ -506,9 +544,8 @@ class TrendObj(object): def __getattr__(self, obs_type): """Return the trend for the given observation type.""" - # This is to get around bugs in the Python version of Cheetah's namemapper: - if obs_type in ['__call__', 'has_key']: - raise AttributeError + if obs_type in IGNORE_ATTR: + raise AttributeError(obs_type) db_manager = self.db_lookup(self.data_binding) # Get the current record, and one "time_delta" ago: diff --git a/bin/weewx/tests/test_cheetah.py b/bin/weewx/tests/test_cheetah.py index d52724fb..3b00bfcb 100644 --- a/bin/weewx/tests/test_cheetah.py +++ b/bin/weewx/tests/test_cheetah.py @@ -22,6 +22,11 @@ log = logging.getLogger(__name__) weeutil.logger.setup('test_cheetah', {}) +class RaiseException(object): + def __str__(self): + raise AttributeError("Fine mess you got me in!") + + class TestFilter(unittest.TestCase): "Test the function filter() in AssureUnicode" def test_none(self): @@ -60,6 +65,12 @@ class TestFilter(unittest.TestCase): filtered_value = au.filter(val_vh) self.assertEqual(filtered_value, u"68.0°F") + def test_RaiseException(self): + r = RaiseException() + au = weewx.cheetahgenerator.AssureUnicode() + filtered_value = au.filter(r) + self.assertEqual(filtered_value, u'Fine mess you got me in!?') + if __name__ == '__main__': unittest.main()