diff --git a/.vscode/launch.json b/.vscode/launch.json index d624380..ce17a37 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "configurations": [ { "name": "Basic examples", - "type": "python", + "type": "debugpy", "request": "launch", "program": "examples/basic_example.py", "cwd": "${workspaceFolder}", @@ -18,7 +18,7 @@ }, { "name": "Complete example set", - "type": "python", + "type": "debugpy", "request": "launch", "program": "examples/complete_example.py", "cwd": "${workspaceFolder}", @@ -29,8 +29,8 @@ "justMyCode": true }, { - "name": "Run Tinta tests", - "type": "python", + "name": "Run Tinta tests (workspace python)", + "type": "debugpy", "request": "launch", "console": "integratedTerminal", "module": "pytest", @@ -38,6 +38,12 @@ "_PYTEST_RAISE": "1" }, "args": ["-xv"] + }, + { + "name": "Run Tinta tests (all versions)", + "type": "debugpy", + "preLaunchTask": "Run All Pytest Versions", + "request": "launch" } ] } diff --git a/examples/basic_example.py b/examples/basic_example.py index b7c7d07..f89b09b 100644 --- a/examples/basic_example.py +++ b/examples/basic_example.py @@ -29,119 +29,140 @@ from pathlib import Path sys.path.append(str(Path().cwd().parent / "tinta")) -# pylint: disable=wrong-import-position, wrong-import-order, import-error -from tinta import Tinta # noqa: E402 - -# End import shim - -Tinta.load_colors("examples/colors.ini") - -# The most basic example we can get. -Tinta("That's a really nice car!").print() - -# Prints the entire string in red. -Tinta().red("That's a really nice red car!").print() - -# Prints the first half in blue, then the rest in red. -Tinta().blue("That's a cool blue car").red("but not as cool as my red one").print() - -# Prints the first few words in green, separated by _*_, -# then the final word in purple. -Tinta().green( - "Sometimes", "We", "Want", "To", "Join", "Words", "Differently", sep=" * " -).purple("Neat!").print() - -# Here we underline a word. -Tinta().gray("It's").underline("really").normal( - "important to be able to style things." -).print() - -# Here we tried to print in pink, but used the plaintext arg in print. -# You'll notice we still support Python's multiline \ feature. -Tinta().pink( - "But it's equally important to be able " "to print things in plaintext, too" -).print(plaintext=True) - -# Let's try some f-strings. -animal = "Tiger" -Tinta().orange(f"Hey, we support f-strings, too! Raaarrr, said Ms. {animal}.") - -# And dimming some text. -Tinta().blue("We can").dim("dim").normal("things").print() - -# Things getting out of hand? You can break them up easily in multiple -# lines, without having to fiddle with \. -tint = Tinta() -tint.push("Sometimes we need to") -tint.pink("break up long lines of text") -tint.gray("to make them easier to read.") -tint.line("We can even write to a new line!") -tint.print() - -# You could do the same using multiple segments, or () -Tinta().vanilla( - "I like ice cream", "it comes in all sorts" "of great and yummy flavors." -).print() - -(Tinta().vanilla("I like ice cream").red("especially with cherries on top.").print()) - -# When you're done printing, Tinta resets itself, but you can still -# reuse the original variable. -tint.push("After a print, Tinta resets itself").green() -tint.line("but you can still use the same initialized version.") -tint.print() - -# Using native print()'s built-in end, we can terminate a string -# without a newline. -Tinta("And of course as always,").print(end="") -Tinta(" you can print with end=''").print() - -# Not enough colors in config.yaml? Add your own on the fly! -Tinta( - "Did you know, you can", "write with ansi codes directly, too?", color=127 -).print() - -# Have some fun with separators. -Tinta("A bird", "I like birds", sep="; ").push( - "And also cats", "and dogs", sep=" " -).print(sep="\n") - -# You could get really fancy and inject some formatted text in the middle, -# using f-strings. -( - Tinta() - .mint("Fate.") - .dark_gray("It protects") - .underline() - .blue(f"fools{Tinta().normal().dark_gray(',').to_str()}", sep="") - .normal() - .pink("little children,") - .dark_gray("and ships named") - .purple("Enterprise.") - .print() -) - -# Tinta is also smart about how we join things together. If you join -# several objects together, it collapses repeated whitespace. You -# can also use 'sep' to force sections to collapse. -t = ( - Tinta() - .pink("A section") - .push() - .white() - .blue("of text", sep="") - .green(",") - .push() - .purple("separated.") -) -t.print() - -# And finally, you can use some helper tools to clear the current -# console and move up a line. -Tinta().yellow("Loading...").print() -time.sleep(1) -Tinta.clearline() -Tinta().green("Done").print() -time.sleep(1) -Tinta.up() -Tinta().green("Done :)").print() + + +def basic(): + # pylint: disable=wrong-import-position, wrong-import-order, import-error + from tinta import Tinta # noqa: E402 + + # End import shim + + Tinta.load_colors("examples/colors.ini") + + # The most basic example we can get. + Tinta("That's a really nice car!").print() + + # Prints the entire string in red. + Tinta().red("That's a really nice red car!").print() + + # Prints the first half in blue, then the rest in red. + Tinta().blue("That's a cool blue car").red("but not as cool as my red one").print() + + # Prints the first few words in green, separated by _*_, + # then the final word in purple. + Tinta().green( + "Sometimes", "We", "Want", "To", "Join", "Words", "Differently", sep=" * " + ).purple("Neat!").print() + + # Here we underline a word. + Tinta().gray("It's").underline("really").normal( + "important to be able to style things." + ).print() + + # Here we tried to print in pink, but used the plaintext arg in print. + # You'll notice we still support Python's multiline \ feature. + Tinta().pink( + "But it's equally important to be able " "to print things in plaintext, too" + ).print(plaintext=True) + + # Let's try some f-strings. + animal = "Tiger" + Tinta().orange(f"Hey, we support f-strings, too! Raaarrr, said Ms. {animal}.") + + # And dimming some text. + Tinta().blue("We can").dim("dim").normal("things").print() + + # Things getting out of hand? You can break them up easily in multiple + # lines, without having to fiddle with \. + tint = Tinta() + tint.push("Sometimes we need to") + tint.pink("break up long lines of text") + tint.gray("to make them easier to read.") + tint.nl("We can even write to a new line!") + tint.print() + + # You could do the same using multiple segments, or () + Tinta().vanilla( + "I like ice cream", "it comes in all sorts" "of great and yummy flavors." + ).print() + + ( + Tinta() + .vanilla("I like ice cream") + .red("especially with cherries on top.") + .print() + ) + + # When you're done printing, Tinta resets itself, but you can still + # reuse the original variable. + tint.push("After a print, Tinta resets itself").green() + tint.nl("but you can still use the same initialized version.") + tint.print() + + # Using native print()'s built-in end, we can terminate a string + # without a newline. + Tinta("And of course as always,").print(end="") + Tinta(" you can print with end=''").print() + + # Not enough colors in config.yaml? Add your own on the fly! + Tinta( + "Did you know, you can", "write with ansi codes directly, too?", color=127 + ).print() + + # Get the string representation of the Tinta object with to_str(). + Tinta("Sometimes you just want to get the string").to_str() + Tinta().purple("Which").pink("is").green("pretty").blue("cool").to_str() + Tinta().vanilla("If you need it in plaintext, you can do that, too.").to_str( + plaintext=True + ) + t = Tinta().mint("⭐like this⭐") + print(f"You can also use a Tinta object in an f-string directly, {t}") + + # Have some fun with separators. + Tinta("A bird", "I like birds", sep="; ").push( + "And also cats", "and dogs", sep=" " + ).print(sep="\n") + + # You could get really fancy and inject some formatted text in the middle, + # using f-strings. + ( + Tinta() + .mint("Fate.") + .dark_gray("It protects") + .underline() + .blue(f"fools{Tinta().normal().dark_gray(',').to_str()}", sep="") + .normal() + .pink("little children,") + .dark_gray("and ships named") + .purple("Enterprise.") + .print() + ) + + # Tinta is also smart about how we join things together. If you join + # several objects together, it collapses repeated whitespace. You + # can also use 'sep' to force sections to collapse. + t = ( + Tinta() + .pink("A section") + .push() + .white() + .blue("of text", sep="") + .green(",") + .push() + .purple("separated.") + ) + t.print() + + # And finally, you can use some helper tools to clear the current + # console and move up a line. + Tinta().yellow("Loading...").print() + time.sleep(1) + Tinta.clearline() + Tinta().green("Done").print() + time.sleep(1) + Tinta.up() + Tinta().green("Done :)").print() + + +if __name__ == "__main__": + basic() diff --git a/examples/complete_example.py b/examples/complete_example.py index 1a9b38f..a7a7ec7 100644 --- a/examples/complete_example.py +++ b/examples/complete_example.py @@ -26,6 +26,7 @@ """ import sys from pathlib import Path +from typing import Iterable, List sys.path.append(str(Path().cwd().parent / "tinta")) # pylint: disable=wrong-import-position, wrong-import-order, import-error @@ -33,85 +34,140 @@ # End import shim -Tinta.load_colors("examples/colors.ini") - -# from colors.ini: - -# [colors] -# green = 35 -# red = 1 -# blue = 32 -# light_blue = 37 -# yellow = 214 -# amber = 208 -# olive = 106 -# orange = 166 -# purple = 18 -# pink = 197 -# gray = 243 -# dark_gray = 235 -# light_gray = 248 -# black = 0 -# white = 255 -# error = 1 -# debug = 160 - -# get names of colors from colors.ini -colors = [] -with open("examples/colors.ini", "r", encoding="utf-8") as f: - colors = [line.split("=")[0].strip() for line in f.readlines() if "=" in line] - -w = 26 -GAP = "\n\n" - -method = 'Tinta().print("plain")'.ljust(w) -Tinta(method, " → plain").print(end=GAP) - -for col in colors: - viz = f" → {col}" - method = f'Tinta().tint("{col}")'.ljust(w) - Tinta().tint(col, method, viz).print() +def header(s): + print("\n\n" + s.center(80, "-") + "\n") + + +def sort_colors(colors: Iterable[str]) -> List[str]: + return list( + sorted( + colors, + key=lambda x: ( + "gre" if "gray" in x or "grey" in x else x.split("_")[-1].lower() + ), + ) + ) + + +def complete(): + + _orig_colors_ini = Tinta.colors._colors_ini_path + + Tinta.load_colors("examples/colors.ini") + + # from colors.ini: + + # [colors] + # green = 35 + # red = 1 + # blue = 32 + # light_blue = 37 + # yellow = 214 + # amber = 208 + # olive = 106 + # orange = 166 + # purple = 18 + # pink = 197 + # gray = 243 + # dark_gray = 235 + # light_gray = 248 + # black = 0 + # white = 255 + # error = 1 + # debug = 160 + + # get names of colors from colors.ini + colors = [] + with open("examples/colors.ini", "r", encoding="utf-8") as f: + colors = [line.split("=")[0].strip() for line in f.readlines() if "=" in line] + + for color in colors.copy(): + if "gray" in color: + # insert grey aliases + colors.append(color.replace("gray", "grey")) + + colors = sort_colors(colors) + + w = 28 + header("Color methods") + GAP = "\n\n" + + method = 'Tinta().print("plain")'.ljust(w) + Tinta(method, " → plain").print(end=GAP) + + for col in colors: + viz = f" → {col}" + method = f'Tinta().tint("{col}")'.ljust(w) + Tinta().tint(col, method, viz).print() + + t = Tinta() + func = getattr(t, col) + method = f't.{col}("{col}")'.ljust(w) + func(method, viz).print(end=GAP) + + w = 32 + + header("Style methods") + + method = 'Tinta().bold("bold")'.ljust(w) + Tinta(method, " →").bold("bold").print() + + method = 'Tinta().underline("underline")'.ljust(w) + Tinta(method, " →").underline("underline").print() + + method = 'Tinta()._("underline")'.ljust(w) + Tinta(method, " →")._("underline").print() + + # dim + method = 'Tinta().dim("dim")'.ljust(w) + Tinta(method).dim(" → dim").print(end=GAP) + + header("Chaining methods (builder pattern)") + + # chain methods + t = Tinta() + for i, col in enumerate(colors): + t.tint(col, col, sep="\n" if i % 2 != 0 else " ") + t.print(end=GAP) + + header("Mixed formatting") + + # complex formatting + t = Tinta() + t.vanilla("vanilla").bold("bold", sep="\n") + t.clear() + t.mint("mint").underline("underline", sep="\n") + t.clear() + t.olive("olive").dim("dim", sep="\n") + + t.print(end=GAP) + + header("Clearing the console") + + # clear t = Tinta() - func = getattr(t, col) - method = f't.{col}("{col}")'.ljust(w) - func(method, viz).print(end=GAP) - -w = 30 - -method = 'Tinta().bold("bold")'.ljust(w) -Tinta(method, " →").bold("bold").print() - -method = 'Tinta().underline("underline")'.ljust(w) -Tinta(method, " →").underline("underline").print() - -# dim -method = 'Tinta().dim("dim")'.ljust(w) -Tinta(method).dim(" → dim").print(end=GAP) - -# chain methods -t = Tinta() -for i, col in enumerate(colors): - t.tint(col, col, sep="\n" if i % 2 != 0 else " ") -t.print(end=GAP) - -# complex formatting -t = Tinta() -t.vanilla("vanilla").bold("bold", sep="\n") -t.clear() -t.mint("mint").underline("underline", sep="\n") -t.clear() -t.olive("olive").dim("dim", sep="\n") - -t.print(end=GAP) - -# clear -t = Tinta() -t.vanilla("vanilla").bold("bold", sep="\n") -t.clear("plain text", sep="\n") -t.mint("mint").underline("underline", sep="\n") -t.olive("olive inherits underline", sep="\n").dim("dim inherits both", sep="\n") -t.clear("clear clears all", sep="\n") -t.amber("so we can start fresh") - -t.print(end=GAP) + t.vanilla("vanilla").bold("bold", sep="\n") + t.clear("plain text", sep="\n") + t.mint("mint").underline("underline", sep="\n") + t.olive("olive inherits underline", sep="\n").dim("dim inherits both", sep="\n") + t.clear("clear clears all", sep="\n") + t.amber("so we can start fresh") + + t.print() + + header("Tint via color codes") + + def print_columns(cols, *, width=4, indent=0): + print("".ljust(width + 1) * indent, end="") + for i in range(256): + end = "\n" if i % cols == cols - indent - 1 else " " + Tinta().tint(f"{str(i).ljust(width)}", color=i).print(end=end) + + print_columns(12, indent=8) + + Tinta.colors._colors_ini_path = _orig_colors_ini + + +if __name__ == "__main__": + complete() diff --git a/tests/launch.json b/tests/launch.json deleted file mode 100644 index 4c49321..0000000 --- a/tests/launch.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Module", - "type": "python", - "request": "launch", - "console": "externalTerminal", - "module": "pytest", - "env": { - "_PYTEST_RAISE": "1" - }, - "args": [ - "-xv" - ] - } - ] -} \ No newline at end of file diff --git a/tests/test_tinta.py b/tests/test_tinta.py index e06cd26..d4e2b13 100644 --- a/tests/test_tinta.py +++ b/tests/test_tinta.py @@ -21,7 +21,7 @@ import re -from typing import Any, Callable, List, Tuple +from typing import Any, Callable, Dict, List, Tuple import pytest from pytest import CaptureFixture @@ -106,7 +106,7 @@ class TestChaining: def test_chaining_resets_correctly( self, Testa: Callable, - kwargs: dict[str, Any], + kwargs: Dict[str, Any], expected: str, capfd: CaptureFixture[str], ): @@ -217,10 +217,28 @@ def test_print_empty_str(self): def test_print_none(self): Tinta(None).print() + def test_empty_color_call(self): + t = Tinta("Plain").green() + t.push("Green") + t.print() + def test_tint_color_0(self): Tinta().tint(0, "Zero").print() +class TestExamples: + + def test_basic_example(self): + from examples.basic_example import basic + + basic() + + def test_complete_example(self): + from examples.complete_example import complete + + complete() + + class TestWhiteSpace: def test_print_whitespace(self): diff --git a/tinta/ansi.py b/tinta/ansi.py index 54e5732..7320f9d 100644 --- a/tinta/ansi.py +++ b/tinta/ansi.py @@ -23,6 +23,17 @@ config = configparser.ConfigParser() +def _alias_key(colors: "AnsiColors", k: str, search: str, repl: str): + """Sets up an alias key for a color.""" + if k not in config["colors"] and k not in colors.__dict__: + raise MissingColorError(f"Color '{k}' not found in colors.ini.") + if search.lower() in k.lower(): + alias_key = k.replace(search.lower(), repl.lower()) + if alias_key not in config["colors"] and alias_key not in colors.__dict__: + colors.__setattr__(alias_key, int(config["colors"][k])) + config["colors"][alias_key] = config["colors"][k] + + class AnsiColors: """Color builder for Tinta's console output. @@ -64,6 +75,11 @@ def __init__(self, path: Optional[Union[str, Path]] = None): for k, v in config["colors"].items(): self.__setattr__(k, int(v)) + _alias_key(self, k, "gray", "grey") + _alias_key(self, k, "grey", "gray") + + self._colors_ini_path = path + def get(self, color: str) -> int: """Returns the ANSI code for a color. diff --git a/tinta/tinta.py b/tinta/tinta.py index 9267a81..83c5e04 100644 --- a/tinta/tinta.py +++ b/tinta/tinta.py @@ -23,7 +23,7 @@ import sys from itertools import zip_longest from pathlib import Path -from typing import Any, cast, List, Optional, overload, Union +from typing import Any, cast, Dict, List, Optional, overload, Union from deprecated import deprecated @@ -109,7 +109,7 @@ class Tinta(metaclass=_MetaTinta): """ _initialized = False - _known_colors: dict[str, Any] = {} + _known_colors: Dict[str, Any] = {} color: Union[int, str] colors: AnsiColors @@ -133,7 +133,7 @@ def __init__( Tinta._known_colors = vars(self.colors) Tinta._initialized = True for c in Tinta._known_colors: - self.__setattr__(c, functools.partial(self.tint, c)) + self.__setattr__(c, functools.partial(self.tint, color=c)) if s: self.push(*s, sep=sep)