Skip to content

Commit

Permalink
Merge pull request #1722 from rmartin16/textwrap
Browse files Browse the repository at this point in the history
Consolidate text wrapping support for console printing
  • Loading branch information
freakboy3742 committed Apr 8, 2024
2 parents 9dc8c15 + 526baf5 commit 31a8b91
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 49 deletions.
1 change: 1 addition & 0 deletions changes/1722.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The support for textwrapping messages printed to the console was consolidated to allow for broader usage.
2 changes: 1 addition & 1 deletion src/briefcase/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def main():
console = Console(printer=printer)
logger = Log(printer=printer)
try:
Command, extra_cmdline = parse_cmdline(sys.argv[1:])
Command, extra_cmdline = parse_cmdline(sys.argv[1:], console=console)
command = Command(logger=logger, console=console)
options, overrides = command.parse_options(extra=extra_cmdline)
command.parse_config(
Expand Down
28 changes: 16 additions & 12 deletions src/briefcase/cmdline.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import argparse
import shutil
import sys
import textwrap
from argparse import RawDescriptionHelpFormatter

from briefcase import __version__
Expand All @@ -18,6 +18,7 @@
UpgradeCommand,
)
from briefcase.commands.base import split_passthrough
from briefcase.console import MAX_TEXT_WIDTH, Console
from briefcase.platforms import get_output_formats, get_platforms

from .exceptions import InvalidFormatError, NoCommandError, UnsupportedCommandError
Expand All @@ -36,19 +37,21 @@
]


def parse_cmdline(args):
def parse_cmdline(args, console: Console | None = None):
"""Parses the command line to determine the Command and its arguments.
:param args: the arguments provided at the command line
:param console: interface for interacting with the console
:return: Command and command-specific arguments
"""
if console is None:
console = Console()

platforms = get_platforms()
width = max(min(shutil.get_terminal_size().columns, 80) - 2, 20)

briefcase_description = textwrap.fill(
briefcase_description = (
"Briefcase is a tool for converting a Python project "
"into a standalone native application for distribution.",
width=width,
"into a standalone native application for distribution."
)

description_max_pad_len = max(len(cmd.command) for cmd in COMMANDS) + 2
Expand All @@ -59,15 +62,14 @@ def parse_cmdline(args):

platform_list = ", ".join(sorted(platforms, key=str.lower))

additional_instruction = textwrap.fill(
additional_instruction = (
"Each command, platform, and format has additional options. "
"Use the -h option on a specific command for more details.",
width=width,
"Use the -h option on a specific command for more details."
)

parser = argparse.ArgumentParser(
prog="briefcase",
description=(
description=console.textwrap(
f"{briefcase_description}\n"
"\n"
"Commands:\n"
Expand All @@ -80,7 +82,9 @@ def parse_cmdline(args):
),
usage="briefcase [-h] <command> [<platform>] [<format>] ...",
add_help=False,
formatter_class=lambda prog: RawDescriptionHelpFormatter(prog, width=width),
formatter_class=(
lambda prog: RawDescriptionHelpFormatter(prog, width=MAX_TEXT_WIDTH)
),
)
parser.add_argument("-V", "--version", action="version", version=__version__)

Expand Down
17 changes: 7 additions & 10 deletions src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
import inspect
import os
import platform
import shutil
import subprocess
import sys
import textwrap
from abc import ABC, abstractmethod
from argparse import RawDescriptionHelpFormatter
from pathlib import Path
Expand All @@ -26,7 +24,7 @@

from briefcase import __version__
from briefcase.config import AppConfig, GlobalConfig, parse_config
from briefcase.console import Console, Log
from briefcase.console import MAX_TEXT_WIDTH, Console, Log
from briefcase.exceptions import (
BriefcaseCommandError,
BriefcaseConfigError,
Expand Down Expand Up @@ -655,24 +653,23 @@ def parse_options(self, extra):
formats = list(get_output_formats(self.platform).keys())
formats[formats.index(default_format)] = f"{default_format} (default)"
supported_formats_helptext = (
"\nSupported formats:\n"
f" {', '.join(sorted(formats, key=str.lower))}"
f"Supported formats:\n {', '.join(sorted(formats, key=str.lower))}"
)
else:
supported_formats_helptext = ""

width = max(min(shutil.get_terminal_size().columns, 80) - 2, 20)
parser = argparse.ArgumentParser(
prog=self.cmd_line.format(
command=self.command,
platform=self.platform,
output_format=self.output_format,
),
description=(
f"{textwrap.fill(self.description, width=width)}\n"
f"{supported_formats_helptext}"
description=self.input.textwrap(
f"{self.description}\n\n{supported_formats_helptext}"
),
formatter_class=(
lambda prog: RawDescriptionHelpFormatter(prog, width=MAX_TEXT_WIDTH)
),
formatter_class=lambda prog: RawDescriptionHelpFormatter(prog, width=width),
)

self.add_default_options(parser)
Expand Down
51 changes: 25 additions & 26 deletions src/briefcase/commands/new.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from __future__ import annotations

import re
import shutil
import sys
import textwrap
import unicodedata
from collections import OrderedDict
from email.utils import parseaddr
Expand All @@ -26,14 +24,12 @@
is_valid_bundle_identifier,
make_class_name,
)
from briefcase.console import select_option
from briefcase.console import MAX_TEXT_WIDTH, select_option
from briefcase.exceptions import BriefcaseCommandError, TemplateUnsupportedVersion
from briefcase.integrations.git import Git

from .base import BaseCommand

MAX_TEXT_WIDTH = max(min(shutil.get_terminal_size().columns, 80) - 2, 20)


def titlecase(s):
"""Convert a string to titlecase.
Expand Down Expand Up @@ -178,17 +174,24 @@ def validate_app_name(self, candidate):
"""
if not is_valid_app_name(candidate):
raise ValueError(
f"{candidate!r} is not a valid app name.\n\n"
"App names must not be reserved keywords such as 'and', 'for' and 'while'.\n"
"They must also be PEP508 compliant (i.e., they can only include letters,\n"
"numbers, '-' and '_'; must start with a letter; and cannot end with '-' or '_')."
self.input.textwrap(
f"{candidate!r} is not a valid app name.\n"
"\n"
"App names must not be reserved keywords such as 'and', 'for' and "
"'while'. They must also be PEP508 compliant (i.e., they can only "
"include letters, numbers, '-' and '_'; must start with a letter; "
"and cannot end with '-' or '_')."
)
)

if (self.base_path / candidate).exists():
raise ValueError(
f"A {candidate!r} directory already exists. Select a different "
"name, move to a different parent directory, or delete the "
"existing folder."
self.input.textwrap(
f"A {candidate!r} directory already exists.\n"
f"\n"
f"Select a different name, move to a different parent directory, or "
f"delete the existing folder."
)
)

return True
Expand All @@ -210,11 +213,14 @@ def validate_bundle(self, candidate):
"""
if not is_valid_bundle_identifier(candidate):
raise ValueError(
f"{candidate!r} is not a valid bundle identifier.\n\n"
"The bundle should be a reversed domain name. It must contain at least 2\n"
"dot-separated sections; each section may only include letters, numbers,\n"
"and hyphens; and each section may not contain any reserved words (like\n"
"'switch', or 'while')."
self.input.textwrap(
f"{candidate!r} is not a valid bundle identifier.\n"
"\n"
"The bundle should be a reversed domain name. It must contain at "
"least 2 dot-separated sections; each section may only include "
"letters, numbers, and hyphens; and each section may not contain any "
"reserved words (like 'switch', or 'while')."
)
)

return True
Expand Down Expand Up @@ -289,14 +295,7 @@ def prompt_divider(self, title: str = ""):
def prompt_intro(self, intro: str):
"""Write the introduction for a prompt."""
self.input.prompt()
# textwrap isn't really designed to format text that already contains newlines.
# So, instead, break the intro by newlines and format each line individually.
self.input.prompt(
"\n".join(
"\n".join(textwrap.wrap(line, MAX_TEXT_WIDTH))
for line in intro.splitlines()
)
)
self.input.prompt(self.input.textwrap(intro))
self.input.prompt()

def validate_user_input(self, validator, answer) -> bool:
Expand Down Expand Up @@ -438,7 +437,7 @@ def build_app_context(self, project_overrides: dict[str, str]) -> dict[str, str]
"usually the domain name of your company or project, in reverse order.\n"
"\n"
"For example, if you are writing an application for Example Corp, "
"whose website is example.com, your bundle would be ``com.example``. "
"whose website is example.com, your bundle would be 'com.example'. "
"The bundle will be combined with your application's machine readable "
"name to form a complete application identifier (e.g., "
f"com.example.{app_name})."
Expand Down
13 changes: 13 additions & 0 deletions src/briefcase/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import os
import platform
import re
import shutil
import sys
import textwrap
import time
import traceback
from contextlib import contextmanager
Expand All @@ -29,6 +31,9 @@

from briefcase import __version__

# Max width for printing to console; matches argparse's default width
MAX_TEXT_WIDTH = max(min(shutil.get_terminal_size().columns, 80) - 2, 20)

# Regex to identify settings likely to contain sensitive information
SENSITIVE_SETTING_RE = re.compile(r"API|TOKEN|KEY|SECRET|PASS|SIGNATURE", flags=re.I)

Expand Down Expand Up @@ -656,6 +661,14 @@ def release_console_control(self):
if is_wait_bar_running:
self._wait_bar.start()

def textwrap(self, text: str, width: int = MAX_TEXT_WIDTH) -> str:
"""Wrap text to the console width, a default max width, or a specified width."""
# textwrap isn't really designed to format text that already contains newlines.
# So, instead, break the text by newlines and format each line individually.
return "\n".join(
"\n".join(textwrap.wrap(line, width)) for line in text.splitlines()
)

def prompt(self, *values, markup=False, **kwargs):
"""Print to the screen for soliciting user interaction if input enabled.
Expand Down
83 changes: 83 additions & 0 deletions tests/console/Console/test_textwrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import pytest

from briefcase.console import Console


@pytest.mark.parametrize(
"in_text, out_text",
[
(
"There is nothing wrong with your television set.",
"There is nothing wrong with your television set.",
),
(
"There is nothing wrong with your television set.\n"
"Do not attempt to adjust the picture.",
"There is nothing wrong with your television set.\n"
"Do not attempt to adjust the picture.",
),
(
"There is nothing\n\n\nwrong with your television set.\n\n"
"Do not attempt to adjust the picture. We are controlling transmission. If we wish to make it louder, "
"we will bring\nup the volume.\n",
"There is nothing\n"
"\n"
"\n"
"wrong with your television set.\n"
"\n"
"Do not attempt to adjust the picture. We are controlling transmission. If we\n"
"wish to make it louder, we will bring\n"
"up the volume.",
),
(
"There is nothing wrong with your television set. Do not "
"attempt to adjust the picture. We are controlling transmission. "
"If we wish to make it louder, we will bring up the volume. If "
"we wish to make it softer, we will tune it to "
"a whisper. We will control the horizontal. We will control the vertical. "
"We can roll the image, make it flutter. We can change the "
"focus to a soft blur or sharpen it to crystal clarity. For the next "
"hour, sit quietly, and we will control all that you see and hear. We repeat: There is nothing "
"wrong with your television set.",
"There is nothing wrong with your television set. Do not attempt to adjust the\n"
"picture. We are controlling transmission. If we wish to make it louder, we\n"
"will bring up the volume. If we wish to make it softer, we will tune it to a\n"
"whisper. We will control the horizontal. We will control the vertical. We can\n"
"roll the image, make it flutter. We can change the focus to a soft blur or\n"
"sharpen it to crystal clarity. For the next hour, sit quietly, and we will\n"
"control all that you see and hear. We repeat: There is nothing wrong with your\n"
"television set.",
),
],
)
def test_textwrap(in_text, out_text):
"""Text is formatted as expected."""
assert Console().textwrap(in_text) == out_text


@pytest.mark.parametrize(
"width, in_text, out_text",
[
(20, "This is 27 characters long.", "This is 27\ncharacters long."),
(
50,
"This is 57 characters long. This is 57 characters long.",
"This is 57 characters long. This is 57 characters\nlong.",
),
(
80,
"This is 83 characters long. This is 83 characters long. This is 83 characters long.",
"This is 83 characters long. This is 83 characters long. This is 83 characters\nlong.",
),
(
120,
"This is 144 characters long. This is 144 characters long. This is 144 characters long. "
"This is 144 characters long. This is 144 characters long.",
"This is 144 characters long. This is 144 characters long. "
"This is 144 characters long. This is 144 characters long. This\nis 144 characters long.",
),
],
)
def test_textwrap_width_override(width, in_text, out_text):
"""Width override is respected."""
assert Console().textwrap(in_text, width=width) == out_text

0 comments on commit 31a8b91

Please sign in to comment.