Skip to content
This repository was archived by the owner on Jun 16, 2026. It is now read-only.

Commit f50c5eb

Browse files
authored
Merge branch 'main' into add-has-preserved-definition-tests-434298203320126471
Signed-off-by: Adam Poulemanos <89049923+bashandbone@users.noreply.github.com>
2 parents 5852575 + db0e3e6 commit f50c5eb

21 files changed

Lines changed: 308 additions & 170 deletions

ruff.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ exclude = [
3232
"venv",
3333
"typings",
3434
"tests/fixtures/malformed.py",
35+
"tests/fixtures/sample_type_aliases.py",
3536
]
3637
extend-include = ["*.ipynb"]
3738
fix = true
@@ -157,6 +158,9 @@ exclude = [
157158
convention = "google"
158159

159160
[lint.per-file-ignores]
161+
"tests/fixtures/sample_type_aliases.py" = [
162+
"UP040", # Intentionally using TypeAlias for backwards compatibility testing
163+
]
160164
"tests/*.py" = [
161165
"ANN",
162166
"C901", # too complex
@@ -204,3 +208,6 @@ force-wrap-aliases = false
204208
lines-after-imports = 2
205209
lines-between-types = 1
206210
split-on-trailing-comma = false
211+
212+
[lint.extend-per-file-ignores]
213+
"tests/fixtures/sample_type_aliases.py" = ["UP040"]

run_sed.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/bash
2+
sed -i 's/issues = validator.validate_file(test_file)/issues, _, _ = validator.validate_file(test_file)/g' tests/test_validator.py
3+
sed -i 's/issues = validator.validate_file(test_file)/issues, _, _ = validator.validate_file(test_file)/g' tests/test_integration.py

src/exportify/analysis/ast_parser.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,11 @@ class ASTParser:
4040
def __init__(self):
4141
"""Initialize AST parser."""
4242

43-
def parse_file(self, file_path: Path, module_path: str) -> AnalysisResult:
43+
def parse_file(self, file_path: Path) -> AnalysisResult:
4444
"""Parse a Python file and extract exports.
4545
4646
Args:
4747
file_path: Path to Python file
48-
module_path: Module path (e.g., "codeweaver.core.types")
49-
5048
Returns:
5149
AnalysisResult with symbols and metadata
5250
"""
@@ -69,8 +67,8 @@ def parse_file(self, file_path: Path, module_path: str) -> AnalysisResult:
6967
)
7068

7169
# Extract symbols (both defined and imported)
72-
defined_symbols = self._extract_symbols(tree, file_path)
73-
imported_symbols = self._extract_import_symbols(tree, file_path)
70+
defined_symbols = self._extract_symbols(tree)
71+
imported_symbols = self._extract_import_symbols(tree)
7472
all_symbols = defined_symbols + imported_symbols
7573

7674
# Extract imports as strings for backward compatibility/caching
@@ -88,12 +86,11 @@ def parse_file(self, file_path: Path, module_path: str) -> AnalysisResult:
8886
declared_all=declared_all,
8987
)
9088

91-
def _extract_symbols(self, tree: ast.Module, file_path: Path) -> list[DetectedSymbol]:
89+
def _extract_symbols(self, tree: ast.Module) -> list[DetectedSymbol]:
9290
"""Extract all exportable symbols from AST.
9391
9492
Args:
9593
tree: Parsed AST module
96-
file_path: Path to source file (for error reporting)
9794
9895
Returns:
9996
List of detected symbols
@@ -254,7 +251,7 @@ def _determine_variable_type(self, name: str, annotation: ast.expr | None) -> Me
254251
# Default to variable
255252
return MemberType.VARIABLE
256253

257-
def _extract_import_symbols(self, tree: ast.Module, file_path: Path) -> list[DetectedSymbol]:
254+
def _extract_import_symbols(self, tree: ast.Module) -> list[DetectedSymbol]:
258255
"""Extract import statements as ParsedSymbol objects.
259256
260257
Categorizes imports with heuristic metadata to help distinguish likely re-exports

src/exportify/export_manager/module_all.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def _compute_rule_actions(
118118
Returns:
119119
Mapping of symbol name to its :class:`~exportify.common.types.RuleAction`.
120120
"""
121-
result = ASTParser().parse_file(file, module_path)
121+
result = ASTParser().parse_file(file)
122122
return {
123123
symbol.name: rules.evaluate(symbol, module_path).action
124124
for symbol in result.symbols

src/exportify/pipeline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ def _process_file(self, file_path: Path, source_root: Path) -> None:
351351
self.stats.cache_misses += 1
352352
self.stats.files_analyzed += 1
353353

354-
analysis = self.ast_parser.parse_file(file_path, module_path)
354+
analysis = self.ast_parser.parse_file(file_path)
355355

356356
# Cache result
357357
self.cache.put(file_path, file_hash, analysis)

src/exportify/validator/validator.py

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,7 @@ def __init__(
5252
self.resolver = ImportResolver(project_root=self.project_root)
5353
self.consistency_checker = ConsistencyChecker(project_root=self.project_root)
5454

55-
def validate_file(self, file_path: Path) -> list[ValidationError | ValidationWarning]:
56-
"""Validate a single Python file.
57-
58-
Args:
59-
file_path: Path to Python file to validate
60-
61-
Returns:
62-
List of validation errors and warnings
63-
"""
64-
issues, _, _ = self._validate_file_with_metrics(file_path)
65-
return issues
66-
67-
def _validate_file_with_metrics(
55+
def validate_file(
6856
self, file_path: Path
6957
) -> tuple[list[ValidationError | ValidationWarning], int, ast.AST | None]:
7058
"""Validate a single Python file and count lateimport calls.
@@ -151,11 +139,11 @@ def _check_structure_and_imports(
151139
if not isinstance(tree, ast.Module):
152140
return has_type_checking_block, has_lateimport_calls
153141

154-
for i, node in enumerate(tree.body):
142+
for node in tree.body:
155143
is_import = isinstance(node, (ast.Import, ast.ImportFrom))
156144
is_code = self._is_code_statement(node, is_import=is_import)
157145

158-
if is_import and seen_code and not self._is_type_checking_block(tree.body[:i]):
146+
if is_import and seen_code:
159147
issues.append(
160148
ValidationWarning(
161149
file=file_path,
@@ -252,7 +240,7 @@ def validate_files(self, file_paths: list[Path]) -> list[ValidationError | Valid
252240
"""
253241
all_issues: list[ValidationError | ValidationWarning] = []
254242
for file_path in file_paths:
255-
issues = self.validate_file(file_path)
243+
issues, _, _ = self.validate_file(file_path)
256244
all_issues.extend(issues)
257245
return all_issues
258246

@@ -286,7 +274,7 @@ def validate(self, file_paths: list[Path] | None = None) -> ValidationReport:
286274
parsed_trees: dict[Path, ast.AST] = {}
287275

288276
for file_path in file_paths:
289-
results, count, tree = self._validate_file_with_metrics(file_path)
277+
results, count, tree = self.validate_file(file_path)
290278

291279
# Separate errors and warnings
292280
errors = [r for r in results if isinstance(r, ValidationError)]
@@ -526,15 +514,12 @@ def _is_type_checking_guard(self, node: ast.If) -> bool:
526514
"""
527515
return isinstance(node.test, ast.Name) and node.test.id == "TYPE_CHECKING"
528516

529-
def _is_type_checking_block(self, nodes: list[ast.stmt]) -> bool:
517+
def _is_type_checking_block(self) -> bool:
530518
"""Check if we're currently in a TYPE_CHECKING block.
531519
532520
This is a simplified check - it just looks for any TYPE_CHECKING if in the nodes.
533521
More sophisticated tracking would be needed for nested structures.
534522
535-
Args:
536-
nodes: List of AST nodes to check (body up to current position)
537-
538523
Returns:
539524
True if there's a TYPE_CHECKING block in the nodes (simplified)
540525
"""

tests/fixtures/sample_type_aliases.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,7 @@
1313
from __future__ import annotations
1414

1515
from pathlib import Path
16-
from typing import TYPE_CHECKING
17-
18-
19-
if TYPE_CHECKING:
20-
# Dummy usage of type aliases to keep static analyzers happy.
21-
test_file_path: FilePath | None = None
22-
23-
from typing import TypeAlias
24-
16+
from typing import TYPE_CHECKING, TypeAlias
2517

2618
# Pre-3.12 style type aliases (X: TypeAlias = Y)
2719
FilePath: TypeAlias = str | Path
@@ -32,9 +24,15 @@
3224
ConfigDict: TypeAlias = dict[str, str | int | bool | list[str]]
3325
NamePair: TypeAlias = tuple[str, str]
3426

27+
if TYPE_CHECKING:
28+
# Dummy usage of type aliases to keep static analyzers happy.
29+
test_file_path: FilePath | None = None
30+
3531
# Python 3.12+ style type aliases (type X = Y)
3632
type FileContent = str
3733
type LineNumber = int
34+
type LineNumber = int
3835
type ColumnNumber = int
3936
type SymbolName = str
4037
type ModulePath = str
38+
# ruff: noqa: UP040

tests/repro_performance.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import time
2+
3+
from pathlib import Path
4+
5+
from exportify.validator.validator import LateImportValidator
6+
7+
8+
def run_benchmark():
9+
# Setup dummy files
10+
tmp_dir = Path("bench_tmp")
11+
tmp_dir.mkdir(exist_ok=True)
12+
files = []
13+
14+
# Create large files
15+
content = "from typing import TYPE_CHECKING\n" * 100
16+
content += "if TYPE_CHECKING:\n"
17+
content += " import os\n" * 100
18+
content += "def foo():\n"
19+
content += " os = lateimport('os', 'os')\n" * 100
20+
21+
for i in range(100):
22+
f = tmp_dir / f"test_file_{i}.py"
23+
f.write_text(content)
24+
files.append(f)
25+
26+
validator = LateImportValidator(project_root=tmp_dir)
27+
28+
start = time.time()
29+
for _ in range(50):
30+
validator.validate(file_paths=files)
31+
end = time.time()
32+
33+
# Cleanup
34+
for f in files:
35+
f.unlink()
36+
tmp_dir.rmdir()
37+
38+
print(f"Validation took {end - start:.4f} seconds for 5000 large files")
39+
40+
41+
if __name__ == "__main__":
42+
run_benchmark()

0 commit comments

Comments
 (0)