Skip to content

Commit

Permalink
Centralize Diagnostic formatting, expose from Rust
Browse files Browse the repository at this point in the history
  • Loading branch information
emdoyle committed Jan 27, 2025
1 parent a6044c6 commit 636af70
Show file tree
Hide file tree
Showing 12 changed files with 296 additions and 41 deletions.
28 changes: 27 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ rayon = "1.10.0"
parking_lot = "0.12.3"
itertools = "0.14.0"
toml_edit = "0.22.22"
console = "0.15.10"

[features]
extension-module = ["pyo3/extension-module"]
Expand Down
21 changes: 13 additions & 8 deletions python/tach/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
check_computation_cache,
create_computation_cache_key,
detect_unused_dependencies,
format_diagnostics,
run_server,
serialize_diagnostics_json,
update_computation_cache,
Expand Down Expand Up @@ -644,7 +645,11 @@ def tach_check(
json.dump({"error": str(e)}, sys.stdout)
sys.exit(1 if has_errors else 0)

print_diagnostics(diagnostics, project_root=project_root)
if diagnostics:
print(
format_diagnostics(project_root=project_root, diagnostics=diagnostics),
file=sys.stderr,
)
exit_code = 1 if has_errors else 0

# If we're checking in exact mode, we want to verify that there are no unused dependencies
Expand Down Expand Up @@ -696,14 +701,14 @@ def tach_check_external(
exclude_paths=exclude_paths,
)

warnings = list(filter(lambda d: d.is_warning(), diagnostics))
errors = list(filter(lambda d: d.is_error(), diagnostics))
for diagnostic in warnings:
print(diagnostic.to_string())
for diagnostic in errors:
print(diagnostic.to_string())
if diagnostics:
print(
format_diagnostics(project_root=project_root, diagnostics=diagnostics),
file=sys.stderr,
)

if errors:
has_errors = any(diagnostic.is_error() for diagnostic in diagnostics)
if has_errors:
sys.exit(1)
else:
print(
Expand Down
4 changes: 4 additions & 0 deletions python/tach/extension.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ def check_external_dependencies(
module_mappings: dict[str, list[str]],
stdlib_modules: list[str],
) -> list[Diagnostic]: ...
def format_diagnostics(
project_root: Path,
diagnostics: list[Diagnostic],
) -> str: ...
def detect_unused_dependencies(
project_root: Path,
project_config: ProjectConfig,
Expand Down
2 changes: 1 addition & 1 deletion python/tests/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_valid_example_dir(example_dir, capfd):
assert exc_info.value.code == 0
captured = capfd.readouterr()
assert SUCCESS in captured.out
assert WARNING in captured.err
assert WARNING in captured.err or "WARN" in captured.err


def test_valid_example_dir_monorepo(example_dir):
Expand Down
24 changes: 1 addition & 23 deletions python/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest

from tach import cli
from tach.extension import Diagnostic, ProjectConfig
from tach.extension import ProjectConfig


@pytest.fixture
Expand Down Expand Up @@ -41,28 +41,6 @@ def test_execute_with_config(capfd, mock_check, mock_project_config):
assert "All modules validated!" in captured.out


def test_execute_with_error(capfd, mock_check, mock_project_config):
# Mock an error returned from check
location = Path("valid_dir/file.py")
message = "Import valid_dir in valid_dir/file.py is blocked by boundary"
mock_diagnostic = Mock(spec=Diagnostic)
mock_diagnostic.is_error.return_value = True
mock_diagnostic.to_string.return_value = message
mock_diagnostic.pyfile_path.return_value = location
mock_diagnostic.pyline_number.return_value = 0
mock_check.return_value = [mock_diagnostic]
with pytest.raises(SystemExit) as sys_exit:
cli.tach_check(
project_root=Path(),
project_config=mock_project_config,
exclude_paths=mock_project_config.exclude,
)
captured = capfd.readouterr()
assert sys_exit.value.code == 1
assert str(location) in captured.err
assert message in captured.err


def test_invalid_command(capfd):
with pytest.raises(SystemExit) as sys_exit:
# Test with an invalid command
Expand Down
52 changes: 52 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::env;
use std::path::Path;

use console::Term;

#[derive(Debug, PartialEq, Eq)]
enum TerminalEnvironment {
Unknown,
Expand Down Expand Up @@ -41,3 +43,53 @@ pub fn create_clickable_link(file_path: &Path, abs_path: &Path, line: &usize) ->
let display_with_line = format!("{}[L{}]", file_path_str, line);
format!("\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\", link, display_with_line)
}

pub fn supports_emoji() -> bool {
let term = Term::stdout();
term.is_term() && term.features().wants_emoji()
}

pub fn supports_colors() -> bool {
let term = Term::stdout();
term.is_term() && term.features().colors_supported()
}

pub struct EmojiIcons;

impl EmojiIcons {
pub const SUCCESS: &str = "✅";
pub const WARNING: &str = "⚠️ ";
pub const FAIL: &str = "❌";
}

pub struct SimpleIcons;

impl SimpleIcons {
pub const SUCCESS: &str = "[OK]";
pub const WARNING: &str = "[WARN]";
pub const FAIL: &str = "[FAIL]";
}

pub fn success() -> &'static str {
if supports_emoji() {
EmojiIcons::SUCCESS
} else {
SimpleIcons::SUCCESS
}
}

pub fn warning() -> &'static str {
if supports_emoji() {
EmojiIcons::WARNING
} else {
SimpleIcons::WARNING
}
}

pub fn fail() -> &'static str {
if supports_emoji() {
EmojiIcons::FAIL
} else {
SimpleIcons::FAIL
}
}
Loading

0 comments on commit 636af70

Please sign in to comment.