diff --git a/.gitignore b/.gitignore index 0495ac7..ac91325 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ website-build/ ## Unit test / coverage reports .coverage .tox +Pipfile diff --git a/README.md b/README.md index d64b99a..c66d57e 100644 --- a/README.md +++ b/README.md @@ -400,6 +400,14 @@ corresponds to the `pipe` format without alignment colons: │ bacon │ 0 │ ╘════════╧═══════╛ +`fancy_dottedline` is the same as the `simple_grid` format. + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="fancy_dottedline")) + ⋅⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯ + ⋮ spam ⋮ 41.9999 ⋮ + ⋮ eggs ⋮ 451 ⋮ + ⋅⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯⋯ + `presto` is like tables formatted by Presto cli: ```pycon @@ -940,6 +948,30 @@ the lines being wrapped would probably be significantly longer than this. +------------+---------+ ``` +Text is preferably wrapped on whitespaces and right after the hyphens in hyphenated words. + +break_long_words (default: True) If true, then words longer than width will be broken in order to ensure that no lines are longer than width. +If it is false, long words will not be broken, and some lines may be longer than width. +(Long words will be put on a line by themselves, in order to minimize the amount by which width is exceeded.) + +break_on_hyphens (default: True) If true, wrapping will occur preferably on whitespaces and right after hyphens in compound words, as it is customary in English. +If false, only whitespaces will be considered as potentially good places for line breaks. + +```pycon +>>> print(tabulate([["John Smith", "Middle-Manager"]], headers=["Name", "Title"], tablefmt="grid", maxcolwidths=[None, 5], break_long_words=False)) ++------------+---------+ +| Name | Title | ++============+=========+ +| John Smith | Middle- | +| | Manager | ++------------+---------+ +>>> print(tabulate([["John Smith", "Middle-Manager"]], headers=["Name", "Title"], tablefmt="grid", maxcolwidths=[None, 5], break_long_words=False, break_on_hyphens=False)) ++------------+----------------+ +| Name | Title | ++============+================+ +| John Smith | Middle-Manager | ++------------+----------------+ +``` ### Adding Separating lines One might want to add one or more separating lines to highlight different sections in a table. diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 3b1a1e1..68a7148 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -11,8 +11,24 @@ import textwrap import dataclasses +from typing import ( + Callable, + Tuple, + Union, + TypeVar, + List, + Dict, + Pattern, + Type, + Any, + Optional, + Sized, + Iterable, +) +from typing_extensions import Literal, TypedDict, Protocol + try: - import wcwidth # optional wide-character (CJK) support + import wcwidth # optional wide-character (CJK) support # type: ignore except ImportError: wcwidth = None @@ -22,38 +38,77 @@ def _is_file(f): __all__ = ["tabulate", "tabulate_formats", "simple_separated_format"] + try: - from .version import version as __version__ # noqa: F401 + from .version import version as __version__ # noqa: F401 # type: ignore except ImportError: pass # running __init__.py as a script, AppVeyor pytests - # minimum extra space in headers -MIN_PADDING = 2 +MIN_PADDING: int = 2 # Whether or not to preserve leading/trailing whitespace in data. -PRESERVE_WHITESPACE = False +PRESERVE_WHITESPACE: bool = False + +# TextWrapper breaks words longer than 'width'. +_BREAK_LONG_WORDS: bool = True +# TextWrapper is breaking hyphenated words. +_BREAK_ON_HYPHENS: bool = True -_DEFAULT_FLOATFMT = "g" -_DEFAULT_INTFMT = "" -_DEFAULT_MISSINGVAL = "" +_DEFAULT_FLOATFMT: str = "g" +_DEFAULT_INTFMT: str = "" +_DEFAULT_MISSINGVAL: str = "" # default align will be overwritten by "left", "center" or "decimal" # depending on the formatter -_DEFAULT_ALIGN = "default" - +_DEFAULT_ALIGN: str = "default" # if True, enable wide-character (CJK) support -WIDE_CHARS_MODE = wcwidth is not None +WIDE_CHARS_MODE: bool = wcwidth is not None # Constant that can be used as part of passed rows to generate a separating line # It is purposely an unprintable character, very unlikely to be used in a table -SEPARATING_LINE = "\001" +SEPARATING_LINE: str = "\001" -Line = namedtuple("Line", ["begin", "hline", "sep", "end"]) +T_ALIGNS = List[str] +T_COLWIDTHS = List[Union[int, str]] +AST = TypeVar("AST", bound="AlignableString") -DataRow = namedtuple("DataRow", ["begin", "sep", "end"]) +ALIGNMENT = TypedDict( + "ALIGNMENT", + { + "left": str, + "right": str, + "center": str, + "decimal": str, + }, +) + +ColWidths = TypedDict( + "ColWidths", + { + "colwidth": int, + }, +) +ColAligns = TypedDict( + "ColAlign", + { + "colalign": str, + }, +) + +Line = namedtuple( + "Line", + [ + "begin", + "hline", + "sep", + "end", + ], +) + +DataRow = namedtuple("DataRow", ["begin", "sep", "end"]) # A table structure is supposed to be: # @@ -101,16 +156,53 @@ def _is_file(f): ) -def _is_separating_line(row): +class TableOptions(TypedDict): + colwidths_ignore: T_COLWIDTHS + colaligns_ignore: T_ALIGNS + + +class EscapeString(Protocol): + def __str__(self) -> str: + ... + + +class Convertible(Protocol): + def __call__(self, string: Union[str, bytes]) -> Any: + ... + + +class AlignableString(Protocol): + def __str__(self) -> str: + ... + + +class IndexType(Protocol): + def __len__(self) -> int: + ... + + def __iter__(self) -> Iterable: + ... + + +class HasLen(Protocol): + def __len__(self) -> int: + ... + + +def _is_separating_line_values(value: str) -> bool: + return type(value) == str and value.strip() == SEPARATING_LINE + + +def _is_separating_line(row: Union[list, str]) -> Literal[True, False]: row_type = type(row) is_sl = (row_type == list or row_type == str) and ( - (len(row) >= 1 and row[0] == SEPARATING_LINE) - or (len(row) >= 2 and row[1] == SEPARATING_LINE) + (len(row) >= 1 and _is_separating_line_values(row[0])) + or (len(row) >= 2 and _is_separating_line_values(row[1])) ) return is_sl -def _pipe_segment_with_colons(align, colwidth): +def _pipe_segment_with_colons(align: str, colwidth: int) -> str: """Return a segment of a horizontal line with optional colons which indicate column's alignment (as in `pipe` output format).""" w = colwidth @@ -124,7 +216,7 @@ def _pipe_segment_with_colons(align, colwidth): return "-" * w -def _pipe_line_with_colons(colwidths, colaligns): +def _pipe_line_with_colons(colwidths: List[int], colaligns: List[str]) -> str: """Return a horizontal line with optional colons to indicate column's alignment (as in `pipe` output format).""" if not colaligns: # e.g. printing an empty data frame (github issue #15) @@ -133,8 +225,13 @@ def _pipe_line_with_colons(colwidths, colaligns): return "|" + "|".join(segments) + "|" -def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns): - alignment = { +def _mediawiki_row_with_attrs( + separator: str, + cell_values: List[str], + colwidths: List[int], + colaligns: List[Literal["left", "right", "center", "decimal"]], +) -> str: + alignment: ALIGNMENT = { "left": "", "right": 'style="text-align: right;"| ', "center": 'style="text-align: center;"| ', @@ -149,20 +246,42 @@ def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns): return (separator + colsep.join(values_with_attrs)).rstrip() -def _textile_row_with_attrs(cell_values, colwidths, colaligns): +def _textile_row_with_attrs( + cell_values: List[str], + colwidths: List[int], + colaligns: List[ + Union[ + Literal["left"], + Literal["right"], + Literal["center"], + Literal["decimal"], + ] + ], +) -> str: cell_values[0] += " " - alignment = {"left": "<.", "right": ">.", "center": "=.", "decimal": ">."} + alignment: ALIGNMENT = { + "left": "<.", + "right": ">.", + "center": "=.", + "decimal": ">.", + } values = (alignment.get(a, "") + v for a, v in zip(colaligns, cell_values)) return "|" + "|".join(values) + "|" -def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore): +def _html_begin_table_without_header(options: TableOptions) -> str: # this table header will be suppressed if there is a header row return "