Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/pyspector/config.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import re
from copy import deepcopy
from pathlib import Path
import toml # type: ignore
import click # type: ignore

import click # type: ignore
import toml # type: ignore

try:
# Python 3.9+
import importlib.resources as pkg_resources
except ImportError:
# Fallback for older Python versions
import importlib_resources as pkg_resources # type: ignore
import importlib_resources as pkg_resources # type: ignore

# Sentinel placed inside any rule's `exclude_pattern` to inherit the shared
# placeholder regex declared at [defaults].exclude_pattern_placeholder. The
Expand Down Expand Up @@ -42,12 +45,12 @@ def load_config(config_path: Path) -> dict:
try:
with config_path.open('r') as f:
user_config = toml.load(f).get('tool', {}).get('pyspector', {})
config = DEFAULT_CONFIG.copy()
config = deepcopy(DEFAULT_CONFIG)
config.update(user_config)
return config
except Exception as e:
click.echo(click.style(f"Warning: Could not parse config file '{config_path}'. Using defaults. Error: {e}", fg="yellow"))
return DEFAULT_CONFIG
return deepcopy(DEFAULT_CONFIG)

def get_default_rules(ai_scan: bool = False) -> str:
"""Loads the built-in TOML rules file from package resources.
Expand Down
94 changes: 94 additions & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import sys
from pathlib import Path

import toml

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))

from pyspector.config import DEFAULT_CONFIG, get_default_rules, load_config


def test_load_config_merges_valid_pyspector_section(tmp_path):
config_path = tmp_path / "pyspector.toml"
config_path.write_text(
"""
[tool.pyspector]
severity = "HIGH"
exclude = ["custom"]
extra_setting = "kept"
""".strip(),
encoding="utf-8",
)

config = load_config(config_path)

assert config["severity"] == "HIGH"
assert config["exclude"] == ["custom"]
assert config["extra_setting"] == "kept"


def test_load_config_uses_defaults_when_file_is_missing(tmp_path):
config = load_config(tmp_path / "missing.toml")

assert config == DEFAULT_CONFIG
assert config["severity"] == "LOW"
assert "node_modules" in config["exclude"]
assert "**/test_*.py" in config["exclude"]


def test_load_config_uses_defaults_for_invalid_toml(tmp_path, capsys):
config_path = tmp_path / "pyspector.toml"
config_path.write_text("[tool.pyspector\nseverity = 'HIGH'\n", encoding="utf-8")

config = load_config(config_path)

assert config == DEFAULT_CONFIG
assert "Could not parse config file" in capsys.readouterr().out


def test_load_config_defaults_are_copied_before_user_updates(tmp_path):
config_path = tmp_path / "pyspector.toml"
config_path.write_text(
"""
[tool.pyspector]
severity = "MEDIUM"
""".strip(),
encoding="utf-8",
)

config = load_config(config_path)
config["exclude"].append("local-only")

assert config["severity"] == "MEDIUM"
assert "local-only" not in DEFAULT_CONFIG["exclude"]


def test_get_default_rules_loads_parseable_builtin_rules():
rules = toml.loads(get_default_rules())

rule_ids = {rule["id"] for rule in rules["rule"]}
assert "PY001" in rule_ids
assert "AI202" not in rule_ids


def test_get_default_rules_includes_ai_rules_when_enabled(capsys):
rules = toml.loads(get_default_rules(ai_scan=True))

rule_ids = {rule["id"] for rule in rules["rule"]}
assert "PY001" in rule_ids
assert "AI202" in rule_ids
assert "AI scanning enabled" in capsys.readouterr().out


def test_get_default_rules_substitutes_shared_placeholder_regex():
rules_text = get_default_rules(ai_scan=True)
rules = toml.loads(rules_text)

placeholder = rules["defaults"]["exclude_pattern_placeholder"]
assert "__SHARED_PLACEHOLDERS__" not in rules_text
assert placeholder

placeholder_backed_rules = [
rule for rule in rules["rule"] if rule.get("exclude_pattern") == placeholder
]
assert placeholder_backed_rules