Skip to content

Commit

Permalink
fix: use rich.print thoughout the wizard for wrapping and highlighting
Browse files Browse the repository at this point in the history
Fixes formatting in many places.
Adds nice highlighting in some.
But, do not redefine builtin print, use rich_print to be clear everywhere.

Fixes #546: question titles being truncated instead of wrapped.
  • Loading branch information
joanise committed Sep 25, 2024
1 parent ef919a5 commit a8f968c
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 71 deletions.
29 changes: 15 additions & 14 deletions everyvoice/tests/test_wizard.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Unit tests for the wizard module"""

import os
import re
import string
import tempfile
from enum import Enum
Expand Down Expand Up @@ -266,7 +267,7 @@ def test_bad_contact_email_step(self):
self.assertFalse(step.validate("test@test."))
self.assertTrue(step.validate("[email protected]"))
self.assertFalse(step.validate(""))
output = stdout.getvalue()
output = stdout.getvalue().replace(" \n", " ")
# Supporting email-validator prior and post 2.2.0 where the error string changed.
self.assertTrue(
"It must have exactly one @-sign" in output
Expand Down Expand Up @@ -382,21 +383,21 @@ def test_dataset_name(self):
with patch_input(("", "bad/name", "good-name"), True):
with capture_stdout() as stdout:
step.run()
output = stdout.getvalue().split("\n")
output = stdout.getvalue().replace(" \n", " ").split("\n")
self.assertIn("your dataset needs a name", output[0])
self.assertIn("is not valid", output[1])
self.assertIn("finished the configuration", output[2])
self.assertIn("finished the configuration", "".join(output[2:]))
self.assertTrue(step.completed)

def test_speaker_name(self):
step = dataset.AddSpeakerStep()
with patch_input(("", "bad/name", "good-name"), True):
with capture_stdout() as stdout:
step.run()
output = stdout.getvalue().split("\n")
output = stdout.getvalue().replace(" \n", " ").split("\n")
self.assertIn("speaker needs an ID", output[0])
self.assertIn("is not valid", output[1])
self.assertIn("will be used as the speaker ID", output[2])
self.assertIn("will be used as the speaker ID", "".join(output[2:]))
self.assertTrue(step.completed)

def test_wavs_dir(self):
Expand Down Expand Up @@ -430,7 +431,7 @@ def test_sample_rate_config(self):
):
with capture_stdout() as stdout:
step.run()
output = stdout.getvalue().split("\n")
output = stdout.getvalue().replace(" \n", " ").split(".\n")
for i in range(4):
self.assertIn("not a valid sample rate", output[i])
self.assertTrue(step.completed)
Expand Down Expand Up @@ -605,7 +606,7 @@ def test_dataset_subtour(self):
with patch_menu_prompt(1), capture_stdout() as out:
validate_wavs_step.run()
self.assertEqual(step.state[SN.validate_wavs_step][:2], "No")
self.assertIn("Warning: 4 wav files were not found", out.getvalue())
self.assertRegex(out.getvalue(), "Warning: .*4.* wav files were not found")

text_processing_step = find_step(SN.text_processing_step, tour.steps)
# 0 is lowercase, 1 is NFC Normalization, select both
Expand Down Expand Up @@ -712,9 +713,9 @@ def test_wrong_fileformat_psv(self):
# try with: 1/tsv (wrong), 2/csv (wrong), 3/festival (wrong) and finally 0 tsv (right)
with patch_menu_prompt((1, 2, 3, 0), multi=True) as stdout:
format_step.run()
output = stdout.getvalue()
self.assertIn("does not look like a 'tsv'", output)
self.assertIn("does not look like a 'csv'", output)
output = re.sub(r" *\n", " ", stdout.getvalue())
self.assertRegex(output, "does not look like a .*'tsv'")
self.assertRegex(output, "does not look like a .*'csv'")
self.assertIn("is not in the festival format", output)
self.assertTrue(format_step.completed)
# print(format_step.state)
Expand All @@ -736,10 +737,10 @@ def test_wrong_fileformat_festival(self):
# try with: 0/psv (wrong), 1/tsv (wrong), 2/csv (wrong), and finally 3/festival (right)
with patch_menu_prompt((0, 1, 2, 3), multi=True) as stdout:
format_step.run()
output = stdout.getvalue()
self.assertIn("does not look like a 'psv'", output)
self.assertIn("does not look like a 'tsv'", output)
self.assertIn("does not look like a 'csv'", output)
output = stdout.getvalue().replace(" \n", " ")
self.assertRegex(output, "does not look like a .*'psv'")
self.assertRegex(output, "does not look like a .*'tsv'")
self.assertRegex(output, "does not look like a .*'csv'")
self.assertTrue(format_step.completed)
# print(format_step.state)

Expand Down
9 changes: 6 additions & 3 deletions everyvoice/wizard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Optional, Sequence

from anytree import RenderTree
from rich import print as rich_print

from .utils import EnumDict as State
from .utils import NodeMixinWithNavigation
Expand Down Expand Up @@ -112,7 +113,9 @@ def run(self):
else:
self._validation_failures += 1
if self._validation_failures > 20:
print(f"ERROR: {self.name} giving up after 20 validation failures.")
rich_print(
f"ERROR: {self.name} giving up after 20 validation failures."
)
sys.exit(1)
self.run()

Expand Down Expand Up @@ -180,7 +183,7 @@ def run(self):
try:
node.run()
except KeyboardInterrupt:
print("\nKeyboard Interrupt")
rich_print("\nKeyboard Interrupt")
node = self.keyboard_interrupt_action(node)
continue
node = node.next()
Expand All @@ -191,7 +194,7 @@ def visualize(self, highlight: Optional[Step] = None):
treestr = f"{pre}{node.name}"
if node == highlight:
treestr += " <========"
print(treestr.ljust(8))
rich_print(treestr.ljust(8))


class StepNames(Enum):
Expand Down
32 changes: 16 additions & 16 deletions everyvoice/wizard/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import questionary
from email_validator import EmailNotValidError, validate_email
from rich import print
from rich import print as rich_print
from rich.panel import Panel
from rich.style import Style

Expand Down Expand Up @@ -51,18 +51,18 @@ def prompt(self):

def validate(self, response):
if len(response) == 0:
print("Sorry, your project needs a name. ")
rich_print("Sorry, your project needs a name. ")
return False
sanitized_path = slugify(response)
if not sanitized_path == response:
print(
rich_print(
f"Sorry, the project name '{response}' is not valid, since it will be used to create a folder and special characters are not permitted for folder names. Please re-type something like '{sanitized_path}' instead."
)
return False
return True

def effect(self):
print(
rich_print(
f"Great! Launching Configuration Wizard 🧙 for project named '{self.response}'."
)

Expand All @@ -77,12 +77,12 @@ def validate(self, response):
# Some languages don't use first and last names, so we can't necessarily check that response.split() > 1
# It would be nice to have a better check here though.
if len(response) < 3:
print("Sorry, EveryVoice requires a name to help prevent misuse.")
rich_print("Sorry, EveryVoice requires a name to help prevent misuse.")
return False
return True

def effect(self):
print(f"Great! Nice to meet you, '{self.response}'.")
rich_print(f"Great! Nice to meet you, '{self.response}'.")


class ContactEmailStep(Step):
Expand Down Expand Up @@ -112,16 +112,16 @@ def validate(self, response):
except EmailNotValidError as e:
# The exception message is a human-readable explanation of why it's
# not a valid (or deliverable) email address.
print("EveryVoice requires a valid email address to prevent misuse.")
print(str(e))
rich_print("EveryVoice requires a valid email address to prevent misuse.")
rich_print(str(e))
return False
return True

def effect(self):
emailinfo = validate_email(self.response, check_deliverability=False)
email = emailinfo.normalized
self.response = email
print(
rich_print(
f"Great! Your contact email '{self.response}' will be saved to your models."
)

Expand Down Expand Up @@ -154,7 +154,7 @@ def can_mkdir(self, path: Path) -> bool:
d.mkdir()
dirs_made.append(d)
except OSError as e:
print(f"Sorry, could not create '{d}': {e}.")
rich_print(f"Sorry, could not create '{d}': {e}.")
return False
finally:
for d in reversed(dirs_made):
Expand All @@ -164,12 +164,12 @@ def can_mkdir(self, path: Path) -> bool:
def validate(self, response) -> bool:
path = Path(response)
if path.is_file():
print(f"Sorry, '{path}' is a file. Please select a directory.")
rich_print(f"Sorry, '{path}' is a file. Please select a directory.")
return False
assert self.state is not None, "OutputPathStep requires NameStep"
output_path = path / self.state.get(StepNames.name_step, "DEFAULT_NAME")
if output_path.exists():
print(
rich_print(
f"Sorry, '{output_path}' already exists. "
"Please choose another output directory or start again and choose a different project name."
)
Expand All @@ -178,14 +178,14 @@ def validate(self, response) -> bool:
# We create the output directory in validate() instead of effect() so that
# failure can be reported to the user and the question asked again if necessary.
if not self.can_mkdir(output_path):
print("Please choose another output directory.")
rich_print("Please choose another output directory.")
return False

self.output_path = output_path
return True

def effect(self):
print(
rich_print(
f"The Configuration Wizard 🧙 will put your files here: '{self.output_path}'"
)

Expand Down Expand Up @@ -463,7 +463,7 @@ def effect(self):
(config_dir / e2e_config_path).absolute(),
)

print(
rich_print(
Panel(
f"You've finished configuring your dataset. Your files are located at {config_dir.absolute()}",
title="Congratulations 🎉",
Expand Down Expand Up @@ -505,7 +505,7 @@ def effect(self):
self,
)
elif len([key for key in self.state.keys() if key.startswith("dataset_")]) == 0:
print("No dataset to save, exiting without saving any configuration.")
rich_print("No dataset to save, exiting without saving any configuration.")
else:
self.tour.add_step(
ConfigFormatStep(name=StepNames.config_format_step), self
Expand Down
Loading

0 comments on commit a8f968c

Please sign in to comment.