Skip to content

Commit 96a864e

Browse files
committed
add testing support with pytest, improve CLI error handling and language formatters
1 parent 8f06957 commit 96a864e

7 files changed

Lines changed: 164 additions & 40 deletions

File tree

‎.github/workflows/main.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ jobs:
1010

1111
- name: Run tests
1212
run: |
13-
pip install .
14-
style50 --help
13+
pip install '.[test]'
14+
python -m pytest
1515
1616
- name: Install pypa/build
1717
run: python -m pip install build --user

‎setup.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
"termcolor",
3131
"jinja2>=2.10",
3232
],
33+
extras_require={
34+
"test": [
35+
"pytest",
36+
],
37+
},
3338
dependency_links=["git+https://github.com/jeffkaufman/icdiff.git"],
3439
keywords=["style", "style50"],
3540
name="style50",

‎style50/__main__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def main():
3333
parser.add_argument("file", metavar="FILE", nargs="+", help="file or directory to lint")
3434
parser.add_argument("-o", "--output", action="store", default="character",
3535
choices=["character", "split", "unified", "score", "json", "html", "format"], metavar="MODE",
36-
help="output mode, which can be character (default), split, unified, score, or json")
36+
help="output mode, which can be character (default), split, unified, score, json, html, or format")
3737
parser.add_argument("-v", "--verbose", action="store_true",
3838
help="print full tracebacks of errors")
3939
parser.add_argument("-V", "--version", action="version",

‎style50/_api.py‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -170,15 +170,7 @@ def _check(self, file):
170170
except IndexError:
171171
pass
172172

173-
# Optional per-run configuration hook (e.g., clang-format style override)
174-
if hasattr(check, "configure"):
175-
try:
176-
check.configure(clang_format_style=self.clang_format_style)
177-
except TypeError:
178-
# Backward compatible if configure() signature differs
179-
check.configure()
180-
181-
return check(code)
173+
return check(code, clang_format_style=self.clang_format_style)
182174

183175
@staticmethod
184176
def split_diff(old, new):
@@ -311,7 +303,8 @@ class StyleCheck(metaclass=StyleMeta):
311303
# Contains substrings to be matched against libmagic's output if file extension not recognized
312304
magic_names = []
313305

314-
def __init__(self, code):
306+
def __init__(self, code, **kwargs):
307+
self._config = kwargs
315308
self.original = code
316309

317310
comments = self.count_comments(code)

‎style50/languages.py‎

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,42 +20,28 @@ class C(StyleCheck):
2020
"clang-format", f"-style={styleConfig}"
2121
]
2222

23-
_clang_format_style_override = None
24-
25-
@classmethod
26-
def configure(cls, clang_format_style=None):
27-
if clang_format_style is None:
28-
cls._clang_format_style_override = None
29-
return
30-
31-
# Guard against VS Code extension accidentally passing stringified undefined/null.
32-
if isinstance(clang_format_style, str):
33-
normalized = clang_format_style.strip().strip('"').strip("'").strip()
34-
if normalized.lower() in {"", "undefined", "null", "none"}:
35-
cls._clang_format_style_override = None
36-
return
37-
38-
cls._clang_format_style_override = clang_format_style
39-
4023
# Match (1) /**/ comments, and (2) // comments.
4124
match_comments = re.compile(r"(\/\*.*?\*\/)|(\/\/[^\n]*)", re.DOTALL)
4225

4326
# Matches string literals.
4427
match_literals = re.compile(r'"(?:\\.|[^"\\])*"', re.DOTALL)
4528

46-
def __init__(self, code):
47-
48-
# Call parent init.
49-
StyleCheck.__init__(self, code)
50-
5129
def count_comments(self, code):
5230
# Remove all string literals.
5331
stripped = self.match_literals.sub("", code)
5432
return sum(1 for _ in self.match_comments.finditer(stripped))
5533

5634
def style(self, code):
57-
if self._clang_format_style_override:
58-
return self.run(["clang-format", f"-style={self._clang_format_style_override}"], input=code)
35+
clang_format_style = self._config.get("clang_format_style")
36+
if isinstance(clang_format_style, str):
37+
normalized = clang_format_style.strip().strip('"').strip("'").strip()
38+
if normalized.lower() in {"", "undefined", "null", "none"}:
39+
clang_format_style = None
40+
41+
if clang_format_style:
42+
cmd = [c for c in self.clangFormat if not str(c).startswith("-style=")]
43+
cmd.append(f"-style={clang_format_style}")
44+
return self.run(cmd, input=code)
5945
return self.run(self.clangFormat, input=code)
6046

6147

@@ -104,9 +90,6 @@ class Js(C):
10490
((?<![\*\/])\/(?![\/\*]).*?(?<![\\])\/) # JS regexes, trying hard not to be tripped up by comments
10591
""", re.VERBOSE)
10692

107-
# C.__init__ checks for clang-format but we don't need this for Js
108-
__init__ = StyleCheck.__init__
109-
11093
# TODO: Determine which options, if any should be passed here
11194
def style(self, code):
11295
opts = jsbeautifier.default_options()

‎tests/test_cli.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import subprocess
2+
import sys
3+
4+
5+
def run_style50(args, *, cwd=None):
6+
return subprocess.run(
7+
[sys.executable, "-m", "style50", *args],
8+
cwd=cwd,
9+
check=False,
10+
text=True,
11+
stdout=subprocess.PIPE,
12+
stderr=subprocess.PIPE,
13+
)
14+
15+
16+
def test_format_mode_requires_one_file(tmp_path):
17+
f1 = tmp_path / "a.py"
18+
f2 = tmp_path / "b.py"
19+
f1.write_text("x=1\n", encoding="utf-8")
20+
f2.write_text("y=2\n", encoding="utf-8")
21+
22+
proc = run_style50(["-o", "format", str(f1), str(f2)])
23+
assert proc.returncode != 0
24+
assert "format mode requires exactly one file" in (proc.stderr + proc.stdout)
25+
26+
27+
def test_format_mode_outputs_formatted_code(tmp_path):
28+
f = tmp_path / "t.py"
29+
f.write_text("x=1\nprint( x)\n", encoding="utf-8")
30+
31+
proc = run_style50(["-o", "format", str(f)])
32+
assert proc.returncode == 0
33+
assert "print(" in proc.stdout
34+
assert proc.stdout.endswith("\n")
35+
36+
37+
def test_clang_format_style_arg_is_accepted_in_format_mode(tmp_path):
38+
"""
39+
The flag should be accepted even for non-clang languages (it will be unused).
40+
This ensures the CLI contract used by the VS Code extension is stable.
41+
"""
42+
f = tmp_path / "t.py"
43+
f.write_text("x=1\n", encoding="utf-8")
44+
45+
proc = run_style50(["-o", "format", "--clang-format-style", "{IndentWidth: 2}", str(f)])
46+
assert proc.returncode == 0
47+
assert proc.stdout.strip() != ""
48+

‎tests/test_languages.py‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import pytest
2+
3+
4+
def test_python_formatter_runs():
5+
from style50.languages import Python
6+
7+
original = "x=1\n\nprint( x)\n"
8+
formatted = Python(original).styled
9+
assert isinstance(formatted, str)
10+
assert formatted.endswith("\n")
11+
12+
13+
def test_js_formatter_runs():
14+
from style50.languages import Js
15+
16+
original = "function f(){return 1;}\n"
17+
formatted = Js(original).styled
18+
assert isinstance(formatted, str)
19+
assert formatted.endswith("\n")
20+
21+
22+
def test_css_formatter_runs():
23+
from style50.languages import Css
24+
25+
original = "body{color:red;}\n"
26+
formatted = Css(original).styled
27+
assert isinstance(formatted, str)
28+
assert formatted.endswith("\n")
29+
30+
31+
def test_sql_formatter_runs():
32+
from style50.languages import Sql
33+
34+
original = "select * from foo;\n"
35+
formatted = Sql(original).styled
36+
assert isinstance(formatted, str)
37+
assert "FROM" in formatted
38+
39+
40+
def test_html_formatter_invokes_djhtml(monkeypatch):
41+
from style50.languages import Html
42+
from style50._api import StyleCheck
43+
44+
calls = {"cmd": None}
45+
46+
def fake_run(command, input=None, exit=0, shell=False):
47+
calls["cmd"] = command
48+
# Pretend the formatter normalizes whitespace, but keep it simple.
49+
return "<!doctype html>\n"
50+
51+
monkeypatch.setattr(StyleCheck, "run", staticmethod(fake_run))
52+
formatted = Html("<html></html>\n").styled
53+
assert calls["cmd"] == ["djhtml", "-"]
54+
assert formatted == "<!doctype html>\n"
55+
56+
57+
def test_clang_format_style_override_preserves_java_flags(monkeypatch):
58+
"""
59+
Ensure override style does not drop Java's '-assume-filename=.java' flag.
60+
This is tested by capturing the command rather than invoking clang-format.
61+
"""
62+
from style50.languages import Java
63+
from style50._api import StyleCheck
64+
65+
seen = {"cmd": None}
66+
67+
def fake_run(command, input=None, exit=0, shell=False):
68+
seen["cmd"] = command
69+
return input.decode() if isinstance(input, (bytes, bytearray)) else (input or "")
70+
71+
monkeypatch.setattr(StyleCheck, "run", staticmethod(fake_run))
72+
_ = Java("class X{}\n", clang_format_style="{IndentWidth: 2}").styled
73+
74+
assert isinstance(seen["cmd"], list)
75+
assert "clang-format" in seen["cmd"][0]
76+
assert any(arg.startswith("-style=") for arg in seen["cmd"])
77+
assert "-assume-filename=.java" in seen["cmd"]
78+
79+
80+
@pytest.mark.parametrize("value", ["undefined", "null", "none", " 'undefined' "])
81+
def test_clang_format_style_override_ignores_stringified_nulls(monkeypatch, value):
82+
from style50.languages import C
83+
from style50._api import StyleCheck
84+
85+
seen = {"cmd": None}
86+
87+
def fake_run(command, input=None, exit=0, shell=False):
88+
seen["cmd"] = command
89+
return input.decode() if isinstance(input, (bytes, bytearray)) else (input or "")
90+
91+
monkeypatch.setattr(StyleCheck, "run", staticmethod(fake_run))
92+
_ = C("int main(){}\n", clang_format_style=value).styled
93+
94+
assert seen["cmd"] == C.clangFormat
95+

0 commit comments

Comments
 (0)