NuWeather: split formatting another module; implement format_temp with only c input

This commit is contained in:
James Lu 2022-05-24 12:55:45 -07:00
parent 608a056ad7
commit cb49620a2d
5 changed files with 396 additions and 254 deletions

6
.pylintrc Normal file
View File

@ -0,0 +1,6 @@
[FORMAT]
max-line-length=120
good-names=ip,f,i,e,s
[MESSAGES CONTROL]
disable=missing-class-docstring,missing-function-docstring,invalid-name

View File

@ -54,14 +54,11 @@ __contributors__ = {}
# This is a url where the most recent plugin package can be downloaded. # This is a url where the most recent plugin package can be downloaded.
__url__ = 'https://github.com/jlu5/SupyPlugins/tree/master/NuWeather' __url__ = 'https://github.com/jlu5/SupyPlugins/tree/master/NuWeather'
from . import config from . import config, formatter, plugin
from . import plugin from importlib import reload
if sys.version_info >= (3, 4):
from importlib import reload
else:
from imp import reload
# In case we're being reloaded. # In case we're being reloaded.
reload(config) reload(config)
reload(formatter)
reload(plugin) reload(plugin)
from .local import accountsdb from .local import accountsdb

250
NuWeather/formatter.py Normal file
View File

@ -0,0 +1,250 @@
###
# Copyright (c) 2011-2014, Valentin Lorentz
# Copyright (c) 2018-2022, James Lu <james@overdrivenetworks.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import re
import string
from supybot import callbacks, conf, ircutils, log, utils
try:
import pendulum
except ImportError:
pendulum = None
log.warning('NuWeather: pendulum is not installed; extended forecasts will not be formatted properly')
try:
from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('NuWeather')
except ImportError:
_ = lambda x: x
from .config import DEFAULT_FORMAT, DEFAULT_FORECAST_FORMAT, DEFAULT_FORMAT_CURRENTONLY
_channel_context = None
# dummy fallback for testing
_registryValue = lambda *args, **kwargs: ''
# Based off https://github.com/ProgVal/Supybot-plugins/blob/master/GitHub/plugin.py
def flatten_subdicts(dicts, flat=None):
"""Flattens a dict containing dicts or lists of dicts. Useful for string formatting."""
if flat is None:
# Instanciate the dictionnary when the function is run and now when it
# is declared; otherwise the same dictionnary instance will be kept and
# it will have side effects (memory exhaustion, ...)
flat = {}
if isinstance(dicts, list):
return flatten_subdicts(dict(enumerate(dicts)))
elif isinstance(dicts, dict):
for key, value in dicts.items():
if isinstance(value, dict):
value = dict(flatten_subdicts(value))
for subkey, subvalue in value.items():
flat['%s__%s' % (key, subkey)] = subvalue
elif isinstance(value, list):
for num, subvalue in enumerate(value):
if isinstance(subvalue, dict):
for subkey, subvalue in subvalue.items():
flat['%s__%s__%s' % (key, num, subkey)] = subvalue
else:
flat['%s__%s' % (key, num)] = subvalue
else:
flat[key] = value
return flat
else:
return dicts
def format_temp(f=None, c=None):
"""
Colorizes temperatures and formats them to show either Fahrenheit, Celsius, or both.
"""
if f is None and c is None:
return _('N/A')
if f is None:
f = c * 9/5 + 32
elif c is None:
c = (f - 32) * 5/9
f = float(f)
if f < 10:
color = 'light blue'
elif f < 32:
color = 'teal'
elif f < 50:
color = 'blue'
elif f < 60:
color = 'light green'
elif f < 70:
color = 'green'
elif f < 80:
color = 'yellow'
elif f < 90:
color = 'orange'
else:
color = 'red'
# Show temp values to one decimal place
c = '%.1f' % c
f = '%.1f' % f
displaymode = _registryValue('units.temperature', channel=_channel_context)
if displaymode == 'F/C':
s = '%sF/%sC' % (f, c)
elif displaymode == 'C/F':
s = '%sC/%sF' % (c, f)
elif displaymode == 'F':
s = '%sF' % f
elif displaymode == 'C':
s = '%sC' % c
else:
raise ValueError("Unknown display mode for temperature.")
return ircutils.mircColor(s, color)
_TEMPERATURES_RE = re.compile(r'((\d+)°?F)') # Only need FtoC conversion so far
def mangle_temperatures(forecast):
"""Runs _format_temp() on temperature values embedded within forecast strings."""
if not forecast:
return forecast
for (text, value) in set(_TEMPERATURES_RE.findall(forecast)):
forecast = forecast.replace(text, format_temp(f=value))
return forecast
def wind_direction(angle):
"""Returns wind direction (N, W, S, E, etc.) given an angle."""
# Adapted from https://stackoverflow.com/a/7490772
directions = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW')
if angle is None:
return directions[0] # dummy output
angle = int(angle)
idx = int((angle/(360/len(directions)))+.5)
return directions[idx % len(directions)]
def format_uv(uv):
"""Formats UV levels with IRC colouring"""
if uv is None:
return _('N/A')
# From https://en.wikipedia.org/wiki/Ultraviolet_index#Index_usage 2018-12-30
uv = float(uv)
if uv <= 2.9:
color, risk = 'green', 'Low'
elif uv <= 5.9:
color, risk = 'yellow', 'Moderate'
elif uv <= 7.9:
color, risk = 'orange', 'High'
elif uv <= 10.9:
color, risk = 'red', 'Very high'
else:
# Closest we have to violet
color, risk = 'pink', 'Extreme'
s = '%d (%s)' % (uv, risk)
return ircutils.mircColor(s, color)
def format_precip(mm=None, inches=None):
"""Formats precipitation to mm/in format"""
if mm is None and inches is None:
return _('N/A')
elif mm == 0 or inches == 0:
return '0' # Don't bother with 2 units if the value is 0
if mm is None:
mm = round(inches * 25.4, 1)
elif inches is None:
inches = round(mm / 25.4, 1)
return _('%smm/%sin') % (mm, inches)
def format_distance(mi=None, km=None, speed=False):
"""Formats distance or speed values in miles and kilometers"""
if mi is None and km is None:
return _('N/A')
elif mi == 0 or km == 0:
return '0' # Don't bother with 2 units if the value is 0
if mi is None:
mi = round(km / 1.609, 1)
elif km is None:
km = round(mi * 1.609, 1)
if speed:
return _('%smph/%skph') % (mi, km)
else:
return _('%smi/%skm') % (mi, km)
def format_percentage(value):
"""
Formats percentage values given either as an int (value%) or float (0 <= value <= 1).
"""
if isinstance(value, float):
return '%.0f%%' % (value * 100)
elif isinstance(value, int):
return '%d%%' % value
else:
return 'N/A'
def get_dayname(ts, idx, *, tz=None):
"""
Returns the day name given a Unix timestamp, day index and (optionally) a timezone.
"""
if pendulum is not None:
p = pendulum.from_timestamp(ts, tz=tz)
return p.format('dddd')
else:
# Fallback
if idx == 0:
return 'Today'
elif idx == 1:
return 'Tomorrow'
else:
return 'Day_%d' % idx
def format_weather(data, forecast=False):
"""
Formats and returns current conditions.
"""
# Work around IRC length limits for config opts...
data['c'] = data['current']
data['f'] = data.get('forecast')
flat_data = flatten_subdicts(data)
if flat_data.get('url'):
flat_data['url'] = utils.str.url(flat_data['url'])
forecast_available = bool(data.get('forecast'))
if forecast: # --forecast option was given
if forecast_available:
fmt = _registryValue('outputFormat.forecast', channel=_channel_context) or DEFAULT_FORECAST_FORMAT
else:
raise callbacks.Error(_("Extended forecast info is not available from this backend."))
else:
if forecast_available:
fmt = _registryValue('outputFormat', channel=_channel_context) or DEFAULT_FORMAT
else:
fmt = _registryValue('outputFormat.currentOnly', channel=_channel_context) or DEFAULT_FORMAT_CURRENTONLY
template = string.Template(fmt)
return template.safe_substitute(flat_data)

View File

@ -1,6 +1,5 @@
### ###
# Copyright (c) 2011-2014, Valentin Lorentz # Copyright (c) 2018-2022, James Lu <james@overdrivenetworks.com>
# Copyright (c) 2018-2020, James Lu <james@overdrivenetworks.com>
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without # Redistribution and use in source and binary forms, with or without
@ -29,8 +28,6 @@
### ###
import json import json
import os import os
import re
import string
from supybot import utils, plugins, ircutils, callbacks, world, conf, log from supybot import utils, plugins, ircutils, callbacks, world, conf, log
from supybot.commands import * from supybot.commands import *
@ -42,48 +39,14 @@ except ImportError:
# without the i18n module # without the i18n module
_ = lambda x: x _ = lambda x: x
try: from .config import BACKENDS, GEOCODE_BACKENDS
import pendulum
except ImportError:
pendulum = None
log.warning('NuWeather: pendulum is not installed; extended forecasts will not be formatted properly')
from .config import BACKENDS, GEOCODE_BACKENDS, DEFAULT_FORMAT, DEFAULT_FORECAST_FORMAT, DEFAULT_FORMAT_CURRENTONLY
from .local import accountsdb from .local import accountsdb
from . import formatter
HEADERS = { HEADERS = {
'User-agent': 'Mozilla/5.0 (compatible; Supybot/Limnoria %s; NuWeather weather plugin)' % conf.version 'User-agent': 'Mozilla/5.0 (compatible; Supybot/Limnoria %s; NuWeather weather plugin)' % conf.version
} }
# Based off https://github.com/ProgVal/Supybot-plugins/blob/master/GitHub/plugin.py
def flatten_subdicts(dicts, flat=None):
"""Flattens a dict containing dicts or lists of dicts. Useful for string formatting."""
if flat is None:
# Instanciate the dictionnary when the function is run and now when it
# is declared; otherwise the same dictionnary instance will be kept and
# it will have side effects (memory exhaustion, ...)
flat = {}
if isinstance(dicts, list):
return flatten_subdicts(dict(enumerate(dicts)))
elif isinstance(dicts, dict):
for key, value in dicts.items():
if isinstance(value, dict):
value = dict(flatten_subdicts(value))
for subkey, subvalue in value.items():
flat['%s__%s' % (key, subkey)] = subvalue
elif isinstance(value, list):
for num, subvalue in enumerate(value):
if isinstance(subvalue, dict):
for subkey, subvalue in subvalue.items():
flat['%s__%s__%s' % (key, num, subkey)] = subvalue
else:
flat['%s__%s' % (key, num)] = subvalue
else:
flat[key] = value
return flat
else:
return dicts
class NuWeather(callbacks.Plugin): class NuWeather(callbacks.Plugin):
"""Weather plugin for Limnoria""" """Weather plugin for Limnoria"""
threaded = True threaded = True
@ -101,7 +64,8 @@ class NuWeather(callbacks.Plugin):
self.geocode_db = {} self.geocode_db = {}
world.flushers.append(self.db.flush) world.flushers.append(self.db.flush)
world.flushers.append(self._flush_geocode_db) world.flushers.append(self._flush_geocode_db)
self._channel_context = None # this is hacky but less annoying than navigating the registry ourselves
formatter._registryValue = self.registryValue
def _flush_geocode_db(self): def _flush_geocode_db(self):
geocode_db_filename = conf.supybot.directories.data.dirize("NuWeather-geocode.json") geocode_db_filename = conf.supybot.directories.data.dirize("NuWeather-geocode.json")
@ -115,150 +79,6 @@ class NuWeather(callbacks.Plugin):
self._flush_geocode_db() self._flush_geocode_db()
super().die() super().die()
def _format_temp(self, f, c=None):
"""
Colorizes temperatures and formats them to show either Fahrenheit, Celsius, or both.
"""
if f is None:
return _('N/A')
f = float(f)
if f < 10:
color = 'light blue'
elif f < 32:
color = 'teal'
elif f < 50:
color = 'blue'
elif f < 60:
color = 'light green'
elif f < 70:
color = 'green'
elif f < 80:
color = 'yellow'
elif f < 90:
color = 'orange'
else:
color = 'red'
# Round to nearest tenth for display purposes
if c is None:
c = round((f - 32) * 5/9, 1)
else:
c = round(c, 1)
f = round(f, 1)
displaymode = self.registryValue('units.temperature', channel=self._channel_context)
if displaymode == 'F/C':
string = '%sF/%sC' % (f, c)
elif displaymode == 'C/F':
string = '%sC/%sF' % (c, f)
elif displaymode == 'F':
string = '%sF' % f
elif displaymode == 'C':
string = '%sC' % c
else:
raise ValueError("Unknown display mode for temperature.")
return ircutils.mircColor(string, color)
_temperatures_re = re.compile(r'((\d+)°?F)') # Only need FtoC conversion so far
def _mangle_temperatures(self, forecast):
"""Runs _format_temp() on temperature values embedded within forecast strings."""
if not forecast:
return forecast
for (text, value) in set(self._temperatures_re.findall(forecast)):
forecast = forecast.replace(text, self._format_temp(f=value))
return forecast
@staticmethod
def _wind_direction(angle):
"""Returns wind direction (N, W, S, E, etc.) given an angle."""
# Adapted from https://stackoverflow.com/a/7490772
directions = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW')
if angle is None:
return directions[0] # dummy output
angle = int(angle)
idx = int((angle/(360/len(directions)))+.5)
return directions[idx % len(directions)]
@staticmethod
def _format_uv(uv):
if uv is None:
return _('N/A')
# From https://en.wikipedia.org/wiki/Ultraviolet_index#Index_usage 2018-12-30
uv = float(uv)
if uv <= 2.9:
color, risk = 'green', 'Low'
elif uv <= 5.9:
color, risk = 'yellow', 'Moderate'
elif uv <= 7.9:
color, risk = 'orange', 'High'
elif uv <= 10.9:
color, risk = 'red', 'Very high'
else:
# Closest we have to violet
color, risk = 'pink', 'Extreme'
string = '%d (%s)' % (uv, risk)
return ircutils.mircColor(string, color)
@staticmethod
def _format_precip(mm=None, inches=None):
if mm is None and inches is None:
return _('N/A')
elif mm == 0 or inches == 0:
return '0' # Don't bother with 2 units if the value is 0
if mm is None:
mm = round(inches * 25.4, 1)
elif inches is None:
inches = round(mm / 25.4, 1)
return _('%smm/%sin') % (mm, inches)
@staticmethod
def _format_distance(mi=None, km=None, speed=False):
if mi is None and km is None:
return _('N/A')
elif mi == 0 or km == 0:
return '0' # Don't bother with 2 units if the value is 0
if mi is None:
mi = round(km / 1.609, 1)
elif km is None:
km = round(mi * 1.609, 1)
if speed:
return _('%smph/%skph') % (mi, km)
else:
return _('%smi/%skm') % (mi, km)
@staticmethod
def _format_percentage(value):
"""
Formats percentage values given either as an int (value%) or float (0 <= value <= 1).
"""
if isinstance(value, float):
return '%.0f%%' % (value * 100)
elif isinstance(value, int):
return '%d%%' % value
else:
return 'N/A'
@staticmethod
def _get_dayname(ts, idx, *, tz=None):
"""
Returns the day name given a Unix timestamp, day index and (optionally) a timezone.
"""
if pendulum is not None:
p = pendulum.from_timestamp(ts, tz=tz)
return p.format('dddd')
else:
# Fallback
if idx == 0:
return 'Today'
elif idx == 1:
return 'Tomorrow'
else:
return 'Day_%d' % idx
def _nominatim_geocode(self, location): def _nominatim_geocode(self, location):
location = location.lower() location = location.lower()
@ -368,7 +188,7 @@ class NuWeather(callbacks.Plugin):
return result return result
def _geocode(self, location, geobackend=None): def _geocode(self, location, geobackend=None):
geocode_backend = geobackend or self.registryValue('geocodeBackend', channel=self._channel_context) geocode_backend = geobackend or self.registryValue('geocodeBackend', channel=formatter._channel_context)
if geocode_backend not in GEOCODE_BACKENDS: if geocode_backend not in GEOCODE_BACKENDS:
raise callbacks.Error(_("Unknown geocode backend %r. Valid ones are: %s") % (geocode_backend, ', '.join(GEOCODE_BACKENDS))) raise callbacks.Error(_("Unknown geocode backend %r. Valid ones are: %s") % (geocode_backend, ', '.join(GEOCODE_BACKENDS)))
@ -387,33 +207,6 @@ class NuWeather(callbacks.Plugin):
self.geocode_db[result_pair] = result # Cache result persistently self.geocode_db[result_pair] = result # Cache result persistently
return result return result
def _format(self, data, forecast=False):
"""
Formats and returns current conditions.
"""
# Work around IRC length limits for config opts...
data['c'] = data['current']
data['f'] = data.get('forecast')
flat_data = flatten_subdicts(data)
if flat_data.get('url'):
flat_data['url'] = utils.str.url(flat_data['url'])
forecast_available = bool(data.get('forecast'))
if forecast: # --forecast option was given
if forecast_available:
fmt = self.registryValue('outputFormat.forecast', channel=self._channel_context) or DEFAULT_FORECAST_FORMAT
else:
raise callbacks.Error(_("Extended forecast info is not available from this backend."))
else:
if forecast_available:
fmt = self.registryValue('outputFormat', channel=self._channel_context) or DEFAULT_FORMAT
else:
fmt = self.registryValue('outputFormat.currentOnly', channel=self._channel_context) or DEFAULT_FORMAT_CURRENTONLY
template = string.Template(fmt)
return template.safe_substitute(flat_data)
def _weatherstack_fetcher(self, location, geobackend=None): def _weatherstack_fetcher(self, location, geobackend=None):
"""Grabs weather data from weatherstack (formerly Apixu).""" """Grabs weather data from weatherstack (formerly Apixu)."""
apikey = self.registryValue('apikeys.weatherstack') apikey = self.registryValue('apikeys.weatherstack')
@ -439,14 +232,14 @@ class NuWeather(callbacks.Plugin):
'url': '', 'url': '',
'current': { 'current': {
'condition': currentdata['weather_descriptions'][0], 'condition': currentdata['weather_descriptions'][0],
'temperature': self._format_temp(f=currentdata['temperature']), 'temperature': formatter.format_temp(f=currentdata['temperature']),
'feels_like': self._format_temp(f=currentdata['feelslike']), 'feels_like': formatter.format_temp(f=currentdata['feelslike']),
'humidity': self._format_percentage(currentdata['humidity']), 'humidity': formatter.format_percentage(currentdata['humidity']),
'precip': self._format_precip(inches=currentdata['precip']), 'precip': formatter.format_precip(inches=currentdata['precip']),
'wind': self._format_distance(mi=currentdata['wind_speed'], speed=True), 'wind': formatter.format_distance(mi=currentdata['wind_speed'], speed=True),
'wind_dir': currentdata['wind_dir'], 'wind_dir': currentdata['wind_dir'],
'uv': self._format_uv(currentdata['uv_index']), 'uv': formatter.format_uv(currentdata['uv_index']),
'visibility': self._format_distance(mi=currentdata.get('visibility')), 'visibility': formatter.format_distance(mi=currentdata.get('visibility')),
} }
} }
@ -479,19 +272,19 @@ class NuWeather(callbacks.Plugin):
'url': 'https://darksky.net/forecast/%s,%s' % (lat, lon), 'url': 'https://darksky.net/forecast/%s,%s' % (lat, lon),
'current': { 'current': {
'condition': currentdata.get('summary', 'N/A'), 'condition': currentdata.get('summary', 'N/A'),
'temperature': self._format_temp(f=currentdata.get('temperature')), 'temperature': formatter.format_temp(f=currentdata.get('temperature')),
'feels_like': self._format_temp(f=currentdata.get('apparentTemperature')), 'feels_like': formatter.format_temp(f=currentdata.get('apparentTemperature')),
'humidity': self._format_percentage(currentdata.get('humidity')), 'humidity': formatter.format_percentage(currentdata.get('humidity')),
'precip': self._format_precip(mm=currentdata.get('precipIntensity')), 'precip': formatter.format_precip(mm=currentdata.get('precipIntensity')),
'wind': self._format_distance(mi=currentdata.get('windSpeed', 0), speed=True), 'wind': formatter.format_distance(mi=currentdata.get('windSpeed', 0), speed=True),
'wind_gust': self._format_distance(mi=currentdata.get('windGust', 0), speed=True), 'wind_gust': formatter.format_distance(mi=currentdata.get('windGust', 0), speed=True),
'wind_dir': self._wind_direction(currentdata.get('windBearing')), 'wind_dir': formatter.wind_direction(currentdata.get('windBearing')),
'uv': self._format_uv(currentdata.get('uvIndex')), 'uv': formatter.format_uv(currentdata.get('uvIndex')),
'visibility': self._format_distance(mi=currentdata.get('visibility')), 'visibility': formatter.format_distance(mi=currentdata.get('visibility')),
}, },
'forecast': [{'dayname': self._get_dayname(forecastdata['time'], idx, tz=data['timezone']), 'forecast': [{'dayname': formatter.get_dayname(forecastdata['time'], idx, tz=data['timezone']),
'max': self._format_temp(f=forecastdata.get('temperatureHigh')), 'max': formatter.format_temp(f=forecastdata.get('temperatureHigh')),
'min': self._format_temp(f=forecastdata.get('temperatureLow')), 'min': formatter.format_temp(f=forecastdata.get('temperatureLow')),
'summary': forecastdata.get('summary', 'N/A').rstrip('.')} for idx, forecastdata in enumerate(data['daily']['data'])] 'summary': forecastdata.get('summary', 'N/A').rstrip('.')} for idx, forecastdata in enumerate(data['daily']['data'])]
} }
@ -522,9 +315,9 @@ class NuWeather(callbacks.Plugin):
# XXX: are the units for this consistent across APIs? # XXX: are the units for this consistent across APIs?
if currentdata.get('snow'): if currentdata.get('snow'):
precip = self._format_precip(mm=currentdata['snow']['1h'] * 10) precip = formatter.format_precip(mm=currentdata['snow']['1h'] * 10)
elif currentdata.get('rain'): elif currentdata.get('rain'):
precip = self._format_precip(mm=currentdata['rain']['1h']) precip = formatter.format_precip(mm=currentdata['rain']['1h'])
else: else:
precip = 'N/A' precip = 'N/A'
@ -538,22 +331,22 @@ class NuWeather(callbacks.Plugin):
}), }),
'current': { 'current': {
'condition': currentdata['weather'][0]['description'], 'condition': currentdata['weather'][0]['description'],
'temperature': self._format_temp(f=currentdata['temp']), 'temperature': formatter.format_temp(f=currentdata['temp']),
'feels_like': self._format_temp(f=currentdata['feels_like']), 'feels_like': formatter.format_temp(f=currentdata['feels_like']),
'humidity': self._format_percentage(currentdata['humidity']), 'humidity': formatter.format_percentage(currentdata['humidity']),
'precip': precip, 'precip': precip,
'wind': self._format_distance(mi=currentdata['wind_speed'], speed=True), 'wind': formatter.format_distance(mi=currentdata['wind_speed'], speed=True),
'wind_dir': self._wind_direction(currentdata['wind_deg']), 'wind_dir': formatter.wind_direction(currentdata['wind_deg']),
'wind_gust': self._format_distance(mi=currentdata.get('wind_gust'), speed=True), 'wind_gust': formatter.format_distance(mi=currentdata.get('wind_gust'), speed=True),
'uv': self._format_uv(currentdata.get('uvi')), 'uv': formatter.format_uv(currentdata.get('uvi')),
'visibility': self._format_distance(km=currentdata['visibility']/1000), 'visibility': formatter.format_distance(km=currentdata['visibility']/1000),
} }
} }
output['forecast'] = [ output['forecast'] = [
{'dayname': self._get_dayname(forecast['dt'], idx, tz=data['timezone']), {'dayname': formatter.get_dayname(forecast['dt'], idx, tz=data['timezone']),
'max': self._format_temp(f=forecast['temp']['max']), 'max': formatter.format_temp(f=forecast['temp']['max']),
'min': self._format_temp(f=forecast['temp']['min']), 'min': formatter.format_temp(f=forecast['temp']['min']),
'summary': forecast['weather'][0]['description']} 'summary': forecast['weather'][0]['description']}
for idx, forecast in enumerate(data['daily']) for idx, forecast in enumerate(data['daily'])
] ]
@ -592,11 +385,11 @@ class NuWeather(callbacks.Plugin):
irc.error(_("Unknown weather backend %s. Valid ones are: %s") % (weather_backend, ', '.join(BACKENDS)), Raise=True) irc.error(_("Unknown weather backend %s. Valid ones are: %s") % (weather_backend, ', '.join(BACKENDS)), Raise=True)
geocode_backend = optlist.get('geocode-backend', self.registryValue('geocodeBackend', msg.args[0])) geocode_backend = optlist.get('geocode-backend', self.registryValue('geocodeBackend', msg.args[0]))
self._channel_context = msg.channel formatter._channel_context = msg.channel
backend_func = getattr(self, '_%s_fetcher' % weather_backend) backend_func = getattr(self, '_%s_fetcher' % weather_backend)
raw_data = backend_func(location, geocode_backend) raw_data = backend_func(location, geocode_backend)
s = self._format(raw_data, forecast='forecast' in optlist) s = formatter.format_weather(raw_data, forecast='forecast' in optlist)
irc.reply(s) irc.reply(s)
@wrap([getopts({'user': 'nick', 'backend': None}), 'text']) @wrap([getopts({'user': 'nick', 'backend': None}), 'text'])

View File

@ -34,7 +34,6 @@ from supybot.test import *
from supybot import log from supybot import log
NO_NETWORK_REASON = "Network-based tests are disabled by --no-network" NO_NETWORK_REASON = "Network-based tests are disabled by --no-network"
class NuWeatherTestCase(): class NuWeatherTestCase():
plugins = ('NuWeather',) plugins = ('NuWeather',)
@ -82,6 +81,103 @@ class NuWeatherWeatherstackTestCase(NuWeatherTestCase, PluginTestCase):
class NuWeatherOpenWeatherMapTestCase(NuWeatherTestCase, PluginTestCase): class NuWeatherOpenWeatherMapTestCase(NuWeatherTestCase, PluginTestCase):
BACKEND = 'openweathermap' BACKEND = 'openweathermap'
from . import formatter
class NuWeatherFormatterTestCase(unittest.TestCase):
def test_format_temp(self):
func = formatter.format_temp
self.assertEqual(func(f=50, c=10), '\x030950.0F/10.0C\x03')
self.assertEqual(func(f=100), '\x0304100.0F/37.8C\x03')
self.assertEqual(func(c=25.55), '\x030878.0F/25.6C\x03')
self.assertEqual(func(), 'N/A')
def test_format_temp_displaymode(self):
func = formatter.format_temp
with conf.supybot.plugins.NuWeather.units.temperature.context('F/C'):
self.assertEqual(func(c=-5.3), '\x031022.5F/-5.3C\x03')
with conf.supybot.plugins.NuWeather.units.temperature.context('C/F'):
self.assertEqual(func(f=50, c=10), '\x030910.0C/50.0F\x03')
with conf.supybot.plugins.NuWeather.units.temperature.context('C'):
self.assertEqual(func(c=36), '\x030436.0C\x03')
with conf.supybot.plugins.NuWeather.units.temperature.context('F'):
self.assertEqual(func(f=72), '\x030872.0F\x03')
def test_format_distance_speed(self):
func = formatter.format_distance
self.assertEqual(func(mi=123), '123mi/197.9km')
self.assertEqual(func(km=42.6), '26.5mi/42.6km')
self.assertEqual(func(mi=26, km=42), '26mi/42km')
self.assertEqual(func(mi=0), '0') # special case
self.assertEqual(func(), 'N/A')
def test_format_default(self):
data = {'location': "Narnia",
'poweredby': 'Dummy',
'url': 'http://dummy.invalid/api/',
'current': {
'condition': 'Sunny',
'temperature': formatter.format_temp(f=80),
'feels_like': formatter.format_temp(f=85),
'humidity': formatter.format_percentage(0.8),
'precip': formatter.format_precip(mm=90),
'wind': formatter.format_distance(mi=12, speed=True),
'wind_gust': formatter.format_distance(mi=20, speed=True),
'wind_dir': formatter.wind_direction(15),
'uv': formatter.format_uv(6),
'visibility': formatter.format_distance(mi=1000),
},
'forecast': [{'dayname': 'Today',
'max': formatter.format_temp(f=100),
'min': formatter.format_temp(f=60),
'summary': 'Cloudy'},
{'dayname': 'Tomorrow',
'max': formatter.format_temp(f=70),
'min': formatter.format_temp(f=55),
'summary': 'Light rain'}]}
self.assertEqual(formatter.format_weather(data),
'\x02Narnia\x02 :: Sunny \x030780.0F/26.7C\x03 (Humidity: 80%) | '
'\x02Feels like:\x02 \x030785.0F/29.4C\x03 | '
'\x02Wind\x02: 12mph/19.3kph NNE | '
'\x02Wind gust\x02: 20mph/32.2kph | '
'\x02Today\x02: Cloudy. High \x0304100.0F/37.8C\x03. Low \x030360.0F/15.6C\x03. | '
'\x02Tomorrow\x02: Light rain. High \x030870.0F/21.1C\x03. Low \x030955.0F/12.8C\x03. | '
'Powered by \x02Dummy\x02 <http://dummy.invalid/api/>')
#print(repr(formatter.format_weather(data)))
def test_format_forecast(self):
data = {'location': "Testville",
'poweredby': 'Dummy',
'url': 'http://dummy.invalid/api/',
'current': {
'condition': 'Sunny',
'temperature': formatter.format_temp(f=80),
'feels_like': formatter.format_temp(f=85),
'humidity': formatter.format_percentage(0.8),
'precip': formatter.format_precip(mm=90),
'wind': formatter.format_distance(mi=12, speed=True),
'wind_gust': formatter.format_distance(mi=20, speed=True),
'wind_dir': formatter.wind_direction(15),
'uv': formatter.format_uv(6),
'visibility': formatter.format_distance(mi=1000),
},
'forecast': [{'dayname': 'Today',
'max': formatter.format_temp(f=100),
'min': formatter.format_temp(f=60),
'summary': 'Cloudy'},
{'dayname': 'Tomorrow',
'max': formatter.format_temp(f=70),
'min': formatter.format_temp(f=55),
'summary': 'Light rain'},
{'dayname': 'Tomorrow',
'max': formatter.format_temp(f=56),
'min': formatter.format_temp(f=40),
'summary': 'Heavy rain'}]}
self.assertIn('\x02Testville\x02 :: \x02Today\x02: Cloudy (\x030360.0F/15.6C\x03 to \x0304100.0F/37.8C\x03) | '
'\x02Tomorrow\x02: Light rain (\x030955.0F/12.8C\x03 to \x030870.0F/21.1C\x03) | '
'\x02Tomorrow\x02: Heavy rain (\x030240.0F/4.4C\x03 to \x030956.0F/13.3C\x03)',
formatter.format_weather(data, True))
#print(repr(formatter.format_weather(data, True)))
# FIXME: test geocode backends # FIXME: test geocode backends