Rewrite scores backends to use a single format string (#833)

* Rewrite scores backends to use a single format string

Since the game status is almost always the only thing that differs, this
simplifies things greatly by using a single format string and building a
game_status formatter depending on format strings set for each game
status type.

* Use formatp on game_status

* Make postponed games appear last by default

* Add score formatters back to defaults

* Fix conditional display of zero scores by treating them as strings
This commit is contained in:
Erik Johnson 2022-01-27 07:22:27 -06:00 committed by GitHub
parent 34af13547d
commit c4876ed551
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 162 additions and 91 deletions

View File

@ -118,11 +118,13 @@ class ScoresBackend(SettingsBase):
return f'{number}{suffix}'
@staticmethod
def force_int(value):
def zero_fallback(value):
try:
return int(value)
int(value)
except (TypeError, ValueError):
return 0
return '0'
else:
return str(value)
def get_nested(self, data, expr, callback=None, default=''):
if callback is None:
@ -219,7 +221,7 @@ class Scores(Module):
.. code-block:: python
from i3pystatus import Status
from i3pystatus.scores import mlb, nhl
from i3pystatus.scores import mlb, nhl, nba
status = Status()
@ -228,9 +230,11 @@ class Scores(Module):
hints={'markup': 'pango'},
colorize_teams=True,
favorite_icon='<span size="small" color="#F5FF00">★</span>',
team_format='abbreviation',
backends=[
mlb.MLB(
teams=['CWS', 'SF'],
team_format='name',
format_no_games='No games today :(',
inning_top='',
inning_bottom='',
@ -240,7 +244,6 @@ class Scores(Module):
teams=['GSW'],
all_games=False,
),
epl.EPL(),
],
)
@ -362,6 +365,7 @@ class Scores(Module):
'shown by the module) when refreshing scores. '
'**NOTE:** Depending on how quickly the update is '
'performed, the icon may not be displayed.'),
('team_format', 'One of ``name``, ``abbreviation``, or ``city``'),
)
backends = []
@ -371,6 +375,7 @@ class Scores(Module):
colorize_teams = False
scroll_arrow = ''
refresh_icon = ''
team_format = 'name'
output = {'full_text': ''}
game_map = {}
@ -417,6 +422,9 @@ class Scores(Module):
)
backend.display_order[index] = order_lc
if backend.team_format is None:
backend.team_format = self.team_format
self.condition = threading.Condition()
self.thread = threading.Thread(target=self.update_thread, daemon=True)
self.thread.start()
@ -566,7 +574,14 @@ class Scores(Module):
self.show_refresh_icon()
cur_id = self.current_game_id
cur_games = self.current_backend.games.keys()
self.current_backend.check_scores()
for game in self.current_backend.games.values():
if game['status'] in ('pregame', 'postponed'):
# Allow formatp to conditionally hide the score when game
# hasn't started (or has been postponed)
game['home_score'] = game['away_score'] = ''
if cur_games == self.current_backend.games.keys():
# Set the index to the scroll position of the current game (it
# may have changed due to this game or other games changing
@ -623,31 +638,55 @@ class Scores(Module):
else:
game = copy.copy(self.current_game)
fstr = str(getattr(self.current_backend, f'format_{game["status"]}'))
# Set the game_status using the formatter
game_status_opt = f'status_{game["status"]}'
try:
game['game_status'] = formatp(
str(getattr(self.current_backend, game_status_opt)),
**game
)
except AttributeError:
self.logger.error(
f'Unable to find {self.current_backend.name} option '
f'{game_status_opt}'
)
game['game_status'] = 'Unknown Status'
for team in ('home', 'away'):
abbrev_key = f'{team}_abbrev'
team_abbrev = game[f'{team}_abbreviation']
# Set favorite icon, if applicable
game[f'{team}_favorite'] = self.favorite_icon \
if game[abbrev_key] in self.current_backend.favorite_teams \
if team_abbrev in self.current_backend.favorite_teams \
else ''
if self.colorize_teams:
# Wrap in Pango markup
color = self.current_backend.team_colors.get(
game.get(abbrev_key)
try:
game[f'{team}_team'] = game[f'{team}_{self.current_backend.team_format}']
except KeyError:
self.logger.debug(
f'Unable to find {self.current_backend.team_format} '
f'value, falling back to {team_abbrev}'
)
if color is not None:
for item in ('abbrev', 'city', 'name', 'name_short'):
key = f'{team}_{item}'
if key in game:
game[key] = f'<span color="{color}">{game[key]}</span>'
game[f'{team}_team'] = team_abbrev
if self.colorize_teams:
try:
color = self.current_backend.team_colors[team_abbrev]
except KeyError:
pass
else:
for val in ('team', 'name', 'city', 'abbreviation'):
# Wrap in Pango markup
game[f'{team}_{val}'] = ''.join((
f'<span color="{color}">',
game[f'{team}_{val}'],
'</span>',
))
game['scroll'] = self.scroll_arrow \
if len(self.current_backend.games) > 1 \
else ''
output = formatp(fstr, **game).strip()
output = formatp(self.current_backend.format, **game).strip()
self.output = {'full_text': output, 'color': self.color}

View File

@ -21,18 +21,16 @@ class MLB(ScoresBackend):
.. rubric:: Available formatters
* `{home_name}` Name of home team
* `{home_city}` Name of home team's city
* `{home_abbrev}` 2 or 3-letter abbreviation for home team's city
* `{home_team}` Depending on the value of the ``team_format`` option,
will contain either the home team's name, abbreviation, or city
* `{home_score}` Home team's current score
* `{home_wins}` Home team's number of wins
* `{home_losses}` Home team's number of losses
* `{home_favorite}` Displays the value for the :py:mod:`.scores` module's
``favorite`` attribute, if the home team is one of the teams being
followed. Otherwise, this formatter will be blank.
* `{away_name}` Name of away team
* `{away_city}` Name of away team's city
* `{away_abbrev}` 2 or 3-letter abbreviation for away team's city
* `{away_team}` Depending on the value of the ``team_format`` option,
will contain either the away team's name, abbreviation, or city
* `{away_score}` Away team's current score
* `{away_wins}` Away team's number of wins
* `{away_losses}` Away team's number of losses
@ -51,6 +49,8 @@ class MLB(ScoresBackend):
this formatter will be blank.
* `{postponed}` Reason for postponement, if game has been postponed.
Otherwise, this formatter will be blank.
* `{suspended}` Reason for suspension, if game has been suspended.
Otherwise, this formatter will be blank.
* `{extra_innings}` When a game lasts longer than 9 innings, this
formatter will show that number of innings. Otherwise, it will blank.
@ -105,11 +105,17 @@ class MLB(ScoresBackend):
('format_no_games', 'Format used when no tracked games are scheduled '
'for the current day (does not support formatter '
'placeholders)'),
('format_pregame', 'Format used when the game has not yet started'),
('format_in_progress', 'Format used when the game is in progress'),
('format_final', 'Format used when the game is complete'),
('format_postponed', 'Format used when the game has been postponed'),
('format_suspended', 'Format used when the game has been suspended'),
('format', 'Format used to display game information'),
('status_pregame', 'Format string used for the ``{game_status}`` '
'formatter when the game has not started '),
('status_in_progress', 'Format string used for the ``{game_status}`` '
'formatter when the game is in progress'),
('status_final', 'Format string used for the ``{game_status}`` '
'formatter when the game has finished'),
('status_postponed', 'Format string used for the ``{game_status}`` '
'formatter when the game has been postponed'),
('status_suspended', 'Format string used for the ``{game_status}`` '
'formatter when the game has been suspended'),
('inning_top', 'Value for the ``{top_bottom}`` formatter when game '
'is in the top half of an inning'),
('inning_bottom', 'Value for the ``{top_bottom}`` formatter when game '
@ -118,6 +124,9 @@ class MLB(ScoresBackend):
'codes. If overridden, the passed values will be '
'merged with the defaults, so it is not necessary to '
'define all teams if specifying this value.'),
('team_format', 'One of ``name``, ``abbreviation``, or ``city``. If '
'not specified, takes the value from the ``scores`` '
'module.'),
('date', 'Date for which to display game scores, in **YYYY-MM-DD** '
'format. If unspecified, the current day\'s games will be '
'displayed starting at 10am Eastern time, with last '
@ -169,15 +178,16 @@ class MLB(ScoresBackend):
}
_valid_teams = [x for x in _default_colors]
_valid_display_order = ['in_progress', 'suspended', 'final', 'postponed', 'pregame']
_valid_display_order = ['in_progress', 'suspended', 'final', 'pregame', 'postponed']
display_order = _valid_display_order
format_no_games = 'MLB: No games'
format_pregame = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} ({home_wins}-{home_losses}) {start_time:%H:%M %Z}[ ({delay} Delay)]'
format_in_progress = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} {away_score}, [{home_favorite} ]{home_abbrev} {home_score} ({top_bottom} {inning}, {outs} Out)[ ({delay} Delay)]'
format_final = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}) (Final[/{extra_innings}])'
format_postponed = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} ({home_wins}-{home_losses}) (PPD: {postponed})'
format_suspended = '[{scroll} ]MLB: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}) (Suspended: {suspended})'
format = '[{scroll} ]MLB: [{away_favorite} ]{away_team} [{away_score} ]({away_wins}-{away_losses}) at [{home_favorite} ]{home_team} [{home_score} ]({home_wins}-{home_losses}) {game_status}'
status_pregame = '{start_time:%H:%M %Z}[ ({delay} Delay)]'
status_in_progress = '({top_bottom} {inning}, {outs} Out)[ ({delay} Delay)]'
status_final = '(Final[/{extra_innings}])'
status_postponed = '(PPD: {postponed})'
status_suspended = '(Suspended: {suspended})'
inning_top = 'Top'
inning_bottom = 'Bot'
team_colors = _default_colors
@ -185,6 +195,9 @@ class MLB(ScoresBackend):
scoreboard_url = SCOREBOARD_URL
api_url = API_URL
# These will inherit from the Scores class if not overridden
team_format = None
@require(internet)
def check_scores(self):
self.get_api_date()
@ -252,7 +265,7 @@ class MLB(ScoresBackend):
ret[f'{team}_name'] = self.get_nested(
team_data,
'team:teamName')
ret[f'{team}_abbrev'] = self.get_nested(
ret[f'{team}_abbreviation'] = self.get_nested(
team_data,
'team:abbreviation')
@ -268,7 +281,7 @@ class MLB(ScoresBackend):
ret[f'{team}_score'] = self.get_nested(
linescore,
f'teams:{team}:runs',
default=0)
default='0')
for key in ('delay', 'postponed', 'suspended'):
ret[key] = ''

View File

@ -18,9 +18,8 @@ class NBA(ScoresBackend):
.. rubric:: Available formatters
* `{home_name}` Name of home team
* `{home_city}` Name of home team's city
* `{home_abbrev}` 3-letter abbreviation for home team's city
* `{home_team}` Depending on the value of the ``team_format`` option,
will contain either the home team's name, abbreviation, or city
* `{home_score}` Home team's current score
* `{home_wins}` Home team's number of wins
* `{home_losses}` Home team's number of losses
@ -29,9 +28,8 @@ class NBA(ScoresBackend):
* `{home_favorite}` Displays the value for the :py:mod:`.scores` module's
``favorite`` attribute, if the home team is one of the teams being
followed. Otherwise, this formatter will be blank.
* `{away_name}` Name of away team
* `{away_city}` Name of away team's city
* `{away_abbrev}` 2 or 3-letter abbreviation for away team's city
* `{away_team}` Depending on the value of the ``team_format`` option,
will contain either the away team's name, abbreviation, or city
* `{away_score}` Away team's current score
* `{away_wins}` Away team's number of wins
* `{away_losses}` Away team's number of losses
@ -99,14 +97,22 @@ class NBA(ScoresBackend):
('format_no_games', 'Format used when no tracked games are scheduled '
'for the current day (does not support formatter '
'placeholders)'),
('format_pregame', 'Format used when the game has not yet started'),
('format_in_progress', 'Format used when the game is in progress'),
('format_final', 'Format used when the game is complete'),
('format_postponed', 'Format used when the game has been postponed'),
('format', 'Format used to display game information'),
('status_pregame', 'Format string used for the ``{game_status}`` '
'formatter when the game has not started '),
('status_in_progress', 'Format string used for the ``{game_status}`` '
'formatter when the game is in progress'),
('status_final', 'Format string used for the ``{game_status}`` '
'formatter when the game has finished'),
('status_postponed', 'Format string used for the ``{game_status}`` '
'formatter when the game has been postponed'),
('team_colors', 'Dictionary mapping team abbreviations to hex color '
'codes. If overridden, the passed values will be '
'merged with the defaults, so it is not necessary to '
'define all teams if specifying this value.'),
('team_format', 'One of ``name``, ``abbreviation``, or ``city``. If '
'not specified, takes the value from the ``scores`` '
'module.'),
('date', 'Date for which to display game scores, in **YYYY-MM-DD** '
'format. If unspecified, the current day\'s games will be '
'displayed starting at 10am Eastern time, with last '
@ -155,18 +161,22 @@ class NBA(ScoresBackend):
}
_valid_teams = [x for x in _default_colors]
_valid_display_order = ['in_progress', 'final', 'postponed', 'pregame']
_valid_display_order = ['in_progress', 'final', 'pregame', 'postponed']
display_order = _valid_display_order
format_no_games = 'NBA: No games'
format_pregame = '[{scroll} ]NBA: [{away_favorite} ][{away_seed} ]{away_abbrev} ({away_wins}-{away_losses}) at [{home_favorite} ][{home_seed} ]{home_abbrev} ({home_wins}-{home_losses}) {start_time:%H:%M %Z}'
format_in_progress = '[{scroll} ]NBA: [{away_favorite} ]{away_abbrev} {away_score}[ ({away_power_play})], [{home_favorite} ]{home_abbrev} {home_score}[ ({home_power_play})] ({time_remaining} {quarter})'
format_postponed = '[{scroll} ]NBA: [{away_favorite} ]{away_abbrev} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} ({home_wins}-{home_losses}) PPD'
format_final = '[{scroll} ]NBA: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}) (Final[/{overtime}])'
format = '[{scroll} ]NBA: [{away_favorite} ][{away_seed} ]{away_team} [{away_score} ]({away_wins}-{away_losses}) at [{home_favorite} ][{home_seed} ]{home_team} [{home_score} ]({home_wins}-{home_losses}) {game_status}'
status_pregame = '{start_time:%H:%M %Z}'
status_in_progress = '({time_remaining} {quarter})'
status_final = '(Final[/{overtime}])'
status_postponed = 'PPD'
team_colors = _default_colors
live_url = LIVE_URL
api_url = API_URL
# These will inherit from the Scores class if not overridden
team_format = None
def check_scores(self):
self.get_api_date()
@ -257,20 +267,20 @@ class NBA(ScoresBackend):
for key in ('home', 'away'):
team_key = f'{key}Team'
_update(f'{key}_score', f'{team_key}:score',
callback=self.force_int, default=0)
callback=self.zero_fallback, default='0')
_update(f'{key}_city', f'{team_key}:teamCity')
_update(f'{key}_name', f'{team_key}:teamName')
_update(f'{key}_abbrev', f'{team_key}:teamTricode')
_update(f'{key}_abbreviation', f'{team_key}:teamTricode')
if 'playoffs' in game:
_update(f'{key}_wins', f'playoffs:{key}_wins',
callback=self.force_int, default=0)
callback=self.zero_fallback, default='0')
_update(f'{key}_seed', f'playoffs:{key}_seed',
callback=self.force_int, default=0)
callback=self.zero_fallback, default='0')
else:
_update(f'{key}_wins', f'{team_key}:wins',
callback=self.force_int, default=0)
callback=self.zero_fallback, default='0')
_update(f'{key}_losses', f'{team_key}:losses',
callback=self.force_int, default=0)
callback=self.zero_fallback, default='0')
ret[f'{key}_seed'] = ''
if 'playoffs' in game:

View File

@ -21,9 +21,8 @@ class NHL(ScoresBackend):
.. rubric:: Available formatters
* `{home_name}` Name of home team
* `{home_city}` Name of home team's city
* `{home_abbrev}` 3-letter abbreviation for home team's city
* `{home_team}` Depending on the value of the ``team_format`` option,
will contain either the home team's name, abbreviation, or city
* `{home_score}` Home team's current score
* `{home_wins}` Home team's number of wins
* `{home_losses}` Home team's number of losses
@ -33,9 +32,8 @@ class NHL(ScoresBackend):
followed. Otherwise, this formatter will be blank.
* `{home_empty_net}` Shows the value from the ``empty_net`` parameter
when the home team's net is empty.
* `{away_name}` Name of away team
* `{away_city}` Name of away team's city
* `{away_abbrev}` 2 or 3-letter abbreviation for away team's city
* `{away_team}` Depending on the value of the ``team_format`` option,
will contain either the away team's name, abbreviation, or city
* `{away_score}` Away team's current score
* `{away_wins}` Away team's number of wins
* `{away_losses}` Away team's number of losses
@ -74,8 +72,7 @@ class NHL(ScoresBackend):
backends=[
nhl.NHL(
favorite_teams=['CHI'],
format_pregame = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} ({away_wins}) at [{home_favorite} ]{home_abbrev} ({home_wins}) {start_time:%H:%M %Z}',
format_final = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}) (Final[/{overtime}])',
format='[{scroll} ]NHL: [{away_favorite} ]{away_team} ({away_wins}) at [{home_favorite} ]{home_team} ({home_wins}) {game_status}'
),
],
)
@ -133,10 +130,15 @@ class NHL(ScoresBackend):
('format_no_games', 'Format used when no tracked games are scheduled '
'for the current day (does not support formatter '
'placeholders)'),
('format_pregame', 'Format used when the game has not yet started'),
('format_in_progress', 'Format used when the game is in progress'),
('format_final', 'Format used when the game is complete'),
('format_postponed', 'Format used when the game has been postponed'),
('format', 'Format used to display game information'),
('status_pregame', 'Format string used for the ``{game_status}`` '
'formatter when the game has not started '),
('status_in_progress', 'Format string used for the ``{game_status}`` '
'formatter when the game is in progress'),
('status_final', 'Format string used for the ``{game_status}`` '
'formatter when the game has finished'),
('status_postponed', 'Format string used for the ``{game_status}`` '
'formatter when the game has been postponed'),
('empty_net', 'Value for the ``{away_empty_net}`` or '
'``{home_empty_net}`` formatter when the net is empty. '
'When the net is not empty, these formatters will be '
@ -145,6 +147,9 @@ class NHL(ScoresBackend):
'codes. If overridden, the passed values will be '
'merged with the defaults, so it is not necessary to '
'define all teams if specifying this value.'),
('team_format', 'One of ``name``, ``abbreviation``, or ``city``. If '
'not specified, takes the value from the ``scores`` '
'module.'),
('date', 'Date for which to display game scores, in **YYYY-MM-DD** '
'format. If unspecified, the current day\'s games will be '
'displayed starting at 10am Eastern time, with last '
@ -198,20 +203,24 @@ class NHL(ScoresBackend):
}
_valid_teams = [x for x in _default_colors]
_valid_display_order = ['in_progress', 'final', 'postponed', 'pregame']
_valid_display_order = ['in_progress', 'final', 'pregame', 'postponed']
display_order = _valid_display_order
format_no_games = 'NHL: No games'
format_pregame = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} ({away_wins}-{away_losses}-{away_otl}) at [{home_favorite} ]{home_abbrev} ({home_wins}-{home_losses}-{home_otl}) {start_time:%H:%M %Z}'
format_in_progress = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} {away_score}[ ({away_power_play})][ ({away_empty_net})], [{home_favorite} ]{home_abbrev} {home_score}[ ({home_power_play})][ ({home_empty_net})] ({time_remaining} {period})'
format_final = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} {away_score} ({away_wins}-{away_losses}-{away_otl}) at [{home_favorite} ]{home_abbrev} {home_score} ({home_wins}-{home_losses}-{home_otl}) (Final[/{overtime}])'
format_postponed = '[{scroll} ]NHL: [{away_favorite} ]{away_abbrev} ({away_wins}-{away_losses}-{away_otl}) at [{home_favorite} ]{home_abbrev} ({home_wins}-{home_losses}-{home_otl}) PPD'
format = '[{scroll} ]NHL: [{away_favorite} ]{away_team} [{away_score} ]({away_wins}-{away_losses}-{away_otl}) at [{home_favorite} ]{home_team} [{home_score} ]({home_wins}-{home_losses}-{home_otl}) {game_status}'
status_pregame = '{start_time:%H:%M %Z}'
status_in_progress = '({time_remaining} {period})'
status_final = '(Final[/{overtime}])'
status_postponed = 'PPD'
empty_net = 'EN'
team_colors = _default_colors
live_url = LIVE_URL
scoreboard_url = SCOREBOARD_URL
api_url = API_URL
# These will inherit from the Scores class if not overridden
team_format = None
@require(internet)
def check_scores(self):
self.get_api_date()
@ -265,48 +274,48 @@ class NHL(ScoresBackend):
pp_strength = self.get_nested(linescore, 'powerPlayStrength')
for team in ('away', 'home'):
team_data = self.get_nested(game, 'teams:%s' % team, default={})
team_data = self.get_nested(game, f'teams:{team}', default={})
if team == 'home':
ret['venue'] = self.get_nested(team_data, 'venue:name')
ret['%s_score' % team] = self.get_nested(
ret[f'{team}_score'] = self.get_nested(
team_data,
'score',
callback=self.force_int,
callback=self.zero_fallback,
default=0)
ret['%s_wins' % team] = self.get_nested(
ret[f'{team}_wins'] = self.get_nested(
team_data,
'leagueRecord:wins',
callback=self.force_int,
callback=self.zero_fallback,
default=0)
ret['%s_losses' % team] = self.get_nested(
ret[f'{team}_losses'] = self.get_nested(
team_data,
'leagueRecord:losses',
callback=self.force_int,
callback=self.zero_fallback,
default=0)
ret['%s_otl' % team] = self.get_nested(
ret[f'{team}_otl'] = self.get_nested(
team_data,
'leagueRecord:ot',
callback=self.force_int,
callback=self.zero_fallback,
default=0)
ret['%s_city' % team] = self.get_nested(
ret[f'{team}_city'] = self.get_nested(
team_data,
'team:shortName')
ret['%s_name' % team] = self.get_nested(
ret[f'{team}_name'] = self.get_nested(
team_data,
'team:teamName')
ret['%s_abbrev' % team] = self.get_nested(
ret[f'{team}_abbreviation'] = self.get_nested(
team_data,
'team:abbreviation')
ret['%s_power_play' % team] = self.get_nested(
ret[f'{team}_power_play'] = self.get_nested(
linescore,
'teams:%s:powerPlay' % team,
f'teams:{team}:powerPlay',
callback=lambda x: pp_strength if x and pp_strength != 'Even' else '')
ret['%s_empty_net' % team] = self.get_nested(
ret[f'{team}_empty_net'] = self.get_nested(
linescore,
'teams:%s:goaliePulled' % team,
f'teams:{team}:goaliePulled',
callback=lambda x: self.empty_net if x else '')
if game.get('gameType') == 'P':