Skip to content

Commit 5def049

Browse files
authored
Merge pull request #877 from python-cmd2/changelog_fix
Changelog fix for removed ansi.FG_COLORS and ansi.BG_COLORS
2 parents 3f07590 + 715af60 commit 5def049

File tree

7 files changed

+128
-100
lines changed

7 files changed

+128
-100
lines changed

CHANGELOG.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
## 1.0.0-rc1 (TBD, 2020)
22
* Enhancements
33
* Changed the default help text to make `help -v` more discoverable
4+
* **set** command now supports tab-completion of values
45
* Added `add_settable()` and `remove_settable()` convenience methods to update `self.settable` dictionary
56
* Added convenience `ansi.fg` and `ansi.bg` enums of foreground and background colors
67
* `ansi.style()` `fg` argument can now either be of type `str` or `ansi.fg`
78
* `ansi.style()` `bg` argument can now either be of type `str` or `ansi.bg`
89
* This supports IDE auto-completion of color names
10+
* The enums also support
11+
* `f-strings` and `format()` calls (e.g. `"{}hello{}".format(fg.blue, fg.reset)`)
12+
* string concatenation (e.g. `fg.blue + "hello" + fg.reset`)
913
* Breaking changes
1014
* Renamed `locals_in_py` attribute of `cmd2.Cmd` to `self_in_py`
1115
* The following public attributes of `cmd2.Cmd` are no longer settable at runtime by default:
@@ -16,8 +20,9 @@
1620
* It is now a Dict[str, Settable] instead of Dict[str, str]
1721
* setting onchange callbacks have a new method signature and must be added to the
1822
Settable instance in order to be called
19-
* **set** command now supports tab-completion of values
2023
* Removed `cast()` utility function
24+
* Removed `ansi.FG_COLORS` and `ansi.BG_COLORS` dictionaries
25+
* Replaced with `ansi.fg` and `ansi.bg` enums providing similar but improved functionality
2126

2227
## 0.9.25 (January 26, 2020)
2328
* Enhancements

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Instructions for implementing each feature follow.
9191
class MyApp(cmd2.Cmd):
9292
def do_foo(self, args):
9393
"""This docstring is the built-in help for the foo command."""
94-
self.poutput(cmd2.style('foo bar baz', fg='red'))
94+
self.poutput(cmd2.style('foo bar baz', fg=cmd2.fg.red))
9595
```
9696
- By default the docstring for your **do_foo** method is the help for the **foo** command
9797
- NOTE: This doesn't apply if you use one of the `argparse` decorators mentioned below

cmd2/ansi.py

+50-41
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
Support for ANSI escape sequences which are used for things like applying style to text,
44
setting the window title, and asynchronous alerts.
55
"""
6-
from enum import Enum, unique
76
import functools
87
import re
8+
from enum import Enum
99
from typing import Any, IO, List, Union
1010

1111
import colorama
@@ -27,11 +27,47 @@
2727
ANSI_STYLE_RE = re.compile(r'\x1b\[[^m]*m')
2828

2929

30+
class ColorBase(Enum):
31+
"""
32+
Base class used for defining color enums. See fg and bg classes for examples.
33+
34+
Child classes should define enums in the follow structure:
35+
key: color name (e.g. black)
36+
value: anything that when cast to a string returns an ANSI sequence
37+
"""
38+
def __str__(self) -> str:
39+
"""
40+
Return ANSI color sequence instead of enum name
41+
This is helpful when using a ColorBase in an f-string or format() call
42+
e.g. my_str = "{}hello{}".format(fg.blue, fg.reset)
43+
"""
44+
return str(self.value)
45+
46+
def __add__(self, other: Any) -> str:
47+
"""
48+
Support building a color string when self is the left operand
49+
e.g. fg.blue + "hello"
50+
"""
51+
return str(self) + other
52+
53+
def __radd__(self, other: Any) -> str:
54+
"""
55+
Support building a color string when self is the right operand
56+
e.g. "hello" + fg.reset
57+
"""
58+
return other + str(self)
59+
60+
@classmethod
61+
def colors(cls) -> List[str]:
62+
"""Return a list of color names."""
63+
# Use __members__ to ensure we get all key names, including those which are aliased
64+
return [color for color in cls.__members__]
65+
66+
3067
# Foreground colors
31-
# noinspection PyPep8Naming,DuplicatedCode
32-
@unique
33-
class fg(Enum):
34-
"""Enum class for foreground colors (to support IDE autocompletion)."""
68+
# noinspection PyPep8Naming
69+
class fg(ColorBase):
70+
"""Enum class for foreground colors"""
3571
black = Fore.BLACK
3672
red = Fore.RED
3773
green = Fore.GREEN
@@ -50,26 +86,11 @@ class fg(Enum):
5086
bright_white = Fore.LIGHTWHITE_EX
5187
reset = Fore.RESET
5288

53-
def __str__(self) -> str:
54-
"""Make the value the string representation instead of the enum name."""
55-
return self.value
56-
57-
@staticmethod
58-
def colors() -> List[str]:
59-
"""Return a list of color names."""
60-
return [color.name for color in fg]
61-
62-
@staticmethod
63-
def get_value(name: str) -> str:
64-
"""Retrieve color code by name string."""
65-
return fg.__members__[name].value
66-
6789

6890
# Background colors
69-
# noinspection PyPep8Naming,DuplicatedCode
70-
@unique
71-
class bg(Enum):
72-
"""Enum class for background colors (to support IDE autocompletion)."""
91+
# noinspection PyPep8Naming
92+
class bg(ColorBase):
93+
"""Enum class for background colors"""
7394
black = Back.BLACK
7495
red = Back.RED
7596
green = Back.GREEN
@@ -88,20 +109,6 @@ class bg(Enum):
88109
bright_white = Back.LIGHTWHITE_EX
89110
reset = Back.RESET
90111

91-
def __str__(self) -> str:
92-
"""Make the value the string representation instead of the enum name."""
93-
return self.value
94-
95-
@staticmethod
96-
def colors() -> List[str]:
97-
"""Return a list of color names."""
98-
return [color.name for color in bg]
99-
100-
@staticmethod
101-
def get_value(name: str) -> str:
102-
"""Retrieve color code by name string."""
103-
return bg.__members__[name].value
104-
105112

106113
FG_RESET = fg.reset.value
107114
BG_RESET = bg.reset.value
@@ -162,7 +169,7 @@ def fg_lookup(fg_name: Union[str, fg]) -> str:
162169
return fg_name.value
163170

164171
try:
165-
ansi_escape = fg.get_value(fg_name.lower())
172+
ansi_escape = fg[fg_name.lower()].value
166173
except KeyError:
167174
raise ValueError('Foreground color {!r} does not exist; must be one of: {}'.format(fg_name, fg.colors()))
168175
return ansi_escape
@@ -180,7 +187,7 @@ def bg_lookup(bg_name: Union[str, bg]) -> str:
180187
return bg_name.value
181188

182189
try:
183-
ansi_escape = bg.get_value(bg_name.lower())
190+
ansi_escape = bg[bg_name.lower()].value
184191
except KeyError:
185192
raise ValueError('Background color {!r} does not exist; must be one of: {}'.format(bg_name, bg.colors()))
186193
return ansi_escape
@@ -195,8 +202,10 @@ def style(text: Any, *, fg: Union[str, fg] = '', bg: Union[str, bg] = '', bold:
195202
to undo whatever styling was done at the beginning.
196203
197204
:param text: Any object compatible with str.format()
198-
:param fg: foreground color. Relies on `fg_lookup()` to retrieve ANSI escape based on name or enum. Defaults to no color.
199-
:param bg: background color. Relies on `bg_lookup()` to retrieve ANSI escape based on name or enum. Defaults to no color.
205+
:param fg: foreground color. Relies on `fg_lookup()` to retrieve ANSI escape based on name or enum.
206+
Defaults to no color.
207+
:param bg: background color. Relies on `bg_lookup()` to retrieve ANSI escape based on name or enum.
208+
Defaults to no color.
200209
:param bold: apply the bold style if True. Can be combined with dim. Defaults to False.
201210
:param dim: apply the dim style if True. Can be combined with bold. Defaults to False.
202211
:param underline: apply the underline style if True. Defaults to False.

cmd2/argparse_custom.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ def _match_argument_wrapper(self, action, arg_strings_pattern) -> int:
529529
############################################################################################################
530530

531531

532-
# noinspection PyCompatibility,PyShadowingBuiltins,PyShadowingBuiltins
532+
# noinspection PyCompatibility,PyShadowingBuiltins
533533
class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
534534
"""Custom help formatter to configure ordering of help text"""
535535

examples/async_printing.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import List
1111

1212
import cmd2
13-
from cmd2 import ansi
13+
from cmd2 import style, fg
1414

1515
ALERTS = ["Watch as this application prints alerts and updates the prompt",
1616
"This will only happen when the prompt is present",
@@ -145,20 +145,20 @@ def _generate_colored_prompt(self) -> str:
145145
"""
146146
rand_num = random.randint(1, 20)
147147

148-
status_color = 'reset'
148+
status_color = fg.reset
149149

150150
if rand_num == 1:
151-
status_color = 'bright_red'
151+
status_color = fg.bright_red
152152
elif rand_num == 2:
153-
status_color = 'bright_yellow'
153+
status_color = fg.bright_yellow
154154
elif rand_num == 3:
155-
status_color = 'cyan'
155+
status_color = fg.cyan
156156
elif rand_num == 4:
157-
status_color = 'bright_green'
157+
status_color = fg.bright_green
158158
elif rand_num == 5:
159-
status_color = 'bright_blue'
159+
status_color = fg.bright_blue
160160

161-
return ansi.style(self.visible_prompt, fg=status_color)
161+
return style(self.visible_prompt, fg=status_color)
162162

163163
def _alerter_thread_func(self) -> None:
164164
""" Prints alerts and updates the prompt any time the prompt is showing """

examples/plumbum_colors.py

+33-32
Original file line numberDiff line numberDiff line change
@@ -31,36 +31,37 @@
3131
from cmd2 import ansi
3232
from plumbum.colors import fg, bg
3333

34-
FG_COLORS = {
35-
'black': fg.Black,
36-
'red': fg.DarkRedA,
37-
'green': fg.MediumSpringGreen,
38-
'yellow': fg.LightYellow,
39-
'blue': fg.RoyalBlue1,
40-
'magenta': fg.Purple,
41-
'cyan': fg.SkyBlue1,
42-
'white': fg.White,
43-
'purple': fg.Purple,
44-
}
45-
46-
BG_COLORS = {
47-
'black': bg.BLACK,
48-
'red': bg.DarkRedA,
49-
'green': bg.MediumSpringGreen,
50-
'yellow': bg.LightYellow,
51-
'blue': bg.RoyalBlue1,
52-
'magenta': bg.Purple,
53-
'cyan': bg.SkyBlue1,
54-
'white': bg.White,
55-
}
56-
57-
58-
def get_fg(fg):
59-
return str(FG_COLORS[fg])
60-
61-
62-
def get_bg(bg):
63-
return str(BG_COLORS[bg])
34+
35+
class FgColors(ansi.ColorBase):
36+
black = fg.Black
37+
red = fg.DarkRedA
38+
green = fg.MediumSpringGreen
39+
yellow = fg.LightYellow
40+
blue = fg.RoyalBlue1
41+
magenta = fg.Purple
42+
cyan = fg.SkyBlue1
43+
white = fg.White
44+
purple = fg.Purple
45+
46+
47+
class BgColors(ansi.ColorBase):
48+
black = bg.BLACK
49+
red = bg.DarkRedA
50+
green = bg.MediumSpringGreen
51+
yellow = bg.LightYellow
52+
blue = bg.RoyalBlue1
53+
magenta = bg.Purple
54+
cyan = bg.SkyBlue1
55+
white = bg.White
56+
purple = bg.Purple
57+
58+
59+
def get_fg(name: str):
60+
return str(FgColors[name])
61+
62+
63+
def get_bg(name: str):
64+
return str(BgColors[name])
6465

6566

6667
ansi.fg_lookup = get_fg
@@ -84,8 +85,8 @@ def __init__(self):
8485
speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
8586
speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
8687
speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times')
87-
speak_parser.add_argument('-f', '--fg', choices=FG_COLORS, help='foreground color to apply to output')
88-
speak_parser.add_argument('-b', '--bg', choices=BG_COLORS, help='background color to apply to output')
88+
speak_parser.add_argument('-f', '--fg', choices=FgColors.colors(), help='foreground color to apply to output')
89+
speak_parser.add_argument('-b', '--bg', choices=BgColors.colors(), help='background color to apply to output')
8990
speak_parser.add_argument('-l', '--bold', action='store_true', help='bold the output')
9091
speak_parser.add_argument('-u', '--underline', action='store_true', help='underline the output')
9192
speak_parser.add_argument('words', nargs='+', help='words to say')

tests/test_ansi.py

+29-16
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ def test_style_none():
3232
def test_style_fg():
3333
base_str = HELLO_WORLD
3434
fg_color = 'blue'
35-
ansi_str = ansi.fg.get_value(fg_color) + base_str + ansi.FG_RESET
35+
ansi_str = ansi.fg[fg_color].value + base_str + ansi.FG_RESET
3636
assert ansi.style(base_str, fg=fg_color) == ansi_str
3737

3838

3939
def test_style_bg():
4040
base_str = HELLO_WORLD
4141
bg_color = 'green'
42-
ansi_str = ansi.bg.get_value(bg_color) + base_str + ansi.BG_RESET
42+
ansi_str = ansi.bg[bg_color].value + base_str + ansi.BG_RESET
4343
assert ansi.style(base_str, bg=bg_color) == ansi_str
4444

4545

@@ -65,7 +65,7 @@ def test_style_multi():
6565
base_str = HELLO_WORLD
6666
fg_color = 'blue'
6767
bg_color = 'green'
68-
ansi_str = (ansi.fg.get_value(fg_color) + ansi.bg.get_value(bg_color) +
68+
ansi_str = (ansi.fg[fg_color].value + ansi.bg[bg_color].value +
6969
ansi.INTENSITY_BRIGHT + ansi.INTENSITY_DIM + ansi.UNDERLINE_ENABLE +
7070
base_str +
7171
ansi.FG_RESET + ansi.BG_RESET +
@@ -85,7 +85,7 @@ def test_style_color_not_exist():
8585

8686
def test_fg_lookup_exist():
8787
fg_color = 'green'
88-
assert ansi.fg_lookup(fg_color) == ansi.fg.get_value(fg_color)
88+
assert ansi.fg_lookup(fg_color) == ansi.fg_lookup(ansi.fg.green)
8989

9090

9191
def test_fg_lookup_nonexist():
@@ -94,8 +94,8 @@ def test_fg_lookup_nonexist():
9494

9595

9696
def test_bg_lookup_exist():
97-
bg_color = 'green'
98-
assert ansi.bg_lookup(bg_color) == ansi.bg.get_value(bg_color)
97+
bg_color = 'red'
98+
assert ansi.bg_lookup(bg_color) == ansi.bg_lookup(ansi.bg.red)
9999

100100

101101
def test_bg_lookup_nonexist():
@@ -121,20 +121,33 @@ def test_async_alert_str(cols, prompt, line, cursor, msg, expected):
121121
assert alert_str == expected
122122

123123

124-
def test_fg_enum():
125-
assert ansi.fg_lookup('bright_red') == ansi.fg_lookup(ansi.fg.bright_red)
124+
def test_cast_color_as_str():
125+
assert str(ansi.fg.blue) == ansi.fg.blue.value
126+
assert str(ansi.bg.blue) == ansi.bg.blue.value
127+
128+
129+
def test_color_str_building():
130+
from cmd2.ansi import fg, bg
131+
assert fg.blue + "hello" == fg.blue.value + "hello"
132+
assert bg.blue + "hello" == bg.blue.value + "hello"
133+
assert fg.blue + "hello" + fg.reset == fg.blue.value + "hello" + fg.reset.value
134+
assert bg.blue + "hello" + bg.reset == bg.blue.value + "hello" + bg.reset.value
135+
assert fg.blue + bg.white + "hello" + fg.reset + bg.reset == \
136+
fg.blue.value + bg.white.value + "hello" + fg.reset.value + bg.reset.value
126137

127-
def test_fg_enum_to_str():
128-
assert str(ansi.fg.black) == ansi.fg_lookup('black')
129138

130-
def test_bg_enum():
139+
def test_color_nonunique_values():
140+
class Matching(ansi.ColorBase):
141+
magenta = ansi.fg_lookup('magenta')
142+
purple = ansi.fg_lookup('magenta')
143+
assert sorted(Matching.colors()) == ['magenta', 'purple']
144+
145+
146+
def test_color_enum():
147+
assert ansi.fg_lookup('bright_red') == ansi.fg_lookup(ansi.fg.bright_red)
131148
assert ansi.bg_lookup('green') == ansi.bg_lookup(ansi.bg.green)
132149

133-
def test_bg_enum_to_str():
134-
assert str(ansi.bg.blue) == ansi.bg_lookup('blue')
135150

136-
def test_fg_colors():
151+
def test_colors_list():
137152
assert list(ansi.fg.__members__.keys()) == ansi.fg.colors()
138-
139-
def test_bg_colors():
140153
assert list(ansi.bg.__members__.keys()) == ansi.bg.colors()

0 commit comments

Comments
 (0)