Skip to content

Commit

Permalink
Rewrite scores backends to use a single format string (#833)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
terminalmage authored Jan 27, 2022
1 parent 34af135 commit c4876ed
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 91 deletions.
75 changes: 57 additions & 18 deletions i3pystatus/scores/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,13 @@ def add_ordinal(number):
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:
Expand Down Expand Up @@ -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()
Expand All @@ -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='⬇',
Expand All @@ -240,7 +244,6 @@ class Scores(Module):
teams=['GSW'],
all_games=False,
),
epl.EPL(),
],
)
Expand Down Expand Up @@ -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 = []
Expand All @@ -371,6 +375,7 @@ class Scores(Module):
colorize_teams = False
scroll_arrow = '⬍'
refresh_icon = '⟳'
team_format = 'name'

output = {'full_text': ''}
game_map = {}
Expand Down Expand Up @@ -417,6 +422,9 @@ def init(self):
)
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()
Expand Down Expand Up @@ -566,7 +574,14 @@ def check_scores(self, force=False):
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
Expand Down Expand Up @@ -623,31 +638,55 @@ def refresh_display(self):
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}

Expand Down
51 changes: 32 additions & 19 deletions i3pystatus/scores/mlb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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 '
Expand All @@ -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 '
Expand Down Expand Up @@ -169,22 +178,26 @@ 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
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()
Expand Down Expand Up @@ -252,7 +265,7 @@ def process_game(self, game):
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')

Expand All @@ -268,7 +281,7 @@ def process_game(self, game):
ret[f'{team}_score'] = self.get_nested(
linescore,
f'teams:{team}:runs',
default=0)
default='0')

for key in ('delay', 'postponed', 'suspended'):
ret[key] = ''
Expand Down
52 changes: 31 additions & 21 deletions i3pystatus/scores/nba.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -257,20 +267,20 @@ def _update(ret_key, game_key=None, callback=None, default='?'):
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:
Expand Down
Loading

0 comments on commit c4876ed

Please sign in to comment.