Skip to content

Commit 24d76b5

Browse files
mnriemCopilot
andauthored
Add pytest and Python linting (ruff) to CI (#1637)
* feat: add GitHub Actions workflow for testing and linting Python code * fix: resolve ruff lint errors in specify_cli - Remove extraneous f-string prefixes (F541) - Split multi-statement lines (E701, E702) - Remove unused variable assignments (F841) - Remove ruff format check from CI workflow (format-only PR to follow) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: strip ANSI codes in ai-skills help text test The Rich/Typer CLI injects ANSI escape codes into option names in --help output, causing plain string matching to fail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0f7d04b commit 24d76b5

File tree

3 files changed

+83
-27
lines changed

3 files changed

+83
-27
lines changed

.github/workflows/test.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Test & Lint Python
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
push:
8+
branches: ["main"]
9+
pull_request:
10+
11+
jobs:
12+
ruff:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Install uv
19+
uses: astral-sh/setup-uv@v6
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: "3.13"
25+
26+
- name: Run ruff check
27+
run: uvx ruff check src/
28+
29+
pytest:
30+
runs-on: ubuntu-latest
31+
strategy:
32+
matrix:
33+
python-version: ["3.11", "3.12", "3.13"]
34+
steps:
35+
- name: Checkout
36+
uses: actions/checkout@v4
37+
38+
- name: Install uv
39+
uses: astral-sh/setup-uv@v6
40+
41+
- name: Set up Python ${{ matrix.python-version }}
42+
uses: actions/setup-python@v5
43+
with:
44+
python-version: ${{ matrix.python-version }}
45+
46+
- name: Install dependencies
47+
run: uv sync --extra test
48+
49+
- name: Run tests
50+
run: uv run pytest

src/specify_cli/__init__.py

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
670670
except ValueError as je:
671671
raise RuntimeError(f"Failed to parse release JSON: {je}\nRaw (truncated 400): {response.text[:400]}")
672672
except Exception as e:
673-
console.print(f"[red]Error fetching release information[/red]")
673+
console.print("[red]Error fetching release information[/red]")
674674
console.print(Panel(str(e), title="Fetch Error", border_style="red"))
675675
raise typer.Exit(1)
676676

@@ -700,7 +700,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
700700

701701
zip_path = download_dir / filename
702702
if verbose:
703-
console.print(f"[cyan]Downloading template...[/cyan]")
703+
console.print("[cyan]Downloading template...[/cyan]")
704704

705705
try:
706706
with client.stream(
@@ -739,7 +739,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
739739
for chunk in response.iter_bytes(chunk_size=8192):
740740
f.write(chunk)
741741
except Exception as e:
742-
console.print(f"[red]Error downloading template[/red]")
742+
console.print("[red]Error downloading template[/red]")
743743
detail = str(e)
744744
if zip_path.exists():
745745
zip_path.unlink()
@@ -823,7 +823,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
823823
tracker.add("flatten", "Flatten nested directory")
824824
tracker.complete("flatten")
825825
elif verbose:
826-
console.print(f"[cyan]Found nested directory structure[/cyan]")
826+
console.print("[cyan]Found nested directory structure[/cyan]")
827827

828828
for item in source_dir.iterdir():
829829
dest_path = project_path / item.name
@@ -848,7 +848,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
848848
console.print(f"[yellow]Overwriting file:[/yellow] {item.name}")
849849
shutil.copy2(item, dest_path)
850850
if verbose and not tracker:
851-
console.print(f"[cyan]Template files merged into current directory[/cyan]")
851+
console.print("[cyan]Template files merged into current directory[/cyan]")
852852
else:
853853
zip_ref.extractall(project_path)
854854

@@ -874,7 +874,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
874874
tracker.add("flatten", "Flatten nested directory")
875875
tracker.complete("flatten")
876876
elif verbose:
877-
console.print(f"[cyan]Flattened nested directory structure[/cyan]")
877+
console.print("[cyan]Flattened nested directory structure[/cyan]")
878878

879879
except Exception as e:
880880
if tracker:
@@ -924,13 +924,17 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None =
924924
continue
925925
except Exception:
926926
continue
927-
st = script.stat(); mode = st.st_mode
927+
st = script.stat()
928+
mode = st.st_mode
928929
if mode & 0o111:
929930
continue
930931
new_mode = mode
931-
if mode & 0o400: new_mode |= 0o100
932-
if mode & 0o040: new_mode |= 0o010
933-
if mode & 0o004: new_mode |= 0o001
932+
if mode & 0o400:
933+
new_mode |= 0o100
934+
if mode & 0o040:
935+
new_mode |= 0o010
936+
if mode & 0o004:
937+
new_mode |= 0o001
934938
if not (new_mode & 0o100):
935939
new_mode |= 0o100
936940
os.chmod(script, new_mode)
@@ -976,7 +980,7 @@ def ensure_constitution_from_template(project_path: Path, tracker: StepTracker |
976980
tracker.add("constitution", "Constitution setup")
977981
tracker.complete("constitution", "copied from template")
978982
else:
979-
console.print(f"[cyan]Initialized constitution from template[/cyan]")
983+
console.print("[cyan]Initialized constitution from template[/cyan]")
980984
except Exception as e:
981985
if tracker:
982986
tracker.add("constitution", "Constitution setup")
@@ -1510,9 +1514,9 @@ def init(
15101514
enhancement_lines = [
15111515
"Optional commands that you can use for your specs [bright_black](improve quality & confidence)[/bright_black]",
15121516
"",
1513-
f"○ [cyan]/speckit.clarify[/] [bright_black](optional)[/bright_black] - Ask structured questions to de-risk ambiguous areas before planning (run before [cyan]/speckit.plan[/] if used)",
1514-
f"○ [cyan]/speckit.analyze[/] [bright_black](optional)[/bright_black] - Cross-artifact consistency & alignment report (after [cyan]/speckit.tasks[/], before [cyan]/speckit.implement[/])",
1515-
f"○ [cyan]/speckit.checklist[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]/speckit.plan[/])"
1517+
"○ [cyan]/speckit.clarify[/] [bright_black](optional)[/bright_black] - Ask structured questions to de-risk ambiguous areas before planning (run before [cyan]/speckit.plan[/] if used)",
1518+
"○ [cyan]/speckit.analyze[/] [bright_black](optional)[/bright_black] - Cross-artifact consistency & alignment report (after [cyan]/speckit.tasks[/], before [cyan]/speckit.implement[/])",
1519+
"○ [cyan]/speckit.checklist[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]/speckit.plan[/])"
15161520
]
15171521
enhancements_panel = Panel("\n".join(enhancement_lines), title="Enhancement Commands", border_style="cyan", padding=(1,2))
15181522
console.print()
@@ -1545,10 +1549,10 @@ def check():
15451549

15461550
# Check VS Code variants (not in agent config)
15471551
tracker.add("code", "Visual Studio Code")
1548-
code_ok = check_tool("code", tracker=tracker)
1552+
check_tool("code", tracker=tracker)
15491553

15501554
tracker.add("code-insiders", "Visual Studio Code Insiders")
1551-
code_insiders_ok = check_tool("code-insiders", tracker=tracker)
1555+
check_tool("code-insiders", tracker=tracker)
15521556

15531557
console.print(tracker.render())
15541558

@@ -1814,14 +1818,14 @@ def extension_add(
18141818
if zip_path.exists():
18151819
zip_path.unlink()
18161820

1817-
console.print(f"\n[green]✓[/green] Extension installed successfully!")
1821+
console.print("\n[green]✓[/green] Extension installed successfully!")
18181822
console.print(f"\n[bold]{manifest.name}[/bold] (v{manifest.version})")
18191823
console.print(f" {manifest.description}")
1820-
console.print(f"\n[bold cyan]Provided commands:[/bold cyan]")
1824+
console.print("\n[bold cyan]Provided commands:[/bold cyan]")
18211825
for cmd in manifest.commands:
18221826
console.print(f" • {cmd['name']} - {cmd.get('description', '')}")
18231827

1824-
console.print(f"\n[yellow]⚠[/yellow] Configuration may be required")
1828+
console.print("\n[yellow]⚠[/yellow] Configuration may be required")
18251829
console.print(f" Check: .specify/extensions/{manifest.id}/")
18261830

18271831
except ValidationError as e:
@@ -1871,11 +1875,11 @@ def extension_remove(
18711875

18721876
# Confirm removal
18731877
if not force:
1874-
console.print(f"\n[yellow]⚠ This will remove:[/yellow]")
1878+
console.print("\n[yellow]⚠ This will remove:[/yellow]")
18751879
console.print(f" • {cmd_count} commands from AI agent")
18761880
console.print(f" • Extension directory: .specify/extensions/{extension}/")
18771881
if not keep_config:
1878-
console.print(f" • Config files (will be backed up)")
1882+
console.print(" • Config files (will be backed up)")
18791883
console.print()
18801884

18811885
confirm = typer.confirm("Continue?")
@@ -1894,7 +1898,7 @@ def extension_remove(
18941898
console.print(f"\nConfig files backed up to .specify/extensions/.backup/{extension}/")
18951899
console.print(f"\nTo reinstall: specify extension add {extension}")
18961900
else:
1897-
console.print(f"[red]Error:[/red] Failed to remove extension")
1901+
console.print("[red]Error:[/red] Failed to remove extension")
18981902
raise typer.Exit(1)
18991903

19001904

@@ -2169,8 +2173,8 @@ def extension_update(
21692173
# TODO: Implement download and reinstall from URL
21702174
# For now, just show message
21712175
console.print(
2172-
f"[yellow]Note:[/yellow] Automatic update not yet implemented. "
2173-
f"Please update manually:"
2176+
"[yellow]Note:[/yellow] Automatic update not yet implemented. "
2177+
"Please update manually:"
21742178
)
21752179
console.print(f" specify extension remove {ext_id} --keep-config")
21762180
console.print(f" specify extension add {ext_id}")
@@ -2270,7 +2274,7 @@ def extension_disable(
22702274
hook_executor.save_project_config(config)
22712275

22722276
console.print(f"[green]✓[/green] Extension '{extension}' disabled")
2273-
console.print(f"\nCommands will no longer be available. Hooks will not execute.")
2277+
console.print("\nCommands will no longer be available. Hooks will not execute.")
22742278
console.print(f"To re-enable: specify extension enable {extension}")
22752279

22762280

tests/test_ai_skills.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- CLI validation: --ai-skills requires --ai
1111
"""
1212

13+
import re
1314
import pytest
1415
import tempfile
1516
import shutil
@@ -626,5 +627,6 @@ def test_ai_skills_flag_appears_in_help(self):
626627
runner = CliRunner()
627628
result = runner.invoke(app, ["init", "--help"])
628629

629-
assert "--ai-skills" in result.output
630-
assert "agent skills" in result.output.lower()
630+
plain = re.sub(r'\x1b\[[0-9;]*m', '', result.output)
631+
assert "--ai-skills" in plain
632+
assert "agent skills" in plain.lower()

0 commit comments

Comments
 (0)