diff --git a/ibis/backends/tests/test_generic.py b/ibis/backends/tests/test_generic.py index 1b4ff815e77f..c0150a10d50f 100644 --- a/ibis/backends/tests/test_generic.py +++ b/ibis/backends/tests/test_generic.py @@ -2,6 +2,7 @@ import importlib import io import operator +from contextlib import redirect_stdout import numpy as np import pandas as pd @@ -435,16 +436,25 @@ def test_select_sort_sort(alltypes): query = query.sort_by(query.year).sort_by(query.bool_col) -def test_table_info(alltypes): +def check_table_info(buf, schema): + info_str = buf.getvalue() + + assert "Null" in info_str + assert all(type.__class__.__name__ in info_str for type in schema.types) + assert all(name in info_str for name in schema.names) + + +def test_table_info_buf(alltypes): buf = io.StringIO() alltypes.info(buf=buf) + check_table_info(buf, alltypes.schema()) - info_str = buf.getvalue() - schema = alltypes.schema() - assert "Nulls" in info_str - assert all(str(type) in info_str for type in schema.types) - assert all(name in info_str for name in schema.names) +def test_table_info_no_buf(alltypes): + buf = io.StringIO() + with redirect_stdout(buf): + alltypes.info() + check_table_info(buf, alltypes.schema()) @pytest.mark.parametrize( diff --git a/ibis/common/grounds.py b/ibis/common/grounds.py index 135e35fb5d6f..3c2e3aba59b1 100644 --- a/ibis/common/grounds.py +++ b/ibis/common/grounds.py @@ -5,12 +5,16 @@ from typing import Any, Hashable from weakref import WeakValueDictionary +from rich.console import Console + from ibis.common.caching import WeakCache from ibis.common.validators import ImmutableProperty, Optional, Validator from ibis.util import frozendict EMPTY = inspect.Parameter.empty # marker for missing argument +console = Console() + class BaseMeta(ABCMeta): diff --git a/ibis/expr/types/relations.py b/ibis/expr/types/relations.py index 9400e8cdd348..4d06c7de0422 100644 --- a/ibis/expr/types/relations.py +++ b/ibis/expr/types/relations.py @@ -4,17 +4,20 @@ import functools import itertools import operator +import sys import warnings from functools import cached_property from typing import IO, TYPE_CHECKING, Any, Iterable, Literal, Mapping, Sequence import numpy as np -import tabulate +import rich.pretty +import rich.table from public import public import ibis from ibis import util from ibis.common import exceptions as com +from ibis.common.grounds import console from ibis.expr.deferred import Deferred from ibis.expr.types.core import Expr @@ -872,6 +875,9 @@ def info(self, buf: IO[str] | None = None) -> None: buf A writable buffer, defaults to stdout """ + if buf is None: + buf = sys.stdout + metrics = [self[col].count().name(col) for col in self.columns] metrics.append(self.count().name("nrows")) @@ -879,22 +885,27 @@ def info(self, buf: IO[str] | None = None) -> None: *items, (_, n) = self.aggregate(metrics).execute().squeeze().items() - tabulated = tabulate.tabulate( - [ - ( - column, - schema[column], - f"{n - non_nulls} ({100 * (1.0 - non_nulls / n):>3.3g}%)", - ) - for column, non_nulls in items - ], - headers=["Column", "Type", "Nulls (%)"], - colalign=("left", "left", "right"), - ) - width = tabulated[tabulated.index("\n") + 1 :].index("\n") - row_count = f"Rows: {n}".center(width) - footer_line = "-" * width - print("\n".join([tabulated, footer_line, row_count]), file=buf) + op = self.op() + title = getattr(op, "name", type(op).__name__) + + table = rich.table.Table(title=f"Summary of {title}\n{n:d} rows") + + table.add_column("Name", justify="left") + table.add_column("Type", justify="left") + table.add_column("# Nulls", justify="right") + table.add_column("% Nulls", justify="right") + + for column, non_nulls in items: + table.add_row( + column, + rich.pretty.Pretty(schema[column]), + str(n - non_nulls), + f"{100 * (1.0 - non_nulls / n):>3.2f}", + ) + + with console.capture() as capture: + console.print(table) + buf.write(capture.get()) def set_column(self, name: str, expr: ir.Value) -> Table: """Replace an existing column with a new expression. diff --git a/poetry.lock b/poetry.lock index 6f63a8d21017..348278b27926 100644 --- a/poetry.lock +++ b/poetry.lock @@ -316,7 +316,7 @@ typing-extensions = ">=4.0.1,<5.0.0" name = "commonmark" version = "0.9.1" description = "Python parser for the CommonMark Markdown spec" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -1690,7 +1690,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "pygments" version = "2.12.0" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -2053,7 +2053,7 @@ use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] name = "rich" version = "12.5.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "dev" +category = "main" optional = false python-versions = ">=3.6.3,<4.0.0" @@ -2170,7 +2170,7 @@ tests = ["cython", "littleutils", "pygments", "typeguard", "pytest"] name = "tabulate" version = "0.8.10" description = "Pretty-print tabular data" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -2403,7 +2403,7 @@ visualization = ["graphviz"] [metadata] lock-version = "1.1" python-versions = ">=3.8,<3.11" -content-hash = "5f1ba45df83838aef0d7f83b4ce289930dbe93eef08df5d4654fae6dc6403889" +content-hash = "711603383c942072e373fc3c3600018d003e69a4092d7d2b53340cb40ed7172f" [metadata.files] absolufy-imports = [ @@ -3663,6 +3663,7 @@ pyparsing = [ ] pyproj = [ {file = "pyproj-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:473961faef7a9fd723c5d432f65220ea6ab3854e606bf84b4d409a75a4261c78"}, + {file = "pyproj-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07c9d8d7ec009bbac09e233cfc725601586fe06880e5538a3a44eaf560ba3a62"}, {file = "pyproj-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fef9c1e339f25c57f6ae0558b5ab1bbdf7994529a30d8d7504fc6302ea51c03"}, {file = "pyproj-3.3.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:140fa649fedd04f680a39f8ad339799a55cb1c49f6a84e1b32b97e49646647aa"}, {file = "pyproj-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b59c08aea13ee428cf8a919212d55c036cc94784805ed77c8f31a4d1f541058c"}, @@ -3675,6 +3676,7 @@ pyproj = [ {file = "pyproj-3.3.1-cp38-cp38-win32.whl", hash = "sha256:c99f7b5757a28040a2dd4a28c9805fdf13eef79a796f4a566ab5cb362d10630d"}, {file = "pyproj-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:5dac03d4338a4c8bd0f69144c527474f517b4cbd7d2d8c532cd8937799723248"}, {file = "pyproj-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56b0f9ee2c5b2520b18db30a393a7b86130cf527ddbb8c96e7f3c837474a9d79"}, + {file = "pyproj-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f1032e5dfb50eae06382bcc7b9011b994f7104d932fe91bd83a722275e30e8ce"}, {file = "pyproj-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f92d8f6514516124abb714dce912b20867831162cfff9fae2678ef07b6fcf0f"}, {file = "pyproj-3.3.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ef1bfbe2dcc558c7a98e2f1836abdcd630390f3160724a6f4f5c818b2be0ad5"}, {file = "pyproj-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca5f32b56210429b367ca4f9a57ffe67975c487af82e179a24370879a3daf68"}, diff --git a/pyproject.toml b/pyproject.toml index adae02eb7538..9317031c8e49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ pandas = ">=1.2.5,<2" parsy = ">=1.3.0,<2" pydantic = ">=1.9.0,<2" regex = ">=2021.7.6" -tabulate = ">=0.8.9,<1" +rich = ">=12.4.4,<13" toolz = ">=0.11,<0.13" clickhouse-cityhash = { version = ">=1.0.2,<2", optional = true } clickhouse-driver = { version = ">=0.1,<0.3", optional = true, extras = [ diff --git a/requirements.txt b/requirements.txt index ebdea1373475..04c717fe368a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -132,7 +132,7 @@ pycparser==2.21; python_version >= "3.7" and python_full_version < "3.0.0" and i pydantic==1.9.1; python_full_version >= "3.6.1" pydocstyle==6.1.1; python_version >= "3.6" pyflakes==2.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -pygments==2.12.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.8" and python_full_version < "4.0.0" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") +pygments==2.12.0; python_full_version >= "3.7.1" and python_full_version < "4.0.0" and python_version >= "3.8" and python_version < "4" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") pymdown-extensions==9.5; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.7" pymysql==1.0.2; python_version >= "3.6" pyparsing==3.0.9; python_full_version >= "3.6.8" and python_version >= "3.8" and python_full_version < "4.0.0" @@ -161,7 +161,7 @@ pyzmq==23.2.0; python_version >= "3.7" questionary==1.10.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.2" and python_full_version < "4.0.0" regex==2022.7.24; python_version >= "3.6" requests==2.28.1; python_version >= "3.7" and python_version < "4" -rich==12.5.1; python_full_version >= "3.6.3" and python_full_version < "4.0.0" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") +rich==12.5.1; python_full_version >= "3.6.3" and python_full_version < "4.0.0" shapely==1.8.2; python_version >= "3.6" six==1.16.0; python_full_version >= "3.7.1" and python_version >= "3.8" and python_version < "3.9" smmap==5.0.0; python_version >= "3.7" @@ -170,7 +170,7 @@ soupsieve==2.3.2.post1; python_full_version >= "3.7.1" and python_version < "4" sqlalchemy==1.4.39; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") sqlparse==0.4.2; python_version >= "3.5" stack-data==0.3.0; python_version >= "3.8" -tabulate==0.8.10; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +tabulate==0.8.10; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" termcolor==1.1.0; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.5" thrift-sasl==0.4.3 thrift==0.11.0; python_version >= "3.0" @@ -182,7 +182,7 @@ tomlkit==0.11.1; python_version >= "3.6" and python_version < "4.0" and python_f toolz==0.12.0; python_version >= "3.5" tornado==6.2; python_version >= "3.7" traitlets==5.3.0; python_full_version >= "3.7.1" and python_version < "4" and python_version >= "3.8" -typing-extensions==4.3.0; python_version >= "3.7" and python_full_version >= "3.6.3" and python_version < "3.9" and python_full_version < "4.0.0" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") +typing-extensions==4.3.0; python_version >= "3.7" and python_full_version >= "3.6.3" and python_full_version < "4.0.0" and python_version < "3.9" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") tzdata==2022.1; python_version >= "3.6" and python_version < "4" and platform_system == "Windows" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "3.6" and python_version < "4" and python_full_version >= "3.6.0") tzlocal==4.2; python_version >= "3.6" and python_version < "4" urllib3==1.26.10; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.7" diff --git a/setup.py b/setup.py index aa73414e247b..2eb6d1428b69 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ 'parsy>=1.3.0,<2', 'pydantic>=1.9.0,<2', 'regex>=2021.7.6', - 'tabulate>=0.8.9,<1', + 'rich>=12.4.4,<13', 'toolz>=0.11,<0.13', ]