Skip to content

Commit cf831dc

Browse files
committed
Refactoring, use parametrized tests
1 parent 1c9a39d commit cf831dc

10 files changed

Lines changed: 613 additions & 951 deletions

apywire/compiler.py

Lines changed: 224 additions & 212 deletions
Large diffs are not rendered by default.

apywire/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@
88
to improve maintainability and reduce duplication.
99
"""
1010

11+
import re
12+
1113
SPEC_KEY_DELIMITER = " " # Separates module.Class from name in spec keys
1214

1315
PLACEHOLDER_START = "{" # Start marker for placeholder references
1416
PLACEHOLDER_END = "}" # End marker for placeholder references
1517

1618
# Regex pattern for placeholders like {name}: captures name, no nested braces
1719
PLACEHOLDER_PATTERN = r"\{([^{}]+)\}"
20+
# Compiled version for better performance
21+
PLACEHOLDER_REGEX = re.compile(PLACEHOLDER_PATTERN)
1822

1923
SYNTHETIC_CONST = "__sconst__" # Synthetic module for promoted constants
2024

apywire/runtime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from operator import itemgetter
1515
from typing import Awaitable, Callable, Protocol, cast, final
1616

17-
from apywire.constants import SYNTHETIC_CONST
17+
from apywire.constants import PLACEHOLDER_REGEX, SYNTHETIC_CONST
1818
from apywire.exceptions import (
1919
CircularWiringError,
2020
UnknownPlaceholderError,
@@ -328,7 +328,7 @@ def replace_placeholder(match: re.Match[str]) -> str:
328328
# Convert to string
329329
return str(value)
330330

331-
return re.sub(self._placeholder_pattern, replace_placeholder, template)
331+
return PLACEHOLDER_REGEX.sub(replace_placeholder, template)
332332

333333

334334
@final

apywire/wiring.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from apywire.constants import (
2525
PLACEHOLDER_END,
26-
PLACEHOLDER_PATTERN,
26+
PLACEHOLDER_REGEX,
2727
PLACEHOLDER_START,
2828
SPEC_KEY_DELIMITER,
2929
SYNTHETIC_CONST,
@@ -131,7 +131,6 @@ def __init__(
131131
self._thread_safe = thread_safe
132132
self._max_lock_attempts = max_lock_attempts
133133
self._lock_retry_sleep = lock_retry_sleep
134-
self._placeholder_pattern = re.compile(PLACEHOLDER_PATTERN)
135134

136135
parsed: list[_UnresolvedParsedEntry] = []
137136
consts: dict[str, _SpecMapping | _ConstantValue] = {}
@@ -342,7 +341,7 @@ def replace_placeholder(match: re.Match[str]) -> str:
342341
str(ref_value) if not isinstance(ref_value, str) else ref_value
343342
)
344343

345-
return re.sub(self._placeholder_pattern, replace_placeholder, template)
344+
return PLACEHOLDER_REGEX.sub(replace_placeholder, template)
346345

347346
def _find_placeholder_names(self, obj: _SpecValue) -> set[str]:
348347
"""Find all placeholder names referenced in a value.
@@ -356,7 +355,7 @@ def _find_placeholder_names(self, obj: _SpecValue) -> set[str]:
356355
names: set[str] = set()
357356
if isinstance(obj, str):
358357
# Find all placeholders in the string (embedded or not)
359-
matches: list[str] = re.findall(self._placeholder_pattern, obj)
358+
matches: list[str] = PLACEHOLDER_REGEX.findall(obj)
360359
names.update(matches)
361360
elif isinstance(obj, dict):
362361
for v in obj.values():

tests/test_cli.py

Lines changed: 55 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,15 @@
1212
from apywire.__main__ import main
1313

1414

15-
def test_cli_version_short_flag() -> None:
16-
"""Test CLI version display with -v flag."""
15+
@pytest.mark.parametrize(
16+
"flag",
17+
["-v", "--version", "-h", "--help"],
18+
ids=["v", "version", "h", "help"],
19+
)
20+
def test_cli_basic_flags_exit_zero(flag: str) -> None:
21+
"""Test that basic CLI flags exit with code 0."""
1722
with pytest.raises(SystemExit) as exc_info:
18-
main(["-v"])
19-
20-
# argparse with action="version" exits with code 0
21-
assert exc_info.value.code == 0
22-
23-
24-
def test_cli_version_long_flag() -> None:
25-
"""Test CLI version display with --version flag."""
26-
with pytest.raises(SystemExit) as exc_info:
27-
main(["--version"])
28-
29-
# argparse with action="version" exits with code 0
30-
assert exc_info.value.code == 0
31-
32-
33-
def test_cli_help_flag() -> None:
34-
"""Test CLI help display with -h flag."""
35-
with pytest.raises(SystemExit) as exc_info:
36-
main(["-h"])
37-
38-
# argparse with action="help" exits with code 0
39-
assert exc_info.value.code == 0
40-
41-
42-
def test_cli_help_long_flag() -> None:
43-
"""Test CLI help display with --help flag."""
44-
with pytest.raises(SystemExit) as exc_info:
45-
main(["--help"])
46-
47-
# argparse with action="help" exits with code 0
23+
main([flag])
4824
assert exc_info.value.code == 0
4925

5026

@@ -54,33 +30,15 @@ def test_cli_no_arguments() -> None:
5430
assert result == 0
5531

5632

57-
def test_cli_version_output_contains_package_name() -> None:
58-
"""Test that version output contains 'apywire'."""
33+
@pytest.mark.parametrize("flag", ["-v", "--version"], ids=["v", "version"])
34+
def test_cli_version_output_format(flag: str) -> None:
35+
"""Test that version output contains package name and version."""
5936
from importlib.metadata import version
6037

6138
expected_version = version("apywire")
62-
63-
# Capture stdout since argparse writes version to stdout
6439
with pytest.raises(SystemExit) as exc_info:
6540
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
66-
main(["--version"])
67-
68-
assert exc_info.value.code == 0
69-
stdout_output = mock_stdout.getvalue()
70-
assert "apywire" in stdout_output
71-
assert expected_version in stdout_output
72-
73-
74-
def test_cli_version_short_output_contains_package_name() -> None:
75-
"""Test that -v output contains 'apywire'."""
76-
from importlib.metadata import version
77-
78-
expected_version = version("apywire")
79-
80-
# Capture stdout since argparse writes version to stdout
81-
with pytest.raises(SystemExit) as exc_info:
82-
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
83-
main(["-v"])
41+
main([flag])
8442

8543
assert exc_info.value.code == 0
8644
stdout_output = mock_stdout.getvalue()
@@ -170,40 +128,25 @@ def test_cli_module_execution() -> None:
170128
assert "apywire" in result.stdout # argparse outputs to stdout
171129

172130

173-
def test_cli_generate_json() -> None:
174-
"""Test generate command with JSON format."""
175-
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
176-
result = main(
177-
["generate", "--format", "json", "collections.OrderedDict d"]
178-
)
179-
180-
assert result == 0
181-
output = mock_stdout.getvalue()
182-
assert "collections.OrderedDict d" in output
183-
184-
185-
def test_cli_generate_ini() -> None:
186-
"""Test generate command with INI format."""
187-
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
188-
result = main(
189-
["generate", "--format", "ini", "collections.OrderedDict d"]
190-
)
191-
192-
assert result == 0
193-
output = mock_stdout.getvalue()
194-
assert "[collections.OrderedDict d]" in output
195-
196-
197-
def test_cli_generate_toml() -> None:
198-
"""Test generate command with TOML format."""
131+
@pytest.mark.parametrize(
132+
"fmt, expected_marker",
133+
[
134+
("json", "collections.OrderedDict d"),
135+
("ini", "[collections.OrderedDict d]"),
136+
("toml", '["collections.OrderedDict d"]'),
137+
],
138+
ids=["json", "ini", "toml"],
139+
)
140+
def test_cli_generate_formats(fmt: str, expected_marker: str) -> None:
141+
"""Test generate command with different output formats."""
199142
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
200143
result = main(
201-
["generate", "--format", "toml", "collections.OrderedDict d"]
144+
["generate", "--format", fmt, "collections.OrderedDict d"]
202145
)
203146

204147
assert result == 0
205148
output = mock_stdout.getvalue()
206-
assert '["collections.OrderedDict d"]' in output
149+
assert expected_marker in output
207150

208151

209152
def test_cli_generate_multiple_entries() -> None:
@@ -225,43 +168,27 @@ def test_cli_generate_multiple_entries() -> None:
225168
assert "collections.OrderedDict b" in output
226169

227170

228-
def test_cli_compile_json_stdin() -> None:
229-
"""Test compile command with JSON from stdin."""
230-
json_input = '{"collections.OrderedDict d": {}}'
231-
with patch("sys.stdin", StringIO(json_input)):
171+
@pytest.mark.parametrize(
172+
"fmt, input_data",
173+
[
174+
("json", '{"collections.OrderedDict d": {}}'),
175+
("ini", "[collections.OrderedDict d]\n"),
176+
("toml", '["collections.OrderedDict d"]\n'),
177+
],
178+
ids=["json", "ini", "toml"],
179+
)
180+
def test_cli_compile_stdin_formats(fmt: str, input_data: str) -> None:
181+
"""Test compile command with different input formats from stdin."""
182+
with patch("sys.stdin", StringIO(input_data)):
232183
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
233-
result = main(["compile", "--format", "json", "-"])
184+
result = main(["compile", "--format", fmt, "-"])
234185

235186
assert result == 0
236187
output = mock_stdout.getvalue()
237188
assert "class Compiled:" in output
238189
assert "def d(self):" in output
239190

240191

241-
def test_cli_compile_ini_stdin() -> None:
242-
"""Test compile command with INI from stdin."""
243-
ini_input = "[collections.OrderedDict d]\n"
244-
with patch("sys.stdin", StringIO(ini_input)):
245-
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
246-
result = main(["compile", "--format", "ini", "-"])
247-
248-
assert result == 0
249-
output = mock_stdout.getvalue()
250-
assert "class Compiled:" in output
251-
252-
253-
def test_cli_compile_toml_stdin() -> None:
254-
"""Test compile command with TOML from stdin."""
255-
toml_input = '["collections.OrderedDict d"]\n'
256-
with patch("sys.stdin", StringIO(toml_input)):
257-
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
258-
result = main(["compile", "--format", "toml", "-"])
259-
260-
assert result == 0
261-
output = mock_stdout.getvalue()
262-
assert "class Compiled:" in output
263-
264-
265192
def test_cli_compile_with_aio_flag() -> None:
266193
"""Test compile command with --aio flag."""
267194
json_input = '{"collections.OrderedDict d": {}}'
@@ -310,43 +237,26 @@ def test_cli_compile_from_file() -> None:
310237
os.unlink(temp_file)
311238

312239

313-
def test_cli_compile_json_parsing_error() -> None:
314-
"""Test CLI error handling for invalid JSON input."""
315-
json_input = '{"invalid": json content}'
316-
317-
with patch("sys.stdin", StringIO(json_input)):
318-
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
319-
result = main(["compile", "--format", "json", "-"])
320-
321-
assert result == 1 # Should return error code
322-
stderr_output = mock_stderr.getvalue()
323-
assert "Error parsing JSON content:" in stderr_output
324-
325-
326-
def test_cli_compile_toml_parsing_error() -> None:
327-
"""Test CLI error handling for invalid TOML input."""
328-
toml_input = '["invalid toml content'
329-
330-
with patch("sys.stdin", StringIO(toml_input)):
331-
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
332-
result = main(["compile", "--format", "toml", "-"])
333-
334-
assert result == 1 # Should return error code
335-
stderr_output = mock_stderr.getvalue()
336-
assert "Error parsing TOML content:" in stderr_output
337-
338-
339-
def test_cli_compile_ini_parsing_error() -> None:
340-
"""Test CLI error handling for invalid INI input."""
341-
ini_input = "[invalid section\nkey = value"
342-
343-
with patch("sys.stdin", StringIO(ini_input)):
240+
@pytest.mark.parametrize(
241+
"fmt, input_data, expected_err",
242+
[
243+
("json", '{"invalid": json content}', "Error parsing JSON content:"),
244+
("toml", '["invalid toml content', "Error parsing TOML content:"),
245+
("ini", "[invalid section\nkey = value", "Error parsing INI content:"),
246+
],
247+
ids=["json", "toml", "ini"],
248+
)
249+
def test_cli_compile_parsing_errors(
250+
fmt: str, input_data: str, expected_err: str
251+
) -> None:
252+
"""Test CLI error handling for invalid input formats."""
253+
with patch("sys.stdin", StringIO(input_data)):
344254
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
345-
result = main(["compile", "--format", "ini", "-"])
255+
result = main(["compile", "--format", fmt, "-"])
346256

347-
assert result == 1 # Should return error code
257+
assert result == 1
348258
stderr_output = mock_stderr.getvalue()
349-
assert "Error parsing INI content:" in stderr_output
259+
assert expected_err in stderr_output
350260

351261

352262
def test_cli_generate_toml_write_error() -> None:

tests/test_compile_aio.py

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -250,38 +250,31 @@ async def get() -> int:
250250
assert result == 42
251251

252252

253-
def test_compile_aio_list_args() -> None:
254-
"""Test async compilation of list arguments."""
255-
spec: apywire.Spec = {"builtins.int myInt": [99]}
256-
# Compile with aio=True
257-
code = apywire.WiringCompiler(spec).compile(aio=True)
253+
@pytest.mark.parametrize(
254+
"class_path, args, expected",
255+
[
256+
("builtins.int", [99], 99),
257+
("builtins.complex", {0: 1.0, "imag": 2.0}, complex(1.0, 2.0)),
258+
],
259+
ids=["int", "complex"],
260+
)
261+
def test_compile_aio_instantiation(
262+
class_path: str, args: object, expected: object
263+
) -> None:
264+
"""Test async compilation of various stdlib classes."""
265+
spec = {f"{class_path} obj": args}
266+
compiler = apywire.WiringCompiler(spec) # type: ignore[arg-type]
267+
code = compiler.compile(aio=True)
258268

259269
execd: dict[str, object] = {"asyncio": asyncio}
260270
exec(code, execd)
261271
compiled = execd["compiled"]
262272

263273
async def run() -> None:
264274
# When compiled with aio=True, the methods themselves are async
265-
coro = cast(Awaitable[int], getattr(compiled, "myInt")())
266-
val = await coro
267-
assert val == 99
268-
269-
asyncio.run(run())
270-
271-
272-
def test_compile_aio_mixed_args() -> None:
273-
"""Test async compilation of mixed numeric/string keys."""
274-
spec: apywire.Spec = {"builtins.complex myComplex": {0: 1.0, "imag": 2.0}}
275-
code = apywire.WiringCompiler(spec).compile(aio=True)
276-
277-
execd: dict[str, object] = {"asyncio": asyncio}
278-
exec(code, execd)
279-
compiled = execd["compiled"]
280-
281-
async def run() -> None:
282-
coro = cast(Awaitable[complex], getattr(compiled, "myComplex")())
275+
coro = cast(Awaitable[object], getattr(compiled, "obj")())
283276
val = await coro
284-
assert val == complex(1.0, 2.0)
277+
assert val == expected
285278

286279
asyncio.run(run())
287280

0 commit comments

Comments
 (0)