Skip to content

Commit fe8842f

Browse files
committed
Automatically use MarkdownConsole in non-interactive contexts or w/env var
1 parent 4772446 commit fe8842f

4 files changed

Lines changed: 109 additions & 25 deletions

File tree

sqlmesh/__init__.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from sqlmesh.utils import (
3434
debug_mode_enabled as debug_mode_enabled,
3535
enable_debug_mode as enable_debug_mode,
36+
str_to_bool,
3637
)
3738
from sqlmesh.utils.date import DatetimeRanges as DatetimeRanges
3839

@@ -54,6 +55,7 @@ class RuntimeEnv(str, Enum):
5455
GOOGLE_COLAB = "google_colab" # Not currently officially supported
5556
JUPYTER = "jupyter"
5657
DEBUGGER = "debugger"
58+
NON_INTERACTIVE = "non_interactive" # CI or other envs that shouldn't use emojis
5759

5860
@classmethod
5961
def get(cls) -> RuntimeEnv:
@@ -75,6 +77,14 @@ def get(cls) -> RuntimeEnv:
7577

7678
if debug_mode_enabled():
7779
return RuntimeEnv.DEBUGGER
80+
81+
if (
82+
is_cicd_environment()
83+
or not is_interactive_environment()
84+
or str_to_bool(os.environ.get("SQLMESH_NON_INTERACTIVE_TERMINAL", "false"))
85+
):
86+
return RuntimeEnv.NON_INTERACTIVE
87+
7888
return RuntimeEnv.TERMINAL
7989

8090
@property
@@ -93,9 +103,24 @@ def is_jupyter(self) -> bool:
93103
def is_google_colab(self) -> bool:
94104
return self == RuntimeEnv.GOOGLE_COLAB
95105

106+
@property
107+
def is_non_interactive(self) -> bool:
108+
return self == RuntimeEnv.NON_INTERACTIVE
109+
96110
@property
97111
def is_notebook(self) -> bool:
98-
return not self.is_terminal
112+
return not self.is_terminal and not self.is_non_interactive
113+
114+
115+
def is_cicd_environment() -> bool:
116+
for key in ("CI", "GITHUB_ACTIONS", "TRAVIS", "CIRCLECI", "GITLAB_CI", "BUILDKITE"):
117+
if str_to_bool(os.environ.get(key, "false")):
118+
return True
119+
return False
120+
121+
122+
def is_interactive_environment() -> bool:
123+
return sys.stdin.isatty() and sys.stdout.isatty()
99124

100125

101126
if RuntimeEnv.get().is_notebook:

sqlmesh/core/console.py

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,6 @@
7878

7979
PROGRESS_BAR_WIDTH = 40
8080
LINE_WRAP_WIDTH = 100
81-
CHECK_MARK = "\u2714"
82-
GREEN_CHECK_MARK = f"[green]{CHECK_MARK}[/green]"
83-
RED_X_MARK = "\u274c"
8481

8582

8683
class LinterConsole(abc.ABC):
@@ -770,6 +767,11 @@ class TerminalConsole(Console):
770767

771768
TABLE_DIFF_SOURCE_BLUE = "#0248ff"
772769
TABLE_DIFF_TARGET_GREEN = "green"
770+
CHECK_MARK = "\u2714"
771+
AUDIT_PASS_MARK = CHECK_MARK
772+
GREEN_AUDIT_PASS_MARK = f"[green]{AUDIT_PASS_MARK}[/green]"
773+
AUDIT_FAIL_MARK = "\u274c"
774+
AUDIT_PADDING = 0
773775

774776
def __init__(
775777
self,
@@ -879,7 +881,9 @@ def start_evaluation_progress(
879881
progress_table.add_row(self.evaluation_total_progress)
880882
progress_table.add_row(self.evaluation_model_progress)
881883

882-
self.evaluation_progress_live = Live(progress_table, refresh_per_second=10)
884+
self.evaluation_progress_live = Live(
885+
progress_table, console=self.console, refresh_per_second=10
886+
)
883887
self.evaluation_progress_live.start()
884888

885889
batch_sizes = {
@@ -891,7 +895,7 @@ def start_evaluation_progress(
891895

892896
# determine column widths
893897
self.evaluation_column_widths["annotation"] = (
894-
_calculate_annotation_str_len(batched_intervals)
898+
_calculate_annotation_str_len(batched_intervals, self.AUDIT_PADDING)
895899
+ 3 # brackets and opening escape backslash
896900
)
897901
self.evaluation_column_widths["name"] = max(
@@ -956,21 +960,24 @@ def update_snapshot_evaluation_progress(
956960
)
957961
audits_str = ""
958962
if num_audits_passed:
959-
audits_str += f" {CHECK_MARK}{num_audits_passed}"
963+
audits_str += f" {self.AUDIT_PASS_MARK}{num_audits_passed}"
960964
if num_audits_failed:
961-
audits_str += f" {RED_X_MARK}{num_audits_failed}"
965+
audits_str += f" {self.AUDIT_FAIL_MARK}{num_audits_failed}"
962966
audits_str = f", audits{audits_str}" if audits_str else ""
963967
annotation_len = self.evaluation_column_widths["annotation"]
968+
# don't adjust the annotation_len if we're using AUDIT_PADDING
964969
annotation = f"\\[{annotation + audits_str}]".ljust(
965-
annotation_len - 1 if num_audits_failed else annotation_len
970+
annotation_len - 1
971+
if num_audits_failed and self.AUDIT_PADDING == 0
972+
else annotation_len
966973
)
967974

968975
duration = f"{(duration_ms / 1000.0):.2f}s".ljust(
969976
self.evaluation_column_widths["duration"]
970977
)
971978

972979
msg = f"{batch} {display_name} {annotation} {duration}".replace(
973-
CHECK_MARK, GREEN_CHECK_MARK
980+
self.AUDIT_PASS_MARK, self.GREEN_AUDIT_PASS_MARK
974981
)
975982

976983
self.evaluation_progress_live.console.print(msg)
@@ -989,7 +996,7 @@ def stop_evaluation_progress(self, success: bool = True) -> None:
989996
if self.evaluation_progress_live:
990997
self.evaluation_progress_live.stop()
991998
if success:
992-
self.log_success(f"{GREEN_CHECK_MARK} Model batches executed")
999+
self.log_success(f"{self.CHECK_MARK} Model batches executed")
9931000

9941001
self.evaluation_progress_live = None
9951002
self.evaluation_total_progress = None
@@ -1053,7 +1060,7 @@ def stop_creation_progress(self, success: bool = True) -> None:
10531060
self.creation_progress.stop()
10541061
self.creation_progress = None
10551062
if success:
1056-
self.log_success(f"\n{GREEN_CHECK_MARK} Physical layer updated")
1063+
self.log_success(f"\n{self.CHECK_MARK} Physical layer updated")
10571064

10581065
self.environment_naming_info = EnvironmentNamingInfo()
10591066
self.default_catalog = None
@@ -1154,7 +1161,7 @@ def stop_promotion_progress(self, success: bool = True) -> None:
11541161
self.promotion_progress.stop()
11551162
self.promotion_progress = None
11561163
if success:
1157-
self.log_success(f"\n{GREEN_CHECK_MARK} Virtual layer updated")
1164+
self.log_success(f"\n{self.CHECK_MARK} Virtual layer updated")
11581165

11591166
self.environment_naming_info = EnvironmentNamingInfo()
11601167
self.default_catalog = None
@@ -2807,6 +2814,12 @@ class MarkdownConsole(CaptureTerminalConsole):
28072814
where you want to display a plan or test results in markdown.
28082815
"""
28092816

2817+
CHECK_MARK = ""
2818+
AUDIT_PASS_MARK = "passed "
2819+
GREEN_AUDIT_PASS_MARK = AUDIT_PASS_MARK
2820+
AUDIT_FAIL_MARK = "failed "
2821+
AUDIT_PADDING = 7
2822+
28102823
def __init__(self, **kwargs: t.Any) -> None:
28112824
super().__init__(**{**kwargs, "console": RichConsole(no_color=True)})
28122825

@@ -2822,18 +2835,19 @@ def show_environment_difference_summary(
28222835
no_diff: Hide the actual environment statements differences.
28232836
"""
28242837
if context_diff.is_new_environment:
2825-
self._print(
2826-
f"**New environment `{context_diff.environment}` will be created from `{context_diff.create_from}`**\n"
2838+
msg = (
2839+
f"\n**`{context_diff.environment}` environment will be initialized**"
2840+
if not context_diff.create_from_env_exists
2841+
else f"\n**New environment `{context_diff.environment}` will be created from `{context_diff.create_from}`**"
28272842
)
2843+
self._print(msg)
28282844
if not context_diff.has_snapshot_changes:
28292845
return
28302846

28312847
if not context_diff.has_changes:
28322848
self._print(f"**No differences when compared to `{context_diff.environment}`**\n")
28332849
return
28342850

2835-
self._print(f"**Summary of differences against `{context_diff.environment}`:**\n")
2836-
28372851
if context_diff.has_requirement_changes:
28382852
self._print(f"Requirements:\n{context_diff.requirements_diff()}")
28392853

@@ -2984,7 +2998,7 @@ def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> N
29842998
dialect=self.dialect,
29852999
)
29863000
snapshots.append(
2987-
f"* `{display_name}`: [{_format_missing_intervals(snapshot, missing)}]{preview_modifier}"
3001+
f"* `{display_name}`: \\[{_format_missing_intervals(snapshot, missing)}]{preview_modifier}"
29883002
)
29893003

29903004
length = len(snapshots)
@@ -3033,6 +3047,21 @@ def _show_categorized_snapshots(self, plan: Plan, default_catalog: t.Optional[st
30333047
self._print(tree)
30343048
self._print("\n```")
30353049

3050+
def stop_evaluation_progress(self, success: bool = True) -> None:
3051+
super().stop_evaluation_progress(success)
3052+
self._print("\n")
3053+
3054+
def stop_creation_progress(self, success: bool = True) -> None:
3055+
super().stop_creation_progress(success)
3056+
self._print("\n")
3057+
3058+
def stop_promotion_progress(self, success: bool = True) -> None:
3059+
super().stop_promotion_progress(success)
3060+
self._print("\n")
3061+
3062+
def log_success(self, message: str) -> None:
3063+
self._print(message)
3064+
30363065
def log_test_results(
30373066
self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str
30383067
) -> None:
@@ -3082,6 +3111,12 @@ def log_warning(self, short_message: str, long_message: t.Optional[str] = None)
30823111
logger.warning(long_message or short_message)
30833112
self._print(f"```\n\\[WARNING] {short_message}```\n\n")
30843113

3114+
def _print(self, value: t.Any, **kwargs: t.Any) -> None:
3115+
self.console.print(value, **kwargs)
3116+
with self.console.capture() as capture:
3117+
self.console.print(value, **kwargs)
3118+
self._captured_outputs.append(capture.get())
3119+
30853120

30863121
class DatabricksMagicConsole(CaptureTerminalConsole):
30873122
"""
@@ -3473,6 +3508,7 @@ def create_console(
34733508
RuntimeEnv.TERMINAL: TerminalConsole,
34743509
RuntimeEnv.GOOGLE_COLAB: NotebookMagicConsole,
34753510
RuntimeEnv.DEBUGGER: DebuggerTerminalConsole,
3511+
RuntimeEnv.NON_INTERACTIVE: MarkdownConsole,
34763512
}
34773513
rich_console_kwargs: t.Dict[str, t.Any] = {"theme": srich.theme}
34783514
if runtime_env.is_jupyter or runtime_env.is_google_colab:
@@ -3598,7 +3634,7 @@ def _calculate_interval_str_len(snapshot: Snapshot, intervals: t.List[Interval])
35983634
return interval_str_len
35993635

36003636

3601-
def _calculate_audit_str_len(snapshot: Snapshot) -> int:
3637+
def _calculate_audit_str_len(snapshot: Snapshot, audit_padding: int = 0) -> int:
36023638
# The annotation includes audit results. We cannot build the audits result string
36033639
# until after evaluation occurs, but we must determine the annotation column width here.
36043640
# Therefore, we add enough padding for the longest possible audits result string.
@@ -3619,21 +3655,38 @@ def _calculate_audit_str_len(snapshot: Snapshot) -> int:
36193655
)
36203656
if num_audits == 1:
36213657
# +1 for "1" audit count, +1 for red X
3622-
audit_len = audit_base_str_len + (2 if num_nonblocking_audits else 1)
3658+
# if audit_padding is > 0 we're using "failed" instead of red X
3659+
audit_len = (
3660+
audit_base_str_len
3661+
+ (2 if num_nonblocking_audits else 1)
3662+
+ (
3663+
audit_padding - 1
3664+
if num_nonblocking_audits and audit_padding > 0
3665+
else audit_padding
3666+
)
3667+
)
36233668
else:
3624-
audit_len = audit_base_str_len + len(str(num_audits))
3669+
audit_len = audit_base_str_len + len(str(num_audits)) + audit_padding
36253670
if num_nonblocking_audits:
36263671
# +1 for space, +1 for red X
3627-
audit_len += len(str(num_nonblocking_audits)) + 2
3672+
# if audit_padding is > 0 we're using "failed" instead of red X
3673+
audit_len += (
3674+
len(str(num_nonblocking_audits))
3675+
+ 2
3676+
+ (audit_padding - 1 if audit_padding > 0 else audit_padding)
3677+
)
36283678
audit_str_len = max(audit_str_len, audit_len)
36293679
return audit_str_len
36303680

36313681

3632-
def _calculate_annotation_str_len(batched_intervals: t.Dict[Snapshot, t.List[Interval]]) -> int:
3682+
def _calculate_annotation_str_len(
3683+
batched_intervals: t.Dict[Snapshot, t.List[Interval]], audit_padding: int = 0
3684+
) -> int:
36333685
annotation_str_len = 0
36343686
for snapshot, intervals in batched_intervals.items():
36353687
annotation_str_len = max(
36363688
annotation_str_len,
3637-
_calculate_interval_str_len(snapshot, intervals) + _calculate_audit_str_len(snapshot),
3689+
_calculate_interval_str_len(snapshot, intervals)
3690+
+ _calculate_audit_str_len(snapshot, audit_padding),
36383691
)
36393692
return annotation_str_len

tests/cli/conftest.py

Whitespace-only changes.

tests/cli/test_cli.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
from click.testing import CliRunner
99
import time_machine
1010
import json
11-
11+
from unittest.mock import MagicMock
12+
from sqlmesh import RuntimeEnv
1213
from sqlmesh.cli.example_project import ProjectTemplate, init_example_project
1314
from sqlmesh.cli.main import cli
1415
from sqlmesh.core.context import Context
@@ -20,6 +21,11 @@
2021
pytestmark = pytest.mark.slow
2122

2223

24+
@pytest.fixture(autouse=True)
25+
def mock_runtime_env(monkeypatch):
26+
monkeypatch.setattr("sqlmesh.RuntimeEnv.get", MagicMock(return_value=RuntimeEnv.TERMINAL))
27+
28+
2329
@pytest.fixture(scope="session")
2430
def runner() -> CliRunner:
2531
return CliRunner()

0 commit comments

Comments
 (0)