claude-dev-env 1.23.1 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/CODE_RULES.md +14 -1
- package/hooks/blocking/_gh_body_arg_utils.py +171 -13
- package/hooks/blocking/code-rules-enforcer.py +490 -15
- package/hooks/blocking/gh-body-arg-blocker.py +27 -21
- package/hooks/blocking/pr-description-enforcer.py +247 -11
- package/hooks/blocking/tdd-enforcer.py +208 -13
- package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +116 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +231 -0
- package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +144 -0
- package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +102 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +76 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +176 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +112 -0
- package/hooks/blocking/test_gh_body_arg_blocker.py +229 -2
- package/hooks/blocking/test_pr_description_enforcer.py +193 -3
- package/hooks/blocking/test_tdd_enforcer.py +249 -0
- package/hooks/validators/exempt_paths.py +99 -0
- package/hooks/validators/magic_value_checks.py +126 -26
- package/hooks/validators/test_magic_value_checks.py +356 -2
- package/package.json +1 -1
- package/rules/gh-body-file.md +11 -2
- package/skills/bugteam/SKILL.md +111 -59
- package/skills/searching-obsidian-vault/SKILL.md +131 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Tests for broadened logging f-string detection.
|
|
2
|
+
|
|
3
|
+
Covers attribute-style calls (logger.*, logging.*, log.*) alongside
|
|
4
|
+
the legacy snake_case helpers (log_info, log_error, ...).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib.util
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import ModuleType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ENFORCER_FILENAME = "code-rules-enforcer.py"
|
|
13
|
+
ENFORCER_MODULE_NAME = "code_rules_enforcer_under_test"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_enforcer_module() -> ModuleType:
|
|
17
|
+
enforcer_path = Path(__file__).parent / ENFORCER_FILENAME
|
|
18
|
+
module_spec = importlib.util.spec_from_file_location(
|
|
19
|
+
ENFORCER_MODULE_NAME, enforcer_path
|
|
20
|
+
)
|
|
21
|
+
assert module_spec is not None
|
|
22
|
+
assert module_spec.loader is not None
|
|
23
|
+
enforcer_module = importlib.util.module_from_spec(module_spec)
|
|
24
|
+
module_spec.loader.exec_module(enforcer_module)
|
|
25
|
+
return enforcer_module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
enforcer = load_enforcer_module()
|
|
29
|
+
|
|
30
|
+
FSTRING_OPENER = 'f"'
|
|
31
|
+
FSTRING_CLOSER = '"'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_fstring_call(call_prefix: str, body: str) -> str:
|
|
35
|
+
return call_prefix + "(" + FSTRING_OPENER + body + FSTRING_CLOSER + ")\n"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_should_flag_logger_info_fstring() -> None:
|
|
39
|
+
source = build_fstring_call("logger.info", "processing {item}")
|
|
40
|
+
issues = enforcer.check_logging_fstrings(source)
|
|
41
|
+
assert len(issues) == 1
|
|
42
|
+
assert "f-string in log call" in issues[0]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_should_flag_logging_error_fstring() -> None:
|
|
46
|
+
source = build_fstring_call("logging.error", "failed: {err}")
|
|
47
|
+
issues = enforcer.check_logging_fstrings(source)
|
|
48
|
+
assert len(issues) == 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_should_flag_log_debug_fstring() -> None:
|
|
52
|
+
source = build_fstring_call("log.debug", "value={x}")
|
|
53
|
+
issues = enforcer.check_logging_fstrings(source)
|
|
54
|
+
assert len(issues) == 1
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_should_flag_logger_exception_fstring() -> None:
|
|
58
|
+
source = build_fstring_call("logger.exception", "boom {e}")
|
|
59
|
+
issues = enforcer.check_logging_fstrings(source)
|
|
60
|
+
assert len(issues) == 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_still_flag_log_info_snake_case() -> None:
|
|
64
|
+
source = build_fstring_call("log_info", "legacy helper {value}")
|
|
65
|
+
issues = enforcer.check_logging_fstrings(source)
|
|
66
|
+
assert len(issues) == 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_should_flag_log_exception_fstring() -> None:
|
|
70
|
+
source = build_fstring_call("log_exception", "snake exception {e}")
|
|
71
|
+
issues = enforcer.check_logging_fstrings(source)
|
|
72
|
+
assert len(issues) == 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_should_flag_uppercase_fstring_prefix() -> None:
|
|
76
|
+
source = 'logger.info(F"uppercase {item}")\n'
|
|
77
|
+
issues = enforcer.check_logging_fstrings(source)
|
|
78
|
+
assert len(issues) == 1
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_should_flag_raw_fstring_prefix_rf() -> None:
|
|
82
|
+
source = 'logger.info(rf"raw path {path}")\n'
|
|
83
|
+
issues = enforcer.check_logging_fstrings(source)
|
|
84
|
+
assert len(issues) == 1
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_should_flag_raw_fstring_prefix_fr() -> None:
|
|
88
|
+
source = 'logger.info(fr"raw path {path}")\n'
|
|
89
|
+
issues = enforcer.check_logging_fstrings(source)
|
|
90
|
+
assert len(issues) == 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_should_allow_logger_info_with_format_args() -> None:
|
|
94
|
+
source = 'logger.info("processing %s", item)\n'
|
|
95
|
+
issues = enforcer.check_logging_fstrings(source)
|
|
96
|
+
assert issues == []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_should_allow_log_info_with_format_args() -> None:
|
|
100
|
+
source = 'log_info("processing %s", item)\n'
|
|
101
|
+
issues = enforcer.check_logging_fstrings(source)
|
|
102
|
+
assert issues == []
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Tests for magic-value allowlist alignment with CODE_RULES §HOOK-ENFORCED.
|
|
2
|
+
|
|
3
|
+
CODE_RULES.md and .github/copilot-instructions.md both state that only
|
|
4
|
+
0, 1, and -1 (plus their float forms 0.0, 1.0) are exempt from the
|
|
5
|
+
magic-value check. Prior to this change, the hook silently allowed 2
|
|
6
|
+
and 100 as well, making the hook more permissive than the written rule.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.util
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_enforcer_module() -> ModuleType:
|
|
17
|
+
module_path = Path(__file__).parent / "code-rules-enforcer.py"
|
|
18
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
19
|
+
assert spec is not None
|
|
20
|
+
assert spec.loader is not None
|
|
21
|
+
module = importlib.util.module_from_spec(spec)
|
|
22
|
+
spec.loader.exec_module(module)
|
|
23
|
+
return module
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
PRODUCTION_FILE_PATH = "packages/claude-dev-env/hooks/blocking/example_production.py"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_check_magic_values_should_flag_literal_two_in_function_body() -> None:
|
|
33
|
+
source = (
|
|
34
|
+
"def compute_something(amount):\n"
|
|
35
|
+
" threshold = amount * 2\n"
|
|
36
|
+
" return threshold\n"
|
|
37
|
+
)
|
|
38
|
+
issues = code_rules_enforcer.check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
39
|
+
assert any(
|
|
40
|
+
issue.endswith("Magic value 2 - extract to named constant") for issue in issues
|
|
41
|
+
), f"Expected magic-value issue for literal 2, got: {issues}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_check_magic_values_should_flag_literal_one_hundred_in_function_body() -> None:
|
|
45
|
+
source = (
|
|
46
|
+
"def compute_percentage(amount):\n"
|
|
47
|
+
" scaled = amount * 100\n"
|
|
48
|
+
" return scaled\n"
|
|
49
|
+
)
|
|
50
|
+
issues = code_rules_enforcer.check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
51
|
+
assert any(
|
|
52
|
+
issue.endswith("Magic value 100 - extract to named constant") for issue in issues
|
|
53
|
+
), f"Expected magic-value issue for literal 100, got: {issues}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_check_magic_values_should_still_allow_zero_one_minus_one() -> None:
|
|
57
|
+
source = (
|
|
58
|
+
"def pick_sign(flag: int) -> int:\n"
|
|
59
|
+
" first = 0\n"
|
|
60
|
+
" second = 1\n"
|
|
61
|
+
" third = -1\n"
|
|
62
|
+
" return first + second + third + flag\n"
|
|
63
|
+
)
|
|
64
|
+
issues = code_rules_enforcer.check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
65
|
+
assert issues == [], f"Expected no issues for 0/1/-1, got: {issues}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_check_magic_values_should_still_allow_float_zero_and_float_one() -> None:
|
|
69
|
+
source = (
|
|
70
|
+
"def pick_float(flag: float) -> float:\n"
|
|
71
|
+
" low = 0.0\n"
|
|
72
|
+
" high = 1.0\n"
|
|
73
|
+
" return low + high + flag\n"
|
|
74
|
+
)
|
|
75
|
+
issues = code_rules_enforcer.check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
76
|
+
assert issues == [], f"Expected no issues for 0.0/1.0, got: {issues}"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Unit tests for code-rules-enforcer boolean naming-pattern check."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import pathlib
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_HOOK_DIRECTORY = pathlib.Path(__file__).parent
|
|
9
|
+
if str(_HOOK_DIRECTORY) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(_HOOK_DIRECTORY))
|
|
11
|
+
|
|
12
|
+
_hook_spec = importlib.util.spec_from_file_location(
|
|
13
|
+
"code_rules_enforcer",
|
|
14
|
+
_HOOK_DIRECTORY / "code-rules-enforcer.py",
|
|
15
|
+
)
|
|
16
|
+
assert _hook_spec is not None
|
|
17
|
+
assert _hook_spec.loader is not None
|
|
18
|
+
_hook_module = importlib.util.module_from_spec(_hook_spec)
|
|
19
|
+
_hook_spec.loader.exec_module(_hook_module)
|
|
20
|
+
check_boolean_naming = _hook_module.check_boolean_naming
|
|
21
|
+
validate_content = _hook_module.validate_content
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
PRODUCTION_FILE_PATH = "src/app/feature.py"
|
|
25
|
+
TEST_FILE_PATH = "src/app/test_feature.py"
|
|
26
|
+
CONFIG_FILE_PATH = "src/config/settings.py"
|
|
27
|
+
WORKFLOW_FILE_PATH = "src/workflow/orders_tab.py"
|
|
28
|
+
HOOK_FILE_PATH = "/home/user/.claude/hooks/blocking/my_hook.py"
|
|
29
|
+
EXPECTED_PREFIX_GUIDANCE = "prefix with is_/has_/should_/can_"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _assert_flags_name(issues: list[str], name: str, line_number: int) -> None:
|
|
33
|
+
expected = f"Line {line_number}: Boolean {name} - {EXPECTED_PREFIX_GUIDANCE}"
|
|
34
|
+
assert expected in issues, f"expected {expected!r} in {issues!r}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_should_flag_boolean_assignment_without_is_prefix() -> None:
|
|
38
|
+
source = "def f() -> None:\n valid = True\n"
|
|
39
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
40
|
+
_assert_flags_name(issues, "valid", 2)
|
|
41
|
+
assert len(issues) == 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_should_flag_boolean_assignment_without_has_prefix() -> None:
|
|
45
|
+
source = "def f() -> None:\n permission = False\n"
|
|
46
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
47
|
+
_assert_flags_name(issues, "permission", 2)
|
|
48
|
+
assert len(issues) == 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_should_allow_is_prefix() -> None:
|
|
52
|
+
source = "def f() -> None:\n is_valid = True\n"
|
|
53
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
54
|
+
assert issues == []
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_should_allow_has_prefix() -> None:
|
|
58
|
+
source = "def f() -> None:\n has_permission = True\n"
|
|
59
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
60
|
+
assert issues == []
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_allow_should_prefix() -> None:
|
|
64
|
+
source = "def f() -> None:\n should_retry = False\n"
|
|
65
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
66
|
+
assert issues == []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_should_allow_can_prefix() -> None:
|
|
70
|
+
source = "def f() -> None:\n can_edit = True\n"
|
|
71
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
72
|
+
assert issues == []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_should_allow_uppercase_constant_boolean() -> None:
|
|
76
|
+
source = "DEBUG_MODE = True\n"
|
|
77
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
78
|
+
assert issues == []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_should_allow_annotated_boolean_with_valid_prefix() -> None:
|
|
82
|
+
source = "def f() -> None:\n is_active: bool = True\n"
|
|
83
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
84
|
+
assert issues == []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_should_flag_annotated_boolean_without_prefix() -> None:
|
|
88
|
+
source = "def f() -> None:\n active: bool = True\n"
|
|
89
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
90
|
+
_assert_flags_name(issues, "active", 2)
|
|
91
|
+
assert len(issues) == 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_should_skip_test_files() -> None:
|
|
95
|
+
source = "def f() -> None:\n valid = True\n"
|
|
96
|
+
issues = check_boolean_naming(source, TEST_FILE_PATH)
|
|
97
|
+
assert issues == []
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_should_skip_bare_bool_annotation_without_literal_value() -> None:
|
|
101
|
+
source = "def f() -> None:\n active: bool\n"
|
|
102
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
103
|
+
assert issues == []
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_should_skip_annotated_bool_with_non_literal_rhs() -> None:
|
|
107
|
+
source = "def f() -> None:\n active: bool = compute()\n"
|
|
108
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
109
|
+
assert issues == []
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_should_flag_tuple_unpacking_of_bool_constants() -> None:
|
|
113
|
+
source = "def f() -> None:\n valid, permitted = True, False\n"
|
|
114
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
115
|
+
_assert_flags_name(issues, "valid", 2)
|
|
116
|
+
_assert_flags_name(issues, "permitted", 2)
|
|
117
|
+
assert len(issues) == 2
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_should_flag_walrus_boolean_assignment() -> None:
|
|
121
|
+
source = "def f() -> None:\n if (matched := True):\n pass\n"
|
|
122
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
123
|
+
_assert_flags_name(issues, "matched", 2)
|
|
124
|
+
assert len(issues) == 1
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_should_allow_class_body_uppercase_constant_boolean() -> None:
|
|
128
|
+
source = "class FeatureFlags:\n DEBUG_MODE: bool = True\n TRACING_ENABLED = False\n"
|
|
129
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
130
|
+
assert issues == []
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_should_skip_hook_infrastructure_files() -> None:
|
|
134
|
+
source = "def f() -> None:\n valid = True\n"
|
|
135
|
+
issues = check_boolean_naming(source, HOOK_FILE_PATH)
|
|
136
|
+
assert issues == []
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_should_skip_config_files() -> None:
|
|
140
|
+
source = "class Settings:\n enabled: bool = True\n"
|
|
141
|
+
issues = check_boolean_naming(source, CONFIG_FILE_PATH)
|
|
142
|
+
assert issues == []
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_should_skip_workflow_registry_files() -> None:
|
|
146
|
+
source = "def f() -> None:\n active = True\n"
|
|
147
|
+
issues = check_boolean_naming(source, WORKFLOW_FILE_PATH)
|
|
148
|
+
assert issues == []
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_should_cap_issues_at_three() -> None:
|
|
152
|
+
source = (
|
|
153
|
+
"def f() -> None:\n"
|
|
154
|
+
" one = True\n"
|
|
155
|
+
" two = False\n"
|
|
156
|
+
" three = True\n"
|
|
157
|
+
" four = False\n"
|
|
158
|
+
" five = True\n"
|
|
159
|
+
)
|
|
160
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
161
|
+
assert len(issues) == 3
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_should_not_flag_syntax_error_as_issue() -> None:
|
|
165
|
+
source = "def f(:\n valid = True\n"
|
|
166
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
167
|
+
assert issues == []
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_validate_content_invokes_boolean_naming_check() -> None:
|
|
171
|
+
source = "def f() -> None:\n valid = True\n"
|
|
172
|
+
issues = validate_content(source, PRODUCTION_FILE_PATH, old_content="")
|
|
173
|
+
matching_issues = [issue for issue in issues if "Boolean valid" in issue]
|
|
174
|
+
assert matching_issues, (
|
|
175
|
+
f"expected validate_content to surface the boolean-naming issue, got {issues!r}"
|
|
176
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Unit tests for TYPE_CHECKING-scoped import exemption in code-rules-enforcer."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import pathlib
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
8
|
+
if str(_HOOK_DIR) not in sys.path:
|
|
9
|
+
sys.path.insert(0, str(_HOOK_DIR))
|
|
10
|
+
|
|
11
|
+
hook_spec = importlib.util.spec_from_file_location(
|
|
12
|
+
"code_rules_enforcer",
|
|
13
|
+
_HOOK_DIR / "code-rules-enforcer.py",
|
|
14
|
+
)
|
|
15
|
+
assert hook_spec is not None
|
|
16
|
+
assert hook_spec.loader is not None
|
|
17
|
+
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
18
|
+
hook_spec.loader.exec_module(hook_module)
|
|
19
|
+
check_imports_at_top = hook_module.check_imports_at_top
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_should_allow_import_inside_if_type_checking_block() -> None:
|
|
23
|
+
content = (
|
|
24
|
+
"from typing import TYPE_CHECKING\n"
|
|
25
|
+
"\n"
|
|
26
|
+
"if TYPE_CHECKING:\n"
|
|
27
|
+
" from foo import Bar\n"
|
|
28
|
+
)
|
|
29
|
+
issues = check_imports_at_top(content)
|
|
30
|
+
assert issues == []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_should_flag_runtime_import_inside_function_even_if_file_uses_type_checking() -> (
|
|
34
|
+
None
|
|
35
|
+
):
|
|
36
|
+
content = (
|
|
37
|
+
"from typing import TYPE_CHECKING\n"
|
|
38
|
+
"\n"
|
|
39
|
+
"if TYPE_CHECKING:\n"
|
|
40
|
+
" from foo import Bar\n"
|
|
41
|
+
"\n"
|
|
42
|
+
"def baz():\n"
|
|
43
|
+
" import os\n"
|
|
44
|
+
" return os\n"
|
|
45
|
+
)
|
|
46
|
+
issues = check_imports_at_top(content)
|
|
47
|
+
assert len(issues) == 1
|
|
48
|
+
assert issues[0].startswith("Line 7:")
|
|
49
|
+
assert "Import inside function" in issues[0]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_should_still_allow_top_level_imports() -> None:
|
|
53
|
+
content = (
|
|
54
|
+
"import os\n"
|
|
55
|
+
"import sys\n"
|
|
56
|
+
"from pathlib import Path\n"
|
|
57
|
+
"\n"
|
|
58
|
+
"def do_something():\n"
|
|
59
|
+
" return os.getcwd()\n"
|
|
60
|
+
)
|
|
61
|
+
issues = check_imports_at_top(content)
|
|
62
|
+
assert issues == []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_should_flag_import_inside_function_in_file_without_type_checking() -> None:
|
|
66
|
+
content = "import os\n\ndef do_something():\n import sys\n return sys\n"
|
|
67
|
+
issues = check_imports_at_top(content)
|
|
68
|
+
assert len(issues) == 1
|
|
69
|
+
assert "Import inside function" in issues[0]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_should_allow_typing_dot_type_checking_block() -> None:
|
|
73
|
+
content = "import typing\n\nif typing.TYPE_CHECKING:\n from foo import Bar\n"
|
|
74
|
+
issues = check_imports_at_top(content)
|
|
75
|
+
assert issues == []
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_should_flag_function_import_after_type_checking_block_ends() -> None:
|
|
79
|
+
content = (
|
|
80
|
+
"from typing import TYPE_CHECKING\n"
|
|
81
|
+
"\n"
|
|
82
|
+
"if TYPE_CHECKING:\n"
|
|
83
|
+
" from foo import Bar\n"
|
|
84
|
+
"\n"
|
|
85
|
+
"def helper():\n"
|
|
86
|
+
" from json import loads\n"
|
|
87
|
+
" return loads\n"
|
|
88
|
+
)
|
|
89
|
+
issues = check_imports_at_top(content)
|
|
90
|
+
assert len(issues) == 1
|
|
91
|
+
assert "Import inside function" in issues[0]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_should_track_only_innermost_type_checking_block() -> None:
|
|
95
|
+
"""Pin documented single-level tracking: after a nested inner block ends,
|
|
96
|
+
subsequent function-body imports at the outer block's indent are flagged
|
|
97
|
+
as if outside any TYPE_CHECKING scope. See check_imports_at_top docstring.
|
|
98
|
+
"""
|
|
99
|
+
content = (
|
|
100
|
+
"from typing import TYPE_CHECKING\n"
|
|
101
|
+
"\n"
|
|
102
|
+
"if TYPE_CHECKING:\n"
|
|
103
|
+
" def helper():\n"
|
|
104
|
+
" if TYPE_CHECKING:\n"
|
|
105
|
+
" from a import A\n"
|
|
106
|
+
" from b import B\n"
|
|
107
|
+
" return B\n"
|
|
108
|
+
)
|
|
109
|
+
issues = check_imports_at_top(content)
|
|
110
|
+
assert len(issues) == 1
|
|
111
|
+
assert issues[0].startswith("Line 7:")
|
|
112
|
+
assert "Import inside function" in issues[0]
|