Skip to content

Commit f5a3725

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

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):
@@ -697,6 +694,11 @@ class TerminalConsole(Console):
697694
"""A rich based implementation of the console."""
698695

699696
TABLE_DIFF_SOURCE_BLUE = "#0248ff"
697+
CHECK_MARK = "\u2714"
698+
AUDIT_PASS_MARK = CHECK_MARK
699+
GREEN_AUDIT_PASS_MARK = f"[green]{AUDIT_PASS_MARK}[/green]"
700+
AUDIT_FAIL_MARK = "\u274c"
701+
AUDIT_PADDING = 0
700702

701703
def __init__(
702704
self,
@@ -801,7 +803,9 @@ def start_evaluation_progress(
801803
progress_table.add_row(self.evaluation_total_progress)
802804
progress_table.add_row(self.evaluation_model_progress)
803805

804-
self.evaluation_progress_live = Live(progress_table, refresh_per_second=10)
806+
self.evaluation_progress_live = Live(
807+
progress_table, console=self.console, refresh_per_second=10
808+
)
805809
self.evaluation_progress_live.start()
806810

807811
batch_sizes = {
@@ -813,7 +817,7 @@ def start_evaluation_progress(
813817

814818
# determine column widths
815819
self.evaluation_column_widths["annotation"] = (
816-
_calculate_annotation_str_len(batched_intervals)
820+
_calculate_annotation_str_len(batched_intervals, self.AUDIT_PADDING)
817821
+ 3 # brackets and opening escape backslash
818822
)
819823
self.evaluation_column_widths["name"] = max(
@@ -878,21 +882,24 @@ def update_snapshot_evaluation_progress(
878882
)
879883
audits_str = ""
880884
if num_audits_passed:
881-
audits_str += f" {CHECK_MARK}{num_audits_passed}"
885+
audits_str += f" {self.AUDIT_PASS_MARK}{num_audits_passed}"
882886
if num_audits_failed:
883-
audits_str += f" {RED_X_MARK}{num_audits_failed}"
887+
audits_str += f" {self.AUDIT_FAIL_MARK}{num_audits_failed}"
884888
audits_str = f", audits{audits_str}" if audits_str else ""
885889
annotation_len = self.evaluation_column_widths["annotation"]
890+
# don't adjust the annotation_len if we're using AUDIT_PADDING
886891
annotation = f"\\[{annotation + audits_str}]".ljust(
887-
annotation_len - 1 if num_audits_failed else annotation_len
892+
annotation_len - 1
893+
if num_audits_failed and self.AUDIT_PADDING == 0
894+
else annotation_len
888895
)
889896

890897
duration = f"{(duration_ms / 1000.0):.2f}s".ljust(
891898
self.evaluation_column_widths["duration"]
892899
)
893900

894901
msg = f"{batch} {display_name} {annotation} {duration}".replace(
895-
CHECK_MARK, GREEN_CHECK_MARK
902+
self.AUDIT_PASS_MARK, self.GREEN_AUDIT_PASS_MARK
896903
)
897904

898905
self.evaluation_progress_live.console.print(msg)
@@ -911,7 +918,7 @@ def stop_evaluation_progress(self, success: bool = True) -> None:
911918
if self.evaluation_progress_live:
912919
self.evaluation_progress_live.stop()
913920
if success:
914-
self.log_success(f"{GREEN_CHECK_MARK} Model batches executed")
921+
self.log_success(f"{self.CHECK_MARK} Model batches executed")
915922

916923
self.evaluation_progress_live = None
917924
self.evaluation_total_progress = None
@@ -975,7 +982,7 @@ def stop_creation_progress(self, success: bool = True) -> None:
975982
self.creation_progress.stop()
976983
self.creation_progress = None
977984
if success:
978-
self.log_success(f"\n{GREEN_CHECK_MARK} Physical layer updated")
985+
self.log_success(f"\n{self.CHECK_MARK} Physical layer updated")
979986

980987
self.environment_naming_info = EnvironmentNamingInfo()
981988
self.default_catalog = None
@@ -1076,7 +1083,7 @@ def stop_promotion_progress(self, success: bool = True) -> None:
10761083
self.promotion_progress.stop()
10771084
self.promotion_progress = None
10781085
if success:
1079-
self.log_success(f"\n{GREEN_CHECK_MARK} Virtual layer updated")
1086+
self.log_success(f"\n{self.CHECK_MARK} Virtual layer updated")
10801087

10811088
self.environment_naming_info = EnvironmentNamingInfo()
10821089
self.default_catalog = None
@@ -2586,6 +2593,12 @@ class MarkdownConsole(CaptureTerminalConsole):
25862593
where you want to display a plan or test results in markdown.
25872594
"""
25882595

2596+
CHECK_MARK = ""
2597+
AUDIT_PASS_MARK = "passed "
2598+
GREEN_AUDIT_PASS_MARK = AUDIT_PASS_MARK
2599+
AUDIT_FAIL_MARK = "failed "
2600+
AUDIT_PADDING = 7
2601+
25892602
def __init__(self, **kwargs: t.Any) -> None:
25902603
super().__init__(**{**kwargs, "console": RichConsole(no_color=True)})
25912604

@@ -2601,18 +2614,19 @@ def show_environment_difference_summary(
26012614
no_diff: Hide the actual environment statements differences.
26022615
"""
26032616
if context_diff.is_new_environment:
2604-
self._print(
2605-
f"**New environment `{context_diff.environment}` will be created from `{context_diff.create_from}`**\n"
2617+
msg = (
2618+
f"\n**`{context_diff.environment}` environment will be initialized**"
2619+
if not context_diff.create_from_env_exists
2620+
else f"\n**New environment `{context_diff.environment}` will be created from `{context_diff.create_from}`**"
26062621
)
2622+
self._print(msg)
26072623
if not context_diff.has_snapshot_changes:
26082624
return
26092625

26102626
if not context_diff.has_changes:
26112627
self._print(f"**No differences when compared to `{context_diff.environment}`**\n")
26122628
return
26132629

2614-
self._print(f"**Summary of differences against `{context_diff.environment}`:**\n")
2615-
26162630
if context_diff.has_requirement_changes:
26172631
self._print(f"Requirements:\n{context_diff.requirements_diff()}")
26182632

@@ -2761,7 +2775,7 @@ def _show_missing_dates(self, plan: Plan, default_catalog: t.Optional[str]) -> N
27612775
dialect=self.dialect,
27622776
)
27632777
snapshots.append(
2764-
f"* `{display_name}`: [{_format_missing_intervals(snapshot, missing)}]{preview_modifier}"
2778+
f"* `{display_name}`: \\[{_format_missing_intervals(snapshot, missing)}]{preview_modifier}"
27652779
)
27662780

27672781
length = len(snapshots)
@@ -2810,6 +2824,21 @@ def _show_categorized_snapshots(self, plan: Plan, default_catalog: t.Optional[st
28102824
self._print(tree)
28112825
self._print("\n```")
28122826

2827+
def stop_evaluation_progress(self, success: bool = True) -> None:
2828+
super().stop_evaluation_progress(success)
2829+
self._print("\n")
2830+
2831+
def stop_creation_progress(self, success: bool = True) -> None:
2832+
super().stop_creation_progress(success)
2833+
self._print("\n")
2834+
2835+
def stop_promotion_progress(self, success: bool = True) -> None:
2836+
super().stop_promotion_progress(success)
2837+
self._print("\n")
2838+
2839+
def log_success(self, message: str) -> None:
2840+
self._print(message)
2841+
28132842
def log_test_results(
28142843
self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str
28152844
) -> None:
@@ -2859,6 +2888,12 @@ def log_warning(self, short_message: str, long_message: t.Optional[str] = None)
28592888
logger.warning(long_message or short_message)
28602889
self._print(f"```\n\\[WARNING] {short_message}```\n\n")
28612890

2891+
def _print(self, value: t.Any, **kwargs: t.Any) -> None:
2892+
self.console.print(value, **kwargs)
2893+
with self.console.capture() as capture:
2894+
self.console.print(value, **kwargs)
2895+
self._captured_outputs.append(capture.get())
2896+
28622897

28632898
class DatabricksMagicConsole(CaptureTerminalConsole):
28642899
"""
@@ -3250,6 +3285,7 @@ def create_console(
32503285
RuntimeEnv.TERMINAL: TerminalConsole,
32513286
RuntimeEnv.GOOGLE_COLAB: NotebookMagicConsole,
32523287
RuntimeEnv.DEBUGGER: DebuggerTerminalConsole,
3288+
RuntimeEnv.NON_INTERACTIVE: MarkdownConsole,
32533289
}
32543290
rich_console_kwargs: t.Dict[str, t.Any] = {"theme": srich.theme}
32553291
if runtime_env.is_jupyter or runtime_env.is_google_colab:
@@ -3375,7 +3411,7 @@ def _calculate_interval_str_len(snapshot: Snapshot, intervals: t.List[Interval])
33753411
return interval_str_len
33763412

33773413

3378-
def _calculate_audit_str_len(snapshot: Snapshot) -> int:
3414+
def _calculate_audit_str_len(snapshot: Snapshot, audit_padding: int = 0) -> int:
33793415
# The annotation includes audit results. We cannot build the audits result string
33803416
# until after evaluation occurs, but we must determine the annotation column width here.
33813417
# Therefore, we add enough padding for the longest possible audits result string.
@@ -3396,21 +3432,38 @@ def _calculate_audit_str_len(snapshot: Snapshot) -> int:
33963432
)
33973433
if num_audits == 1:
33983434
# +1 for "1" audit count, +1 for red X
3399-
audit_len = audit_base_str_len + (2 if num_nonblocking_audits else 1)
3435+
# if audit_padding is > 0 we're using "failed" instead of red X
3436+
audit_len = (
3437+
audit_base_str_len
3438+
+ (2 if num_nonblocking_audits else 1)
3439+
+ (
3440+
audit_padding - 1
3441+
if num_nonblocking_audits and audit_padding > 0
3442+
else audit_padding
3443+
)
3444+
)
34003445
else:
3401-
audit_len = audit_base_str_len + len(str(num_audits))
3446+
audit_len = audit_base_str_len + len(str(num_audits)) + audit_padding
34023447
if num_nonblocking_audits:
34033448
# +1 for space, +1 for red X
3404-
audit_len += len(str(num_nonblocking_audits)) + 2
3449+
# if audit_padding is > 0 we're using "failed" instead of red X
3450+
audit_len += (
3451+
len(str(num_nonblocking_audits))
3452+
+ 2
3453+
+ (audit_padding - 1 if audit_padding > 0 else audit_padding)
3454+
)
34053455
audit_str_len = max(audit_str_len, audit_len)
34063456
return audit_str_len
34073457

34083458

3409-
def _calculate_annotation_str_len(batched_intervals: t.Dict[Snapshot, t.List[Interval]]) -> int:
3459+
def _calculate_annotation_str_len(
3460+
batched_intervals: t.Dict[Snapshot, t.List[Interval]], audit_padding: int = 0
3461+
) -> int:
34103462
annotation_str_len = 0
34113463
for snapshot, intervals in batched_intervals.items():
34123464
annotation_str_len = max(
34133465
annotation_str_len,
3414-
_calculate_interval_str_len(snapshot, intervals) + _calculate_audit_str_len(snapshot),
3466+
_calculate_interval_str_len(snapshot, intervals)
3467+
+ _calculate_audit_str_len(snapshot, audit_padding),
34153468
)
34163469
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)